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:
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
compliance-core = { workspace = true, features = ["mongodb"] }
|
||||
compliance-core = { workspace = true, features = ["mongodb", "axum"] }
|
||||
rmcp = { version = "0.16", features = ["server", "macros", "transport-io", "transport-streamable-http-server"] }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -19,3 +19,6 @@ bson = { version = "2", features = ["chrono-0_4"] }
|
||||
schemars = "1.0"
|
||||
axum = "0.8"
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Bearer-token authentication for incoming MCP HTTP requests.
|
||||
//!
|
||||
//! LLM clients (Claude Desktop / Cursor / ChatGPT / etc.) can't run
|
||||
//! Keycloak OIDC, so the MCP server uses opaque static tokens minted
|
||||
//! per-tenant via the agent's `POST /api/v1/mcp-tokens` endpoint.
|
||||
//!
|
||||
//! Flow per request:
|
||||
//! 1. Extract `Authorization: Bearer <token>`. Missing → 401.
|
||||
//! 2. SHA-256 hash the token.
|
||||
//! 3. Look up the hash in `<prefix>__admin.mcp_tokens`. Missing or
|
||||
//! revoked → 401.
|
||||
//! 4. Fire-and-forget update of `last_used_at` so the dashboard can
|
||||
//! show staleness without blocking the handler.
|
||||
//! 5. Stash the tenant_id in [`TENANT_ID`] (a `tokio::task_local`) so
|
||||
//! the MCP tool handlers can read it without modifying rmcp's
|
||||
//! handler signatures.
|
||||
//!
|
||||
//! The `task_local` is scoped around the inner service call via
|
||||
//! [`bearer_auth`], so every handler invoked downstream sees the
|
||||
//! tenant_id without us having to thread it through the macro-
|
||||
//! generated tool router.
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Request, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use mongodb::bson::doc;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::database::DatabasePool;
|
||||
|
||||
tokio::task_local! {
|
||||
/// Tenant id resolved from the bearer for this request. Set by
|
||||
/// [`bearer_auth`] before the inner service runs; read by the
|
||||
/// MCP tool handlers via [`current_tenant_id`].
|
||||
pub static TENANT_ID: String;
|
||||
}
|
||||
|
||||
/// Mongo collection name in `<prefix>__admin`.
|
||||
const COLLECTION: &str = "mcp_tokens";
|
||||
|
||||
/// Returns the tenant_id set by the auth middleware. `None` outside a
|
||||
/// request scope (e.g. unit tests that bypass the middleware).
|
||||
pub fn current_tenant_id() -> Option<String> {
|
||||
TENANT_ID.try_with(|s| s.clone()).ok()
|
||||
}
|
||||
|
||||
/// Axum middleware: validate bearer → set [`TENANT_ID`] → call inner.
|
||||
pub async fn bearer_auth(
|
||||
State(pool): State<DatabasePool>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let Some(token) = extract_bearer(&request) else {
|
||||
return (StatusCode::UNAUTHORIZED, "Missing bearer token").into_response();
|
||||
};
|
||||
if !token.starts_with("mcpt_") {
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid token format").into_response();
|
||||
}
|
||||
let token_hash = sha256_hex(&token);
|
||||
|
||||
let col = pool.admin_db().collection::<TokenLookup>(COLLECTION);
|
||||
let found = match col
|
||||
.find_one(doc! { "token_hash": &token_hash, "revoked": false })
|
||||
.await
|
||||
{
|
||||
Ok(Some(t)) => t,
|
||||
Ok(None) => {
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid or revoked token").into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("MCP token lookup failed: {e}");
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Token lookup error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Fire-and-forget last_used_at update — never block the handler.
|
||||
let col2 = pool.admin_db().collection::<TokenLookup>(COLLECTION);
|
||||
let hash_for_update = token_hash.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = col2
|
||||
.update_one(
|
||||
doc! { "token_hash": &hash_for_update },
|
||||
doc! { "$set": { "last_used_at": mongodb::bson::DateTime::now() } },
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
let tenant_id = found.tenant_id;
|
||||
let inner = next.run(request);
|
||||
TENANT_ID.scope(tenant_id, inner).await
|
||||
}
|
||||
|
||||
/// Bare-bones projection — we don't need the whole `McpToken` here,
|
||||
/// just enough to route and confirm validity.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TokenLookup {
|
||||
tenant_id: String,
|
||||
}
|
||||
|
||||
fn extract_bearer(req: &Request<Body>) -> Option<String> {
|
||||
req.headers()
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn sha256_hex(s: &str) -> String {
|
||||
let mut h = Sha256::new();
|
||||
h.update(s.as_bytes());
|
||||
hex::encode(h.finalize())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sha256_known_value() {
|
||||
// python -c 'import hashlib; print(hashlib.sha256(b"mcpt_known").hexdigest())'
|
||||
assert_eq!(
|
||||
sha256_hex("mcpt_known"),
|
||||
"27cf6cf678a44244106863c1c031be8e57b84c2b3019d742f755f8e7afa75dfd"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,127 @@
|
||||
use mongodb::{Client, Collection};
|
||||
//! Per-tenant Mongo broker for the MCP server.
|
||||
//!
|
||||
//! Mirror of the agent's `compliance_agent::database::DatabasePool` —
|
||||
//! duplicated here rather than lifted into `compliance-core` to keep
|
||||
//! this PR focused. If a third consumer ever needs it, lift then.
|
||||
//!
|
||||
//! Bearer tokens (validated by the auth middleware) carry a tenant_id
|
||||
//! and the handler resolves the per-tenant database via
|
||||
//! [`DatabasePool::for_tenant_id`]. The admin database
|
||||
//! (`<db_prefix>__admin`) holds the cross-tenant `mcp_tokens`
|
||||
//! collection that the middleware queries on every request.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use mongodb::{bson::doc, Client, Collection};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use compliance_core::models::*;
|
||||
|
||||
/// 63-byte Mongo db-name cap; same invariant as the agent's pool.
|
||||
const MAX_DB_NAME_LEN: usize = 63;
|
||||
/// 16-byte SHA-256 truncation, hex-encoded → 32 chars.
|
||||
const HASH_HEX_LEN: usize = 32;
|
||||
const MAX_PREFIX_LEN: usize = MAX_DB_NAME_LEN - 1 - HASH_HEX_LEN;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatabasePool {
|
||||
client: Client,
|
||||
db_prefix: String,
|
||||
/// Tenants we've handed out a [`Database`] for. The MCP server
|
||||
/// doesn't ensure indexes (the agent owns that side of the
|
||||
/// schema), so the marker exists only to satisfy the parallel
|
||||
/// shape — current code never reads it.
|
||||
#[allow(dead_code)]
|
||||
seen: Arc<DashMap<String, ()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DbError {
|
||||
#[error("db_prefix '{prefix}' is {len} chars; max is {max} so the hash-fallback DB name fits Mongo's 63-byte cap")]
|
||||
PrefixTooLong {
|
||||
prefix: String,
|
||||
len: usize,
|
||||
max: usize,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Mongo(#[from] mongodb::error::Error),
|
||||
}
|
||||
|
||||
impl DatabasePool {
|
||||
pub async fn connect(uri: &str, db_prefix: &str) -> Result<Self, DbError> {
|
||||
if db_prefix.len() > MAX_PREFIX_LEN {
|
||||
return Err(DbError::PrefixTooLong {
|
||||
prefix: db_prefix.to_string(),
|
||||
len: db_prefix.len(),
|
||||
max: MAX_PREFIX_LEN,
|
||||
});
|
||||
}
|
||||
let client = Client::with_uri_str(uri).await?;
|
||||
client
|
||||
.database("admin")
|
||||
.run_command(doc! { "ping": 1 })
|
||||
.await?;
|
||||
tracing::info!(
|
||||
"MCP MongoDB cluster reachable; per-tenant pool ready (db prefix '{db_prefix}')"
|
||||
);
|
||||
Ok(Self {
|
||||
client,
|
||||
db_prefix: db_prefix.to_string(),
|
||||
seen: Arc::new(DashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Read-only handle to the tenant's database. No indexes are
|
||||
/// ensured here — the agent owns writes, MCP only reads.
|
||||
pub fn for_tenant_id(&self, tenant_id: &str) -> Database {
|
||||
let db_name = self.tenant_db_name(tenant_id);
|
||||
self.seen.insert(tenant_id.to_string(), ());
|
||||
Database::new(self.client.database(&db_name))
|
||||
}
|
||||
|
||||
/// Cross-tenant admin DB — holds the `mcp_tokens` collection that
|
||||
/// the auth middleware queries to map bearer → tenant_id.
|
||||
pub fn admin_db(&self) -> mongodb::Database {
|
||||
self.client.database(&format!("{}__admin", self.db_prefix))
|
||||
}
|
||||
|
||||
pub fn tenant_db_name(&self, tenant_id: &str) -> String {
|
||||
let sanitized = sanitize_tenant_id(tenant_id);
|
||||
let natural = format!("{}_{}", self.db_prefix, sanitized);
|
||||
if natural.len() <= MAX_DB_NAME_LEN {
|
||||
natural
|
||||
} else {
|
||||
let mut h = Sha256::new();
|
||||
h.update(tenant_id.as_bytes());
|
||||
let digest = h.finalize();
|
||||
let suffix = hex::encode(&digest[..HASH_HEX_LEN / 2]);
|
||||
format!("{}_{}", self.db_prefix, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_tenant_id(tenant_id: &str) -> String {
|
||||
tenant_id
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'/' | '\\' | '.' | '"' | '$' | ' ' | '\0' => '_',
|
||||
c => c,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Typed accessors for the MCP-readable collections in a tenant DB.
|
||||
/// Matches the agent's `Database` shape but only exposes what the MCP
|
||||
/// tool handlers actually need.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Database {
|
||||
inner: mongodb::Database,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn connect(uri: &str, db_name: &str) -> Result<Self, mongodb::error::Error> {
|
||||
let client = Client::with_uri_str(uri).await?;
|
||||
let db = client.database(db_name);
|
||||
db.run_command(mongodb::bson::doc! { "ping": 1 }).await?;
|
||||
tracing::info!("MCP server connected to MongoDB '{db_name}'");
|
||||
Ok(Self { inner: db })
|
||||
pub(crate) fn new(inner: mongodb::Database) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn findings(&self) -> Collection<Finding> {
|
||||
|
||||
+35
-10
@@ -1,10 +1,11 @@
|
||||
mod auth;
|
||||
mod database;
|
||||
mod server;
|
||||
mod tools;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use database::Database;
|
||||
use database::DatabasePool;
|
||||
use rmcp::transport::{
|
||||
streamable_http_server::session::local::LocalSessionManager, StreamableHttpServerConfig,
|
||||
StreamableHttpService,
|
||||
@@ -24,36 +25,60 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let mongo_uri =
|
||||
std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string());
|
||||
let db_name =
|
||||
// MONGODB_DATABASE is reused as the per-tenant DB-name prefix —
|
||||
// same convention as the agent so `<prefix>__admin.mcp_tokens`
|
||||
// and `<prefix>_<tenant_id>` line up across services.
|
||||
let db_prefix =
|
||||
std::env::var("MONGODB_DATABASE").unwrap_or_else(|_| "compliance_scanner".to_string());
|
||||
|
||||
let db = Database::connect(&mongo_uri, &db_name).await?;
|
||||
let pool = DatabasePool::connect(&mongo_uri, &db_prefix).await?;
|
||||
|
||||
// If MCP_PORT is set, run as Streamable HTTP server; otherwise use stdio.
|
||||
// HTTP transport: bind a small axum router with bearer-auth in
|
||||
// front of the rmcp service. `/health` stays public for orca's
|
||||
// container probe.
|
||||
if let Ok(port_str) = std::env::var("MCP_PORT") {
|
||||
let port: u16 = port_str.parse()?;
|
||||
tracing::info!("Starting MCP server on HTTP port {port}");
|
||||
|
||||
let db_clone = db.clone();
|
||||
let pool_for_factory = pool.clone();
|
||||
let service = StreamableHttpService::new(
|
||||
move || Ok(ComplianceMcpServer::new(db_clone.clone())),
|
||||
move || Ok(ComplianceMcpServer::new(pool_for_factory.clone())),
|
||||
Arc::new(LocalSessionManager::default()),
|
||||
StreamableHttpServerConfig::default(),
|
||||
);
|
||||
|
||||
let router = axum::Router::new()
|
||||
.route("/health", axum::routing::get(|| async { "ok" }))
|
||||
.nest_service("/mcp", service);
|
||||
.nest_service(
|
||||
"/mcp",
|
||||
axum::Router::new().fallback_service(service).layer(
|
||||
axum::middleware::from_fn_with_state(pool.clone(), auth::bearer_auth),
|
||||
),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
|
||||
tracing::info!("MCP HTTP server listening on 0.0.0.0:{port}");
|
||||
axum::serve(listener, router).await?;
|
||||
} else {
|
||||
// stdio transport — used when run as a local MCP server next
|
||||
// to the LLM client. There's no HTTP layer to do bearer auth,
|
||||
// so we synthesize a tenant_id from STDIO_TENANT_ID for local
|
||||
// development. NEVER use this in production.
|
||||
tracing::info!("Starting MCP server on stdio");
|
||||
let server = ComplianceMcpServer::new(db);
|
||||
let synth_tenant = std::env::var("STDIO_TENANT_ID").unwrap_or_else(|_| "dev".to_string());
|
||||
tracing::warn!(
|
||||
tenant_id = %synth_tenant,
|
||||
"stdio transport — using synthetic tenant id; DO NOT use in production"
|
||||
);
|
||||
let server = ComplianceMcpServer::new(pool);
|
||||
let transport = rmcp::transport::stdio();
|
||||
use rmcp::ServiceExt;
|
||||
let handle = server.serve(transport).await?;
|
||||
handle.waiting().await?;
|
||||
auth::TENANT_ID
|
||||
.scope(synth_tenant, async {
|
||||
let handle = server.serve(transport).await?;
|
||||
handle.waiting().await?;
|
||||
Ok::<_, Box<dyn std::error::Error>>(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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