diff --git a/examples/mcp/streamablehttp_custom_client_example/README.md b/examples/mcp/streamablehttp_custom_client_example/README.md index fc269a0644..bf5882b876 100644 --- a/examples/mcp/streamablehttp_custom_client_example/README.md +++ b/examples/mcp/streamablehttp_custom_client_example/README.md @@ -1,14 +1,17 @@ -# Custom HTTP Client Factory Example +# Custom HTTP Configuration 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. +This example demonstrates how to configure custom HTTP client behaviour for +`MCPServerStreamableHttp` connections when using MCP Python SDK v2. + +> **Note (mcp SDK v2):** The `httpx_client_factory` parameter has been removed. +> The `MCPServerStreamableHttp` transport now uses `httpx2` internally. +> To customise HTTP behaviour, use the built-in params shown below. ## 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) +- **Custom Authentication**: Pass an `httpx2.Auth` instance via the `auth` param ## Running the Example @@ -22,42 +25,48 @@ This example demonstrates how to use the new `httpx_client_factory` parameter in ## Code Examples -### Basic Custom Client +### Custom Headers and Timeout (recommended) ```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", + name="Custom Config Server", params={ "url": "http://localhost:/mcp", - "httpx_client_factory": create_custom_http_client, + "headers": { + "X-Custom-Client": "my-app", + "User-Agent": "MyApp/1.0", + }, + "timeout": 60.0, # connect timeout in seconds + "sse_read_timeout": 120.0, # SSE read timeout in seconds }, ) as server: # Use the server... ``` -## Use Cases +### Basic Authentication -- **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 +```python +import httpx2 +from agents.mcp import MCPServerStreamableHttp + +async with MCPServerStreamableHttp( + name="Auth Server", + params={ + "url": "http://localhost:/mcp", + "auth": httpx2.BasicAuth(username="user", password="secret"), + }, +) as server: + # Use the server... +``` -## Benefits +## Use Cases -- **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 +- **Corporate Networks**: Add proxy-bypass headers or authentication +- **Custom Authentication**: Use `httpx2.Auth` subclasses for OAuth token refresh +- **Network Optimization**: Set timeouts appropriate for your environment +- **Debugging**: Inspect headers via the `headers` param -This example will auto-pick a free localhost port unless you set `STREAMABLE_HTTP_PORT`; use `STREAMABLE_HTTP_HOST` to change the bind address. +This example will auto-pick a free localhost port unless you set `STREAMABLE_HTTP_PORT`; +use `STREAMABLE_HTTP_HOST` to change the bind address. diff --git a/examples/mcp/streamablehttp_custom_client_example/main.py b/examples/mcp/streamablehttp_custom_client_example/main.py index 20cbef1cdc..70b8efeb6f 100644 --- a/examples/mcp/streamablehttp_custom_client_example/main.py +++ b/examples/mcp/streamablehttp_custom_client_example/main.py @@ -1,7 +1,9 @@ -"""Example demonstrating custom httpx_client_factory for MCPServerStreamableHttp. +"""Example demonstrating custom HTTP configuration for MCPServerStreamableHttp. -This example shows how to configure custom HTTP client behavior for MCP StreamableHTTP -connections, including SSL certificates, proxy settings, and custom timeouts. +With MCP Python SDK v2, the underlying transport uses httpx2 and the +``httpx_client_factory`` parameter is no longer supported. To customise HTTP +behaviour pass the ``headers``, ``timeout``, ``sse_read_timeout``, or ``auth`` +(``httpx2.Auth``) keys in ``MCPServerStreamableHttpParams``. """ import asyncio @@ -12,7 +14,7 @@ import time from typing import Any, cast -import httpx +import httpx2 # noqa: F401 — available for the auth example in main() from agents import Agent, Runner, gen_trace_id, trace from agents.mcp import MCPServer, MCPServerStreamableHttp @@ -36,48 +38,13 @@ def _choose_port() -> int: STREAMABLE_HTTP_URL = f"http://{STREAMABLE_HTTP_HOST}:{STREAMABLE_HTTP_PORT}/mcp" -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.""" +async def run_with_server(mcp_server: MCPServer): 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) @@ -85,31 +52,39 @@ async def run_with_custom_client(mcp_server: MCPServer): async def main(): - """Main function demonstrating different HTTP client configurations.""" + """Demonstrate custom HTTP configuration for StreamableHTTP (mcp SDK v2).""" + + print("=== Example: StreamableHTTP with custom headers and timeout ===") - print("=== Example: Custom HTTP Client with SSL disabled and custom headers ===") + # Use ``headers``, ``timeout``, ``sse_read_timeout``, and ``auth`` + # (``httpx2.Auth`` instance) to customise the underlying httpx2 client. + # The ``httpx_client_factory`` parameter was removed in mcp SDK v2. async with MCPServerStreamableHttp( - name="Streamable HTTP with Custom Client", + name="Streamable HTTP – custom config", params={ "url": STREAMABLE_HTTP_URL, - "httpx_client_factory": create_custom_http_client, + "headers": { + "X-Custom-Client": "agents-mcp-example", + "User-Agent": "OpenAI-Agents-MCP/2.0", + }, + "timeout": 60.0, + "sse_read_timeout": 120.0, + # To add authentication, pass an httpx2.Auth instance: + # "auth": httpx2.BasicAuth(username="user", password="secret"), }, ) as server: trace_id = gen_trace_id() - with trace(workflow_name="Custom HTTP Client Example", trace_id=trace_id): + with trace(workflow_name="Custom HTTP Config 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) + await run_with_server(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 STREAMABLE_HTTP_URL process: subprocess.Popen[Any] | None = None try: this_dir = os.path.dirname(os.path.abspath(__file__)) @@ -117,12 +92,10 @@ async def main(): print(f"Starting Streamable HTTP server at {STREAMABLE_HTTP_URL} ...") - # Run `uv run server.py` to start the Streamable HTTP server env = os.environ.copy() env.setdefault("STREAMABLE_HTTP_HOST", STREAMABLE_HTTP_HOST) env.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT)) process = subprocess.Popen(["uv", "run", server_file], env=env) - # Give it 3 seconds to start time.sleep(3) print("Streamable HTTP server started. Running example...\n\n") diff --git a/pyproject.toml b/pyproject.toml index b41334ea89..7f5378b5ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "websockets>=15.0, <17", - "mcp>=1.19.0, <2; python_version >= '3.10'", + "mcp>=2.0.0b2,<3; python_version >= '3.10'", ] classifiers = [ "Typing :: Typed", diff --git a/src/agents/extensions/experimental/codex/items.py b/src/agents/extensions/experimental/codex/items.py index 5c4029c6ba..bf51767ee0 100644 --- a/src/agents/extensions/experimental/codex/items.py +++ b/src/agents/extensions/experimental/codex/items.py @@ -9,7 +9,7 @@ # Item payloads are emitted inside item.* events from the Codex CLI JSONL stream. if TYPE_CHECKING: - from mcp.types import ContentBlock as McpContentBlock + from mcp_types import ContentBlock as McpContentBlock else: McpContentBlock = Any # type: ignore[assignment] diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index 4f8f89c293..bd1fd6b196 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -10,8 +10,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, cast -import anyio import httpx +import httpx2 if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup # pyright: ignore[reportMissingImports] @@ -20,14 +20,10 @@ from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client from mcp.client.session import MessageHandlerFnT from mcp.client.sse import sse_client -from mcp.client.streamable_http import ( - GetSessionIdCallback, - StreamableHTTPTransport, - streamablehttp_client, -) -from mcp.shared.exceptions import McpError +from mcp.client.streamable_http import streamable_http_client +from mcp.shared.exceptions import MCPError as McpError from mcp.shared.message import SessionMessage -from mcp.types import ( +from mcp_types import ( CallToolResult, GetPromptResult, InitializeResult, @@ -59,6 +55,10 @@ ToolFilterStatic, ) +# mcp SDK v2 removed GetSessionIdCallback (stateless protocol, SEP-2577). +# Define a local alias so type annotations in this module continue to resolve. +GetSessionIdCallback = Callable[[], str | None] + class RequireApprovalToolList(TypedDict, total=False): tool_names: list[str] @@ -104,9 +104,10 @@ class RequireApprovalObject(TypedDict, total=False): def _create_default_streamable_http_client( headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, -) -> httpx.AsyncClient: + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, +) -> httpx2.AsyncClient: + """Default HTTP client factory using httpx2 (required by mcp SDK v2).""" kwargs: dict[str, Any] = {"follow_redirects": False} if timeout is not None: kwargs["timeout"] = timeout @@ -114,95 +115,7 @@ def _create_default_streamable_http_client( kwargs["headers"] = headers if auth is not None: kwargs["auth"] = auth - return httpx.AsyncClient(**kwargs) - - -class _InitializedNotificationTolerantStreamableHTTPTransport(StreamableHTTPTransport): - async def _handle_post_request(self, ctx: Any) -> None: - message = ctx.session_message.message - if not self._is_initialized_notification(message): - await super()._handle_post_request(ctx) - return - - try: - await super()._handle_post_request(ctx) - except httpx.HTTPError as exc: - log_tool_action_warning( - logger, - "Ignoring initialized notification HTTP failure", - exc, - ) - return - - -@asynccontextmanager -async def _streamablehttp_client_with_transport( - url: str, - *, - headers: dict[str, str] | None = None, - # This configures the HTTP client rather than an async cancellation scope. - timeout: float | timedelta = 30, # noqa: ASYNC109 - sse_read_timeout: float | timedelta = 60 * 5, - terminate_on_close: bool = True, - httpx_client_factory: HttpClientFactory = _create_default_streamable_http_client, - auth: httpx.Auth | None = None, - transport_factory: Callable[[str], StreamableHTTPTransport] = StreamableHTTPTransport, -) -> AsyncGenerator[MCPStreamTransport, None]: - timeout_seconds = timeout.total_seconds() if isinstance(timeout, timedelta) else timeout - sse_read_timeout_seconds = ( - sse_read_timeout.total_seconds() - if isinstance(sse_read_timeout, timedelta) - else sse_read_timeout - ) - - client = httpx_client_factory( - headers=headers, - timeout=httpx.Timeout(timeout_seconds, read=sse_read_timeout_seconds), - auth=auth, - ) - transport = transport_factory(url) - read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception]( - 0 - ) - write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0) - - async with client: - async with anyio.create_task_group() as tg: - try: - if _debug.DONT_LOG_TOOL_DATA: - logger.debug("Connecting to StreamableHTTP endpoint") - else: - logger.debug( - "Connecting to StreamableHTTP endpoint: %s", - get_mcp_server_log_name(url), - ) - - def start_get_stream() -> None: - tg.start_soon(transport.handle_get_stream, client, read_stream_writer) - - tg.start_soon( - transport.post_writer, - client, - write_stream_reader, - read_stream_writer, - write_stream, - start_get_stream, - tg, - ) - - try: - yield ( - read_stream, - write_stream, - transport.get_session_id, - ) - finally: - if transport.session_id and terminate_on_close: - await transport.terminate_session(client) - tg.cancel_scope.cancel() - finally: - await read_stream_writer.aclose() - await write_stream.aclose() + return httpx2.AsyncClient(**kwargs) class _SharedSessionRequestNeedsIsolation(Exception): @@ -739,7 +652,15 @@ def invalidate_tools_cache(self): def _extract_http_error_from_exception(self, e: BaseException) -> Exception | None: """Extract HTTP error from exception or ExceptionGroup.""" - if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException): + if isinstance( + e, + httpx.HTTPStatusError + | httpx.ConnectError + | httpx.TimeoutException + | httpx2.HTTPStatusError + | httpx2.ConnectError + | httpx2.TimeoutException, + ): return e # Recursively check ExceptionGroups for HTTP errors @@ -754,15 +675,12 @@ def _extract_http_error_from_exception(self, e: BaseException) -> Exception | No def _raise_user_error_for_http_error(self, http_error: Exception) -> None: """Raise appropriate UserError for HTTP error.""" error_message = f"Failed to connect to MCP server '{self.name}': " - if isinstance(http_error, httpx.HTTPStatusError): + if isinstance(http_error, httpx.HTTPStatusError | httpx2.HTTPStatusError): error_message += f"HTTP error {http_error.response.status_code} ({http_error.response.reason_phrase})" # noqa: E501 - - elif isinstance(http_error, httpx.ConnectError): + elif isinstance(http_error, httpx.ConnectError | httpx2.ConnectError): error_message += "Could not reach the server." - - elif isinstance(http_error, httpx.TimeoutException): + elif isinstance(http_error, httpx.TimeoutException | httpx2.TimeoutException): error_message += "Connection timeout." - raise UserError(error_message) from http_error async def _run_with_retries(self, func: Callable[[], Awaitable[T]]) -> T: @@ -793,9 +711,7 @@ async def connect(self): ClientSession( read, write, - timedelta(seconds=self.client_session_timeout_seconds) - if self.client_session_timeout_seconds - else None, + self.client_session_timeout_seconds, # mcp SDK v2: float, not timedelta message_handler=self.message_handler, ) ) @@ -815,7 +731,15 @@ async def connect(self): raise # For HTTP-related errors, wrap them - if isinstance(e, httpx.HTTPStatusError | httpx.ConnectError | httpx.TimeoutException): + if isinstance( + e, + httpx.HTTPStatusError + | httpx.ConnectError + | httpx.TimeoutException + | httpx2.HTTPStatusError + | httpx2.ConnectError + | httpx2.TimeoutException, + ): self._raise_user_error_for_http_error(e) # For other errors, re-raise as-is (don't wrap non-HTTP errors) @@ -880,16 +804,20 @@ async def list_tools( if self.tool_filter is not None: filtered_tools = await self._apply_tool_filter(filtered_tools, run_context, agent) return filtered_tools - except httpx.HTTPStatusError as e: + except (httpx.HTTPStatusError, httpx2.HTTPStatusError) as e: status_code = e.response.status_code raise UserError( f"Failed to list tools from MCP server '{self.name}': HTTP error {status_code}" ) from e - except httpx.ConnectError as e: + except (httpx.ConnectError, httpx2.ConnectError) as e: raise UserError( f"Failed to list tools from MCP server '{self.name}': Connection lost. " f"The server may have disconnected." ) from e + except (httpx.TimeoutException, httpx2.TimeoutException) as e: + raise UserError( + f"Failed to list tools from MCP server '{self.name}': Connection timeout." + ) from e async def call_tool( self, @@ -916,17 +844,22 @@ async def call_tool( lambda: session.call_tool(tool_name, arguments, meta=meta) ) ) - except httpx.HTTPStatusError as e: + except (httpx.HTTPStatusError, httpx2.HTTPStatusError) as e: status_code = e.response.status_code raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " f"HTTP error {status_code}" ) from e - except httpx.ConnectError as e: + except (httpx.ConnectError, httpx2.ConnectError) as e: raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': Connection lost. " f"The server may have disconnected." ) from e + except (httpx.TimeoutException, httpx2.TimeoutException) as e: + raise UserError( + f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " + f"Connection timeout." + ) from e def _validate_required_parameters( self, tool_name: str, arguments: dict[str, Any] | None @@ -936,10 +869,10 @@ def _validate_required_parameters( return tool = next((item for item in self._tools_list if item.name == tool_name), None) - if tool is None or not isinstance(tool.inputSchema, dict): + if tool is None or not isinstance(tool.input_schema, dict): return - raw_required = tool.inputSchema.get("required") + raw_required = tool.input_schema.get("required") if not isinstance(raw_required, list) or not raw_required: return @@ -1041,11 +974,11 @@ async def cleanup(self): error_message = f"Failed to connect to MCP server '{self.name}': " for exc in eg.exceptions: - if isinstance(exc, httpx.HTTPStatusError): + if isinstance(exc, httpx.HTTPStatusError | httpx2.HTTPStatusError): http_error = exc - elif isinstance(exc, httpx.ConnectError): + elif isinstance(exc, httpx.ConnectError | httpx2.ConnectError): connect_error = exc - elif isinstance(exc, httpx.TimeoutException): + elif isinstance(exc, httpx.TimeoutException | httpx2.TimeoutException): timeout_error = exc # Only raise HTTP errors if we're cleaning up after a failed connection. @@ -1271,15 +1204,20 @@ class MCPServerSseParams(TypedDict): sse_read_timeout: NotRequired[float] """The timeout for the SSE connection, in seconds. Defaults to 5 minutes.""" - auth: NotRequired[httpx.Auth | None] - """Optional httpx authentication handler (e.g. ``httpx.BasicAuth``, a custom - ``httpx.Auth`` subclass for OAuth token refresh, etc.). When provided, it is - passed directly to the underlying ``httpx.AsyncClient`` used by the SSE transport. + auth: NotRequired[httpx2.Auth | None] + """Optional authentication handler for the SSE transport. + + Pass an ``httpx2.Auth`` instance (e.g. ``httpx2.BasicAuth``) or a callable + that conforms to the ``httpx2.Auth`` protocol. With MCP SDK v2 the + underlying transport uses ``httpx2``; ``httpx`` auth objects are not + accepted. """ httpx_client_factory: NotRequired[HttpClientFactory] - """Custom HTTP client factory for configuring httpx.AsyncClient behavior (e.g. - to set custom SSL certificates, proxies, or other transport options). + """Custom HTTP client factory for the SSE transport. + + The factory must return an ``httpx2.AsyncClient`` and must accept the same + keyword signature as :func:`_create_default_streamable_http_client`. """ @@ -1404,22 +1342,21 @@ class MCPServerStreamableHttpParams(TypedDict): terminate_on_close: NotRequired[bool] """Terminate on close""" - httpx_client_factory: NotRequired[HttpClientFactory] - """Custom HTTP client factory for configuring httpx.AsyncClient behavior.""" + auth: NotRequired[httpx2.Auth | None] + """Optional authentication handler for the Streamable HTTP transport. - auth: NotRequired[httpx.Auth | None] - """Optional httpx authentication handler (e.g. ``httpx.BasicAuth``, a custom - ``httpx.Auth`` subclass for OAuth token refresh, etc.). When provided, it is - passed directly to the underlying ``httpx.AsyncClient`` used by the Streamable HTTP - transport. + Pass an ``httpx2.Auth`` instance (e.g. ``httpx2.BasicAuth``) or a callable + that conforms to the ``httpx2.Auth`` protocol. With MCP SDK v2 the + underlying transport uses ``httpx2``; ``httpx`` auth objects are not + accepted. """ ignore_initialized_notification_failure: NotRequired[bool] - """Whether to ignore failures when sending the best-effort - ``notifications/initialized`` POST. + """Not supported with MCP SDK v2; passing ``True`` raises :exc:`UserError`. - Defaults to ``False``. When set to ``True``, initialized-notification failures are - logged and ignored so subsequent requests on the same transport can continue. + In MCP SDK v1 this option allowed initialized-notification failures to be + swallowed. The stateless v2 transport (SEP-2577) removed the transport + path this option depended on. """ @@ -1509,27 +1446,48 @@ def create_streams( self, ) -> AbstractAsyncContextManager[MCPStreamTransport]: """Create the streams for the server.""" - kwargs: dict[str, Any] = { - "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.get("httpx_client_factory") + # mcp SDK v2 dropped the httpx-based transport; options that relied on it + # cannot be bridged without a complete rewrite. if self.params.get("ignore_initialized_notification_failure", False): - return _streamablehttp_client_with_transport( - **kwargs, - httpx_client_factory=httpx_client_factory or _create_default_streamable_http_client, - auth=self.params.get("auth"), - transport_factory=_InitializedNotificationTolerantStreamableHTTPTransport, + raise UserError( + "ignore_initialized_notification_failure is not supported with MCP SDK v2. " + "The v2 streamable-HTTP transport no longer exposes the httpx-based " + "session path that this option depends on." ) - kwargs["httpx_client_factory"] = ( - httpx_client_factory or _create_default_streamable_http_client - ) - if "auth" in self.params: - kwargs["auth"] = self.params["auth"] - return streamablehttp_client(**kwargs) + if self.params.get("httpx_client_factory") is not None: + raise UserError( + "httpx_client_factory is not supported with MCP SDK v2. " + "The v2 transport uses httpx2 internally. Customise HTTP behaviour " + "via the `headers`, `timeout`, `sse_read_timeout`, or `auth` parameters instead." + ) + + # mcp SDK v2: streamable_http_client(url, *, http_client, terminate_on_close). + # Wrap both the httpx2.AsyncClient and the connection in a single context + # manager so the client is properly closed when the server disconnects. + url = self.params["url"] + _timeout = self.params.get("timeout", 5) + timeout_sec = _timeout.total_seconds() if isinstance(_timeout, timedelta) else _timeout + _sse = self.params.get("sse_read_timeout", 60 * 5) + sse_timeout = _sse.total_seconds() if isinstance(_sse, timedelta) else _sse + headers = self.params.get("headers") or {} + auth = self.params.get("auth") + terminate_on_close = self.params.get("terminate_on_close", True) + + @asynccontextmanager + async def _managed() -> AsyncGenerator[Any, None]: # yields mcp v2 TransportStreams + async with httpx2.AsyncClient( + headers=headers, + timeout=httpx2.Timeout(timeout_sec, read=sse_timeout), + auth=auth, + ) as http_client: + async with streamable_http_client( + url=url, + http_client=http_client, + terminate_on_close=terminate_on_close, + ) as transport: + yield transport + + return _managed() @asynccontextmanager async def _isolated_client_session(self): @@ -1540,9 +1498,7 @@ async def _isolated_client_session(self): ClientSession( read, write, - timedelta(seconds=self.client_session_timeout_seconds) - if self.client_session_timeout_seconds - else None, + self.client_session_timeout_seconds, # mcp SDK v2: float, not timedelta message_handler=self.message_handler, ) ) @@ -1556,9 +1512,12 @@ async def _call_tool_with_session( arguments: dict[str, Any] | None, meta: dict[str, Any] | None = None, ) -> CallToolResult: + # mcp SDK v2: call_tool returns a union; we cast to the expected type. if meta is None: - return await session.call_tool(tool_name, arguments) - return await session.call_tool(tool_name, arguments, meta=meta) + return cast(CallToolResult, await session.call_tool(tool_name, arguments)) + return cast( + CallToolResult, await session.call_tool(tool_name, arguments, meta=cast(Any, meta)) + ) def _should_retry_in_isolated_session(self, exc: BaseException) -> bool: if isinstance( @@ -1566,13 +1525,15 @@ def _should_retry_in_isolated_session(self, exc: BaseException) -> bool: asyncio.CancelledError | ClosedResourceError | httpx.ConnectError - | httpx.TimeoutException, + | httpx.TimeoutException + | httpx2.ConnectError + | httpx2.TimeoutException, ): return True - if isinstance(exc, httpx.HTTPStatusError): + if isinstance(exc, httpx.HTTPStatusError | httpx2.HTTPStatusError): return exc.response.status_code >= 500 if isinstance(exc, McpError): - return exc.error.code == httpx.codes.REQUEST_TIMEOUT + return exc.code == 408 # HTTP 408 Request Timeout (mcp SDK v2: code is a direct attr) if isinstance(exc, BaseExceptionGroup): return bool(exc.exceptions) and all( self._should_retry_in_isolated_session(inner) for inner in exc.exceptions @@ -1688,31 +1649,31 @@ async def call_tool( backoff = self.retry_backoff_seconds_base * (2**retries_used) await asyncio.sleep(backoff) first_attempt = False - except httpx.HTTPStatusError as e: + except (httpx.HTTPStatusError, httpx2.HTTPStatusError) as e: status_code = e.response.status_code raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " f"HTTP error {status_code}" ) from e - except httpx.ConnectError as e: + except (httpx.ConnectError, httpx2.ConnectError) as e: raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': Connection lost. " f"The server may have disconnected." ) from e except BaseExceptionGroup as e: http_error = self._extract_http_error_from_exception(e) - if isinstance(http_error, httpx.HTTPStatusError): + if isinstance(http_error, httpx.HTTPStatusError | httpx2.HTTPStatusError): status_code = http_error.response.status_code raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " f"HTTP error {status_code}" ) from http_error - if isinstance(http_error, httpx.ConnectError): + if isinstance(http_error, httpx.ConnectError | httpx2.ConnectError): raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " "Connection lost. The server may have disconnected." ) from http_error - if isinstance(http_error, httpx.TimeoutException): + if isinstance(http_error, httpx.TimeoutException | httpx2.TimeoutException): raise UserError( f"Failed to call tool '{tool_name}' on MCP server '{self.name}': " "Connection timeout." @@ -1726,25 +1687,15 @@ def name(self) -> str: @property def session_id(self) -> str | None: - """The MCP session ID assigned by the server, or None if not yet connected - or if the server did not issue a session ID. - - The session ID is stable for the lifetime of this server instance's connection. - You can persist it and pass it back via the Mcp-Session-Id request header - (params["headers"]) on a new MCPServerStreamableHttp instance to resume - the same server-side session across process restarts or stateless workers. - - Example:: - - async with MCPServerStreamableHttp(params={"url": url}) as server: - session_id = server.session_id - - # In a new worker / process: - async with MCPServerStreamableHttp( - params={"url": url, "headers": {"Mcp-Session-Id": session_id}} - ) as server: - # Resumes the same server-side session. - ... + """The MCP session ID assigned by the server, or ``None``. + + .. note:: + With **MCP SDK v2** (SEP-2577 stateless protocol), this property + always returns ``None``. The streamable-HTTP transport no longer + issues server-assigned session-ID callbacks, so ``_get_session_id`` + is never populated. For session continuity across process restarts + under v2, use application-level session management instead of + relying on this property. """ if self._get_session_id is None: return None diff --git a/src/agents/mcp/util.py b/src/agents/mcp/util.py index 049b4561ba..bf528207aa 100644 --- a/src/agents/mcp/util.py +++ b/src/agents/mcp/util.py @@ -12,7 +12,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, Protocol, Union -import httpx +import httpx2 from typing_extensions import NotRequired, TypedDict from .. import _debug @@ -20,7 +20,7 @@ from ..exceptions import AgentsException, MCPToolCancellationError, ModelBehaviorError, UserError try: - from mcp.shared.exceptions import McpError as _McpError + from mcp.shared.exceptions import MCPError as _McpError except ImportError: # pragma: no cover – mcp is optional on Python < 3.10 _McpError = None # type: ignore[assignment, misc] from ..logger import log_tool_action_error, logger @@ -52,7 +52,7 @@ ToolOutput = Union[str, ToolOutputItem, list[ToolOutputItem]] # noqa: UP007 if TYPE_CHECKING: - from mcp.types import Tool as MCPTool + from mcp_types import Tool as MCPTool from ..agent import AgentBase from .server import MCPServer @@ -73,18 +73,19 @@ class _PrefixedToolNameCandidate: class HttpClientFactory(Protocol): - """Protocol for HTTP client factory functions. + """Protocol for HTTP client factory functions (mcp SDK v2). - This interface matches the MCP SDK's McpHttpClientFactory but is defined locally - to avoid accessing internal MCP SDK modules. + Implementations must return an ``httpx2.AsyncClient``. This interface + matches ``mcp.shared._httpx_utils.McpHttpClientFactory`` and is defined + locally to avoid importing from internal mcp SDK modules. """ def __call__( self, headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: ... + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: ... @dataclass @@ -531,7 +532,7 @@ def to_function_tool( effective_failure_error_function = server._get_failure_error_function( failure_error_function ) - schema, is_strict = copy.deepcopy(tool.inputSchema), False + schema, is_strict = copy.deepcopy(tool.input_schema), False # MCP spec doesn't require the inputSchema to have `properties`, but OpenAI spec does. if "properties" not in schema: @@ -619,8 +620,8 @@ async def _extract_custom_data( tool_display_name=tool_display_name, arguments=MappingProxyType(copy.deepcopy(arguments)), result_meta=cls._copy_mapping_proxy(getattr(result, "meta", None)), - structured_content=cls._copy_mapping_proxy(getattr(result, "structuredContent", None)), - is_error=getattr(result, "isError", None), + structured_content=cls._copy_mapping_proxy(getattr(result, "structured_content", None)), + is_error=getattr(result, "is_error", None), tool_output=copy.deepcopy(tool_output), ) return await maybe_extract_custom_data(extractor, extractor_context) @@ -758,8 +759,8 @@ async def invoke_mcp_tool( # If structured content is requested and available, use it exclusively tool_output: ToolOutput - if server.use_structured_content and result.structuredContent: - tool_output = json.dumps(result.structuredContent) + if server.use_structured_content and result.structured_content: + tool_output = json.dumps(result.structured_content) else: tool_output_list: list[ToolOutputItem] = [] for item in result.content: @@ -768,7 +769,7 @@ async def invoke_mcp_tool( elif item.type == "image": tool_output_list.append( ToolOutputImageDict( - type="image", image_url=f"data:{item.mimeType};base64,{item.data}" + type="image", image_url=f"data:{item.mime_type};base64,{item.data}" ) ) else: diff --git a/tests/mcp/helpers.py b/tests/mcp/helpers.py index 59a5b9a8f9..451ae99e9e 100644 --- a/tests/mcp/helpers.py +++ b/tests/mcp/helpers.py @@ -6,9 +6,9 @@ from typing import Any from mcp import Tool as MCPTool -from mcp.types import ( +from mcp_types import ( CallToolResult, - Content, + ContentBlock as Content, # mcp SDK v2: Content renamed to ContentBlock GetPromptResult, ListPromptsResult, ListResourcesResult, @@ -95,7 +95,7 @@ def __init__( self._response_meta: dict[str, Any] | None = None def add_tool(self, name: str, input_schema: dict[str, Any]): - self.tools.append(MCPTool(name=name, inputSchema=input_schema)) + self.tools.append(MCPTool(name=name, input_schema=input_schema)) async def connect(self): pass @@ -153,7 +153,7 @@ async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: """Return empty list of resource templates for fake server.""" - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: """Return empty resource contents for fake server.""" diff --git a/tests/mcp/test_caching.py b/tests/mcp/test_caching.py index f31cdf9518..5034705f81 100644 --- a/tests/mcp/test_caching.py +++ b/tests/mcp/test_caching.py @@ -1,7 +1,7 @@ from unittest.mock import AsyncMock, patch import pytest -from mcp.types import ListToolsResult, Tool as MCPTool +from mcp_types import ListToolsResult, Tool as MCPTool from agents import Agent from agents.mcp import MCPServerStdio @@ -28,8 +28,8 @@ async def test_server_caching_works( ) tools = [ - MCPTool(name="tool1", inputSchema={}), - MCPTool(name="tool2", inputSchema={}), + MCPTool(name="tool1", input_schema={}), + MCPTool(name="tool2", input_schema={}), ] mock_list_tools.return_value = ListToolsResult(tools=tools) diff --git a/tests/mcp/test_client_session_retries.py b/tests/mcp/test_client_session_retries.py index 4187e1afb0..d4297670e0 100644 --- a/tests/mcp/test_client_session_retries.py +++ b/tests/mcp/test_client_session_retries.py @@ -7,8 +7,8 @@ import pytest from anyio import ClosedResourceError from mcp import ClientSession, Tool as MCPTool -from mcp.shared.exceptions import McpError -from mcp.types import CallToolResult, ErrorData, GetPromptResult, ListPromptsResult, ListToolsResult +from mcp.shared.exceptions import MCPError +from mcp_types import CallToolResult, GetPromptResult, ListPromptsResult, ListToolsResult from agents.exceptions import UserError from agents.mcp.server import MCPServerStreamableHttp, _MCPServerWithClientSession @@ -34,7 +34,7 @@ async def list_tools(self): self.list_tools_attempts += 1 if self.list_tools_attempts <= self.fail_list_tools: raise RuntimeError("list_tools failure") - return ListToolsResult(tools=[MCPTool(name="tool", inputSchema={})]) + return ListToolsResult(tools=[MCPTool(name="tool", input_schema={})]) class DummyServer(_MCPServerWithClientSession): @@ -82,7 +82,7 @@ async def test_call_tool_validates_required_parameters_before_remote_call(): server._tools_list = [ # noqa: SLF001 MCPTool( name="tool", - inputSchema={ + input_schema={ "type": "object", "properties": {"param_a": {"type": "string"}}, "required": ["param_a"], @@ -103,7 +103,7 @@ async def test_call_tool_with_required_parameters_still_calls_remote_tool(): server._tools_list = [ # noqa: SLF001 MCPTool( name="tool", - inputSchema={ + input_schema={ "type": "object", "properties": {"param_a": {"type": "string"}}, "required": ["param_a"], @@ -120,7 +120,7 @@ async def test_call_tool_with_required_parameters_still_calls_remote_tool(): async def test_call_tool_skips_validation_when_tool_is_missing_from_cache(): session = DummySession() server = DummyServer(session=session, retries=0) - server._tools_list = [MCPTool(name="different_tool", inputSchema={"required": ["param_a"]})] # noqa: SLF001 + server._tools_list = [MCPTool(name="different_tool", input_schema={"required": ["param_a"]})] # noqa: SLF001 await server.call_tool("tool", {}) assert session.call_tool_attempts == 1 @@ -130,7 +130,7 @@ async def test_call_tool_skips_validation_when_tool_is_missing_from_cache(): async def test_call_tool_skips_validation_when_required_list_is_absent(): session = DummySession() server = DummyServer(session=session, retries=0) - server._tools_list = [MCPTool(name="tool", inputSchema={"type": "object"})] # noqa: SLF001 + server._tools_list = [MCPTool(name="tool", input_schema={"type": "object"})] # noqa: SLF001 await server.call_tool("tool", None) assert session.call_tool_attempts == 1 @@ -140,7 +140,7 @@ async def test_call_tool_skips_validation_when_required_list_is_absent(): async def test_call_tool_validates_required_parameters_when_arguments_is_none(): session = DummySession() server = DummyServer(session=session, retries=0) - server._tools_list = [MCPTool(name="tool", inputSchema={"required": ["param_a"]})] # noqa: SLF001 + server._tools_list = [MCPTool(name="tool", input_schema={"required": ["param_a"]})] # noqa: SLF001 with pytest.raises(UserError, match="missing required parameters: param_a"): await server.call_tool("tool", None) @@ -152,7 +152,7 @@ async def test_call_tool_validates_required_parameters_when_arguments_is_none(): async def test_call_tool_rejects_non_object_arguments_before_remote_call(): session = DummySession() server = DummyServer(session=session, retries=0) - server._tools_list = [MCPTool(name="tool", inputSchema={"required": ["param_a"]})] # noqa: SLF001 + server._tools_list = [MCPTool(name="tool", input_schema={"required": ["param_a"]})] # noqa: SLF001 with pytest.raises(UserError, match="arguments must be an object"): await server.call_tool("tool", cast(dict[str, object] | None, ["bad"])) @@ -236,9 +236,7 @@ def __init__(self, message: str = "timed out"): async def call_tool(self, tool_name, arguments, meta=None): self.call_tool_attempts += 1 - raise McpError( - ErrorData(code=httpx.codes.REQUEST_TIMEOUT, message=self.message), - ) + raise MCPError(code=httpx.codes.REQUEST_TIMEOUT, message=self.message) class IsolatedRetrySession: @@ -270,7 +268,7 @@ async def _isolated_client_session(self): yield self._isolated_session async def list_tools(self, run_context=None, agent=None): - return [MCPTool(name="tool", inputSchema={})] + return [MCPTool(name="tool", input_schema={})] async def list_prompts(self): return ListPromptsResult(prompts=[]) @@ -449,7 +447,7 @@ async def test_streamable_http_preserves_outer_cancellation_during_isolated_retr class ConcurrentPromptCancellationSession(ConcurrentCancellationSession): async def list_tools(self): - return ListToolsResult(tools=[MCPTool(name="tool", inputSchema={})]) + return ListToolsResult(tools=[MCPTool(name="tool", input_schema={})]) async def list_prompts(self): await self._slow_started.wait() diff --git a/tests/mcp/test_connect_disconnect.py b/tests/mcp/test_connect_disconnect.py index b001303974..9829e2dcec 100644 --- a/tests/mcp/test_connect_disconnect.py +++ b/tests/mcp/test_connect_disconnect.py @@ -1,7 +1,7 @@ from unittest.mock import AsyncMock, patch import pytest -from mcp.types import ListToolsResult, Tool as MCPTool +from mcp_types import ListToolsResult, Tool as MCPTool from agents.mcp import MCPServerStdio @@ -24,8 +24,8 @@ async def test_async_ctx_manager_works( ) tools = [ - MCPTool(name="tool1", inputSchema={}), - MCPTool(name="tool2", inputSchema={}), + MCPTool(name="tool1", input_schema={}), + MCPTool(name="tool2", input_schema={}), ] mock_list_tools.return_value = ListToolsResult(tools=tools) @@ -54,8 +54,8 @@ async def test_manual_connect_disconnect_works( ) tools = [ - MCPTool(name="tool1", inputSchema={}), - MCPTool(name="tool2", inputSchema={}), + MCPTool(name="tool1", input_schema={}), + MCPTool(name="tool2", input_schema={}), ] mock_list_tools.return_value = ListToolsResult(tools=tools) diff --git a/tests/mcp/test_mcp_approval.py b/tests/mcp/test_mcp_approval.py index 791fa71c24..49c1b682b0 100644 --- a/tests/mcp/test_mcp_approval.py +++ b/tests/mcp/test_mcp_approval.py @@ -1,7 +1,7 @@ import asyncio import pytest -from mcp.types import Tool as MCPTool +from mcp_types import Tool as MCPTool from agents import Agent, RunContextWrapper, Runner from agents.exceptions import UserError diff --git a/tests/mcp/test_mcp_auth_params.py b/tests/mcp/test_mcp_auth_params.py index ebc6c1934e..7c5e81f985 100644 --- a/tests/mcp/test_mcp_auth_params.py +++ b/tests/mcp/test_mcp_auth_params.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch -import httpx +import httpx2 import pytest from agents.mcp import MCPServerSse, MCPServerStreamableHttp @@ -32,7 +32,7 @@ async def test_sse_default_no_auth_no_factory(self): @pytest.mark.asyncio async def test_sse_with_auth(self): """SSE create_streams forwards auth and still applies the hardened default factory.""" - auth = httpx.BasicAuth(username="user", password="pass") + auth = httpx2.BasicAuth(username="user", password="pass") with patch("agents.mcp.server.sse_client") as mock_client: mock_client.return_value = MagicMock() server = MCPServerSse(params={"url": "http://localhost:8000/sse", "auth": auth}) @@ -52,10 +52,10 @@ async def test_sse_with_httpx_client_factory(self): def custom_factory( headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient(verify=False) # pragma: no cover + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient(verify=False) # pragma: no cover with patch("agents.mcp.server.sse_client") as mock_client: mock_client.return_value = MagicMock() @@ -77,14 +77,14 @@ def custom_factory( @pytest.mark.asyncio async def test_sse_with_auth_and_factory(self): """SSE create_streams forwards both auth and httpx_client_factory together.""" - auth = httpx.BasicAuth(username="user", password="pass") + auth = httpx2.BasicAuth(username="user", password="pass") def custom_factory( headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient(verify=False) # pragma: no cover + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient(verify=False) # pragma: no cover with patch("agents.mcp.server.sse_client") as mock_client: mock_client.return_value = MagicMock() @@ -108,72 +108,42 @@ def custom_factory( class TestMCPServerStreamableHttpAuth: - """Tests for the auth parameter added to MCPServerStreamableHttpParams.""" + """Tests for MCPServerStreamableHttp behaviour under MCP SDK v2.""" - @pytest.mark.asyncio - async def test_streamable_http_default_no_auth(self): - """StreamableHttp create_streams omits auth when not provided.""" - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - server = MCPServerStreamableHttp(params={"url": "http://localhost:8000/mcp"}) - server.create_streams() - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers=None, - timeout=5, - sse_read_timeout=300, - terminate_on_close=True, - httpx_client_factory=_create_default_streamable_http_client, - ) + def test_streamable_http_default_returns_context_manager(self): + """create_streams() returns an async CM; no old-style httpx_client_factory call.""" + server = MCPServerStreamableHttp(params={"url": "http://localhost:8000/mcp"}) + cm = server.create_streams() + assert hasattr(cm, "__aenter__") and hasattr(cm, "__aexit__") - @pytest.mark.asyncio - async def test_streamable_http_with_auth(self): - """StreamableHttp create_streams forwards the auth parameter when provided.""" - auth = httpx.BasicAuth(username="user", password="pass") - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - server = MCPServerStreamableHttp( - params={"url": "http://localhost:8000/mcp", "auth": auth} - ) - server.create_streams() - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers=None, - timeout=5, - sse_read_timeout=300, - terminate_on_close=True, - httpx_client_factory=_create_default_streamable_http_client, - auth=auth, - ) + def test_streamable_http_with_auth_returns_context_manager(self): + """auth is accepted and create_streams() still returns an async CM.""" + import httpx2 - @pytest.mark.asyncio - async def test_streamable_http_with_auth_and_factory(self): - """StreamableHttp create_streams forwards both auth and httpx_client_factory.""" - auth = httpx.BasicAuth(username="user", password="pass") + auth = httpx2.BasicAuth(username="user", password="pass") + server = MCPServerStreamableHttp(params={"url": "http://localhost:8000/mcp", "auth": auth}) + cm = server.create_streams() + assert hasattr(cm, "__aenter__") and hasattr(cm, "__aexit__") + + def test_streamable_http_with_auth_and_factory_raises_user_error(self): + """httpx_client_factory is not supported in mcp SDK v2; UserError is raised.""" + from agents.exceptions import UserError + + auth = httpx2.BasicAuth(username="user", password="pass") def custom_factory( headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient(verify=False) # pragma: no cover - - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - server = MCPServerStreamableHttp( - params={ - "url": "http://localhost:8000/mcp", - "auth": auth, - "httpx_client_factory": custom_factory, - } - ) + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient(verify=False) # pragma: no cover + + server = MCPServerStreamableHttp( + params={ + "url": "http://localhost:8000/mcp", + "auth": auth, + "httpx_client_factory": custom_factory, + } + ) + with pytest.raises(UserError, match="httpx_client_factory is not supported"): server.create_streams() - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers=None, - timeout=5, - sse_read_timeout=300, - terminate_on_close=True, - auth=auth, - httpx_client_factory=custom_factory, - ) diff --git a/tests/mcp/test_mcp_resources.py b/tests/mcp/test_mcp_resources.py index 75bacc99f7..634b48b941 100644 --- a/tests/mcp/test_mcp_resources.py +++ b/tests/mcp/test_mcp_resources.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from mcp.types import ( +from mcp_types import ( ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, @@ -11,7 +11,6 @@ ResourceTemplate, TextResourceContents, ) -from pydantic import AnyUrl from agents.mcp import MCPServerStreamableHttp @@ -54,7 +53,7 @@ async def test_list_resources_returns_result(server: MCPServerStreamableHttp): mock_session = MagicMock() expected = ListResourcesResult( resources=[ - Resource(uri=AnyUrl("file:///readme.md"), name="readme.md", mimeType="text/markdown"), + Resource(uri="file:///readme.md", name="readme.md", mime_type="text/markdown"), ] ) mock_session.list_resources = AsyncMock(return_value=expected) @@ -85,7 +84,7 @@ async def test_list_resource_templates_returns_result(server: MCPServerStreamabl """list_resource_templates delegates to the underlying MCP session.""" mock_session = MagicMock() expected = ListResourceTemplatesResult( - resourceTemplates=[ + resource_templates=[ ResourceTemplate(uriTemplate="file:///{path}", name="file"), ] ) @@ -102,7 +101,7 @@ async def test_list_resource_templates_returns_result(server: MCPServerStreamabl async def test_list_resource_templates_forwards_cursor(server: MCPServerStreamableHttp): """list_resource_templates forwards the cursor argument for pagination.""" mock_session = MagicMock() - page2 = ListResourceTemplatesResult(resourceTemplates=[]) + page2 = ListResourceTemplatesResult(resource_templates=[]) mock_session.list_resource_templates = AsyncMock(return_value=page2) server.session = mock_session @@ -119,7 +118,7 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): uri = "file:///readme.md" expected = ReadResourceResult( contents=[ - TextResourceContents(uri=AnyUrl(uri), text="# Hello", mimeType="text/markdown"), + TextResourceContents(uri=uri, text="# Hello", mime_type="text/markdown"), ] ) mock_session.read_resource = AsyncMock(return_value=expected) @@ -128,13 +127,15 @@ async def test_read_resource_returns_result(server: MCPServerStreamableHttp): result = await server.read_resource(uri) assert result is expected - mock_session.read_resource.assert_awaited_once_with(AnyUrl(uri)) + # mcp SDK v2 converts str URIs to AnyUrl internally; compare as strings. + args, _ = mock_session.read_resource.await_args + assert str(args[0]) == uri @pytest.mark.asyncio async def test_base_methods_raise_not_implemented(): """Bare MCPServer subclasses that don't override resource methods get NotImplementedError.""" - from mcp.types import CallToolResult, GetPromptResult, ListPromptsResult + from mcp_types import CallToolResult, GetPromptResult, ListPromptsResult from agents.mcp import MCPServer diff --git a/tests/mcp/test_mcp_server_manager.py b/tests/mcp/test_mcp_server_manager.py index f1f769eb6f..63565874d8 100644 --- a/tests/mcp/test_mcp_server_manager.py +++ b/tests/mcp/test_mcp_server_manager.py @@ -3,7 +3,7 @@ from typing import Any, cast import pytest -from mcp.types import ( +from mcp_types import ( CallToolResult, GetPromptResult, ListPromptsResult, @@ -66,7 +66,7 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: return ReadResourceResult(contents=[]) @@ -118,7 +118,7 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: return ReadResourceResult(contents=[]) @@ -184,7 +184,7 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: return ReadResourceResult(contents=[]) @@ -314,7 +314,7 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: return ReadResourceResult(contents=[]) diff --git a/tests/mcp/test_mcp_util.py b/tests/mcp/test_mcp_util.py index e1a06f17eb..64b6aa40ca 100644 --- a/tests/mcp/test_mcp_util.py +++ b/tests/mcp/test_mcp_util.py @@ -7,8 +7,8 @@ import pytest from inline_snapshot import snapshot -from mcp.shared.exceptions import McpError -from mcp.types import CallToolResult, ErrorData, ImageContent, TextContent, Tool as MCPTool +from mcp.shared.exceptions import MCPError +from mcp_types import CallToolResult, ImageContent, TextContent, Tool as MCPTool from pydantic import BaseModel, TypeAdapter import agents._debug as _debug @@ -512,7 +512,7 @@ async def test_invoke_mcp_tool(): server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") # Just making sure it doesn't crash @@ -533,7 +533,7 @@ def resolve_meta(context): server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context={"request_id": "req-123"}) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) await MCPUtil.invoke_mcp_tool( server, @@ -561,7 +561,7 @@ def resolve_meta(context): server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) await MCPUtil.invoke_mcp_tool(server, tool, ctx, '{"foo": "bar"}') @@ -577,7 +577,7 @@ async def test_to_function_tool_passes_static_mcp_meta(): server = FakeMCPServer() tool = MCPTool( name="test_tool_1", - inputSchema={}, + input_schema={}, _meta={"locale": "en", "extra": "value"}, ) @@ -608,7 +608,7 @@ def resolve_meta(context): server = FakeMCPServer(tool_meta_resolver=resolve_meta) tool = MCPTool( name="test_tool_1", - inputSchema={}, + input_schema={}, _meta={"locale": "en", "extra": "value"}, ) @@ -644,7 +644,7 @@ async def call_tool( server = MutatingMetaServer() tool = MCPTool( name="test_tool_1", - inputSchema={}, + input_schema={}, _meta={"nested": {"headers": ["original"]}}, ) @@ -672,7 +672,7 @@ async def test_mcp_invoke_bad_json_errors(caplog: pytest.LogCaptureFixture): server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) with pytest.raises(ModelBehaviorError): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "not_json") @@ -691,7 +691,7 @@ async def test_mcp_invoke_bad_json_redacts_payload_when_dont_log_tool_data( server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) bad_json = '{"secret":"SECRET_TOKEN_123"' with pytest.raises(ModelBehaviorError) as exc_info: @@ -715,7 +715,7 @@ async def test_mcp_invoke_bad_json_includes_payload_when_tool_logging_enabled( server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) bad_json = '{"secret":"SECRET_TOKEN_123"' with pytest.raises(ModelBehaviorError) as exc_info: @@ -735,7 +735,7 @@ async def test_mcp_invoke_rejects_non_object_json_input(input_json: str): server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) with pytest.raises(ModelBehaviorError, match="expected a JSON object"): await MCPUtil.invoke_mcp_tool(server, tool, ctx, input_json) @@ -802,7 +802,7 @@ async def test_mcp_invocation_crash_causes_error(caplog: pytest.LogCaptureFixtur server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) with pytest.raises(AgentsException): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") @@ -827,7 +827,7 @@ async def call_tool( arguments: dict[str, Any] | None, meta: dict[str, Any] | None = None, ): - raise McpError(ErrorData(code=-32000, message="upstream said SECRET_MCP_123")) + raise MCPError(code=-32000, message="upstream said SECRET_MCP_123") @pytest.mark.asyncio @@ -840,7 +840,7 @@ async def test_mcp_invocation_crash_redacts_error_when_dont_log_tool_data( server = SecretCrashingFakeMCPServer(server_name="SECRET_CUSTOM_MCP_SERVER") server.add_tool("SECRET_MCP_TOOL_NAME", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="SECRET_MCP_TOOL_NAME", inputSchema={}) + tool = MCPTool(name="SECRET_MCP_TOOL_NAME", input_schema={}) with pytest.raises(AgentsException): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") @@ -866,7 +866,7 @@ async def test_mcp_invocation_crash_includes_error_when_tool_logging_enabled( ) server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) with pytest.raises(AgentsException): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") @@ -887,9 +887,9 @@ async def test_mcp_tool_returned_error_redacts_message_when_dont_log_tool_data( server = McpErrorFakeMCPServer(server_name="SECRET_CUSTOM_MCP_SERVER") server.add_tool("SECRET_MCP_TOOL_NAME", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="SECRET_MCP_TOOL_NAME", inputSchema={}) + tool = MCPTool(name="SECRET_MCP_TOOL_NAME", input_schema={}) - with pytest.raises(McpError): + with pytest.raises(MCPError): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") assert "MCP tool returned an error" in caplog.text @@ -908,9 +908,9 @@ async def test_mcp_tool_returned_error_includes_message_when_tool_logging_enable server = McpErrorFakeMCPServer() server.add_tool("test_tool_1", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool_1", inputSchema={}) + tool = MCPTool(name="test_tool_1", input_schema={}) - with pytest.raises(McpError): + with pytest.raises(MCPError): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") assert "SECRET_MCP_123" in caplog.text @@ -922,7 +922,7 @@ async def test_mcp_tool_inner_cancellation_becomes_tool_error(): server.add_tool("cancel_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="cancel_tool", inputSchema={}) + tool = MCPTool(name="cancel_tool", input_schema={}) with pytest.raises(MCPToolCancellationError, match="tool execution was cancelled"): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -956,7 +956,7 @@ async def test_mcp_tool_inner_cancellation_still_becomes_tool_error_with_prior_c server.add_tool("cancel_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="cancel_tool", inputSchema={}) + tool = MCPTool(name="cancel_tool", input_schema={}) with pytest.raises(MCPToolCancellationError, match="tool execution was cancelled"): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -968,7 +968,7 @@ async def test_mcp_tool_outer_cancellation_still_propagates(): server.add_tool("slow_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="slow_tool", inputSchema={}) + tool = MCPTool(name="slow_tool", input_schema={}) task = asyncio.create_task(MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}")) await asyncio.sleep(0.05) @@ -986,7 +986,7 @@ async def test_mcp_tool_outer_cancellation_after_inner_completion_still_propagat server.add_tool("fast_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="fast_tool", inputSchema={}) + tool = MCPTool(name="fast_tool", input_schema={}) async def fake_wait(tasks, *, return_when): del return_when @@ -1008,7 +1008,7 @@ async def test_mcp_tool_outer_cancellation_after_inner_exception_still_propagate server.add_tool("boom_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="boom_tool", inputSchema={}) + tool = MCPTool(name="boom_tool", input_schema={}) async def fake_wait(tasks, *, return_when): del return_when @@ -1033,7 +1033,7 @@ async def test_mcp_tool_outer_cancellation_after_inner_cancellation_still_propag server.add_tool("slow_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="slow_tool", inputSchema={}) + tool = MCPTool(name="slow_tool", input_schema={}) async def fake_wait(tasks, *, return_when): del return_when @@ -1057,7 +1057,7 @@ async def test_mcp_tool_outer_cancellation_waits_for_inner_cleanup(): server.add_tool("slow_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="slow_tool", inputSchema={}) + tool = MCPTool(name="slow_tool", input_schema={}) task = asyncio.create_task(MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}")) await asyncio.sleep(0.05) @@ -1071,19 +1071,16 @@ async def test_mcp_tool_outer_cancellation_waits_for_inner_cleanup(): @pytest.mark.asyncio async def test_mcp_invocation_mcp_error_reraises(caplog: pytest.LogCaptureFixture): - """Test that McpError from server.call_tool is re-raised so the FunctionTool failure + """Test that MCPError from server.call_tool is re-raised so the FunctionTool failure pipeline (failure_error_function) can handle it. - When an MCP server raises McpError (e.g. upstream HTTP 4xx/5xx), invoke_mcp_tool + When an MCP server raises MCPError (e.g. upstream HTTP 4xx/5xx), invoke_mcp_tool re-raises so the configured failure_error_function shapes the model-visible error. With the default failure_error_function the FunctionTool returns a string error result; with failure_error_function=None the error is propagated to the caller. """ caplog.set_level(logging.DEBUG) - from mcp.shared.exceptions import McpError - from mcp.types import ErrorData - class McpErrorFakeMCPServer(FakeMCPServer): async def call_tool( self, @@ -1091,23 +1088,23 @@ async def call_tool( arguments: dict[str, Any] | None, meta: dict[str, Any] | None = None, ): - raise McpError(ErrorData(code=-32000, message="upstream 422 Unprocessable Entity")) + raise MCPError(code=-32000, message="upstream 422 Unprocessable Entity") server = McpErrorFakeMCPServer() server.add_tool("search", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="search", inputSchema={}) + tool = MCPTool(name="search", input_schema={}) - # invoke_mcp_tool itself should re-raise McpError - with pytest.raises(McpError): + # invoke_mcp_tool itself should re-raise MCPError + with pytest.raises(MCPError): await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") # Warning (not error) should be logged before re-raising assert "returned an error" in caplog.text # Via FunctionTool with default failure_error_function: error becomes a string result - mcp_tool = MCPTool(name="search", inputSchema={}) + mcp_tool = MCPTool(name="search", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( mcp_tool, server, convert_schemas_to_strict=False, agent=agent @@ -1138,7 +1135,7 @@ async def test_mcp_tool_graceful_error_handling(caplog: pytest.LogCaptureFixture server.add_tool("crashing_tool", {}) # Convert MCP tool to FunctionTool (this wraps invoke_mcp_tool with error handling) - mcp_tool = MCPTool(name="crashing_tool", inputSchema={}) + mcp_tool = MCPTool(name="crashing_tool", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( mcp_tool, server, convert_schemas_to_strict=False, agent=agent @@ -1199,7 +1196,7 @@ async def call_tool( server.add_tool("timeout_tool", {}) # Convert MCP tool to FunctionTool - mcp_tool = MCPTool(name="timeout_tool", inputSchema={}) + mcp_tool = MCPTool(name="timeout_tool", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( mcp_tool, server, convert_schemas_to_strict=False, agent=agent @@ -1227,7 +1224,7 @@ async def test_mcp_tool_cancellation_returns_error_message(): server = CancelledFakeMCPServer() server.add_tool("cancelled_tool", {}) - mcp_tool = MCPTool(name="cancelled_tool", inputSchema={}) + mcp_tool = MCPTool(name="cancelled_tool", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( mcp_tool, server, convert_schemas_to_strict=False, agent=agent @@ -1255,7 +1252,7 @@ async def test_to_function_tool_legacy_call_without_agent_uses_server_policy(): # Backward compatibility: old call style omitted the `agent` argument. function_tool = MCPUtil.to_function_tool( - MCPTool(name="legacy_tool", inputSchema={}), + MCPTool(name="legacy_tool", input_schema={}), server, convert_schemas_to_strict=False, ) @@ -1295,7 +1292,7 @@ def require_approval( server._needs_approval_policy = require_approval # type: ignore[assignment] function_tool = MCPUtil.to_function_tool( - MCPTool(name="legacy_callable_tool", inputSchema={}), + MCPTool(name="legacy_callable_tool", input_schema={}), server, convert_schemas_to_strict=False, ) @@ -1320,7 +1317,7 @@ def require_approval( return tool.name == "guarded_tool" server = FakeMCPServer(require_approval=require_approval) - tool = MCPTool(name="guarded_tool", inputSchema={}) + tool = MCPTool(name="guarded_tool", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( @@ -1354,7 +1351,7 @@ async def require_approval( return tool.name == "async_guarded_tool" server = FakeMCPServer(require_approval=require_approval) - tool = MCPTool(name="async_guarded_tool", inputSchema={}) + tool = MCPTool(name="async_guarded_tool", input_schema={}) agent = Agent(name="test-agent") function_tool = MCPUtil.to_function_tool( @@ -1616,13 +1613,13 @@ async def test_mcp_fastmcp_behavior_verification(): - [[]] → content=[] → MCPUtil returns "[]" (recursive empty) """ - from mcp.types import TextContent + from mcp_types import TextContent server = FakeMCPServer() server.add_tool("test_tool", {}) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool", inputSchema={}) + tool = MCPTool(name="test_tool", input_schema={}) # Case 1: None -> []. server._custom_content = [] @@ -1658,7 +1655,7 @@ async def test_mcp_fastmcp_behavior_verification(): assert result == expected, f"String should return {expected}, got {result}" # Case 7: Image content works normally. - server._custom_content = [ImageContent(data="AAAA", mimeType="image/png", type="image")] + server._custom_content = [ImageContent(data="AAAA", mime_type="image/png", type="image")] result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "") expected = {"type": "image", "image_url": "data:image/png;base64,AAAA"} assert result == expected, f"Image should return {expected}, got {result}" @@ -1731,7 +1728,7 @@ async def test_util_adds_properties(): def test_to_function_tool_does_not_mutate_mcp_input_schema(): schema = {"type": "object", "description": "Test tool"} - tool = MCPTool(name="test_tool", inputSchema=schema) + tool = MCPTool(name="test_tool", input_schema=schema) function_tool = MCPUtil.to_function_tool(tool, FakeMCPServer(), convert_schemas_to_strict=False) @@ -1741,7 +1738,7 @@ def test_to_function_tool_does_not_mutate_mcp_input_schema(): "properties": {}, } assert schema == {"type": "object", "description": "Test tool"} - assert tool.inputSchema == {"type": "object", "description": "Test tool"} + assert tool.input_schema == {"type": "object", "description": "Test tool"} def test_to_function_tool_failed_strict_conversion_keeps_original_schema(): @@ -1755,7 +1752,7 @@ def test_to_function_tool_failed_strict_conversion_keeps_original_schema(): "x": {"type": "object", "additionalProperties": True}, }, } - tool = MCPTool(name="test_tool", inputSchema=schema) + tool = MCPTool(name="test_tool", input_schema=schema) function_tool = MCPUtil.to_function_tool(tool, FakeMCPServer(), convert_schemas_to_strict=True) @@ -1792,7 +1789,7 @@ async def call_tool( self.tool_calls.append(tool_name) return CallToolResult( - content=self._test_content, structuredContent=self._test_structured_content + content=self._test_content, structured_content=self._test_structured_content ) @@ -1881,7 +1878,7 @@ async def test_structured_content_handling( server.set_test_result(content, structured_content) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="test_tool", inputSchema={}) + tool = MCPTool(name="test_tool", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") assert result == expected_output @@ -1904,7 +1901,7 @@ async def test_structured_content_priority_over_text(): server.set_test_result(text_content, structured_content) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="priority_test", inputSchema={}) + tool = MCPTool(name="priority_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -1933,7 +1930,7 @@ async def test_structured_content_fallback_behavior(): server.set_test_result(text_content, None) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="fallback_test", inputSchema={}) + tool = MCPTool(name="fallback_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -1960,7 +1957,7 @@ async def test_backwards_compatibility_unchanged(): server.set_test_result(text_content, structured_content) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="compat_test", inputSchema={}) + tool = MCPTool(name="compat_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -1975,7 +1972,7 @@ async def test_backwards_compatibility_unchanged(): async def test_empty_structured_content_fallback(): """Test that empty structured content (falsy values) falls back to text content. - This tests the condition: if server.use_structured_content and result.structuredContent + This tests the condition: if server.use_structured_content and result.structured_content where empty dict {} should be falsy and trigger fallback. """ @@ -1988,7 +1985,7 @@ async def test_empty_structured_content_fallback(): server.set_test_result(text_content, empty_structured) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="empty_structured_test", inputSchema={}) + tool = MCPTool(name="empty_structured_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -2018,7 +2015,7 @@ async def test_complex_structured_content(): server.set_test_result([], complex_structured) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="complex_test", inputSchema={}) + tool = MCPTool(name="complex_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -2053,7 +2050,7 @@ async def test_multiple_content_items_with_structured(): server.set_test_result(text_content, structured_content) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="multi_content_test", inputSchema={}) + tool = MCPTool(name="multi_content_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -2080,7 +2077,7 @@ async def test_multiple_content_items_without_structured(): server.set_test_result(text_content, None) ctx = RunContextWrapper(context=None) - tool = MCPTool(name="multi_text_test", inputSchema={}) + tool = MCPTool(name="multi_text_test", input_schema={}) result = await MCPUtil.invoke_mcp_tool(server, tool, ctx, "{}") @@ -2097,7 +2094,7 @@ def test_to_function_tool_preserves_mcp_title_metadata(): server = FakeMCPServer() tool = MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description="Search the docs.", title="Search Docs", ) @@ -2112,7 +2109,7 @@ def test_to_function_tool_description_falls_back_to_mcp_title(): server = FakeMCPServer() tool = MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description=None, title="Search Docs", ) diff --git a/tests/mcp/test_message_handler.py b/tests/mcp/test_message_handler.py index 193815c2e7..7dacc569a3 100644 --- a/tests/mcp/test_message_handler.py +++ b/tests/mcp/test_message_handler.py @@ -8,7 +8,7 @@ from mcp.client.session import MessageHandlerFnT from mcp.shared.message import SessionMessage from mcp.shared.session import RequestResponder -from mcp.types import ( +from mcp_types import ( ClientResult, Implementation, InitializeResult, @@ -53,9 +53,9 @@ async def initialize(self) -> InitializeResult: capabilities = ServerCapabilities.model_construct() server_info = Implementation.model_construct(name="stub", version="1.0") return InitializeResult( - protocolVersion="2024-11-05", + protocol_version="2024-11-05", capabilities=capabilities, - serverInfo=server_info, + server_info=server_info, ) diff --git a/tests/mcp/test_prompt_server.py b/tests/mcp/test_prompt_server.py index cf6254e5dd..69eef19b18 100644 --- a/tests/mcp/test_prompt_server.py +++ b/tests/mcp/test_prompt_server.py @@ -1,7 +1,7 @@ from typing import Any import pytest -from mcp.types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult +from mcp_types import ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult from agents import Agent, Runner from agents.mcp import MCPServer, MCPToolMetaResolver @@ -25,7 +25,7 @@ def __init__( def add_prompt(self, name: str, description: str, arguments: dict[str, Any] | None = None): """Add a prompt to the fake server""" - from mcp.types import Prompt + from mcp_types import Prompt prompt = Prompt(name=name, description=description, arguments=[]) self.prompts.append(prompt) @@ -42,13 +42,13 @@ async def cleanup(self): async def list_prompts(self, run_context=None, agent=None): """List available prompts""" - from mcp.types import ListPromptsResult + from mcp_types import ListPromptsResult return ListPromptsResult(prompts=self.prompts) async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None): """Get a prompt with arguments""" - from mcp.types import GetPromptResult, PromptMessage, TextContent + from mcp_types import GetPromptResult, PromptMessage, TextContent if name not in self.prompt_results: raise ValueError(f"Prompt '{name}' not found") @@ -83,7 +83,7 @@ async def list_resources(self, cursor: str | None = None) -> ListResourcesResult async def list_resource_templates( self, cursor: str | None = None ) -> ListResourceTemplatesResult: - return ListResourceTemplatesResult(resourceTemplates=[]) + return ListResourceTemplatesResult(resource_templates=[]) async def read_resource(self, uri: str) -> ReadResourceResult: return ReadResourceResult(contents=[]) diff --git a/tests/mcp/test_streamable_http_client_factory.py b/tests/mcp/test_streamable_http_client_factory.py index 32f258b0f9..6e696af172 100644 --- a/tests/mcp/test_streamable_http_client_factory.py +++ b/tests/mcp/test_streamable_http_client_factory.py @@ -1,442 +1,76 @@ -"""Tests for MCPServerStreamableHttp httpx_client_factory functionality.""" +"""Tests for MCPServerStreamableHttp with MCP Python SDK v2 transport.""" from __future__ import annotations -import base64 -from unittest.mock import MagicMock, patch +from datetime import timedelta -import httpx +import httpx2 import pytest -from anyio import create_memory_object_stream -from mcp.shared.message import SessionMessage -from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest +from agents.exceptions import UserError from agents.mcp import MCPServerStreamableHttp -from agents.mcp.server import ( - _create_default_streamable_http_client, - _InitializedNotificationTolerantStreamableHTTPTransport, - _streamablehttp_client_with_transport, -) class TestMCPServerStreamableHttpClientFactory: - """Test cases for custom httpx_client_factory parameter.""" + """Tests for MCPServerStreamableHttp behaviour under MCP SDK v2.""" - @pytest.mark.asyncio - async def test_default_httpx_client_factory(self): - """Test that default behavior works when no custom factory is provided.""" - # Mock the streamablehttp_client to avoid actual network calls - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "http://localhost:8000/mcp", - "headers": {"Authorization": "Bearer token"}, - "timeout": 10, - } - ) - - server.create_streams() - - # Verify streamablehttp_client was called with the hardened default factory. - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers={"Authorization": "Bearer token"}, - timeout=10, - sse_read_timeout=300, # Default value - terminate_on_close=True, # Default value - httpx_client_factory=_create_default_streamable_http_client, - ) - - @pytest.mark.asyncio - async def test_custom_httpx_client_factory(self): - """Test that custom httpx_client_factory is passed correctly.""" - - # Create a custom factory function - def custom_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient( - verify=False, # Disable SSL verification for testing - timeout=httpx.Timeout(60.0), - headers={"X-Custom-Header": "test"}, - ) - - # Mock the streamablehttp_client to avoid actual network calls - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "http://localhost:8000/mcp", - "headers": {"Authorization": "Bearer token"}, - "timeout": 10, - "httpx_client_factory": custom_factory, - } - ) - - # Create streams should pass the custom factory - server.create_streams() - - # Verify streamablehttp_client was called with the custom factory - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers={"Authorization": "Bearer token"}, - timeout=10, - sse_read_timeout=300, # Default value - terminate_on_close=True, # Default value - httpx_client_factory=custom_factory, - ) - - @pytest.mark.asyncio - async def test_custom_httpx_client_factory_with_ssl_cert(self): - """Test custom factory with SSL certificate configuration.""" - - def ssl_cert_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient( - verify="/path/to/cert.pem", # Custom SSL certificate - timeout=httpx.Timeout(120.0), - ) - - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "https://secure-server.com/mcp", - "timeout": 30, - "httpx_client_factory": ssl_cert_factory, - } - ) - - server.create_streams() - - mock_client.assert_called_once_with( - url="https://secure-server.com/mcp", - headers=None, - timeout=30, - sse_read_timeout=300, - terminate_on_close=True, - httpx_client_factory=ssl_cert_factory, - ) - - @pytest.mark.asyncio - async def test_custom_httpx_client_factory_with_proxy(self): - """Test custom factory with proxy configuration.""" - - def proxy_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient( - proxy="http://proxy.example.com:8080", - timeout=httpx.Timeout(60.0), - ) - - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "http://localhost:8000/mcp", - "httpx_client_factory": proxy_factory, - } - ) - - server.create_streams() - - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers=None, - timeout=5, # Default value - sse_read_timeout=300, - terminate_on_close=True, - httpx_client_factory=proxy_factory, - ) - - @pytest.mark.asyncio - async def test_custom_httpx_client_factory_with_retry_logic(self): - """Test custom factory with retry logic configuration.""" - - def retry_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient( - timeout=httpx.Timeout(30.0), - # Note: httpx doesn't have built-in retry, but this shows how - # a custom factory could be used to configure retry behavior - # through middleware or other mechanisms - ) - - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "http://localhost:8000/mcp", - "httpx_client_factory": retry_factory, - } - ) - - server.create_streams() - - mock_client.assert_called_once_with( - url="http://localhost:8000/mcp", - headers=None, - timeout=5, - sse_read_timeout=300, - terminate_on_close=True, - httpx_client_factory=retry_factory, - ) - - def test_httpx_client_factory_type_annotation(self): - """Test that the type annotation is correct for httpx_client_factory.""" - from agents.mcp.server import MCPServerStreamableHttpParams - - # This test ensures the type annotation is properly set - # We can't easily test the TypedDict at runtime, but we can verify - # that the import works and the type is available - assert hasattr(MCPServerStreamableHttpParams, "__annotations__") - - # Verify that the httpx_client_factory parameter is in the annotations - annotations = MCPServerStreamableHttpParams.__annotations__ - assert "httpx_client_factory" in annotations - - # The annotation should contain the string representation of the type - annotation_str = str(annotations["httpx_client_factory"]) - assert "HttpClientFactory" in annotation_str - - @pytest.mark.asyncio - async def test_all_parameters_with_custom_factory(self): - """Test that all parameters work together with custom factory.""" - - def comprehensive_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - return httpx.AsyncClient( - verify=False, - timeout=httpx.Timeout(90.0), - headers={"X-Test": "value"}, - ) - - with patch("agents.mcp.server.streamablehttp_client") as mock_client: - mock_client.return_value = MagicMock() - - server = MCPServerStreamableHttp( - params={ - "url": "https://api.example.com/mcp", - "headers": {"Authorization": "Bearer token"}, - "timeout": 45, - "sse_read_timeout": 600, - "terminate_on_close": False, - "httpx_client_factory": comprehensive_factory, - } - ) - - server.create_streams() - - mock_client.assert_called_once_with( - url="https://api.example.com/mcp", - headers={"Authorization": "Bearer token"}, - timeout=45, - sse_read_timeout=600, - terminate_on_close=False, - httpx_client_factory=comprehensive_factory, - ) - - -@pytest.mark.asyncio -async def test_initialized_notification_failure_returns_synthetic_success(): - async def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(503, request=request) - - transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") - read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - ctx = MagicMock() - ctx.client = client - ctx.read_stream_writer = read_stream_writer - ctx.session_message = SessionMessage( - JSONRPCMessage( - JSONRPCNotification( - jsonrpc="2.0", - method="notifications/initialized", - params={}, - ) - ) + def test_default_create_streams_returns_context_manager(self): + """create_streams() returns an async context manager without network calls.""" + server = MCPServerStreamableHttp( + params={ + "url": "http://localhost:8000/mcp", + "headers": {"Authorization": "Bearer token"}, + "timeout": 10, + } ) + cm = server.create_streams() + assert hasattr(cm, "__aenter__") and hasattr(cm, "__aexit__") - await transport._handle_post_request(ctx) - finally: - await client.aclose() - await read_stream_writer.aclose() - + def test_custom_httpx_client_factory_raises_user_error(self): + """httpx_client_factory is unsupported in mcp SDK v2 and raises UserError.""" -@pytest.mark.asyncio -async def test_initialized_notification_transport_exception_returns_synthetic_success(): - async def handler(request: httpx.Request) -> httpx.Response: - raise httpx.ConnectError("boom", request=request) + def _factory( + headers: dict[str, str] | None = None, + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient() # pragma: no cover - transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") - read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - ctx = MagicMock() - ctx.client = client - ctx.read_stream_writer = read_stream_writer - ctx.session_message = SessionMessage( - JSONRPCMessage( - JSONRPCNotification( - jsonrpc="2.0", - method="notifications/initialized", - params={}, - ) - ) + server = MCPServerStreamableHttp( + params={ + "url": "http://localhost:8000/mcp", + "httpx_client_factory": _factory, + } ) + with pytest.raises(UserError, match="httpx_client_factory is not supported"): + server.create_streams() - await transport._handle_post_request(ctx) - finally: - await client.aclose() - await read_stream_writer.aclose() - - -@pytest.mark.asyncio -async def test_streamable_http_server_passes_ignore_initialized_notification_failure(): - with patch("agents.mcp.server._streamablehttp_client_with_transport") as mock_client: - mock_client.return_value = MagicMock() - + def test_ignore_initialized_notification_failure_raises_user_error(self): + """ignore_initialized_notification_failure is unsupported in mcp SDK v2.""" server = MCPServerStreamableHttp( params={ "url": "http://localhost:8000/mcp", "ignore_initialized_notification_failure": True, } ) + with pytest.raises(UserError, match="ignore_initialized_notification_failure"): + server.create_streams() - server.create_streams() - - kwargs = mock_client.call_args.kwargs - assert kwargs["url"] == "http://localhost:8000/mcp" - assert kwargs["headers"] is None - assert kwargs["timeout"] == 5 - assert kwargs["sse_read_timeout"] == 300 - assert kwargs["terminate_on_close"] is True - assert kwargs["httpx_client_factory"] is _create_default_streamable_http_client - assert ( - kwargs["transport_factory"] is _InitializedNotificationTolerantStreamableHTTPTransport - ) - - -@pytest.mark.asyncio -async def test_transport_preserves_non_initialized_failures(): - async def handler(request: httpx.Request) -> httpx.Response: - raise httpx.ConnectError("boom", request=request) - - transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp") - read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - ctx = MagicMock() - ctx.client = client - ctx.read_stream_writer = read_stream_writer - ctx.session_message = SessionMessage( - JSONRPCMessage( - JSONRPCRequest( - jsonrpc="2.0", - id=1, - method="tools/list", - params={}, - ) - ) - ) - - with pytest.raises(httpx.ConnectError): - await transport._handle_post_request(ctx) - finally: - await client.aclose() - await read_stream_writer.aclose() - - -@pytest.mark.asyncio -async def test_stream_client_preserves_custom_factory_headers_timeout_and_auth(): - seen: dict[str, object] = {} - - class RecordingAuth(httpx.Auth): - def auth_flow(self, request: httpx.Request): - request.headers["Authorization"] = f"Basic {base64.b64encode(b'user:pass').decode()}" - yield request - - async def handler(request: httpx.Request) -> httpx.Response: - seen["request_headers"] = dict(request.headers) - return httpx.Response(200, request=request) - - def base_factory( - headers: dict[str, str] | None = None, - timeout: httpx.Timeout | None = None, - auth: httpx.Auth | None = None, - ) -> httpx.AsyncClient: - seen["factory_headers"] = headers - seen["factory_timeout"] = timeout - seen["factory_auth"] = auth - return httpx.AsyncClient( - headers=headers, - timeout=timeout, - auth=auth, - transport=httpx.MockTransport(handler), + def test_timedelta_timeout_converted_to_seconds(self): + """timedelta values for timeout/sse_read_timeout are converted to float.""" + server = MCPServerStreamableHttp( + params={ + "url": "http://localhost:8000/mcp", + "timeout": timedelta(seconds=30), + "sse_read_timeout": timedelta(minutes=5), + } ) + cm = server.create_streams() + assert hasattr(cm, "__aenter__") - timeout = httpx.Timeout(12.0) - auth = RecordingAuth() - async with _streamablehttp_client_with_transport( - "https://example.test/mcp", - headers={"X-Test": "value"}, - timeout=12.0, - sse_read_timeout=30.0, - httpx_client_factory=base_factory, - auth=auth, - transport_factory=_InitializedNotificationTolerantStreamableHTTPTransport, - ): - pass - - assert seen["factory_headers"] == {"X-Test": "value"} - seen_timeout = seen["factory_timeout"] - assert isinstance(seen_timeout, httpx.Timeout) - assert seen_timeout.connect == timeout.connect - assert seen_timeout.read == 30.0 - assert seen_timeout.write == timeout.write - assert seen_timeout.pool == timeout.pool - assert seen["factory_auth"] is auth - - -@pytest.mark.asyncio -async def test_default_streamable_http_client_matches_expected_defaults(): - timeout = httpx.Timeout(12.0) - auth = httpx.BasicAuth("user", "pass") + def test_httpx_client_factory_removed_from_streamable_http_params(self): + """httpx_client_factory is removed from MCPServerStreamableHttpParams in mcp SDK v2.""" + from agents.mcp.server import MCPServerStreamableHttpParams - client = _create_default_streamable_http_client( - headers={"X-Test": "value"}, - timeout=timeout, - auth=auth, - ) - try: - assert client.headers["X-Test"] == "value" - assert client.timeout.connect == timeout.connect - assert client.timeout.read == timeout.read - assert client.timeout.write == timeout.write - assert client.timeout.pool == timeout.pool - assert client.auth is auth - assert client.follow_redirects is False - finally: - await client.aclose() + assert hasattr(MCPServerStreamableHttpParams, "__annotations__") + assert "httpx_client_factory" not in MCPServerStreamableHttpParams.__annotations__ diff --git a/tests/test_agent_as_tool.py b/tests/test_agent_as_tool.py index 3872bbb8f6..678d892aff 100644 --- a/tests/test_agent_as_tool.py +++ b/tests/test_agent_as_tool.py @@ -7,8 +7,7 @@ from typing import Any, cast import pytest -from mcp.shared.exceptions import McpError -from mcp.types import ErrorData +from mcp.shared.exceptions import MCPError from openai.types.responses import ResponseOutputMessage, ResponseOutputText from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall from pydantic import BaseModel, Field @@ -2022,7 +2021,7 @@ async def call_tool( ): self.tool_calls.append(tool_name) del arguments, meta - raise McpError(ErrorData(code=-32000, message="synthetic upstream 422")) + raise MCPError(code=-32000, message="synthetic upstream 422") nested_server: FakeMCPServer if server == "cancelled": diff --git a/tests/test_process_model_response.py b/tests/test_process_model_response.py index f21d65911f..7df8625603 100644 --- a/tests/test_process_model_response.py +++ b/tests/test_process_model_response.py @@ -91,7 +91,7 @@ def test_process_model_response_shell_call_without_tool_raises() -> None: def test_process_model_response_sets_title_for_local_mcp_function_tool() -> None: agent = Agent(name="local-mcp", model=FakeModel()) - mcp_tool = MCPTool(name="search_docs", inputSchema={}, description=None, title="Search Docs") + mcp_tool = MCPTool(name="search_docs", input_schema={}, description=None, title="Search Docs") function_tool = MCPUtil.to_function_tool( mcp_tool, FakeMCPServer(), diff --git a/tests/test_stream_events.py b/tests/test_stream_events.py index 741449af71..3db0062c38 100644 --- a/tests/test_stream_events.py +++ b/tests/test_stream_events.py @@ -130,7 +130,7 @@ async def test_stream_events_tool_called_includes_local_mcp_title() -> None: tools=[ MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description=None, title="Search Docs", ) diff --git a/tests/test_tool_origin.py b/tests/test_tool_origin.py index 31ba25561b..8add43e5e4 100644 --- a/tests/test_tool_origin.py +++ b/tests/test_tool_origin.py @@ -144,7 +144,7 @@ async def test_runner_attaches_local_mcp_tool_origin_to_call_and_output_items() tools=[ MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description="Search the docs.", title="Search Docs", ) @@ -173,7 +173,7 @@ async def test_streamed_tool_call_item_includes_local_mcp_origin() -> None: tools=[ MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description=None, title="Search Docs", ) @@ -307,7 +307,7 @@ def test_local_mcp_tool_origin_does_not_retain_server_object() -> None: function_tool = MCPUtil.to_function_tool( MCPTool( name="search_docs", - inputSchema={}, + input_schema={}, description="Search the docs.", title="Search Docs", ), diff --git a/uv.lock b/uv.lock index 7bc73f5b8a..b53ecb4c3f 100644 --- a/uv.lock +++ b/uv.lock @@ -179,12 +179,12 @@ name = "any-llm-sdk" version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx", marker = "python_full_version >= '3.11'" }, - { name = "openai", marker = "python_full_version >= '3.11'" }, - { name = "openresponses-types", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, - { name = "rich", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "httpx" }, + { name = "openai" }, + { name = "openresponses-types" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/18/161747c16bbe4b15122ac690e7941f3c58f24b3df382189fdbadf0624595/any_llm_sdk-1.11.0.tar.gz", hash = "sha256:cabda4135041127e728d6d6fe6a3c0d77f45c0dd50b38a8f0bc132a2ad948a6a", size = 148392, upload-time = "2026-03-12T13:18:29.74Z" } wheels = [ @@ -960,7 +960,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1006,7 +1006,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, - { name = "starlette" }, + { name = "starlette", version = "0.47.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] @@ -1440,6 +1441,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/fe/6a3f9f1a8bb8733326140737446aaf72fddb8b54b8f202302f5c84960613/httpcore2-2.7.0.tar.gz", hash = "sha256:6dc0fedf329a52a990930a5579edfebaea81118ea700ea0dd7de2b5e5be49efc", size = 65593, upload-time = "2026-07-14T20:40:01.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl", hash = "sha256:1452f589fe23f55b44546cd884294c41a29330af902bc0b71a761fd52d18f92b", size = 81506, upload-time = "2026-07-14T20:39:58.053Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1456,12 +1470,19 @@ wheels = [ ] [[package]] -name = "httpx-sse" -version = "0.4.1" +name = "httpx2" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/4a/129b2e21b90ac2985d3928d96792bccc39bc6dfe796c5eee2d8ec06d4105/httpx2-2.7.0.tar.gz", hash = "sha256:8b30709aed5c8465b0dd3b95c09ce301c8f79e7e7a2d00ab0af551e0d0375b07", size = 94487, upload-time = "2026-07-14T20:40:02.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl", hash = "sha256:ed2a2719c696789e09493bd8e2bec3d8bd925cc6e26b68389ec25ade132f7bf4", size = 90234, upload-time = "2026-07-14T20:39:59.531Z" }, ] [[package]] @@ -1494,11 +1515,11 @@ wheels = [ [[package]] name = "idna" -version = "3.10" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1792,27 +1813,42 @@ wheels = [ [[package]] name = "mcp" -version = "1.26.0" +version = "2.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx2" }, { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, - { name = "starlette" }, + { name = "starlette", version = "0.47.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "starlette", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "typing-extensions" }, { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/aa/c5d38e0199304494be6370667d04d93d8681d9bdc864d56678250dbd3f3b/mcp-2.0.0b2.tar.gz", hash = "sha256:0528d0d38ae798fbff251616ec687faaaa8f5309571e0b2bc553c530fa10b8b1", size = 1590650, upload-time = "2026-07-14T16:47:57.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, + { url = "https://files.pythonhosted.org/packages/a7/90/187d6283a304acc6954987992ef17e2515739a649f148dc8b67b302e1bbd/mcp-2.0.0b2-py3-none-any.whl", hash = "sha256:9c50ae5afa08960ab76d50aa3adab3184952d9bea7ef87f4a4a5ba68bdefcf0a", size = 334286, upload-time = "2026-07-14T16:47:54.768Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/21/db529130ac8edd1d844fc322862afeceaf2b7f610a591fa528002b022e07/mcp_types-2.0.0b2.tar.gz", hash = "sha256:094fa7160106819ab39a1586179c3a9f070bfd833d0a6f8fcb30a54a986cc402", size = 65877, upload-time = "2026-07-14T16:47:59.198Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e1/c466ceacdaa929396d35ad45176398acd0c416fead0970d3ad22618a19d7/mcp_types-2.0.0b2-py3-none-any.whl", hash = "sha256:35c9c33abb90a77dc6ad1daecaa6407c788c2f32d14d52dad8f843c4f008eae2", size = 68944, upload-time = "2026-07-14T16:47:56.493Z" }, ] [[package]] @@ -2570,7 +2606,7 @@ requires-dist = [ { name = "griffelib", specifier = ">=2,<3" }, { name = "grpcio", marker = "extra == 'dapr'", specifier = ">=1.60.0" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.83.0" }, - { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.19.0,<2" }, + { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=2.0.0b2,<3" }, { name = "modal", marker = "extra == 'modal'", specifier = "==1.4.3" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=2.45.0,<3" }, @@ -2630,7 +2666,7 @@ name = "openresponses-types" version = "2.3.0.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/26/b612c3215f5599714fa94d63eb5ee59b4eb66dbdeeaf86bb4d848359484d/openresponses_types-2.3.0.post1.tar.gz", hash = "sha256:11b8896d3621d2ac2439f6ff106f34ddcb1bbd517c317a6c852a9df2e98a0753", size = 19254, upload-time = "2026-01-22T20:02:03.933Z" } wheels = [ @@ -3920,15 +3956,35 @@ wheels = [ name = "starlette" version = "0.47.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] dependencies = [ { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version != '3.13.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + [[package]] name = "synchronicity" version = "0.12.2" @@ -4140,6 +4196,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "types-certifi" version = "2021.10.8.3" @@ -4282,10 +4347,10 @@ name = "vercel-workers" version = "0.0.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, - { name = "vercel", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "httpx" }, + { name = "python-dotenv" }, + { name = "vercel" }, ] sdist = { url = "https://files.pythonhosted.org/packages/73/d8/17ba256fceff42be231ca8ff0567dcf2da54ee8de633e949fa08b9403b1f/vercel_workers-0.0.16.tar.gz", hash = "sha256:38df45dbf42fbae39ffa0e419f0908bf1beb047e38fc5ddd0a479feac340fb8c", size = 51615, upload-time = "2026-04-13T21:23:27.649Z" } wheels = [