Skip to content

Add StreamableHttpHandler and WithHttpTransport #291

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
merged 13 commits into from
Apr 14, 2025

Conversation

halter73
Copy link
Contributor

@halter73 halter73 commented Apr 12, 2025

This is still not an implementation of the "Streamable HTTP" transport, but it gets us a little bit closer.

This is a breaking change that now requires WithHttpTransport() to be called for MapMcp to work. This change also adds testing for IHttpContextAcessor and ensure that requests from different users for the same session are rejected. It also cleans up a few test namespaces like removing ModelContextProtocol.Test.Utils in favor of Tests.Utils.

I plan to remove also removed SseClientTransportOptions.MaxReconnectAttempts and ReconnectDelay. I don't think we want to be in the business of handling retry logic. That's better left to something like Polly.

@stephentoub
Copy link
Contributor

I plan to remove SseClientTransportOptions.MaxReconnectAttempts and ReconnectDelay.

Yes, please.

@halter73
Copy link
Contributor Author

I plan to remove SseClientTransportOptions.MaxReconnectAttempts and ReconnectDelay.

Yes, please.

Done.

@@ -24,136 +12,20 @@ public static class McpEndpointRouteBuilderExtensions
{
/// <summary>
/// Sets up endpoints for handling MCP HTTP Streaming transport.
Copy link
Contributor

Choose a reason for hiding this comment

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

And we don't want it to automatically do WithHttpTransport? Could/should it do it but then if a user calls it themselves it overrides whatever default MapMcp would have used?

Copy link
Contributor Author

@halter73 halter73 Apr 14, 2025

Choose a reason for hiding this comment

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

It's typical for things that need to configure routes like Mvc and SignalR to require that you call two methods: one to configure service, and then another to add the routes. For MVC, it's AddMvc and MapControllers/MapControllerRoutes. For SignalR, it's AddSignalR and MapHub. Even for minimal APIs, it's AddRouting, UseRouting, and MapGet/Post, but AddRouting and UseRouting get added explicitly for WebApplicationBuilder.

This is why I was so hesitant to add WithHttpTransport. The MapMcp call is the one thing we cannot easily get rid of. We could get rid of WithHttpTransport if we newed up the StreamableHttpHandler and any other services we need when they were missing, but it would be inconvenient for us as MapMcp implementers. And it is very conventional to need to call both an "Add" and a "Map" method, even if I'm personally not a fan of requiring the extra code.

else
{
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
await context.Response.WriteAsync("Method Not Allowed");
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this common practice, to write the status description into the response body as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Copilot did this, and I hit tab 😄 If this were routing producing the response, it'd be an empty. Fortunately, it's not too big of a deal, and as of now, this part is unreachable. I'll probably switch this to throw as a sort of assert later.

context.Features.GetRequiredFeature<IHttpResponseBodyFeature>().DisableBuffering();

var sessionId = MakeNewSessionId();
await using var transport = new SseResponseStreamTransport(response.Body, $"message?sessionId={sessionId}");
Copy link
Contributor

Choose a reason for hiding this comment

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

Were you saying something about wanting to data protect session IDs?

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 think I still might have to do this for stateless requests similar to what we do for antiforgery, since we won't be able to rely on the ConcurrentDictionary to track if things like the user ID changed if the client sends requests to multiple servers, but I haven't got there yet.

@stephentoub
Copy link
Contributor

I'm going to merge your change in order to rebase my logging change on top of it. My comments are all minor and can be addressed subsequently.

@stephentoub stephentoub merged commit 9c124b1 into modelcontextprotocol:main Apr 14, 2025
8 checks passed
@sonukaloshiya24
Copy link

@stephentoub

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.

None yet

3 participants