-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
When using MCPStreamableHTTPTool with sequential agent runs in long-running Python process (FastAPI), httpx client never closes, causing session/transport reuse and 409 errors from Microsoft Foundry when multiple sequential agent runs in the same process reuse the same httpx connection pool.
Environment
- agent-framework-core:
1.0.0b251218 - mcp:
1.25.0 - httpx:
0.28.1 - Python:
3.13
Reproduction
import pytest
from agent_framework import MCPStreamableHTTPTool
@pytest.mark.asyncio
async def test_httpx_client_reuse():
# Create and use first tool
tool1 = MCPStreamableHTTPTool(
name="test",
url="http://localhost:8081/mcp",
load_tools=False,
load_prompts=False,
approval_mode="never_require",
terminate_on_close=False,
timeout=30,
)
await tool1.connect()
client1_id = id(getattr(tool1, "_httpx_client", None))
await tool1.close()
del tool1
# Create and use second tool
tool2 = MCPStreamableHTTPTool(
name="test",
url="http://localhost:8081/mcp",
load_tools=False,
load_prompts=False,
approval_mode="never_require",
terminate_on_close=False,
timeout=30,
)
await tool2.connect()
client2_id = id(getattr(tool2, "_httpx_client", None))
# BUG: Same httpx client object reused (should be different)
print(f"Client 1 ID: {client1_id}")
print(f"Client 2 ID: {client2_id}")
assert client1_id == client2_id, "BUG: httpx client reused!"
await tool2.close()Root Cause
agent_framework/_mcp.py#L981-L1001 uses the deprecated streamablehttp_client() which creates an httpx client that is never closed:
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
args: dict[str, Any] = {"url": self.url, ...}
return streamablehttp_client(**args)When MCPTool.close() is called, it only closes the MCP session, not the httpx client.
Proposed Solution
The fix involves 3 changes to agent_framework/_mcp.py:
1. Add httpx import and new API
import httpx
from mcp.client.streamable_http import streamable_http_client, streamablehttp_client2. Track httpx client in MCPStreamableHTTPTool
In __init__() (line ~982):
self._httpx_client: httpx.AsyncClient | None = None3. Create and manage httpx client
Replace get_mcp_client() (lines 985-1005):
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
"""Get an MCP streamable HTTP client."""
timeout_value = self.timeout if self.timeout is not None else 30.0
sse_timeout_value = self.sse_read_timeout if self.sse_read_timeout is not None else 300.0
# Create and track httpx client
self._httpx_client = httpx.AsyncClient(
headers=self.headers,
timeout=httpx.Timeout(timeout_value, read=sse_timeout_value),
**self._client_kwargs
)
# Use new API instead of deprecated streamablehttp_client()
return streamable_http_client(
url=self.url,
http_client=self._httpx_client,
terminate_on_close=self.terminate_on_close if self.terminate_on_close is not None else True,
)4. Override close() to cleanup httpx client
Add after get_mcp_client():
async def close(self) -> None:
"""Disconnect from the MCP server and close httpx client."""
await super().close()
if self._httpx_client is not None:
await self._httpx_client.aclose()
self._httpx_client = NoneThis ensures httpx clients are properly closed and connection pools don't leak between tool instances.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status