Description
Disclaimer
I haven't tried using the recent OAuth samples (my source isn't 100% setup to support it yet), but I'm pretty sure this issue applies based on analyzing it. I also did not try implementing a custom transport.
Goal
Get authentication details (specifically the authenticated identity) visible to my tool methods.
What I've done
My source supports API keys - just straight auth, no token exchange or the like. Using the MCP inspector, I've set up the connection with Bearer tokens with the key. I've successfully implemented an Axum middleware that handles the Authentication, returning to the server when it fails (example below).
The problem
Due to the abstractions between Axum and the rest of the MCP framework, there doesn't appear to be a clean way to get any details of the middleware (or anything else from the router for that matter) to the MCP server.
My approach was to assign an Identity
object to the MCP toolbox struct and assign it "at some point". But this was just my initial thought. I don't care as long as I can get it from the tool calls.
Ex:
#[derive(Clone)]
pub struct Counter {
counter: Arc<Mutex<i32>>,
identity: Identity
}
#[tool(tool_box)]
impl Counter {
#[allow(dead_code)]
pub fn new() -> Self {
let identity = get_identity(); // <- unclear how!
Self {
counter: Arc::new(Mutex::new(0)),
identity: identity // Either set it here or make it optional and assign it after construction
}
}
#[tool(description = "Increment the counter by 1")]
async fn increment(&self) -> Result<CallToolResult, McpError> {
let mut counter = self.counter.lock().await;
*counter += 1;
do_something_with_identity(self.identity);
Ok(CallToolResult::success(vec![Content::text(
counter.to_string(),
)]))
}
}
Since there's no obvious way when constructing the service, I tried assigning it during initialization. For the calls in ServerHandler
that have it, the context
parameter doesn't expose - publicly or privately - info about the Axum Router
. I tried using the Extensions
from the SDK (those available in context for some of the calls) but I couldn't find a way to set them from the Axum "side".
The question
Am I missing something obvious?
If not, I think this would be a critical feature. I'm happy to implement it if there's consensus on how (or if not, I can propose a solution).
Example code that sets Axum extensions
I can provide a full example if it helps.
pub async fn auth(mut req: Request, next: Next) -> Result<Response, StatusCode> {
// This middlware works as expected
let auth_header = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|header| header.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
// Actual authentication happens here
if let Some(identity) = authorize_current_user(auth_header).await {
// Set an Axum extension for the request
req.extensions_mut().insert(identity.clone());
Ok(next.run(req).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
async fn server_entrypoint() -> Result<()> {
let mcp_config = MCPServerConfig::default();
tracing::info!("Config for MCP Server: {:?}", mcp_config);
let bind_address = format!("{}:{}", mcp_config.mcp_bind_address, mcp_config.mcp_bind_port);
let config = SseServerConfig {
bind: bind_address.parse()?,
sse_path: "/sse".to_string(),
post_path: "/message".to_string(),
ct: tokio_util::sync::CancellationToken::new(),
sse_keep_alive: None
};
let (sse_server, router) = SseServer::new(config);
let listener = tokio::net::TcpListener::bind(sse_server.config.bind).await?;
let ct = sse_server.config.ct.child_token();
// Add auth middleware
let router = router
.route_layer(middleware::from_fn(auth));
let server = axum::serve(listener, router).with_graceful_shutdown(async move {
ct.cancelled().await;
tracing::info!("SSE server cancelled");
});
tokio::spawn(async move {
if let Err(e) = server.await {
tracing::error!(error = %e, "sse server shutdown with error");
}
});
let ct = sse_server.with_service(service::Counter::new);
graceful_shutdown().await;
ct.cancel();
Ok(())
}