feat(m7.3): MCP tenant-scoped bearer tokens (#92)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 5s
CI / Deploy Agent (push) Successful in 8m13s
CI / Deploy Dashboard (push) Successful in 7m3s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 1m50s

MCP server validates per-tenant bearer tokens on incoming calls and routes each tool to the caller's tenant DB. Closes the cross-tenant data leak in the MCP path identified in M7.3.
This commit was merged in pull request #92.
This commit is contained in:
2026-06-30 15:27:21 +00:00
parent ac24ca766a
commit a3a96fe2cc
14 changed files with 622 additions and 35 deletions
+46 -17
View File
@@ -2,20 +2,37 @@ use rmcp::{
handler::server::wrapper::Parameters, model::*, tool, tool_handler, tool_router, ServerHandler,
};
use crate::database::Database;
use crate::auth::current_tenant_id;
use crate::database::{Database, DatabasePool};
use crate::tools::{dast, findings, pentest, sbom};
pub struct ComplianceMcpServer {
db: Database,
pool: DatabasePool,
#[allow(dead_code)]
tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>,
}
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).
fn tenant_db(&self) -> Result<Database, rmcp::ErrorData> {
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))
}
}
#[tool_router]
impl ComplianceMcpServer {
pub fn new(db: Database) -> Self {
pub fn new(pool: DatabasePool) -> Self {
Self {
db,
pool,
tool_router: Self::tool_router(),
}
}
@@ -29,7 +46,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<findings::ListFindingsParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
findings::list_findings(&self.db, params).await
let db = self.tenant_db()?;
findings::list_findings(&db, params).await
}
#[tool(description = "Get a single finding by its ID")]
@@ -37,7 +55,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<findings::GetFindingParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
findings::get_finding(&self.db, params).await
let db = self.tenant_db()?;
findings::get_finding(&db, params).await
}
#[tool(description = "Get a summary of findings counts grouped by severity and status")]
@@ -45,7 +64,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<findings::FindingsSummaryParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
findings::findings_summary(&self.db, params).await
let db = self.tenant_db()?;
findings::findings_summary(&db, params).await
}
// ── SBOM ──────────────────────────────────────────────
@@ -57,7 +77,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<sbom::ListSbomPackagesParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
sbom::list_sbom_packages(&self.db, params).await
let db = self.tenant_db()?;
sbom::list_sbom_packages(&db, params).await
}
#[tool(
@@ -67,7 +88,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<sbom::SbomVulnReportParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
sbom::sbom_vuln_report(&self.db, params).await
let db = self.tenant_db()?;
sbom::sbom_vuln_report(&db, params).await
}
// ── DAST ──────────────────────────────────────────────
@@ -79,7 +101,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<dast::ListDastFindingsParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
dast::list_dast_findings(&self.db, params).await
let db = self.tenant_db()?;
dast::list_dast_findings(&db, params).await
}
#[tool(description = "Get a summary of recent DAST scan runs and finding counts")]
@@ -87,7 +110,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<dast::DastScanSummaryParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
dast::dast_scan_summary(&self.db, params).await
let db = self.tenant_db()?;
dast::dast_scan_summary(&db, params).await
}
// ── Pentest ─────────────────────────────────────────────
@@ -99,7 +123,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<pentest::ListPentestSessionsParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
pentest::list_pentest_sessions(&self.db, params).await
let db = self.tenant_db()?;
pentest::list_pentest_sessions(&db, params).await
}
#[tool(description = "Get a single AI pentest session by its ID")]
@@ -107,7 +132,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<pentest::GetPentestSessionParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
pentest::get_pentest_session(&self.db, params).await
let db = self.tenant_db()?;
pentest::get_pentest_session(&db, params).await
}
#[tool(
@@ -117,7 +143,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<pentest::GetAttackChainParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
pentest::get_attack_chain(&self.db, params).await
let db = self.tenant_db()?;
pentest::get_attack_chain(&db, params).await
}
#[tool(description = "Get chat messages from a pentest session")]
@@ -125,7 +152,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<pentest::GetPentestMessagesParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
pentest::get_pentest_messages(&self.db, params).await
let db = self.tenant_db()?;
pentest::get_pentest_messages(&db, params).await
}
#[tool(
@@ -135,7 +163,8 @@ impl ComplianceMcpServer {
&self,
Parameters(params): Parameters<pentest::PentestStatsParams>,
) -> Result<CallToolResult, rmcp::ErrorData> {
pentest::pentest_stats(&self.db, params).await
let db = self.tenant_db()?;
pentest::pentest_stats(&db, params).await
}
}
@@ -149,7 +178,7 @@ impl ServerHandler for ComplianceMcpServer {
.build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"Compliance Scanner MCP server. Query security findings, SBOM data, DAST results, and AI pentest sessions."
"Compliance Scanner MCP server. Query security findings, SBOM data, DAST results, and AI pentest sessions for your tenant."
.to_string(),
),
}