mcp: add bearer token verification seam
This commit is contained in:
@@ -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(®istry, "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;
|
||||
|
||||
Reference in New Issue
Block a user