Skip to content

StreamableHttpClientTransport fails to send auth header in SSE stream requests causing 401 errors #464

@Ejb503

Description

@Ejb503

Bug Report: Auth header not passed to SSE GET requests in StreamableHttpClientTransport

Describe the bug

rmcp does not pass the auth_header configuration to SSE (Server-Sent Events) GET requests, causing authentication failures when connecting to MCP servers that require authentication for all requests per the MCP specification.

The MCP specification (https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) states that implementations should "Implement proper authentication for all connections", but rmcp only sends the auth header in POST requests, not in GET requests used for SSE streams.

To Reproduce

Steps to reproduce the behavior:

  1. Connect to an MCP server that requires Bearer token authentication and returns a session ID (e.g., GitHub Copilot MCP server at https://api.githubcopilot.com/mcp/)

  2. Configure StreamableHttpClientTransportConfig with an auth header:

let config = StreamableHttpClientTransportConfig::with_uri("https://api.githubcopilot.com/mcp/")
    .auth_header("github_pat_xxxxx");
  1. Initialize the client:
let client_info = ClientInfo {
    protocol_version: ProtocolVersion::V_2025_03_26,
    capabilities: ClientCapabilities::default(),
    client_info: Implementation {
        name: "test-client".to_string(),
        title: None,
        version: "1.0.0".to_string(),
        website_url: None,
        icons: None,
    },
};
let client = client_info.serve(transport).await?;
  1. Attempt to list tools:
let tools = client.list_tools(Default::default()).await?;
  1. Observe error: Transport closed or 401 Unauthorized

Expected behavior

The auth header should be sent with ALL HTTP requests (POST and GET), allowing the client to:

  1. Successfully initialize with session ID
  2. Attempt to open SSE stream with proper authentication
  3. Either succeed (if server supports SSE) or gracefully handle 405 Method Not Allowed
  4. Continue operating in stateless or session-based mode as appropriate

Root Cause

In src/transport/streamable_http_client.rs, there are three locations where get_stream() is called with None for the auth parameter:

Location 1: Line ~381 - Initial SSE stream attempt

.get_stream(config.uri.clone(), session_id.clone(), None, None)
//                                                         ^^^^ should be config.auth_header.clone()

Location 2: Line ~190 - SSE reconnection

client.get_stream(uri, session_id, last_event_id, None)
//                                                 ^^^^ should pass auth_header

Location 3: Line ~474 - StreamableHttpClientReconnect struct

StreamableHttpClientReconnect {
    client: self.client.clone(),
    session_id: session_id.clone(),
    uri: config.uri.clone(),
    // Missing: auth_header field
}

Logs

Without the fix

ℹ Validating GitHub token from .env
Using token: github_pat...i8fB
ℹ Connecting to GitHub MCP server...
Attempting connection...
Connection established! Listing tools...
DEBUG: list_tools failed: TransportClosed

Token validation failed
Error: Failed to list tools: Transport closed

Testing with curl (proper auth header)

# This works - returns 405 Method Not Allowed (expected, GitHub doesn't support SSE)
curl -I -X GET \
  -H "Accept: text/event-stream" \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  https://api.githubcopilot.com/mcp/

# HTTP/2 405

Testing without auth header (rmcp's current behavior)

# This fails - returns 401 Unauthorized
curl -I -X GET \
  -H "Accept: text/event-stream" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  https://api.githubcopilot.com/mcp/

# HTTP/2 401

Proposed Fix

Step 1: Add auth_header field to struct

struct StreamableHttpClientReconnect<C> {
    pub client: C,
    pub session_id: Arc<str>,
    pub uri: Arc<str>,
    pub auth_header: Option<String>,  // Add this field
}

Step 2: Pass auth header in retry_connection()

fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future {
    let client = self.client.clone();
    let uri = self.uri.clone();
    let session_id = self.session_id.clone();
    let auth_header = self.auth_header.clone();  // Add this line
    let last_event_id = last_event_id.map(|s| s.to_owned());
    Box::pin(async move {
        client
            .get_stream(uri, session_id, last_event_id, auth_header)  // Pass auth_header
            .await
    })
}

Step 3: Update instantiations

Line ~381:

.get_stream(config.uri.clone(), session_id.clone(), None, config.auth_header.clone())

Lines ~387 and ~474:

StreamableHttpClientReconnect {
    client: self.client.clone(),
    session_id: session_id.clone(),
    uri: config.uri.clone(),
    auth_header: config.auth_header.clone(),  // Add this field
}

Environment

  • rmcp version: 0.7.0
  • Rust version: 1.83+ (tested with rustc 1.83)
  • Platform: Linux (WSL2), but issue is platform-independent
  • Transport: StreamableHttpClientTransport with reqwest

Impact

  • Severity: Medium/High
  • Prevents connection to any MCP server requiring authentication on GET/SSE requests
  • Violates MCP specification requirement for "proper authentication for all connections"
  • Affects production use cases like GitHub Copilot MCP integration

Additional context

  • This affects any MCP server that requires authentication for all requests, not just POST requests
  • The issue is particularly problematic for servers that return session IDs, as rmcp will attempt SSE streams which fail due to missing auth
  • Workaround: Use [patch.crates-io] with a locally patched version
  • Successfully validated fix against GitHub Copilot MCP server (https://api.githubcopilot.com/mcp/)

Metadata

Metadata

Assignees

No one assigned

    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