Repository navigation
Description
Slice 2 of #789. Startable now. It is independent of #788. Build this before any tool exists.
This slice is the identity bridge. McpCallContext is the only object a tool may learn "who is calling" from.
The hazard
The MCP SDK's McpServerOptions.ScopeRequests defaults to true, which gives each tool call a fresh DI scope. Every Cluckwork identity primitive is scoped and populated by middleware acting on the HTTP request's scope. In a fresh scope the failure is asymmetric and both halves are bad. Writes throw (IAuditWriter fails closed, #500). Reads go silently wrong: EF filters match AccountId == Guid.Empty, and FlockScope.IsUnrestricted defaults to true, widening a #388-narrowed Worker to the whole farm.
Under SessionMode.Stateless the SDK sets ScopeRequests = false itself. The realistic regression is therefore a later SessionMode change, not a deleted flag.
Scope
McpCallContext, scoped, whose factory throws unless all of these hold:
- an
HttpContextis present; - the injected
TenantContextis reference-equal to the one inHttpContext.RequestServices. This is the only check that detects aSessionModeflip; TenantContext.IsResolved && CurrentUserContext.IsResolved && FlockScope.IsResolved. NoteIsResolved, neverIsUnrestricted;UserId != Guid.Empty.ResolveSystemActorsetsIsResolved = truewith an empty id and no roles. That resolves to Worker with zero assignment rows, which isFlockScopeGuard's account-wide case.
The slice also adds the assembly walk forbidding a tool from injecting TenantContext / CurrentUserContext / FlockScope / AppDbContext / IServiceProvider / IServiceScopeFactory / HttpContext / IHttpContextAccessor.
What this slice cannot fully close, and must not claim to
The parameter list is not the boundary. The dependency graph is. A tool taking only McpCallContext can inject a helper that opens a scope via a factory registration (sp => new Helper(() => sp.CreateScope())), whose constructor exposes only a delegate. Guard 7b walks transitive dependencies through reviewed registrations, but detached background work changes no graph at all.
So bound the hazard, name the residue, and do not write that the branch is unreachable. #787 is the fail-closed backstop for what remains, which is why slice 6 is blocked on it.
Done when
The four throwing cases pass with no tools and no MapMcp. This is testable before a single tool exists, and it is the design's load-bearing claim.
Design
docs/plans/770-mcp-server/02-guards.md rows 1-7b. Row 3b names the plausible wrong fix: asserting IsUnrestricted instead of IsResolved passes while the hole stays open.
Activity
mforce commentedon Oct 6, 2026
Resized L to M in the 2026-10-06 re-evaluation.
Since #514, a class in
Cluckwork.Api.Mcpcan reach only moduleContractsand Platform types. CW1004 andAdapterReachRealTreeTestsenforce this, and both cover tier namespaces. A tool can no longer inject a repository whose graph you would need to walk. Most of guard 7b therefore becomes a ban onIServiceProviderandIServiceScopeFactoryin that namespace. Keep the causal second-scope test, and keep the factory-delegate mutation, because a Platform helper registered withsp => ...can still hide a scope.McpCallContextand its four throwing cases are unchanged.ResolveSystemActorstill yields a resolved actor with an empty id, so check 4 still matters.TenantContext,CurrentUserContextandFlockScopeare undersrc/Cluckwork.Infrastructure/Persistence/.mforce commentedon Oct 10, 2026
Re-check after milestone 10, 2026-10-10. Add a fifth throwing case:
CurrentUserContext.ConnectedAppmust be non-null.TenantResolutionMiddleware.cs:43-65sets it from the token'sclient_id, andAuditWriterrecords it on every audit row. A tool call that runs without it has lost its app attribution, or arrived on a session JWT.AcceptOAuthTokensrefuses session JWTs at the route, so this check is the second line. ExposeConnectedApponMcpCallContext. Granted scopes do not need to be exposed: tools check scope through[Authorize](see #806).Size stays M.
fix(mcp): treat request-service access as a second-scope route; prove…
fix(mcp): keep walking a reviewed request reader's own registration