From 7d5c95ddb8e4312ad0979a1818442ba25bba4977 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Wed, 22 Jul 2026 09:30:07 +0000 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20bind=20tenant=20to=20session=20?= =?UTF-8?q?=E2=80=94=20bearer=20context=20was=20lost=20over=20HTTP=20(#226?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compliance-mcp/src/main.rs | 25 ++++++++++++++++--------- compliance-mcp/src/server.rs | 22 +++++++++------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/compliance-mcp/src/main.rs b/compliance-mcp/src/main.rs index b35547f..a98e680 100644 --- a/compliance-mcp/src/main.rs +++ b/compliance-mcp/src/main.rs @@ -42,7 +42,19 @@ async fn main() -> Result<(), Box> { let pool_for_factory = pool.clone(); let service = StreamableHttpService::new( - move || Ok(ComplianceMcpServer::new(pool_for_factory.clone())), + move || { + // The factory runs in the request task, still inside the bearer + // middleware's `TENANT_ID` scope, and BEFORE rmcp spawns the + // session task (which would lose the task_local). So bind the + // tenant into the session's server instance here, once. + let tenant_id = auth::current_tenant_id().ok_or_else(|| { + std::io::Error::other("no tenant context when creating MCP session") + })?; + Ok(ComplianceMcpServer::new( + pool_for_factory.clone(), + tenant_id, + )) + }, Arc::new(LocalSessionManager::default()), StreamableHttpServerConfig::default(), ); @@ -69,16 +81,11 @@ async fn main() -> Result<(), Box> { tenant_id = %synth_tenant, "stdio transport — using synthetic tenant id; DO NOT use in production" ); - let server = ComplianceMcpServer::new(pool); + let server = ComplianceMcpServer::new(pool, synth_tenant); let transport = rmcp::transport::stdio(); use rmcp::ServiceExt; - auth::TENANT_ID - .scope(synth_tenant, async { - let handle = server.serve(transport).await?; - handle.waiting().await?; - Ok::<_, Box>(()) - }) - .await?; + let handle = server.serve(transport).await?; + handle.waiting().await?; } Ok(()) diff --git a/compliance-mcp/src/server.rs b/compliance-mcp/src/server.rs index 6e29505..2306877 100644 --- a/compliance-mcp/src/server.rs +++ b/compliance-mcp/src/server.rs @@ -2,37 +2,33 @@ use rmcp::{ handler::server::wrapper::Parameters, model::*, tool, tool_handler, tool_router, ServerHandler, }; -use crate::auth::current_tenant_id; use crate::database::{Database, DatabasePool}; use crate::tools::{dast, findings, oscal, pentest, sbom}; pub struct ComplianceMcpServer { pool: DatabasePool, + /// Tenant this session serves. Bound once at session creation (the HTTP + /// factory reads the bearer-set tenant while still in the request scope; + /// stdio passes a synthetic id) — NOT a per-request `task_local`, which is + /// lost across the `tokio::spawn` that runs the Streamable-HTTP session. + tenant_id: String, #[allow(dead_code)] tool_router: rmcp::handler::server::router::tool::ToolRouter, } impl ComplianceMcpServer { - /// Resolve the per-tenant `Database` from the bearer-set - /// `task_local`. Every tool handler calls this; missing context - /// surfaces as `internal_error` because it means the auth - /// middleware was misconfigured (handler ran without scope). + /// The per-tenant `Database` for this session. fn tenant_db(&self) -> Result { - let tenant_id = current_tenant_id().ok_or_else(|| { - rmcp::ErrorData::internal_error( - "no tenant context — bearer middleware not in chain".to_string(), - None, - ) - })?; - Ok(self.pool.for_tenant_id(&tenant_id)) + Ok(self.pool.for_tenant_id(&self.tenant_id)) } } #[tool_router] impl ComplianceMcpServer { - pub fn new(pool: DatabasePool) -> Self { + pub fn new(pool: DatabasePool, tenant_id: String) -> Self { Self { pool, + tenant_id, tool_router: Self::tool_router(), } }