Skip to content

Commit 23744f2

Browse files
felixweinbergermaxisbey
authored andcommitted
refactor: Remove header mutation in streamable_http_client
Remove client.headers.update() call that was unnecessarily mutating user-provided httpx.AsyncClient instances. The mutation was defensive but unnecessary since: 1. All transport methods pass headers explicitly to httpx requests 2. httpx merges request headers with client defaults, with request headers taking precedence 3. HTTP requests are identical with or without the mutation 4. Not mutating respects user's client object integrity Add comprehensive test coverage for header behavior: - Verify client headers are not mutated after use - Verify MCP protocol headers override httpx defaults in requests - Verify custom and MCP headers coexist correctly in requests All existing tests pass, confirming no behavior change to actual HTTP requests.
1 parent 43971c1 commit 23744f2

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

src/mcp/client/streamable_http.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,6 @@ async def streamable_http_client(
506506
# Create transport with extracted configuration
507507
transport = StreamableHTTPTransport(url, headers_dict, timeout, sse_read_timeout, auth)
508508

509-
# Sync client headers with transport's merged headers (includes MCP protocol requirements)
510-
client.headers.update(transport.request_headers)
511-
512509
async with anyio.create_task_group() as tg:
513510
try:
514511
logger.debug(f"Connecting to StreamableHTTP endpoint: {url}")

tests/shared/test_streamable_http.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,3 +1613,112 @@ async def bad_client():
16131613
assert isinstance(result, InitializeResult)
16141614
tools = await session.list_tools()
16151615
assert tools.tools
1616+
1617+
1618+
@pytest.mark.anyio
1619+
async def test_streamable_http_client_does_not_mutate_provided_client(
1620+
basic_server: None, basic_server_url: str
1621+
) -> None:
1622+
"""Test that streamable_http_client does not mutate the provided httpx client's headers."""
1623+
# Create a client with custom headers
1624+
original_headers = {
1625+
"X-Custom-Header": "custom-value",
1626+
"Authorization": "Bearer test-token",
1627+
}
1628+
1629+
async with httpx.AsyncClient(headers=original_headers, follow_redirects=True) as custom_client:
1630+
# Use the client with streamable_http_client
1631+
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=custom_client) as (
1632+
read_stream,
1633+
write_stream,
1634+
_,
1635+
):
1636+
async with ClientSession(read_stream, write_stream) as session:
1637+
result = await session.initialize()
1638+
assert isinstance(result, InitializeResult)
1639+
1640+
# Verify client headers were not mutated with MCP protocol headers
1641+
# If accept header exists, it should still be httpx default, not MCP's
1642+
if "accept" in custom_client.headers:
1643+
assert custom_client.headers.get("accept") == "*/*"
1644+
# MCP content-type should not have been added
1645+
assert custom_client.headers.get("content-type") != "application/json"
1646+
1647+
# Verify custom headers are still present and unchanged
1648+
assert custom_client.headers.get("X-Custom-Header") == "custom-value"
1649+
assert custom_client.headers.get("Authorization") == "Bearer test-token"
1650+
1651+
1652+
@pytest.mark.anyio
1653+
async def test_streamable_http_client_mcp_headers_override_defaults(
1654+
context_aware_server: None, basic_server_url: str
1655+
) -> None:
1656+
"""Test that MCP protocol headers override httpx.AsyncClient default headers."""
1657+
# httpx.AsyncClient has default "accept: */*" header
1658+
# We need to verify that our MCP accept header overrides it in actual requests
1659+
1660+
async with httpx.AsyncClient(follow_redirects=True) as client:
1661+
# Verify client has default accept header
1662+
assert client.headers.get("accept") == "*/*"
1663+
1664+
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (
1665+
read_stream,
1666+
write_stream,
1667+
_,
1668+
):
1669+
async with ClientSession(read_stream, write_stream) as session:
1670+
await session.initialize()
1671+
1672+
# Use echo_headers tool to see what headers the server actually received
1673+
tool_result = await session.call_tool("echo_headers", {})
1674+
assert len(tool_result.content) == 1
1675+
assert isinstance(tool_result.content[0], TextContent)
1676+
headers_data = json.loads(tool_result.content[0].text)
1677+
1678+
# Verify MCP protocol headers were sent (not httpx defaults)
1679+
assert "accept" in headers_data
1680+
assert "application/json" in headers_data["accept"]
1681+
assert "text/event-stream" in headers_data["accept"]
1682+
1683+
assert "content-type" in headers_data
1684+
assert headers_data["content-type"] == "application/json"
1685+
1686+
1687+
@pytest.mark.anyio
1688+
async def test_streamable_http_client_preserves_custom_with_mcp_headers(
1689+
context_aware_server: None, basic_server_url: str
1690+
) -> None:
1691+
"""Test that both custom headers and MCP protocol headers are sent in requests."""
1692+
custom_headers = {
1693+
"X-Custom-Header": "custom-value",
1694+
"X-Request-Id": "req-123",
1695+
"Authorization": "Bearer test-token",
1696+
}
1697+
1698+
async with httpx.AsyncClient(headers=custom_headers, follow_redirects=True) as client:
1699+
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (
1700+
read_stream,
1701+
write_stream,
1702+
_,
1703+
):
1704+
async with ClientSession(read_stream, write_stream) as session:
1705+
await session.initialize()
1706+
1707+
# Use echo_headers tool to verify both custom and MCP headers are present
1708+
tool_result = await session.call_tool("echo_headers", {})
1709+
assert len(tool_result.content) == 1
1710+
assert isinstance(tool_result.content[0], TextContent)
1711+
headers_data = json.loads(tool_result.content[0].text)
1712+
1713+
# Verify custom headers are present
1714+
assert headers_data.get("x-custom-header") == "custom-value"
1715+
assert headers_data.get("x-request-id") == "req-123"
1716+
assert headers_data.get("authorization") == "Bearer test-token"
1717+
1718+
# Verify MCP protocol headers are also present
1719+
assert "accept" in headers_data
1720+
assert "application/json" in headers_data["accept"]
1721+
assert "text/event-stream" in headers_data["accept"]
1722+
1723+
assert "content-type" in headers_data
1724+
assert headers_data["content-type"] == "application/json"

0 commit comments

Comments
 (0)