-
Notifications
You must be signed in to change notification settings - Fork 992
Add Streamable HTTP Transport to MCPServerHTTP #1905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
PR Change SummaryRe-implemented Streamable HTTP Transport as an opt-in feature, allowing users to specify the server type when connecting.
Modified Files
How can I customize these reviews?Check out the Hyperlint AI Reviewer docs for more information on how to customize the review. If you just want to ignore it on this PR, you can add the Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add |
@@ -358,22 +357,38 @@ async def main(): | |||
For example, if `tool_prefix='foo'`, then a tool named `bar` will be registered as `foo_bar` | |||
""" | |||
|
|||
transport: Literal['sse', 'streamable-http'] = 'sse' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These arguments match the underlying terms that FastMCP uses
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have a chance, could you see if it would be easy to implement an 'auto'
option that still defaults to SSE for backward compatibility, but switches to streamable-http when the path is /mcp
?
|
||
|
||
@pytest.fixture | ||
def mcp_server(request: pytest.FixtureRequest): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels complex, but I couldn't figure out a simpler way.
@@ -31,7 +36,6 @@ | |||
pytestmark = [ | |||
pytest.mark.skipif(not imports_successful(), reason='mcp and openai not installed'), | |||
pytest.mark.anyio, | |||
pytest.mark.vcr, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calling this out, I think all VCR tests are marked individually, but something about this blocked not the initial connection but the subsequent calls within the server.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this is correct. You need to use --record-mode=rewrite
when running the first time for the first test.
I am confused about the test failure. Seems like a dependency issue maybe just in CI? I hate to say the tests pass on my machine, but... 😄 |
@@ -18,22 +18,19 @@ pip/uv-add "pydantic-ai-slim[mcp]" | |||
|
|||
PydanticAI comes with two ways to connect to MCP servers: | |||
|
|||
- [`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 | |||
- [`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) or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- [`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) or [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) transport | |
- [`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] which connects to an MCP server using the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) or older [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport |
- [`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 | ||
|
||
Examples of both are shown below; [mcp-run-python](run-python.md) is used as the MCP server in both examples. | ||
|
||
### SSE Client | ||
### SSE or Streamable HTTP Client |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
### SSE or Streamable HTTP Client | |
### HTTP Client |
|
||
[`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. | ||
[`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) or the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) to a server. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[`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) or the [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) to a server. | |
[`MCPServerHTTP`][pydantic_ai.mcp.MCPServerHTTP] connects to a server over HTTP using [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) or [HTTP + Server Sent Events](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse). |
[Streamable HTTP](https://github.com/modelcontextprotocol/specification/pull/206) currently in development. | ||
|
||
Before creating the SSE client, we need to run the server (docs [here](run-python.md)): | ||
Before creating the client, we need to run the server (docs [here](run-python.md)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before creating the client, we need to run the server (docs [here](run-python.md)): | |
For backwards compatibility, the default transport is SSE. If your server instead uses Streamable HTTP, you have to specify the `transport='streamable-http'` option when creating `MCPServerHTTP`. | |
Before creating the client, we need to run the server (docs [here](run-python.md)): |
@@ -84,6 +81,10 @@ Will display as follows: | |||
|
|||
 | |||
|
|||
#### Transport | |||
|
|||
`MCPServerHTTP` supports both SSE (`sse`) and Streamable HTTP (`streamable-http`) servers which can be specified via the `transport` parameter. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can drop this section as I suggested mentioning the transport
arg above.
It'd be nice if we could also have a Streamable HTTP example!
@pytest.fixture | ||
def mcp_server(request: pytest.FixtureRequest): | ||
proc = multiprocessing.Process(target=request.param, daemon=True) | ||
print('Staring streamable http server process on port port') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print('Staring streamable http server process on port port') |
try: | ||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
s.connect(('localhost', 8000)) | ||
print('MCP server started.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
print('MCP server started.') |
|
||
|
||
def run_streamable_http_server() -> None: | ||
"""Run the SSE MCP Server""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Run the SSE MCP Server""" | |
"""Run the Streamable HTTP MCP Server""" |
@@ -42,6 +46,71 @@ def agent(openai_api_key: str): | |||
return Agent(model, mcp_servers=[server]) | |||
|
|||
|
|||
def run_sse_server() -> None: | |||
"""Run the Streamable HTTP MCP server.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Run the Streamable HTTP MCP server.""" | |
"""Run the SSE MCP server.""" |
while attempt < max_attempts: | ||
try: | ||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
s.connect(('localhost', 8000)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use a randomly assigned port here, we can't be sure 8000 will be available
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like FastMCP has its own host and port settings, and the run
method runs the entire server. Do we need to run it in a separate process at all? Could we use a thread? It looks like FastMCP.run
calls anyio.run(self.run_sse_async)
. We could likely do something ourselves to run the run_sse_async
async method in a thread.
@BrandonShar Thanks for submitting this! Finally got around to reviewing it :) It looks like you're trying to use a newer version of mcp, could you please update |
@@ -31,7 +36,6 @@ | |||
pytestmark = [ | |||
pytest.mark.skipif(not imports_successful(), reason='mcp and openai not installed'), | |||
pytest.mark.anyio, | |||
pytest.mark.vcr, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this is correct. You need to use --record-mode=rewrite
when running the first time for the first test.
I'm not sure which PR should go though. |
I think we'll be adding a lot more stuff on the streaming implementation, so I think it's preferable to have those 2 classes. |
But thanks for the PR. 🙏 |
Unfortunately for #1716, it turns out the underlying library was not backwards compatible.
This re-implementation adds a param to allow the user to specify what type of server they're connecting to.
Note: I considered adding an auto option (and that still may be a good idea), but I thought i'd keep it simple for the
firstattempt at this.Fixes #1632