Skip to content

StreamableHttp client transport #573

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

Open
wants to merge 10 commits into
base: ihrpr/get-sse
Choose a base branch
from
Open

StreamableHttp client transport #573

wants to merge 10 commits into from

Conversation

ihrpr
Copy link
Contributor

@ihrpr ihrpr commented Apr 23, 2025

Adding support for Streamable Http client transport

Follow ups

  • Resumability
  • Example for fast mcp
  • client example

Copy link
Contributor

@jerome3o-anthropic jerome3o-anthropic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable - some questions. Also it would be nice to refactor this to be less nested (I will re-review quick if you decide to - also, I asked claude what it thought for a refactor - I don't love it but it could be a good start to iterate on)

Tuple of (read_stream, write_stream, terminate_callback)
"""

read_stream_writer, read_stream = anyio.create_memory_object_stream[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] I find these names kinda confusing - I've started using client_to_server_read/client_to_server_write etc because I find it easier to understand

I know that we use this names in a lot of places, just flagging here because I think it would be good to start changing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to change it everywhere when I get a moment. I literally had to write it down on a piece of paper to remember which one is which, lol. I think having them in one place different will complicate things, but agree need to change it everywhere!

continue
# Check for 404 (session expired/invalid)
if response.status_code == 404:
if is_initialization and session_id:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing with an existing session-id seems like an odd edge case - if initializing wouldn't we expect there to be no session id? perhaps ignoring session id on initialization (or raising?) would be better / more in line with the spec?

Copy link
Contributor Author

@ihrpr ihrpr Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, it's wrong. Basically this is to cover the re-connect for:

When a client receives HTTP 404 in response to a request containing an Mcp-Session-Id, it MUST start a new session by sending a new InitializeRequest without a session ID attached.

if is_initialization - we just need to fail
if session_id - try to reconnect, BUT we need to do it on the client not client transport to have the correct initializatoin handshake

@@ -0,0 +1,309 @@
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] some of the code here is getting super nested - would love to see things broken out into functions where possible

url: str,
headers: dict[str, Any] | None = None,
timeout: float = 30,
sse_read_timeout: float = 60 * 5,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a timedelta?

async def streamablehttp_client(
url: str,
headers: dict[str, Any] | None = None,
timeout: float = 30,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here? I assume generally we have used float for timeouts, so this is likely fine, but i assume using timedeltas would be more correct here.

Comment on lines 77 to 81
async def post_writer():
nonlocal session_id
try:
async with write_stream_reader:
async for message in write_stream_reader:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move post_writer out or at least find another way to not have so many indentions that the code becomes very much squeezed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package name is weird and not according to pep 8 recommendations: https://peps.python.org/pep-0008/#package-and-module-names. Should be all lowercase, something like mcp.client.http.streamable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, Typescript... thanks, will update!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants