Skip to content

HttpServlet Streamable HTTP server implementation #290

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

ZachGerman
Copy link
Contributor

@ZachGerman ZachGerman commented Jun 2, 2025

Not planned:

  • Backward-compatible endpoint combo

Motivation and Context

This has been created by collaboration between @ZachGerman, @chemicL, & @tzolov to bring the much desired feature of Streamable HTTP transport to the Java SDK with three transport providers to choose from.

How Has This Been Tested?

Unit testing along with a suite of abstract integration tests.

Breaking Changes

N/A

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

Copy link
Member

@chemicL chemicL left a comment

Choose a reason for hiding this comment

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

Hey 👋 Thanks for a comprehensive PR! I did my first round focusing on the main themes. Happy to offer guidance to cover the essential aspects (simple/stateful servers, multiple streams per session, lifecycle) if you'd like to push this forward.

@ZachGerman
Copy link
Contributor Author

Thank you very much for all of the input @chemicL! I will begin making changes accordingly this afternoon.

@ZachGerman
Copy link
Contributor Author

Today I'm targeting origin header validation and moving the dedicated GET stream to the StreamableHttpSession class, then adding an integ test for GET on /mcp to start the listening stream.
After that, I believe we should have all core functionality except sessionless and proper SSE response upgrade logic.

@sivankri
Copy link

Hi @ZachGerman I tried using your StreamableHttpServerTransportProvider.java file + java MCP SDK 0.10.0. My backend server is Jetty 12. The request from MCP Interceptor hangs in the below call.

return streamSession.handle(message).then(Mono.just(responseType)).onErrorReturn(ResponseType.IMMEDIATE);

This is the actuall line which gets blocked
--McpServerSession.java --> return var10000.flatMap(var10001::sendMessage);

Can you please check this?

@tzolov
Copy link
Contributor

tzolov commented Jun 26, 2025

Today I'm targeting origin header validation and moving the dedicated GET stream to the StreamableHttpSession class, then adding an integ test for GET on /mcp to start the listening stream. After that, I believe we should have all core functionality except sessionless and proper SSE response upgrade logic.

@ZachGerman what do you mean by origin header validation? I hope it is not overlapping with the #284

Copy link
Member

@chemicL chemicL left a comment

Choose a reason for hiding this comment

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

Added a few more comments.

if (sessionId == null) {
response.setContentType(APPLICATION_JSON);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write(createErrorJson("Session ID missing in request header"));
Copy link
Member

@chemicL chemicL Jun 26, 2025

Choose a reason for hiding this comment

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

modelcontextprotocol/modelcontextprotocol#282 please check this issue. I'm leaning towards separating the JSON-RPC lifecycle from the lower level transport lifecycle. In that world, GET and POST and session concepts are not at the JSON-RPC layer, so the lifecycle does not apply. In fact, the previous SSE transport did begin with a HTTP GET request before the initialization request could have been sent. For the stateless servers it should definitely be improved in the spec to mention that initialization is not required before other requests.

[EDIT]

For the record, the above was wishful thinking. I agree we are only able to generate the session ID upon POST of the initialize request. The spec is in fact melding the json-rpc layer with http, so the session ID gets generated only when processing a initialization request:

A server using the Streamable HTTP transport MAY assign a session ID at initialization time, by including it in an Mcp-Session-Id header on the HTTP response containing the InitializeResult

// Subscribe to the SSE stream and write events to the response
sseStream.getEventFlux().doOnNext(event -> {
try {
if (event.id() != null) {
Copy link
Member

Choose a reason for hiding this comment

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

Right, but do we emit events with no ID ? Reading the code I don't see a situation like this. My understanding of the mentioned spec is that if the ID is missing then the client would not use this id for Last-Event-ID tracking, but we are on the server side and we always generate an ID.

@ZachGerman
Copy link
Contributor Author

ZachGerman commented Jun 26, 2025

@sivankri: Changing the related logic after my meeting with Dariusz this morning, so (hopefully) your issue goes away with the new response type mapping.
@tzolov: It's related, but I don't think it's overlapping as I'm just adding the ability to set a list of allowed origins to the server transport provider and enforcing it if it's set.

@viyaviya
Copy link

merge commit 6c4830b is bad

@ZachGerman
Copy link
Contributor Author

ZachGerman commented Jun 27, 2025

merge commit 6c4830b is bad

Oops! Thanks for the heads up! Fixed!

@ZachGerman ZachGerman force-pushed the StreamableHttpServerTransportProvider branch from 6c4830b to 7817da6 Compare June 27, 2025 16:45
@ZachGerman
Copy link
Contributor Author

ZachGerman commented Jun 27, 2025

@sivankri the new logic added streamTools to McpServerFeatures, used in the Async constructor, which facilitates a list of tool specifications that return Flux instead of Mono via the new AsyncStreamingToolSpecification record type and uses the return type of the call to differentiate between direct-HTTP and SSE-stream responses.

@sivankri
Copy link

sivankri commented Jul 1, 2025

@ZachGerman I tried running my mcp remote server against the mcp sdk generated from your branch. When i used the MCP inspector tool by setting query parameter transport=streamable-http, i could not get the connection established. Can you please help me with this?

@ZachGerman ZachGerman changed the title Adding StreamableHttpServerTransportProvider class and unit tests Adding StreamableHttpServerTransportProvider class along with unit & integration tests Jul 1, 2025
@ZachGerman
Copy link
Contributor Author

ZachGerman commented Jul 1, 2025

@sivankri I'm unfamiliar with the MCP inspector tool, but I imagine it's related to the fact that this JDK has public static final String LATEST_PROTOCOL_VERSION = "2024-11-05"; in the McpSchema spec class & that is currently used for all server instantiation. You can update it locally and the server from my branch, as well as @chemicL's webClient, should work with the update, but it will break a lot of tests. I plan on tackling protocol version negotiation soon if nobody else does.

@ZachGerman ZachGerman force-pushed the StreamableHttpServerTransportProvider branch 2 times, most recently from 244208d to 25ff3dc Compare July 8, 2025 06:40
@ZachGerman
Copy link
Contributor Author

ZachGerman commented Jul 8, 2025

Once I realized that I needed to identify the transports in the exchange's session from the tool handlers, I realized I need to either pass the entire JSONRPCRequest to the handler (instead of the request.params()), or add a third request.id() parameter to tool handlers.

Either way, it brought more attention to the fact that we were using Object for JSONRPCRequest/Response id, so I added a little sub-class in the McpSchema to enforce the "(String | Number) && Non-null" requirements of that field.

Tomorrow I will be making the JSONRPCRequest id available to tool handlers and it can be passed to the McpAsyncServerExchange class methods in order to route notifications and requests to the related SSE stream.


Edit: Ended up going with a per-request exchange instance instead of passing transport ID to tool call handlers. This allows tool implementers to not worry about transport routing with their exchange method calls.

chemicL added 3 commits July 22, 2025 18:24
Signed-off-by: Dariusz Jędrzejczyk <[email protected]>
Signed-off-by: Dariusz Jędrzejczyk <[email protected]>
@tzolov tzolov added this to the 0.11.0 milestone Jul 25, 2025
@ZachGerman ZachGerman force-pushed the StreamableHttpServerTransportProvider branch from 29a39f7 to c23c5df Compare July 29, 2025 20:17
@ZachGerman ZachGerman changed the title Adding StreamableHttpServerTransportProvider class along with unit & integration tests Streamable HTTP Server abstractions and HttpServlet, WebFlux, & WebMVC transport providers Jul 30, 2025
chemicL and others added 11 commits July 30, 2025 11:03
Signed-off-by: Dariusz Jędrzejczyk <[email protected]>
…protocol#425)

- Add WebMvcStreamableServerTransportProvider with SSE support for streamable sessions
- Support GET, POST, DELETE endpoints for MCP protocol operations
- Implement thread-safe SSE operations using ReentrantLock in WebMvcSseServerTransportProvider
- Add test infrastructure with AbstractMcpClientServerIntegrationTests
  - Refactor WebMvcStreamableIntegrationTests to use parameterized tests
  - Support testing with both HttpClient and WebFlux transports
- Add streamable transport tests for both async and sync server modes
- Refactor existing WebMVC SSE integration tests to use shared test base
- Add error handling improvements in McpStreamableServerSession
- Update dependencies: add json-unit-assertj for enhanced JSON testing
- Reorganize POM dependencies and add mcp-spring-webflux test dependency

This change enables Spring WebMVC applications to use the streamable MCP transport
protocol with proper thread safety guarantees and comprehensive test coverage.

Related to modelcontextprotocol#72

Signed-off-by: Christian Tzolov <[email protected]>
- Wrap consumer, tool, resource, prompt, and completion handler calls with Mono.defer()
- Ensures proper lazy evaluation and error handling in reactive streams

Signed-off-by: Christian Tzolov <[email protected]>
- Add HttpServletStreamableServerTransportProvider for MCP streamable transport
- Provides HttpServlet equivalent of WebMvcStreamableServerTransportProvider without Spring dependencies
- Support for GET (SSE connections/replay), POST (JSON-RPC messages), and DELETE (session cleanup) operations
- Include comprehensive integration test suite with sampling, elicitation, roots, and tools testing
- Add async/sync server test implementations for HttpServlet transport

Signed-off-by: Christian Tzolov <[email protected]>
Signed-off-by: Christian Tzolov <[email protected]>
@tzolov tzolov force-pushed the StreamableHttpServerTransportProvider branch from 5c09fa5 to b4b33d6 Compare July 30, 2025 14:43
@tzolov tzolov changed the title Streamable HTTP Server abstractions and HttpServlet, WebFlux, & WebMVC transport providers HttpServlet Streamable HTTP server implementation Jul 30, 2025
@chemicL chemicL merged commit bde1b6b into modelcontextprotocol:main Jul 30, 2025
1 of 2 checks passed
@qcloop
Copy link

qcloop commented Jul 30, 2025

Thanks for great work. Is there a coordination with Spring AI MCP? Would love to take it for a test drive

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.

7 participants