From 86f4acc239fa520e7450af5b332f1caba50ce2a3 Mon Sep 17 00:00:00 2001 From: Brandon Shar Date: Thu, 29 May 2025 18:21:04 -0400 Subject: [PATCH 1/4] Skip initialization for http mcp --- docs/mcp/client.md | 23 +++++++++++++++++++++++ pydantic_ai_slim/pydantic_ai/mcp.py | 12 ++++++++++-- tests/test_mcp.py | 6 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index b1a5994d8..5bb3e357c 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -83,6 +83,29 @@ Will display as follows: ![Logfire run python code](../img/logfire-run-python-code.png) +#### Initialization + +If you're connecting to an HTTP server that is stateless (one that doesn't require a persistent connection or session), you +may want to skip sending the initialization request and only send requests as needed. To do this, you can set the +`skip_initialization` parameter to `True` when instantiating the server. + +This allows you to de-couple instantiating your agents with connecting to MCP servers for situations like multi-agent systems or health checks. + +```python {title="mcp_http_client_skip_initialization.py" py="3.10"} +from pydantic_ai import Agent +from pydantic_ai.mcp import MCPServerHTTP + +server = MCPServerHTTP(url='http://localhost:3001/mcp', skip_initialization=True) +agent = Agent('openai:gpt-4o', mcp_servers=[server]) + + +async def main(): + async with agent.run_mcp_servers(): # No call to the MCP server is made here + result = await agent.run('What tools do you have access to?') # the first call is made here +``` + + + ### MCP "stdio" Server The other transport offered by MCP is the [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class. diff --git a/pydantic_ai_slim/pydantic_ai/mcp.py b/pydantic_ai_slim/pydantic_ai/mcp.py index 2aae85a6c..62376bf33 100644 --- a/pydantic_ai_slim/pydantic_ai/mcp.py +++ b/pydantic_ai_slim/pydantic_ai/mcp.py @@ -56,6 +56,9 @@ class MCPServer(ABC): e.g. if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar` """ + skip_initialization: bool = False + """Determines whether to send the initialization request when first entering context""" + _client: ClientSession _read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] _write_stream: MemoryObjectSendStream[SessionMessage] @@ -139,11 +142,13 @@ async def __aenter__(self) -> Self: client = ClientSession(read_stream=self._read_stream, write_stream=self._write_stream) self._client = await self._exit_stack.enter_async_context(client) - with anyio.fail_after(self._get_client_initialize_timeout()): - await self._client.initialize() + if not self.skip_initialization: + with anyio.fail_after(self._get_client_initialize_timeout()): + await self._client.initialize() if log_level := self._get_log_level(): await self._client.set_logging_level(log_level) + self.is_running = True return self @@ -353,6 +358,9 @@ async def main(): For example, if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar` """ + skip_initialization: bool = False + """Determines whether to send the initialization request when first entering context""" + def __post_init__(self): # streamablehttp_client expects timedeltas, so we accept them too to match, # but primarily work with floats for a simpler user API. diff --git a/tests/test_mcp.py b/tests/test_mcp.py index ee95885ff..30f17aa91 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -215,6 +215,12 @@ def get_none() -> None: # pragma: no cover await agent.run('No conflict') +async def test_with_skipped_initialization(): + server = MCPServerHTTP('no-url', skip_initialization=True) + async with server: + assert server.is_running # no error occurs because we haven't made a call yet + + async def test_agent_with_server_not_running(openai_api_key: str): server = MCPServerStdio('python', ['-m', 'tests.mcp_server']) model = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key)) From 6d14105462f4ed464a9d9d12088c452cd22e7c8d Mon Sep 17 00:00:00 2001 From: Brandon Shar Date: Thu, 29 May 2025 18:48:12 -0400 Subject: [PATCH 2/4] CI fix --- docs/mcp/client.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 5bb3e357c..30534ac37 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -101,7 +101,7 @@ agent = Agent('openai:gpt-4o', mcp_servers=[server]) async def main(): async with agent.run_mcp_servers(): # No call to the MCP server is made here - result = await agent.run('What tools do you have access to?') # the first call is made here + await agent.run('What tools do you have access to?') # the first call is made here ``` From 287bf084a7949a9d84d5790766ad87be3a6ff6b5 Mon Sep 17 00:00:00 2001 From: Brandon Shar Date: Thu, 29 May 2025 18:59:21 -0400 Subject: [PATCH 3/4] we don't need to run this --- docs/mcp/client.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mcp/client.md b/docs/mcp/client.md index 30534ac37..511ee63a4 100644 --- a/docs/mcp/client.md +++ b/docs/mcp/client.md @@ -91,7 +91,7 @@ may want to skip sending the initialization request and only send requests as ne This allows you to de-couple instantiating your agents with connecting to MCP servers for situations like multi-agent systems or health checks. -```python {title="mcp_http_client_skip_initialization.py" py="3.10"} +```python {title="mcp_http_client_skip_initialization.py" test="skip"} from pydantic_ai import Agent from pydantic_ai.mcp import MCPServerHTTP From 4b8032ba4f2688cc51f37cb37da290c0b50b01d8 Mon Sep 17 00:00:00 2001 From: Brandon Shar Date: Thu, 29 May 2025 19:24:25 -0400 Subject: [PATCH 4/4] remove no-coverage and see if the coverage drops --- pydantic_ai_slim/pydantic_ai/mcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_ai_slim/pydantic_ai/mcp.py b/pydantic_ai_slim/pydantic_ai/mcp.py index 62376bf33..078ca4d78 100644 --- a/pydantic_ai_slim/pydantic_ai/mcp.py +++ b/pydantic_ai_slim/pydantic_ai/mcp.py @@ -376,7 +376,7 @@ async def client_streams( self, ) -> AsyncIterator[ tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]] - ]: # pragma: no cover + ]: async with streamablehttp_client( url=self.url, headers=self.headers,