python-sdk icon indicating copy to clipboard operation
python-sdk copied to clipboard

Issue: Custom HTTP Headers (e.g., mcp-session-id) Not Received by FastMCP Server

Open hileamlakB opened this issue 7 months ago • 3 comments

Initial Checks

  • [x] I confirm that I'm using the latest version of MCP Python SDK
  • [x] I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue

Description

When using FastMCP with the streamable-http transport, the client sends a custom header (mcp-session-id) as confirmed by debug output. However, the FastMCP server does not receive this header—it's missing from the request headers seen by the tool handler. This breaks session persistence and any feature relying on custom headers.

I have attached a demo code below, to run server python demo.py server and to run client python demo.py server

As can be seen in the small demo code Client debug: Shows mcp-session-id is present in outgoing headers. Server debug: mcp-session-id is missing from all received headers.

Example Code

import sys, asyncio
from fastmcp import FastMCP, Client
from fastmcp.server.dependencies import get_http_headers

PORT = 10000

async def print_headers_tool(ctx):
    headers = get_http_headers(include_all=True)
    print("SERVER RECEIVED HEADERS:", headers)
    return headers

def run_server():
    mcp = FastMCP(name="header-demo")
    mcp.tool(name="print_headers")(print_headers_tool)
    mcp.run(transport="streamable-http", host="0.0.0.0", port=PORT)

def run_client():
    async def main():
        async with Client(f"http://localhost:{PORT}/mcp") as client:
            
            transport = getattr(client, 'transport', None)
            if transport and hasattr(transport, 'headers'):
                print("CLIENT OUTGOING HEADERS:", transport.headers)

            result = await client.call_tool("print_headers", {})
        
    asyncio.run(main())

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python demo.py [server|client]")
    elif sys.argv[1] == "server":
        run_server()
    elif sys.argv[1] == "client":
        run_client()
    else:
        print("Unknown mode", sys.argv[1])

Python & MCP Python SDK

macOS 15.0.1, BuildVersion:   24A348
Python 3.13.5
fastmcp 2.9.0

hileamlakB avatar Jun 30 '25 21:06 hileamlakB

ok, after a bit more digging, here is what I found

the mcp-session-id is lost in Streamable-HTTP tools

Request saved then resetRequestContextMiddleware stores the current starlette.Request in _current_http_request, but resets it when the HTTP handler returns (fastmcp/server/http.py 56-69).

Message queued later – Streamable-HTTP pushes the MCP message after the handler finishes, together with the full Request, into an in-memory channel (mcp/server/streamable_http.py 368-375).

Background routing – a long-lived message_router task forwards the message to the MCP server (mcp/server/streamable_http.py 808-811).

Tool executes_current_http_request is now None, so Context.session_idget_http_headers() returns None (fastmcp/server/context.py 181-210). The header is still present in ctx.request_context.request.headers, but never used.
Result: ctx.session_id is empty for Streamable-HTTP.

Minimal library fix

# fastmcp/server/context.py
from fastmcp.server.dependencies import get_http_headers

@property
def session_id(self) -> str | None:
    # 1. Usual path (works for SSE)
    try:
        hdrs = get_http_headers(include_all=True)
        sid = hdrs.get("mcp-session-id") or hdrs.get("x-mcp-session-id")
        if sid:
            return sid
    except RuntimeError:
        pass  # no active _current_http_request

    # 2. Fallback for Streamable-HTTP
    req = self.request_context.request  # Starlette Request saved in metadata
    if req:
        return req.headers.get("mcp-session-id") 
    return None

Back-compatible: other transports keep using branch ①; no extra allocations.

Temporary workaround for tools

# inside tools
sid = ctx.session_id  # may be None on Streamable-HTTP
if not sid and ctx.request_context.request:
    sid = ctx.request_context.request.headers.get("mcp-session-id") 

hileamlakB avatar Jun 30 '25 23:06 hileamlakB

I had similar issue and I figured out this worked for me: basically I need to forward a custom access token to the server, so --

server:

mcp = FastMCP(
    name="MCPServer",
)

def _get_access_token() -> str:
    headers = get_http_headers()
    print(f"headers: {headers}") # print headers out
    access_token = headers.get("x-forwarded-access-token", None)
    if not access_token:
        raise ToolError("No access token found in headers")
    return access_token


@mcp.tool(
    name="get_access_token",
)
def get_access_token_tool() -> str:
    return _get_access_token()

mcp.run(
        transport="streamable-http", # fixed to streamable-http
        host="0.0.0.0",
        port=8000,
        path=mcp,
    )

client:

async with Client(transport=StreamableHttpTransport( ## this line helps
        f"http://127.0.0.1:8000/mcp", 
        headers={"x-forwarded-access-token": CUSTOM_TOKEN},
    )) as client: 
        transport = getattr(client, 'transport', None)
        if transport and hasattr(transport, 'headers'):
            print("CLIENT OUTGOING HEADERS:", transport.headers)
        else:
            print("No transport or headers found")

tybalex avatar Jul 01 '25 03:07 tybalex

this is super easy to do using LiteMCP (lightweight version of FastMCP) -

from typing import Annotated
from agentor.mcp import MCPAPIRouter, Context, get_context
from fastapi import Depends

mcp_router = MCPAPIRouter()

@mcp_router.tool()
def check_auth(
    resource: str, 
    ctx: Annotated[Context, Depends(get_context)]
) -> str:
    """Check authorization for a resource"""
    auth_header = ctx.headers.get("authorization", "no-auth")
    return f"Accessing {resource} with {auth_header}"

aniketmaurya avatar Nov 13 '25 10:11 aniketmaurya