fix(mcp): bind tenant to session — bearer context was lost over HTTP (#226)
CI / Check (push) Skipped
CI / Detect Changes (push) Successful in 2s
CI / Deploy Agent (push) Skipped
CI / Deploy Dashboard (push) Skipped
CI / Deploy Docs (push) Skipped
CI / Deploy MCP (push) Successful in 1m57s

This commit was merged in pull request #226.
This commit is contained in:
2026-07-22 09:30:07 +00:00
parent 3e233da128
commit 7d5c95ddb8
2 changed files with 25 additions and 22 deletions
+16 -9
View File
@@ -42,7 +42,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool_for_factory = pool.clone(); let pool_for_factory = pool.clone();
let service = StreamableHttpService::new( 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()), Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(), StreamableHttpServerConfig::default(),
); );
@@ -69,16 +81,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tenant_id = %synth_tenant, tenant_id = %synth_tenant,
"stdio transport — using synthetic tenant id; DO NOT use in production" "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(); let transport = rmcp::transport::stdio();
use rmcp::ServiceExt; use rmcp::ServiceExt;
auth::TENANT_ID let handle = server.serve(transport).await?;
.scope(synth_tenant, async { handle.waiting().await?;
let handle = server.serve(transport).await?;
handle.waiting().await?;
Ok::<_, Box<dyn std::error::Error>>(())
})
.await?;
} }
Ok(()) Ok(())
+9 -13
View File
@@ -2,37 +2,33 @@ use rmcp::{
handler::server::wrapper::Parameters, model::*, tool, tool_handler, tool_router, ServerHandler, handler::server::wrapper::Parameters, model::*, tool, tool_handler, tool_router, ServerHandler,
}; };
use crate::auth::current_tenant_id;
use crate::database::{Database, DatabasePool}; use crate::database::{Database, DatabasePool};
use crate::tools::{dast, findings, oscal, pentest, sbom}; use crate::tools::{dast, findings, oscal, pentest, sbom};
pub struct ComplianceMcpServer { pub struct ComplianceMcpServer {
pool: DatabasePool, 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)] #[allow(dead_code)]
tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>, tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>,
} }
impl ComplianceMcpServer { impl ComplianceMcpServer {
/// Resolve the per-tenant `Database` from the bearer-set /// The per-tenant `Database` for this session.
/// `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).
fn tenant_db(&self) -> Result<Database, rmcp::ErrorData> { fn tenant_db(&self) -> Result<Database, rmcp::ErrorData> {
let tenant_id = current_tenant_id().ok_or_else(|| { Ok(self.pool.for_tenant_id(&self.tenant_id))
rmcp::ErrorData::internal_error(
"no tenant context — bearer middleware not in chain".to_string(),
None,
)
})?;
Ok(self.pool.for_tenant_id(&tenant_id))
} }
} }
#[tool_router] #[tool_router]
impl ComplianceMcpServer { impl ComplianceMcpServer {
pub fn new(pool: DatabasePool) -> Self { pub fn new(pool: DatabasePool, tenant_id: String) -> Self {
Self { Self {
pool, pool,
tenant_id,
tool_router: Self::tool_router(), tool_router: Self::tool_router(),
} }
} }