Skip to content

[Docs/Help/Feature Request]: Getting authentication details in tool calls #153

Open
@wolf-sigma

Description

@wolf-sigma

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(())
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions