Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions examples/mcp/streamablehttp_custom_client_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Custom HTTP Client Factory Example

This example demonstrates how to use the new `httpx_client_factory` parameter in `MCPServerStreamableHttp` to configure custom HTTP client behavior for MCP StreamableHTTP connections.

## Features Demonstrated

- **Custom SSL Configuration**: Configure SSL certificates and verification settings
- **Custom Headers**: Add custom headers to all HTTP requests
- **Custom Timeouts**: Set custom timeout values for requests
- **Proxy Configuration**: Configure HTTP proxy settings
- **Custom Retry Logic**: Set up custom retry behavior (through httpx configuration)

## Running the Example

1. Make sure you have `uv` installed: https://docs.astral.sh/uv/getting-started/installation/

2. Run the example:
```bash
cd examples/mcp/streamablehttp_custom_client_example
uv run main.py
```

## Code Examples

### Basic Custom Client

```python
import httpx
from agents.mcp import MCPServerStreamableHttp

def create_custom_http_client() -> httpx.AsyncClient:
return httpx.AsyncClient(
verify=False, # Disable SSL verification for testing
timeout=httpx.Timeout(60.0, read=120.0),
headers={"X-Custom-Client": "my-app"},
)

async with MCPServerStreamableHttp(
name="Custom Client Server",
params={
"url": "http://localhost:8000/mcp",
"httpx_client_factory": create_custom_http_client,
},
) as server:
# Use the server...
```

## Use Cases

- **Corporate Networks**: Configure proxy settings for corporate environments
- **SSL/TLS Requirements**: Use custom SSL certificates for secure connections
- **Custom Authentication**: Add custom headers for API authentication
- **Network Optimization**: Configure timeouts and connection pooling
- **Debugging**: Disable SSL verification for development environments

## Benefits

- **Flexibility**: Configure HTTP client behavior to match your network requirements
- **Security**: Use custom SSL certificates and authentication methods
- **Performance**: Optimize timeouts and connection settings for your use case
- **Compatibility**: Work with corporate proxies and network restrictions

116 changes: 116 additions & 0 deletions examples/mcp/streamablehttp_custom_client_example/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Example demonstrating custom httpx_client_factory for MCPServerStreamableHttp.

This example shows how to configure custom HTTP client behavior for MCP StreamableHTTP
connections, including SSL certificates, proxy settings, and custom timeouts.
"""

import asyncio
import os
import shutil
import subprocess
import time
from typing import Any

import httpx

from agents import Agent, Runner, gen_trace_id, trace
from agents.mcp import MCPServer, MCPServerStreamableHttp
from agents.model_settings import ModelSettings


def create_custom_http_client(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient:
"""Create a custom HTTP client with specific configurations.

This function demonstrates how to configure:
- Custom SSL verification settings
- Custom timeouts
- Custom headers
- Proxy settings (commented out)
"""
if headers is None:
headers = {
"X-Custom-Client": "agents-mcp-example",
"User-Agent": "OpenAI-Agents-MCP/1.0",
}
if timeout is None:
timeout = httpx.Timeout(60.0, read=120.0)
if auth is None:
auth = None
return httpx.AsyncClient(
# Disable SSL verification for testing (not recommended for production)
verify=False,
# Set custom timeout
timeout=httpx.Timeout(60.0, read=120.0),
# Add custom headers that will be sent with every request
headers=headers,
)


async def run_with_custom_client(mcp_server: MCPServer):
"""Run the agent with a custom HTTP client configuration."""
agent = Agent(
name="Assistant",
instructions="Use the tools to answer the questions.",
mcp_servers=[mcp_server],
model_settings=ModelSettings(tool_choice="required"),
)

# Use the `add` tool to add two numbers
message = "Add these numbers: 7 and 22."
print(f"Running: {message}")
result = await Runner.run(starting_agent=agent, input=message)
print(result.final_output)


async def main():
"""Main function demonstrating different HTTP client configurations."""

print("=== Example: Custom HTTP Client with SSL disabled and custom headers ===")
async with MCPServerStreamableHttp(
name="Streamable HTTP with Custom Client",
params={
"url": "http://localhost:8000/mcp",
"httpx_client_factory": create_custom_http_client,
},
) as server:
trace_id = gen_trace_id()
with trace(workflow_name="Custom HTTP Client Example", trace_id=trace_id):
print(f"View trace: https://platform.openai.com/logs/trace?trace_id={trace_id}\n")
await run_with_custom_client(server)


if __name__ == "__main__":
# Let's make sure the user has uv installed
if not shutil.which("uv"):
raise RuntimeError(
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
)

# We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this
# demo, we'll run it locally at http://localhost:8000/mcp
process: subprocess.Popen[Any] | None = None
try:
this_dir = os.path.dirname(os.path.abspath(__file__))
server_file = os.path.join(this_dir, "server.py")

print("Starting Streamable HTTP server at http://localhost:8000/mcp ...")

# Run `uv run server.py` to start the Streamable HTTP server
process = subprocess.Popen(["uv", "run", server_file])
# Give it 3 seconds to start
time.sleep(3)

print("Streamable HTTP server started. Running example...\n\n")
except Exception as e:
print(f"Error starting Streamable HTTP server: {e}")
exit(1)

try:
asyncio.run(main())
finally:
if process:
process.terminate()
23 changes: 23 additions & 0 deletions examples/mcp/streamablehttp_custom_client_example/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import random

from mcp.server.fastmcp import FastMCP

# Create server
mcp = FastMCP("Echo Server")


@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
print(f"[debug-server] add({a}, {b})")
return a + b


@mcp.tool()
def get_secret_word() -> str:
print("[debug-server] get_secret_word()")
return random.choice(["apple", "banana", "cherry"])


if __name__ == "__main__":
mcp.run(transport="streamable-http")
35 changes: 25 additions & 10 deletions src/agents/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client
from mcp.client.sse import sse_client
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
from mcp.shared._httpx_utils import McpHttpClientFactory
from mcp.shared.message import SessionMessage
from mcp.types import CallToolResult, GetPromptResult, InitializeResult, ListPromptsResult
from typing_extensions import NotRequired, TypedDict
Expand Down Expand Up @@ -575,6 +576,9 @@ class MCPServerStreamableHttpParams(TypedDict):
terminate_on_close: NotRequired[bool]
"""Terminate on close"""

httpx_client_factory: NotRequired[McpHttpClientFactory]
"""Custom HTTP client factory for configuring httpx.AsyncClient behavior."""


class MCPServerStreamableHttp(_MCPServerWithClientSession):
"""MCP server implementation that uses the Streamable HTTP transport. See the [spec]
Expand All @@ -597,9 +601,9 @@ def __init__(

Args:
params: The params that configure the server. This includes the URL of the server,
the headers to send to the server, the timeout for the HTTP request, and the
timeout for the Streamable HTTP connection and whether we need to
terminate on close.
the headers to send to the server, the timeout for the HTTP request, the
timeout for the Streamable HTTP connection, whether we need to
terminate on close, and an optional custom HTTP client factory.

cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
cached and only fetched from the server once. If `False`, the tools list will be
Expand Down Expand Up @@ -645,13 +649,24 @@ def create_streams(
]
]:
"""Create the streams for the server."""
return streamablehttp_client(
url=self.params["url"],
headers=self.params.get("headers", None),
timeout=self.params.get("timeout", 5),
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
terminate_on_close=self.params.get("terminate_on_close", True),
)
# Only pass httpx_client_factory if it's provided
if "httpx_client_factory" in self.params:
return streamablehttp_client(
url=self.params["url"],
headers=self.params.get("headers", None),
timeout=self.params.get("timeout", 5),
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
terminate_on_close=self.params.get("terminate_on_close", True),
httpx_client_factory=self.params["httpx_client_factory"],
)
else:
return streamablehttp_client(
url=self.params["url"],
headers=self.params.get("headers", None),
timeout=self.params.get("timeout", 5),
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
terminate_on_close=self.params.get("terminate_on_close", True),
)

@property
def name(self) -> str:
Expand Down
Loading