Skip to content

Fix support for streamable-http connections #339

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

Merged

Conversation

cliffhall
Copy link
Contributor

@cliffhall cliffhall commented Apr 21, 2025

Description

  • In server/index.js

    • add get/post handlers for /mcp
    • amend console log on SSE connect, with deprecation message
    • add /stdio GET handler and refactored /sse GET handler to not also do stdio. Each transport has its own handler now
    • add required headers to streamable-http request
    • add mcp-session-id and last-event-id to STREAMABLE_HTTP_HEADERS_PASSTHROUGH
  • In /client/src/lib/hooks/useConnection.ts

    • in connect function
      • create server url properly based on new transport type.

Motivation and Context

The PR that added support for streamable-http servers was incomplete, but it actually seemed to work because the proxy server had /sse and /message endpoints.

This adds /mcp GET/POST endpoint support to the MCP proxy server, and slightly refactors the creation of the transport in the UI.

How Has This Been Tested?

Locally against stdio, sse, and streamble-http servers

Streamable HTTP (/mcp)

mcp-endpooint

Stdio (/stdio + /message)

Screenshot 2025-04-21 at 11 43 41 AM

SSE (/sse + /message)

Screenshot 2025-04-21 at 11 45 48 AM

Breaking Changes

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Testing streamable-http with this PR on the everything server.

* In server/index.js
  - add get/post handlers for /mcp
  - amend console log on SSE connect, with deprecation message
  - add /stdio GET handler and refactored /sse GET handler to not also do stdio. Each transport has its own handler now
  - add appropriate headers to streamable-http request

* In /client/src/lib/hooks/useConnection.ts
  - in connect function
    - create server url properly based on new transport type.
@nbarbettini
Copy link

@cliffhall Small bug I found when testing with a different server that supports streamable HTTP:

useConnection.ts:370 Failed to connect to MCP Server via the MCP Inspector Proxy: http://localhost:6277/mcp?url=http%3A%2F%2Flocalhost%3A9099%2Fv1%2Fmcp&transportType=streamable-http: Error: Server's protocol version is not supported: 2025-03-26
    at Client.connect (@modelcontextprotocol_sdk_client_index__js.js?v=3f0711cd:414:15)
    at async connect (useConnection.ts:358:9)
connect @ useConnection.ts:370
await in connect
... <snip>

useConnection.ts:405 Error: Server's protocol version is not supported: 2025-03-26
    at Client.connect (@modelcontextprotocol_sdk_client_index__js.js?v=3f0711cd:414:15)
    at async connect (useConnection.ts:358:9)

In my case, since streamable HTTP was added in 2025-03-26, I was expecting the Inspector client to support it. Rewriting the server's version to 2024-11-05 got past this error.

@QuantGeekDev
Copy link

QuantGeekDev commented Apr 22, 2025

@cliffhall Small bug I found when testing with a different server that supports streamable HTTP:


useConnection.ts:370 Failed to connect to MCP Server via the MCP Inspector Proxy: http://localhost:6277/mcp?url=http%3A%2F%2Flocalhost%3A9099%2Fv1%2Fmcp&transportType=streamable-http: Error: Server's protocol version is not supported: 2025-03-26

    at Client.connect (@modelcontextprotocol_sdk_client_index__js.js?v=3f0711cd:414:15)

    at async connect (useConnection.ts:358:9)

connect @ useConnection.ts:370

await in connect

... <snip>



useConnection.ts:405 Error: Server's protocol version is not supported: 2025-03-26

    at Client.connect (@modelcontextprotocol_sdk_client_index__js.js?v=3f0711cd:414:15)

    at async connect (useConnection.ts:358:9)

In my case, since streamable HTTP was added in 2025-03-26, I was expecting the Inspector client to support it. Rewriting the server's version to 2024-11-05 got past this error.

Should be possible to fix by changing the inspector's protocol version that it sends to the server - server returns what it receives from the client

@ferrants
Copy link

I pulled this version of the inspector.
And ran the proposed streamable HTTP everything server npm run start:streamableHttp from modelcontextprotocol/servers#1496

I get the "SSE connection not established" (also referenced here #295) I mentioned in the original Streamable HTTP PR (#294 (comment)) very frequently. I was also getting this error with another server (https://github.com/ferrants/mcp-streamable-http-typescript-server), but was unsure if my server was to blame, so the official everything would be better to test with.

This is what I see

Screenshot from 2025-04-21 22-18-32

In the inspector logs

Set up MCP proxy
Received message for sessionId 4f0e9338-cb70-4699-9590-1ac931f5511c
Error in /message route: Error: SSE connection not established
    at SSEServerTransport.handlePostMessage (file:///home/matt/code/inspector/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sse.js:61:19)
    at file:///home/matt/code/inspector/server/build/index.js:150:25
    at Layer.handleRequest (/home/matt/code/inspector/node_modules/router/lib/layer.js:152:17)
    at next (/home/matt/code/inspector/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/home/matt/code/inspector/node_modules/router/lib/route.js:117:3)
    at handle (/home/matt/code/inspector/node_modules/router/index.js:435:11)
    at Layer.handleRequest (/home/matt/code/inspector/node_modules/router/lib/layer.js:152:17)
    at /home/matt/code/inspector/node_modules/router/index.js:295:15
    at processParams (/home/matt/code/inspector/node_modules/router/index.js:582:12)
    at next (/home/matt/code/inspector/node_modules/router/index.js:291:5)
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at new NodeError (node:internal/errors:406:5)
    at ServerResponse.setHeader (node:_http_outgoing:652:11)
    at ServerResponse.header (/home/matt/code/inspector/node_modules/express/lib/response.js:684:10)
    at ServerResponse.json (/home/matt/code/inspector/node_modules/express/lib/response.js:247:10)
    at file:///home/matt/code/inspector/server/build/index.js:154:25
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

It doesn't seem like others get this error in testing the Streamable HTTP transport, so my testing is probably invalid in some way, but I wanted to report my experience and see if i can help at all.

Thanks for your efforts on this.

@shivdeepak
Copy link
Contributor

FWIW, I tested this PR against modelcontextprotocol/servers#1496. It works fine for me.

Steps to test:

  1. Stop the everything server
  2. Stop the inspector
  3. Start the everything server
  4. Start the inspector
  5. Test the everything server from inspector Web UI.

I do this because I don't want any stale connections on the inspector backend to interact with the everything MCP Server.

@ferrants are there a different set of steps you are following to test this. If you are, would you be able to share steps to reproduce the error you are seeing?

@cliffhall
Copy link
Contributor Author

cliffhall commented Apr 22, 2025

FWIW, I tested this PR against modelcontextprotocol/servers#1496. It works fine for me.

It is the case that one instance of the inspector using this PR can connect to that server.

Problem is that the proxy server connection to the actual MCP server is still using an SSEServerTransport, just with /mcp as the endpoint. Most posts will work, as if they were going to the /message endpoint of an SSE server.

The problem that we're not seeing is in handling SSE events, which this new protocol fetches via a GET request. I believe that server will handle them properly, but the proxy server will not.

@cliffhall
Copy link
Contributor Author

NOTE: I'm working on updates to this PR, just resuming this morning.

  • The proxy server did not support /mcp endpoint.
  • The protocol is different from /sse + /message which was a GET to the former followed by POSTs to the latter.
  • Now it is all POSTs to /mcp except GETs are used to pick up SSE messages.
  • When the connection between the UI and the proxy server are worked out, the messages going to the everything server will hopefully be what is expected there.

@ferrants
Copy link

@shivdeepak , that is the set of steps I was using. I was setting up a recording and did a fresh npm install on both the inspector and server and am now unable to reproduce, so I wonder if I had some stale dependency. Ignore my error. I will report in a separate issue if I encounter it again.

Working with the everything server for me! Also worked with the template one I'm building off of the repo examples: https://github.com/ferrants/mcp-streamable-http-typescript-server

Thanks!

:shipit: 🚢

@cliffhall
Copy link
Contributor Author

cliffhall commented Apr 22, 2025

@nbarbettini @QuantGeekDev

Small bug I found when testing with a different server that supports streamable HTTP:
...
In my case, since streamable HTTP was added in 2025-03-26, I was expecting the Inspector client to support it. Rewriting the server's version to 2024-11-05 got past this error.

I think this is owing to the fact that the Inspector's

It has the illusion of working, but it really isn't properly proxying yet. I've been pushing air around in a balloon dog all day trying to get that to work properly, but it's just not having any of it.

@QuantGeekDev
Copy link

QuantGeekDev commented Apr 22, 2025

I forked the inspector a few weeks ago and ran into this - for me, removing the backend connection and instead connecting directly from browser helped with this but ti's been a while and I'm fuzzy about the coding. I have a version of it here: https://github.com/QuantGeekDev/mcp-debug you can try it with npx mcp-debug - you can check the browser network tab to verify how the connection is being made. It's experimental so it probably won't be useful 1:1 but I find the direct connection to be more representative of how I expect to connect to an mcp server from a client. I'm not planning on updating mcp-debug since it was just a quick workaround to get the http inspector live and I intend to switch back to the official inspector when streamable http is implemented

I'm at an offsite for the this week but if it's still an issue next week I'm happy to jump in and try my hand at it

@cliffhall cliffhall marked this pull request as draft April 22, 2025 21:56
… works fine with STDIO and SSE servers.

* In index.ts,
  - refactor transport webAppTransports to be a map with the session id as key and transport as value.

* Implement /mcp GET and POST endpoints using StreamableHTTPServerTransport and doing the new session in the POST (opposite from SSE) handler.

* In package.json
  - update the SDK to 1.10.2

* In useConnection.ts
  - import StreamableHTTPClientTransport
    - NOTE: while we NEED to do this, it causes useConnection.test.ts to fail with " ReferenceError: TransformStream is not defined"
  - in connect method
    - instantiate the appropriate transport
@shivdeepak
Copy link
Contributor

shivdeepak commented Apr 23, 2025

@cliffhall I have spend close 5 hours trying to figure out why MCPProxy with StreamableHTTPClient on the browser is not working.

Here are my findings:

I tried following scenarios:

Scenario 1 (Current Impl on main):

SSEClient (Browser) -> SSEServer (Inspector Backend) -> StreamableHTTPClient (Inspector Backend) -> StreamableHTTPServer (Everything Server)

This works fine.

Scenario 2 (Using StreamableHTTPClient/Server as a Proxy):

StreamableHTTPClient (Browser) -> StreamableHTTPServer (Inspector Backend) -> StreamableHTTPClient (Inspector Backend) -> StreamableHTTPServer (Everything Server)

This doesn't work because the initialize request's response payload seems empty on the browser dev console.. It works fine when run from Node.js.

Scenario 3 (Independent Vite App that uses StreamableHTTPClient):

I am running into the same issue as Scenario 2.

On the browser, I don't see the response payload for the initialize request.

Screenshot 2025-04-23 at 12 30 28 AM Screenshot 2025-04-23 at 12 30 45 AM

This leads me to believe that there is some issue with StreamableHTTPServer that is preventing it from sending data properly to the web browser. But that is not the case.

I also ran mcp-sniffer between the client and server, to see if the server is not sending the data properly. But looks like it does, the response is present in the payload.

Screenshot 2025-04-23 at 12 32 57 AM Screenshot 2025-04-23 at 12 34 58 AM

So, finally, I believe it's the browser (chrome and safari) that is not able to deserialize the SSE Message and Possibly the HTTP Response is not up to standard. i.e. Node.js can understand it, but Google Chrome cannot.

it's also possible that the StreamableHTTPClient behaves as expected on Node.js, but not the browser. But Chrome not showing the response payload is not normal

However, I do see Postman, and Safari show the response payload. But Safari has the same behavior as Chrome. The StreamableHTTPClient is not sending the following initialize notification with mcp-session-id header.

Screenshot 2025-04-23 at 12 53 53 AM Screenshot 2025-04-23 at 12 54 15 AM Screenshot 2025-04-23 at 12 53 24 AM

Note: I also have cors enabled on the server side, so I am pretty sure CORS is not the issue.

@shivdeepak
Copy link
Contributor

Found the fix! It's indeed a CORS related issue. Headers are not visible in the cross origin requests until they are whitelisted by the server.

Once I added this in the middleware:

app.use((req, res, next) => {
  res.header("Access-Control-Expose-Headers", "mcp-session-id");
  next();
});

I am able to see the mcp-session-id in the frontend.

Screenshot 2025-04-23 at 1 16 29 AM

This was not a problem for HTTP+SSE transport because session id was in the SSE payload, and not in the header.

@shivdeepak
Copy link
Contributor

Here is the PR with the fix -- cliffhall#1

Tested on local. Seems to be working fine.

fix cors issue with accessing mcp-session-id header
@cliffhall
Copy link
Contributor Author

cliffhall commented Apr 23, 2025

After @shivdeepak's fix for the session id header problem, we are cooking with gas now. Full streamable-http connection from client to proxy to server and back.

The only outstanding problem is that the React unit tests are failing now with ReferenceError: TransformStream is not defined, apparently just from importing StreamableHTTPClientTransport from the SDK. I'm looking into what's going on there.
Screenshot 2025-04-23 at 5 13 27 PM

I'm currently running Node 20 and TransformStream exists.
Screenshot 2025-04-23 at 5 19 37 PM

EDIT: This is problem is now fixed with jest-fixed-jsdom.

halter73
halter73 previously approved these changes Apr 24, 2025
Copy link

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

I tested this with the sample Streamable HTTP server at https://github.com/modelcontextprotocol/csharp-sdk/tree/v0.1.0-preview.11/samples/AspNetCoreSseServer (it has yet to be renamed from "Sse"), and things seem to work pretty well. I tested sampling using the "sampleLLM" tool, and that worked nicely.

Unfortunately, the Streamable HTTP transport also does not immediately report a connection error and ask "Connection Error, is your MCP server running?" when the connection is refused because there's no server listening like the SSE transport does. It just stays in the "Disconnected" state not indicating that it's connecting or that it even registered you clicking the connect button until about a minute later when it shows an error.

SSE ECONREFUSED logs
[1] New SSE connection. NOTE: The sse transport is deprecated and has been replaced by streamable-http
[1] Query parameters: [Object: null prototype] {
[1]   url: 'http://localhost:3001/',
[1]   transportType: 'sse'
[1] }
[1] SSE transport: url=http://localhost:3001/, headers=Accept
[1] Error in /sse route: SseError: SSE error: TypeError: fetch failed: connect ECONNREFUSED ::1:3001, connect ECONNREFUSED 127.0.0.1:3001
[1]     at EventSource._eventSource.onerror (H:\modelcontextprotocol\inspector\node_modules\@modelcontextprotocol\sdk\src\client\sse.ts:133:23)
[1]     at EventSource.scheduleReconnect_fn (H:\modelcontextprotocol\inspector\node_modules\eventsource\src\EventSource.ts:557:5)
[1]     at <anonymous> (H:\modelcontextprotocol\inspector\node_modules\eventsource\src\EventSource.ts:441:5)
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
[1]   code: undefined,
[1]   event: {
[1]     type: 'error',
[1]     message: 'TypeError: fetch failed: connect ECONNREFUSED ::1:3001, connect ECONNREFUSED 127.0.0.1:3001',
[1]     code: undefined,
[1]     defaultPrevented: false,
[1]     cancelable: false,
[1]     timeStamp: 2422561.2383
[1]   }
[1] }
Streamable HTTP ECONREFUSED logs
[1] New streamable-http connection
[1] Query parameters: [Object: null prototype] {
[1]   url: 'http://localhost:3001/',
[1]   transportType: 'streamable-http'
[1] }
[1] Connected to Streamable HTTP transport
[1] Connected MCP client to backing server transport
[1] Created streamable web app transport e020a675-58df-4165-bc34-a20b4ba4763c
[1] Error from MCP server: TypeError: fetch failed
[1]     at node:internal/deps/undici/undici:13502:13
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]     at async StreamableHTTPClientTransport.send (H:\modelcontextprotocol\inspector\node_modules\@modelcontextprotocol\sdk\src\client\streamableHttp.ts:394:24) {
[1]   [cause]: AggregateError [ECONNREFUSED]:
[1]       at internalConnectMultiple (node:net:1139:18)
[1]       at afterConnectMultiple (node:net:1712:7) {
[1]     code: 'ECONNREFUSED',
[1]     [errors]: [ [Error], [Error] ]
[1]   }
[1] }

The connection error reporting issue could probably be fixed in a follow up. It's great to have the mcp-session-id header now forwarded and see the inspector working end-to-end with the Streamable HTTP transport. Thanks!

olaservo
olaservo previously approved these changes Apr 24, 2025
Copy link
Collaborator

@olaservo olaservo left a comment

Choose a reason for hiding this comment

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

Should we also add anything to the Readme related to this update?

@cliffhall
Copy link
Contributor Author

... I find the direct connection to be more representative of how I expect to connect to an MCP server from a client.

@QuantGeekDev As I was working on this PR I questioned why we even needed this proxy. I came to the conclusion that it is only truly necessary for STDIO servers, which you could not access directly from a web browser. It would be possible to make the client connect directly to SSE/StreamableHTTP servers, and there's an argument to be made that it would be more illustrative to devs than the complicated proxy path we are using. But since the proxy is required for STDIO, it's just easier to use the proxy for all connections, and I suspect this is how it will continue to work for the near future.

- Add last-event-id to STREAMABLE_HTTP_HEADERS_PASSTHROUGH.
@cliffhall cliffhall dismissed stale reviews from olaservo and halter73 via 7b9cd1e April 24, 2025 15:39
@cliffhall
Copy link
Contributor Author

Should we also add anything to the Readme related to this update?

@olaservo As we were discussing in Discord, I think we need to expand the README into a more full-blown set of docs that would go deeper on the different transport types and features, etc.

But currently, there's no section that talks about SSE vs STDIO where we would, so I don't think any of these changes necessitate doc updates. A new transport type is available, the old ones are still there. IMO, there's nothing operating differently to callout now.

@cliffhall cliffhall requested a review from olaservo April 24, 2025 15:52
@cliffhall
Copy link
Contributor Author

The connection error reporting issue could probably be fixed in a follow up.

Hi @halter73! Thanks for reporting this discrepancy. I opened a new issue based on it.

@olaservo
Copy link
Collaborator

Are we missing specifying the protocolVersion in the client initialization? I see it mentioned that we need to specify the latest spec but I was having trouble finding that part of the client code.

@cliffhall
Copy link
Contributor Author

Are we missing specifying the protocolVersion in the client initialization? I see it mentioned that we need to specify the latest spec but I was having trouble finding that part of the client code.

@olaservo protocolVersion is added by the connect method of the Client class in the SDK.

@sbosio
Copy link

sbosio commented Apr 24, 2025

@cliffhall We're building an authenticated Remote MCP Server based on the Streamable HTTP Transport and we tested it with the changes on your branch, but it seems the authentication flow isn't being triggered when the server returns a 401 Unauthorized as opposed to the SSE transport implementation. Do you know why is that?

@cliffhall
Copy link
Contributor Author

cliffhall commented Apr 24, 2025

@cliffhall We're building an authenticated Remote MCP Server based on the Streamable HTTP Transport and we tested it with the changes on your branch, but it seems the authentication flow isn't being triggered when the server returns a 401 Unauthorized as opposed to the SSE transport implementation. Do you know why is that?

Hi @sbosio! That's a great and timely catch.

  • In the inspector's proxy we are handling 401s the same way for sse as for streamable-http.
  • In the inspector client, we handle it in two separate places, neither of which are unique to sse vs streamable-http.
  • In the typescript-sdk, I notice that we deal with it similarly in both sse.ts and streamableHttp.ts but only sse has an explicit unit test for the case.

@ihrpr can you add a unit test to streamableHttp.test.ts that is similar to the "attempts auth flow on 401 during SSE connection" or "attempts auth flow on 401 during POST request" so we can rule out the SDK?

@ferrants
Copy link

Is the authentication flow supposed to be working already? I figured that would be follow-up work after this gets in. I'm also looking to leverage that. Very timely.

@sbosio
Copy link

sbosio commented Apr 25, 2025

Is the authentication flow supposed to be working already? I figured that would be follow-up work after this gets in. I'm also looking to leverage that. Very timely.

I didn't follow. Were you referring to the SDK or the Inspector? We're putting up a POC and we were able to make everything work using the latest version of the Inspector using the mcp-remote proxy.

Then I tried this branch and selected the new Transport type from the dropdown, but when I put the MCP endpoint in the URL and click Connect I only get the 401 error and the OAuth flow isn't triggered.

OTOH, if I use the current Inspector and the SSE transport type (which the server really doesn't implement because it's a stateless server with the Streamable HTTP transport) and set the URL to the base domain of the server, the OAuth flow works perfectly, but it obviously fails when getting the root path, because that endpoint doesn't exists.

@ferrants
Copy link

@sbosio I was referring to the inspector. I didn't know the oauth flow was set up to work in the inspector yet. Thanks.

@fredericbarthelet
Copy link

@ihrpr can you add a unit test to streamableHttp.test.ts that is similar to the "attempts auth flow on 401 during SSE connection" or "attempts auth flow on 401 during POST request" so we can rule out the SDK?

@cliffhall, the test "attempts auth flow on 401 during SSE connection" asserts UnauthorizedError is thrown on SSE transport start (the trigger that actually redirect user to auth).
However, StreamableHTTP transport start implementation is a no-op, it'll never throw.

As for the second test, I opened up a PR - assertion checks out, like for SSE -> modelcontextprotocol/typescript-sdk#411

@cliffhall
Copy link
Contributor Author

@sbosio I was referring to the inspector. I didn't know the oauth flow was set up to work in the inspector yet. Thanks.

There is support for OAuth in the inspector. See this previously merged PR which has a demo video showing operation.

That said the new authorization spec recently landed and there is more work to do to ensure compliance.

@cliffhall
Copy link
Contributor Author

Is the authentication flow supposed to be working already? I figured that would be follow-up work after this gets in. I'm also looking to leverage that. Very timely.

Given that this fixes streamable-http and that we are preparing to do a bunch of work to get the inspector into compliance with the new auth spec, I feel like holding this one up for another week of reviews is counterproductive.

We will get a reference server implementing streamable-http that we can test against and work that issue separately.

@cliffhall cliffhall merged commit cdab12a into modelcontextprotocol:main Apr 25, 2025
2 checks passed
@cliffhall cliffhall deleted the fix-streamable-endpoint branch April 25, 2025 15:26
This was referenced Apr 27, 2025
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.

9 participants