mcp: add bearer token verification seam

This commit is contained in:
a.tolmachev
2026-05-03 18:43:48 +00:00
parent ffcce360fc
commit 46f8e6cbeb
5 changed files with 178 additions and 13 deletions
+81 -1
View File
@@ -1,4 +1,5 @@
mod app;
mod auth;
mod catalog;
mod jsonrpc;
mod session;
@@ -13,7 +14,10 @@ use sqlx::postgres::PgConnectOptions;
use tokio::net::TcpListener;
use tracing::info;
use crate::{app::build_app, session::PostgresTransportSessionStore};
use crate::{
app::build_app, auth::CommunityMachineCredentialVerifier,
session::PostgresTransportSessionStore,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -56,6 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
runtime,
RequestRateLimiter::new(api_rate_limit),
std::sync::Arc::new(session_store),
std::sync::Arc::new(CommunityMachineCredentialVerifier),
);
let listener = TcpListener::bind(socket_addr).await?;
@@ -162,6 +167,11 @@ mod tests {
use crate::{
app::build_app,
auth::{
CommunityMachineCredentialVerifier, MachineCredentialVerifier,
MachineCredentialVerifierError, SharedMachineCredentialVerifier,
VerifiedMachineCredential,
},
session::{InMemorySessionStore, SharedSessionStore, TransportSessionStore},
};
@@ -238,6 +248,7 @@ mod tests {
public_base_url,
rate_limit_config,
std::sync::Arc::new(InMemorySessionStore::default()),
std::sync::Arc::new(CommunityMachineCredentialVerifier),
)
}
@@ -247,6 +258,7 @@ mod tests {
public_base_url: Option<String>,
rate_limit_config: RequestRateLimitConfig,
sessions: SharedSessionStore,
credential_verifier: SharedMachineCredentialVerifier,
) -> axum::Router {
build_app(
registry,
@@ -256,9 +268,32 @@ mod tests {
RuntimeExecutor::new(),
RequestRateLimiter::new(rate_limit_config),
sessions,
credential_verifier,
)
}
#[derive(Clone)]
struct StubMachineCredentialVerifier {
token: String,
credential: VerifiedMachineCredential,
}
#[async_trait::async_trait]
impl MachineCredentialVerifier for StubMachineCredentialVerifier {
async fn verify_bearer_token(
&self,
_workspace_slug: &str,
_agent_slug: &str,
token: &str,
) -> Result<Option<VerifiedMachineCredential>, MachineCredentialVerifierError> {
if token == self.token {
return Ok(Some(self.credential.clone()));
}
Ok(None)
}
}
#[tokio::test]
async fn initializes_lists_and_calls_published_tool_via_mcp() {
let registry = test_registry().await;
@@ -950,6 +985,7 @@ mod tests {
Some("https://crank.example.com".to_owned()),
RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
session_store,
std::sync::Arc::new(CommunityMachineCredentialVerifier),
))
.await;
let client = reqwest::Client::new();
@@ -1417,6 +1453,50 @@ mod tests {
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn accepts_initialize_with_verified_bearer_token_seam() {
let registry = test_registry().await;
publish_agent_with_bindings(&registry, "sales-token-seam", vec![]).await;
let verifier = Arc::new(StubMachineCredentialVerifier {
token: "issued_token_demo".to_owned(),
credential: VerifiedMachineCredential {
machine_access_mode: crank_core::MachineAccessMode::ShortLivedToken,
max_security_level: crank_core::OperationSecurityLevel::Elevated,
scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
},
});
let base_url = spawn_mcp_server(build_test_app_with_store(
registry,
Duration::from_millis(0),
Some("https://crank.example.com".to_owned()),
RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
std::sync::Arc::new(InMemorySessionStore::default()),
verifier,
))
.await;
let client = reqwest::Client::new();
let response = client
.post(agent_mcp_url(&base_url, "sales-token-seam"))
.header(header::ACCEPT, "application/json, text/event-stream")
.header(header::AUTHORIZATION, "Bearer issued_token_demo")
.json(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25"
}
}))
.send()
.await
.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
assert!(response.headers().get("MCP-Session-Id").is_some());
}
#[tokio::test]
async fn rejects_initialize_without_platform_api_key() {
let registry = test_registry().await;