Skip to content

Commit 947e174

Browse files
committed
Revert "Add support for MCP's Streamable HTTP transport (#1716)"
This reverts commit 8f83407. # Conflicts: # pydantic_ai_slim/pydantic_ai/mcp.py # pydantic_ai_slim/pyproject.toml # tests/test_mcp.py # uv.lock
1 parent a7b8074 commit 947e174

File tree

4 files changed

+53
-71
lines changed

4 files changed

+53
-71
lines changed

docs/mcp/client.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,24 @@ pip/uv-add "pydantic-ai-slim[mcp]"
1818

1919
PydanticAI comes with two ways to connect to MCP servers:
2020

21-
- [`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] which connects to an MCP server using the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport
21+
- [`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] which connects to an MCP server using the [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport
2222
- [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] which runs the server as a subprocess and connects to it using the [stdio](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) transport
2323

2424
Examples of both are shown below; [mcp-run-python](run-python.md) is used as the MCP server in both examples.
2525

26-
### HTTP Client
26+
### SSE Client
2727

28-
[`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] connects over HTTP using the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) to a server.
28+
[`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] connects over HTTP using the [HTTP + Server Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) to a server.
2929

3030
!!! note
3131
[`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] requires an MCP server to be running and accepting HTTP connections before calling [`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers]. Running the server is not managed by PydanticAI.
3232

33-
The StreamableHTTP Transport is able to connect to both stateless HTTP and older Server Sent Events (SSE) servers.
33+
The name "HTTP" is used since this implemented will be adapted in future to use the new
34+
[Streamable HTTP](https://github.com/modelcontextprotocol/specification/pull/206) currently in development.
3435

35-
Before creating the HTTP client, we need to run the server (docs [here](run-python.md)):
36+
Before creating the SSE client, we need to run the server (docs [here](run-python.md)):
3637

37-
```bash {title="terminal (run http server)"}
38+
```bash {title="terminal (run sse server)"}
3839
deno run \
3940
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
4041
jsr:@pydantic/mcp-run-python sse
@@ -55,7 +56,7 @@ async def main():
5556
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
5657
```
5758

58-
1. Define the MCP server with the URL used to connect. This will typically end in `/mcp` for HTTP servers and `/sse` for SSE.
59+
1. Define the MCP server with the URL used to connect.
5960
2. Create an agent with the MCP server attached.
6061
3. Create a client session to connect to the server.
6162

pydantic_ai_slim/pydantic_ai/mcp.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@
66
from collections.abc import AsyncIterator, Sequence
77
from contextlib import AsyncExitStack, asynccontextmanager
88
from dataclasses import dataclass
9-
from datetime import timedelta
109
from pathlib import Path
1110
from types import TracebackType
1211
from typing import Any
1312

1413
import anyio
1514
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
16-
from mcp.shared.message import SessionMessage
1715
from mcp.types import (
1816
BlobResourceContents,
1917
EmbeddedResource,
2018
ImageContent,
19+
JSONRPCMessage,
2120
LoggingLevel,
2221
TextContent,
2322
TextResourceContents,
@@ -30,8 +29,8 @@
3029

3130
try:
3231
from mcp.client.session import ClientSession
32+
from mcp.client.sse import sse_client
3333
from mcp.client.stdio import StdioServerParameters, stdio_client
34-
from mcp.client.streamable_http import streamablehttp_client
3534
except ImportError as _import_error:
3635
raise ImportError(
3736
'Please install the `mcp` package to use the MCP server, '
@@ -57,16 +56,19 @@ class MCPServer(ABC):
5756
"""
5857

5958
_client: ClientSession
60-
_read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
61-
_write_stream: MemoryObjectSendStream[SessionMessage]
59+
_read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception]
60+
_write_stream: MemoryObjectSendStream[JSONRPCMessage]
6261
_exit_stack: AsyncExitStack
6362

6463
@abstractmethod
6564
@asynccontextmanager
6665
async def client_streams(
6766
self,
6867
) -> AsyncIterator[
69-
tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
68+
tuple[
69+
MemoryObjectReceiveStream[JSONRPCMessage | Exception],
70+
MemoryObjectSendStream[JSONRPCMessage],
71+
]
7072
]:
7173
"""Create the streams for the MCP server."""
7274
raise NotImplementedError('MCP Server subclasses must implement this method.')
@@ -263,7 +265,10 @@ async def main():
263265
async def client_streams(
264266
self,
265267
) -> AsyncIterator[
266-
tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
268+
tuple[
269+
MemoryObjectReceiveStream[JSONRPCMessage | Exception],
270+
MemoryObjectSendStream[JSONRPCMessage],
271+
]
267272
]:
268273
server = StdioServerParameters(command=self.command, args=list(self.args), env=self.env, cwd=self.cwd)
269274
async with stdio_client(server=server) as (read_stream, write_stream):
@@ -283,11 +288,11 @@ def _get_client_initialize_timeout(self) -> float:
283288
class MCPServerHTTP(MCPServer):
284289
"""An MCP server that connects over streamable HTTP connections.
285290
286-
This class implements the Streamable HTTP transport from the MCP specification.
287-
See <https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http> for more information.
291+
This class implements the SSE transport from the MCP specification.
292+
See <https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse> for more information.
288293
289-
The Streamable HTTP transport is intended to replace the SSE transport from the previous protocol, but it is fully
290-
backwards compatible with SSE-based servers.
294+
The name "HTTP" is used since this implemented will be adapted in future to use the new
295+
[Streamable HTTP](https://github.com/modelcontextprotocol/specification/pull/206) currently in development.
291296
292297
!!! note
293298
Using this class as an async context manager will create a new pool of HTTP connections to connect
@@ -298,7 +303,7 @@ class MCPServerHTTP(MCPServer):
298303
from pydantic_ai import Agent
299304
from pydantic_ai.mcp import MCPServerHTTP
300305
301-
server = MCPServerHTTP('http://localhost:3001/mcp') # (1)!
306+
server = MCPServerHTTP('http://localhost:3001/sse') # (1)!
302307
agent = Agent('openai:gpt-4o', mcp_servers=[server])
303308
304309
async def main():
@@ -311,13 +316,13 @@ async def main():
311316
"""
312317

313318
url: str
314-
"""The URL of the SSE or MCP endpoint on the MCP server.
319+
"""The URL of the SSE endpoint on the MCP server.
315320
316-
For example for a server running locally, this might be `http://localhost:3001/mcp`.
321+
For example for a server running locally, this might be `http://localhost:3001/sse`.
317322
"""
318323

319324
headers: dict[str, Any] | None = None
320-
"""Optional HTTP headers to be sent with each request to the endpoint.
325+
"""Optional HTTP headers to be sent with each request to the SSE endpoint.
321326
322327
These headers will be passed directly to the underlying `httpx.AsyncClient`.
323328
Useful for authentication, custom headers, or other HTTP-specific configurations.
@@ -330,8 +335,8 @@ async def main():
330335
If the connection cannot be established within this time, the operation will fail.
331336
"""
332337

333-
sse_read_timeout: float = 300
334-
"""Maximum time as in seconds to wait for new SSE messages before timing out.
338+
sse_read_timeout: float = 5 * 60
339+
"""Maximum time in seconds to wait for new SSE messages before timing out.
335340
336341
This timeout applies to the long-lived SSE connection after it's established.
337342
If no new messages are received within this time, the connection will be considered stale
@@ -353,28 +358,21 @@ async def main():
353358
For example, if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar`
354359
"""
355360

356-
def __post_init__(self):
357-
# streamablehttp_client expects timedeltas, so we accept them too to match,
358-
# but primarily work with floats for a simpler user API.
359-
360-
if isinstance(self.timeout, timedelta):
361-
self.timeout = self.timeout.total_seconds()
362-
363-
if isinstance(self.sse_read_timeout, timedelta):
364-
self.sse_read_timeout = self.sse_read_timeout.total_seconds()
365-
366361
@asynccontextmanager
367362
async def client_streams(
368363
self,
369364
) -> AsyncIterator[
370-
tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]
365+
tuple[
366+
MemoryObjectReceiveStream[JSONRPCMessage | Exception],
367+
MemoryObjectSendStream[JSONRPCMessage],
368+
]
371369
]: # pragma: no cover
372-
async with streamablehttp_client(
370+
async with sse_client(
373371
url=self.url,
374372
headers=self.headers,
375-
timeout=timedelta(seconds=self.timeout),
376-
sse_read_timeout=timedelta(seconds=self.sse_read_timeout),
377-
) as (read_stream, write_stream, _):
373+
timeout=self.timeout,
374+
sse_read_timeout=self.sse_read_timeout,
375+
) as (read_stream, write_stream):
378376
yield read_stream, write_stream
379377

380378
def _get_log_level(self) -> LoggingLevel | None:

tests/test_mcp.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tests for the MCP (Model Context Protocol) server implementation."""
22

33
import re
4-
from datetime import timedelta
54
from pathlib import Path
65

76
import pytest
@@ -71,40 +70,25 @@ async def test_stdio_server_with_cwd():
7170
assert len(tools) == 10
7271

7372

74-
def test_http_server():
75-
http_server = MCPServerHTTP(url='http://localhost:8000/sse')
76-
assert http_server.url == 'http://localhost:8000/sse'
77-
assert http_server._get_log_level() is None # pyright: ignore[reportPrivateUsage]
73+
def test_sse_server():
74+
sse_server = MCPServerHTTP(url='http://localhost:8000/sse')
75+
assert sse_server.url == 'http://localhost:8000/sse'
76+
assert sse_server._get_log_level() is None # pyright: ignore[reportPrivateUsage]
7877

7978

80-
def test_http_server_with_header_and_timeout():
81-
http_server = MCPServerHTTP(
79+
def test_sse_server_with_header_and_timeout():
80+
sse_server = MCPServerHTTP(
8281
url='http://localhost:8000/sse',
8382
headers={'my-custom-header': 'my-header-value'},
8483
timeout=10,
8584
sse_read_timeout=100,
8685
log_level='info',
8786
)
88-
assert http_server.url == 'http://localhost:8000/sse'
89-
assert http_server.headers is not None and http_server.headers['my-custom-header'] == 'my-header-value'
90-
assert http_server.timeout == 10
91-
assert http_server.sse_read_timeout == 100
92-
assert http_server._get_log_level() == 'info' # pyright: ignore[reportPrivateUsage]
93-
94-
95-
def test_http_server_with_timedelta_arguments():
96-
http_server = MCPServerHTTP(
97-
url='http://localhost:8000/sse',
98-
headers={'my-custom-header': 'my-header-value'},
99-
timeout=timedelta(seconds=10), # type: ignore[arg-type]
100-
sse_read_timeout=timedelta(seconds=100), # type: ignore[arg-type]
101-
log_level='info',
102-
)
103-
assert http_server.url == 'http://localhost:8000/sse'
104-
assert http_server.headers is not None and http_server.headers['my-custom-header'] == 'my-header-value'
105-
assert http_server.timeout == 10
106-
assert http_server.sse_read_timeout == 100
107-
assert http_server._get_log_level() == 'info' # pyright: ignore[reportPrivateUsage]
87+
assert sse_server.url == 'http://localhost:8000/sse'
88+
assert sse_server.headers is not None and sse_server.headers['my-custom-header'] == 'my-header-value'
89+
assert sse_server.timeout == 10
90+
assert sse_server.sse_read_timeout == 100
91+
assert sse_server._get_log_level() == 'info' # pyright: ignore[reportPrivateUsage]
10892

10993

11094
@pytest.mark.vcr()

uv.lock

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)