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
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:
@@ -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(),
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user