Found while starting A (the breakpilot MCP loop). Exercising the deployed MCP with a valid token, every tool call returned -32603: no tenant context — bearer middleware not in chain — while initialize and tools/list worked.
Root cause: the bearer middleware stashes the tenant in a tokiotask_local scoped around the request, but rmcp's StreamableHttpServicespawns the session's tool-serving task (tower.rs: "spawn a task to serve the session"), and task_locals don't cross tokio::spawn. So the tenant was never visible to tool handlers over HTTP — meaning the MCP loop has never worked over HTTP, and Breakpilot's list_findings client has been silently falling back to demo data.
Fix: bind the tenant to the per-session server instance instead of a per-request task_local. rmcp calls the service factory in the request task (before the spawn) and still inside the middleware's scope, so the factory reads the bearer-set tenant once and bakes it into ComplianceMcpServer; tenant_db() reads self.tenant_id. stdio passes its synthetic tenant the same way. No tool-handler changes (all 14 unchanged), and the middleware still validates + can revoke the token on every request.
Verified: compiles, clippy -D warnings clean, fmt clean. Will verify live against comp-mcp-dev (calling oscal_assessment) once the MCP redeploys.
**Found while starting A (the breakpilot MCP loop).** Exercising the deployed MCP with a valid token, every tool call returned `-32603: no tenant context — bearer middleware not in chain` — while `initialize` and `tools/list` worked.
**Root cause:** the bearer middleware stashes the tenant in a `tokio` **task_local** scoped around the request, but rmcp's `StreamableHttpService` **spawns the session's tool-serving task** (`tower.rs`: *"spawn a task to serve the session"*), and task_locals don't cross `tokio::spawn`. So the tenant was never visible to tool handlers over HTTP — meaning **the MCP loop has never worked over HTTP**, and Breakpilot's `list_findings` client has been silently falling back to demo data.
**Fix:** bind the tenant to the **per-session server instance** instead of a per-request task_local. rmcp calls the service factory in the request task (before the spawn) and still inside the middleware's scope, so the factory reads the bearer-set tenant once and bakes it into `ComplianceMcpServer`; `tenant_db()` reads `self.tenant_id`. stdio passes its synthetic tenant the same way. **No tool-handler changes** (all 14 unchanged), and the middleware still validates + can revoke the token on every request.
Verified: compiles, clippy `-D warnings` clean, fmt clean. Will verify live against `comp-mcp-dev` (calling `oscal_assessment`) once the MCP redeploys.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Root cause (found while wiring the breakpilot MCP loop, A): every MCP tool call
over Streamable HTTP failed with '-32603 no tenant context — bearer middleware
not in chain'. The bearer middleware sets the tenant in a tokio task_local scoped
around the request, but rmcp's StreamableHttpService spawns the session's
tool-serving task (tower.rs: 'spawn a task to serve the session'), and task_locals
do NOT cross tokio::spawn. So initialize/tools-list worked but every real tool call
(list_findings, oscal_assessment, ...) failed — meaning the loop never worked over
HTTP and breakpilot's client fell back to demo data.
Fix: bind the tenant to the per-session server instance instead of a per-request
task_local. rmcp calls the service factory in the request task (before the spawn)
and still inside the middleware's scope, so the factory reads the bearer-set
tenant once and bakes it into ComplianceMcpServer; tool_db() then reads
self.tenant_id. stdio passes its synthetic tenant the same way. No tool-handler
changes; the middleware still validates (and can revoke) the token per request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sharang
merged commit 7d5c95ddb8 into main2026-07-22 09:30:07 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while starting A (the breakpilot MCP loop). Exercising the deployed MCP with a valid token, every tool call returned
-32603: no tenant context — bearer middleware not in chain— whileinitializeandtools/listworked.Root cause: the bearer middleware stashes the tenant in a
tokiotask_local scoped around the request, but rmcp'sStreamableHttpServicespawns the session's tool-serving task (tower.rs: "spawn a task to serve the session"), and task_locals don't crosstokio::spawn. So the tenant was never visible to tool handlers over HTTP — meaning the MCP loop has never worked over HTTP, and Breakpilot'slist_findingsclient has been silently falling back to demo data.Fix: bind the tenant to the per-session server instance instead of a per-request task_local. rmcp calls the service factory in the request task (before the spawn) and still inside the middleware's scope, so the factory reads the bearer-set tenant once and bakes it into
ComplianceMcpServer;tenant_db()readsself.tenant_id. stdio passes its synthetic tenant the same way. No tool-handler changes (all 14 unchanged), and the middleware still validates + can revoke the token on every request.Verified: compiles, clippy
-D warningsclean, fmt clean. Will verify live againstcomp-mcp-dev(callingoscal_assessment) once the MCP redeploys.🤖 Generated with Claude Code