Skip to content

Commit 6816cc7

Browse files
authored
feat: #1829 add httpx_client_factory to MCPServerStreamableHttp initialization options (#1833)
1 parent 02ebf0e commit 6816cc7

File tree

6 files changed

+490
-12
lines changed

6 files changed

+490
-12
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Custom HTTP Client Factory Example
2+
3+
This example demonstrates how to use the new `httpx_client_factory` parameter in `MCPServerStreamableHttp` to configure custom HTTP client behavior for MCP StreamableHTTP connections.
4+
5+
## Features Demonstrated
6+
7+
- **Custom SSL Configuration**: Configure SSL certificates and verification settings
8+
- **Custom Headers**: Add custom headers to all HTTP requests
9+
- **Custom Timeouts**: Set custom timeout values for requests
10+
- **Proxy Configuration**: Configure HTTP proxy settings
11+
- **Custom Retry Logic**: Set up custom retry behavior (through httpx configuration)
12+
13+
## Running the Example
14+
15+
1. Make sure you have `uv` installed: https://docs.astral.sh/uv/getting-started/installation/
16+
17+
2. Run the example:
18+
```bash
19+
cd examples/mcp/streamablehttp_custom_client_example
20+
uv run main.py
21+
```
22+
23+
## Code Examples
24+
25+
### Basic Custom Client
26+
27+
```python
28+
import httpx
29+
from agents.mcp import MCPServerStreamableHttp
30+
31+
def create_custom_http_client() -> httpx.AsyncClient:
32+
return httpx.AsyncClient(
33+
verify=False, # Disable SSL verification for testing
34+
timeout=httpx.Timeout(60.0, read=120.0),
35+
headers={"X-Custom-Client": "my-app"},
36+
)
37+
38+
async with MCPServerStreamableHttp(
39+
name="Custom Client Server",
40+
params={
41+
"url": "http://localhost:8000/mcp",
42+
"httpx_client_factory": create_custom_http_client,
43+
},
44+
) as server:
45+
# Use the server...
46+
```
47+
48+
## Use Cases
49+
50+
- **Corporate Networks**: Configure proxy settings for corporate environments
51+
- **SSL/TLS Requirements**: Use custom SSL certificates for secure connections
52+
- **Custom Authentication**: Add custom headers for API authentication
53+
- **Network Optimization**: Configure timeouts and connection pooling
54+
- **Debugging**: Disable SSL verification for development environments
55+
56+
## Benefits
57+
58+
- **Flexibility**: Configure HTTP client behavior to match your network requirements
59+
- **Security**: Use custom SSL certificates and authentication methods
60+
- **Performance**: Optimize timeouts and connection settings for your use case
61+
- **Compatibility**: Work with corporate proxies and network restrictions
62+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Example demonstrating custom httpx_client_factory for MCPServerStreamableHttp.
2+
3+
This example shows how to configure custom HTTP client behavior for MCP StreamableHTTP
4+
connections, including SSL certificates, proxy settings, and custom timeouts.
5+
"""
6+
7+
import asyncio
8+
import os
9+
import shutil
10+
import subprocess
11+
import time
12+
from typing import Any
13+
14+
import httpx
15+
16+
from agents import Agent, Runner, gen_trace_id, trace
17+
from agents.mcp import MCPServer, MCPServerStreamableHttp
18+
from agents.model_settings import ModelSettings
19+
20+
21+
def create_custom_http_client(
22+
headers: dict[str, str] | None = None,
23+
timeout: httpx.Timeout | None = None,
24+
auth: httpx.Auth | None = None,
25+
) -> httpx.AsyncClient:
26+
"""Create a custom HTTP client with specific configurations.
27+
28+
This function demonstrates how to configure:
29+
- Custom SSL verification settings
30+
- Custom timeouts
31+
- Custom headers
32+
- Proxy settings (commented out)
33+
"""
34+
if headers is None:
35+
headers = {
36+
"X-Custom-Client": "agents-mcp-example",
37+
"User-Agent": "OpenAI-Agents-MCP/1.0",
38+
}
39+
if timeout is None:
40+
timeout = httpx.Timeout(60.0, read=120.0)
41+
if auth is None:
42+
auth = None
43+
return httpx.AsyncClient(
44+
# Disable SSL verification for testing (not recommended for production)
45+
verify=False,
46+
# Set custom timeout
47+
timeout=httpx.Timeout(60.0, read=120.0),
48+
# Add custom headers that will be sent with every request
49+
headers=headers,
50+
)
51+
52+
53+
async def run_with_custom_client(mcp_server: MCPServer):
54+
"""Run the agent with a custom HTTP client configuration."""
55+
agent = Agent(
56+
name="Assistant",
57+
instructions="Use the tools to answer the questions.",
58+
mcp_servers=[mcp_server],
59+
model_settings=ModelSettings(tool_choice="required"),
60+
)
61+
62+
# Use the `add` tool to add two numbers
63+
message = "Add these numbers: 7 and 22."
64+
print(f"Running: {message}")
65+
result = await Runner.run(starting_agent=agent, input=message)
66+
print(result.final_output)
67+
68+
69+
async def main():
70+
"""Main function demonstrating different HTTP client configurations."""
71+
72+
print("=== Example: Custom HTTP Client with SSL disabled and custom headers ===")
73+
async with MCPServerStreamableHttp(
74+
name="Streamable HTTP with Custom Client",
75+
params={
76+
"url": "http://localhost:8000/mcp",
77+
"httpx_client_factory": create_custom_http_client,
78+
},
79+
) as server:
80+
trace_id = gen_trace_id()
81+
with trace(workflow_name="Custom HTTP Client Example", trace_id=trace_id):
82+
print(f"View trace: https://platform.openai.com/logs/trace?trace_id={trace_id}\n")
83+
await run_with_custom_client(server)
84+
85+
86+
if __name__ == "__main__":
87+
# Let's make sure the user has uv installed
88+
if not shutil.which("uv"):
89+
raise RuntimeError(
90+
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
91+
)
92+
93+
# We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this
94+
# demo, we'll run it locally at http://localhost:8000/mcp
95+
process: subprocess.Popen[Any] | None = None
96+
try:
97+
this_dir = os.path.dirname(os.path.abspath(__file__))
98+
server_file = os.path.join(this_dir, "server.py")
99+
100+
print("Starting Streamable HTTP server at http://localhost:8000/mcp ...")
101+
102+
# Run `uv run server.py` to start the Streamable HTTP server
103+
process = subprocess.Popen(["uv", "run", server_file])
104+
# Give it 3 seconds to start
105+
time.sleep(3)
106+
107+
print("Streamable HTTP server started. Running example...\n\n")
108+
except Exception as e:
109+
print(f"Error starting Streamable HTTP server: {e}")
110+
exit(1)
111+
112+
try:
113+
asyncio.run(main())
114+
finally:
115+
if process:
116+
process.terminate()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import random
2+
3+
from mcp.server.fastmcp import FastMCP
4+
5+
# Create server
6+
mcp = FastMCP("Echo Server")
7+
8+
9+
@mcp.tool()
10+
def add(a: int, b: int) -> int:
11+
"""Add two numbers"""
12+
print(f"[debug-server] add({a}, {b})")
13+
return a + b
14+
15+
16+
@mcp.tool()
17+
def get_secret_word() -> str:
18+
print("[debug-server] get_secret_word()")
19+
return random.choice(["apple", "banana", "cherry"])
20+
21+
22+
if __name__ == "__main__":
23+
mcp.run(transport="streamable-http")

src/agents/mcp/server.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from ..exceptions import UserError
2121
from ..logger import logger
2222
from ..run_context import RunContextWrapper
23-
from .util import ToolFilter, ToolFilterContext, ToolFilterStatic
23+
from .util import HttpClientFactory, ToolFilter, ToolFilterContext, ToolFilterStatic
2424

2525
T = TypeVar("T")
2626

@@ -575,6 +575,9 @@ class MCPServerStreamableHttpParams(TypedDict):
575575
terminate_on_close: NotRequired[bool]
576576
"""Terminate on close"""
577577

578+
httpx_client_factory: NotRequired[HttpClientFactory]
579+
"""Custom HTTP client factory for configuring httpx.AsyncClient behavior."""
580+
578581

579582
class MCPServerStreamableHttp(_MCPServerWithClientSession):
580583
"""MCP server implementation that uses the Streamable HTTP transport. See the [spec]
@@ -597,9 +600,9 @@ def __init__(
597600
598601
Args:
599602
params: The params that configure the server. This includes the URL of the server,
600-
the headers to send to the server, the timeout for the HTTP request, and the
601-
timeout for the Streamable HTTP connection and whether we need to
602-
terminate on close.
603+
the headers to send to the server, the timeout for the HTTP request, the
604+
timeout for the Streamable HTTP connection, whether we need to
605+
terminate on close, and an optional custom HTTP client factory.
603606
604607
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
605608
cached and only fetched from the server once. If `False`, the tools list will be
@@ -645,13 +648,24 @@ def create_streams(
645648
]
646649
]:
647650
"""Create the streams for the server."""
648-
return streamablehttp_client(
649-
url=self.params["url"],
650-
headers=self.params.get("headers", None),
651-
timeout=self.params.get("timeout", 5),
652-
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
653-
terminate_on_close=self.params.get("terminate_on_close", True),
654-
)
651+
# Only pass httpx_client_factory if it's provided
652+
if "httpx_client_factory" in self.params:
653+
return streamablehttp_client(
654+
url=self.params["url"],
655+
headers=self.params.get("headers", None),
656+
timeout=self.params.get("timeout", 5),
657+
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
658+
terminate_on_close=self.params.get("terminate_on_close", True),
659+
httpx_client_factory=self.params["httpx_client_factory"],
660+
)
661+
else:
662+
return streamablehttp_client(
663+
url=self.params["url"],
664+
headers=self.params.get("headers", None),
665+
timeout=self.params.get("timeout", 5),
666+
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
667+
terminate_on_close=self.params.get("terminate_on_close", True),
668+
)
655669

656670
@property
657671
def name(self) -> str:

src/agents/mcp/util.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import functools
22
import json
33
from dataclasses import dataclass
4-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
4+
from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol, Union
55

6+
import httpx
67
from typing_extensions import NotRequired, TypedDict
78

89
from .. import _debug
@@ -21,6 +22,21 @@
2122
from .server import MCPServer
2223

2324

25+
class HttpClientFactory(Protocol):
26+
"""Protocol for HTTP client factory functions.
27+
28+
This interface matches the MCP SDK's McpHttpClientFactory but is defined locally
29+
to avoid accessing internal MCP SDK modules.
30+
"""
31+
32+
def __call__(
33+
self,
34+
headers: Optional[dict[str, str]] = None,
35+
timeout: Optional[httpx.Timeout] = None,
36+
auth: Optional[httpx.Auth] = None,
37+
) -> httpx.AsyncClient: ...
38+
39+
2440
@dataclass
2541
class ToolFilterContext:
2642
"""Context information available to tool filter functions."""

0 commit comments

Comments
 (0)