Skip to content

Commit c92dd49

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 732fa05 commit c92dd49

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
@@ -509,9 +509,6 @@ async def streamable_http_client(
509509
# Create transport with extracted configuration
510510
transport = StreamableHTTPTransport(url, headers_dict, timeout, sse_read_timeout, auth)
511511

512-
# Sync client headers with transport's merged headers (includes MCP protocol requirements)
513-
client.headers.update(transport.request_headers)
514-
515512
async with anyio.create_task_group() as tg:
516513
try:
517514
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
@@ -1644,3 +1644,112 @@ async def test_handle_sse_event_skips_empty_data():
16441644
finally:
16451645
await write_stream.aclose()
16461646
await read_stream.aclose()
1647+
1648+
1649+
@pytest.mark.anyio
1650+
async def test_streamable_http_client_does_not_mutate_provided_client(
1651+
basic_server: None, basic_server_url: str
1652+
) -> None:
1653+
"""Test that streamable_http_client does not mutate the provided httpx client's headers."""
1654+
# Create a client with custom headers
1655+
original_headers = {
1656+
"X-Custom-Header": "custom-value",
1657+
"Authorization": "Bearer test-token",
1658+
}
1659+
1660+
async with httpx.AsyncClient(headers=original_headers, follow_redirects=True) as custom_client:
1661+
# Use the client with streamable_http_client
1662+
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=custom_client) as (
1663+
read_stream,
1664+
write_stream,
1665+
_,
1666+
):
1667+
async with ClientSession(read_stream, write_stream) as session:
1668+
result = await session.initialize()
1669+
assert isinstance(result, InitializeResult)
1670+
1671+
# Verify client headers were not mutated with MCP protocol headers
1672+
# If accept header exists, it should still be httpx default, not MCP's
1673+
if "accept" in custom_client.headers: # pragma: no branch
1674+
assert custom_client.headers.get("accept") == "*/*"
1675+
# MCP content-type should not have been added
1676+
assert custom_client.headers.get("content-type") != "application/json"
1677+
1678+
# Verify custom headers are still present and unchanged
1679+
assert custom_client.headers.get("X-Custom-Header") == "custom-value"
1680+
assert custom_client.headers.get("Authorization") == "Bearer test-token"
1681+
1682+
1683+
@pytest.mark.anyio
1684+
async def test_streamable_http_client_mcp_headers_override_defaults(
1685+
context_aware_server: None, basic_server_url: str
1686+
) -> None:
1687+
"""Test that MCP protocol headers override httpx.AsyncClient default headers."""
1688+
# httpx.AsyncClient has default "accept: */*" header
1689+
# We need to verify that our MCP accept header overrides it in actual requests
1690+
1691+
async with httpx.AsyncClient(follow_redirects=True) as client:
1692+
# Verify client has default accept header
1693+
assert client.headers.get("accept") == "*/*"
1694+
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 see what headers the server actually received
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 MCP protocol headers were sent (not httpx defaults)
1710+
assert "accept" in headers_data
1711+
assert "application/json" in headers_data["accept"]
1712+
assert "text/event-stream" in headers_data["accept"]
1713+
1714+
assert "content-type" in headers_data
1715+
assert headers_data["content-type"] == "application/json"
1716+
1717+
1718+
@pytest.mark.anyio
1719+
async def test_streamable_http_client_preserves_custom_with_mcp_headers(
1720+
context_aware_server: None, basic_server_url: str
1721+
) -> None:
1722+
"""Test that both custom headers and MCP protocol headers are sent in requests."""
1723+
custom_headers = {
1724+
"X-Custom-Header": "custom-value",
1725+
"X-Request-Id": "req-123",
1726+
"Authorization": "Bearer test-token",
1727+
}
1728+
1729+
async with httpx.AsyncClient(headers=custom_headers, follow_redirects=True) as client:
1730+
async with streamable_http_client(f"{basic_server_url}/mcp", http_client=client) as (
1731+
read_stream,
1732+
write_stream,
1733+
_,
1734+
):
1735+
async with ClientSession(read_stream, write_stream) as session:
1736+
await session.initialize()
1737+
1738+
# Use echo_headers tool to verify both custom and MCP headers are present
1739+
tool_result = await session.call_tool("echo_headers", {})
1740+
assert len(tool_result.content) == 1
1741+
assert isinstance(tool_result.content[0], TextContent)
1742+
headers_data = json.loads(tool_result.content[0].text)
1743+
1744+
# Verify custom headers are present
1745+
assert headers_data.get("x-custom-header") == "custom-value"
1746+
assert headers_data.get("x-request-id") == "req-123"
1747+
assert headers_data.get("authorization") == "Bearer test-token"
1748+
1749+
# Verify MCP protocol headers are also present
1750+
assert "accept" in headers_data
1751+
assert "application/json" in headers_data["accept"]
1752+
assert "text/event-stream" in headers_data["accept"]
1753+
1754+
assert "content-type" in headers_data
1755+
assert headers_data["content-type"] == "application/json"

0 commit comments

Comments
 (0)