Skip to content

Commit a7f33ca

Browse files
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 30905b6 commit a7f33ca

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
@@ -504,9 +504,6 @@ async def streamable_http_client(
504504
# Create transport with extracted configuration
505505
transport = StreamableHTTPTransport(url, headers_dict, timeout, sse_read_timeout, auth)
506506

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

0 commit comments

Comments
 (0)