Issue: Custom HTTP Headers (e.g., mcp-session-id) Not Received by FastMCP Server
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
ok, after a bit more digging, here is what I found
the mcp-session-id is lost in Streamable-HTTP tools
• Request saved then reset – RequestContextMiddleware 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_id → get_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")
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")
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}"