Skip to content

Commit 0243ce7

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 953d0de commit 0243ce7

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

0 commit comments

Comments
 (0)