fix(mcp): bind tenant to session — bearer context was lost over HTTP #226

Merged
sharang merged 1 commits from fix/mcp-tenant-context into main 2026-07-22 09:30:07 +00:00
Owner

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

**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)
sharang added 1 commit 2026-07-22 09:23:41 +00:00
fix(mcp): bind tenant to session — bearer context was lost over HTTP
CI / Check (push) Skipped
CI / Check (pull_request) Successful in 5m44s
CI / Detect Changes (pull_request) Skipped
CI / Deploy Agent (pull_request) Skipped
CI / Deploy Dashboard (pull_request) Skipped
CI / Deploy Docs (pull_request) Skipped
CI / Deploy MCP (pull_request) Skipped
02c7552725
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 main 2026-07-22 09:30:07 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sharang/compliance-scanner-agent#226