-
Notifications
You must be signed in to change notification settings - Fork 235
Use Kestrel for all in-memory HTTP tests #225
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
Use Kestrel for all in-memory HTTP tests #225
Conversation
I still hope to have a PR open tomorrow with a lot more Streamable HTTP functionality similar to what's currently in these draft PRs:
But as you can tell from both of these still being draft PRs, we're still in the very early days of support for Streamable HTTP as defined in the 2025-03-26 spec. I also expect the MapMcp signature to change significantly going forward. Instead of taking the configureOptionsAsync and runTransportAsync as direct parameters, I plan to add HttpMcpServerOptions to configure these callbacks. I also plan to move most of the implementation into a proper service with constructor-injected dependencies. I held off on this for now as this will require people update their code to call IMcpServerBuilder.WithAspNetCoreTransport() or something like that, and this PR was already getting large enough. This sets the groundwork for better support by significantly improving the testing and development inner loop by making the test runs much faster and allowing you to easily put both server-side and client-side code in the same test method. I decided to send a PR now instead of when more of the Streamable HTTP work is done, because these changes affect more than just the ModelContextProtocol.AspNetCore package, and could change how other people write their HTTP-based tests. |
# Conflicts: # tests/ModelContextProtocol.Tests/SseIntegrationTests.cs
…on the stream returned by the ConnectCallback - Move workaround to KestrelInMemoryConnection instead of SseClientSessionTransport which is product code
I figured out what was going on. SocketsHttpHandler will call Dispose on the Stream returned by the ConnectCallback, but not DisposeAsync. I updated KestrelInMemoryConnection.ClientStream to implement Dispose instead of DisposeAsync, and reverted the workaround in SseClientSessionTransport, so the workaround is now completely in test code. I'll also submit a PR to the runtime repo to fix SocketsHttpHandler to call DisposeAsync in this case. I do think it would be even better if the CancellationToken passed into ReadAsync also got cancelled, but at least what we have now works. |
@stephentoub It looks like DisposeAsyncCompletesImmediatelyWhenInvokedFromHandler is flaky. |
Delete that assert (L245). It's bogus. The handler might not yet have fully exited by the time we get to that line, in which case the DisposeAsync may complete asynchronously. |
src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
- Go back to not processing messages until IMcpServer.RunAsync is called - style: consistently use mcpServer from parameter rather than features Failed test run: https://github.com/modelcontextprotocol/csharp-sdk/actions/runs/14299175970/job/40070612363?pr=225
- Get most of the changes that were supposed to be in the last commit - Go back to not processing messages until IMcpServer.RunAsync is called Failed test run: https://github.com/modelcontextprotocol/csharp-sdk/actions/runs/14299175970/job/40070612363?pr=225
The bulk of this PR is adding a Kestrel in-memory transport for testing, but it also does a few other things.
One of the most unfortunate things I learned while making this PR is that SocketsHttpHandler doesn't consistently respect the CancellationToken parameter at least when reading an unbuffered SSE response stream. Without the following change:SseClientSessionTransport.DisposeAsync would hang waiting on this line HttpConnection.FillAsync were it callsReadAsync
on the response stream with CancellationToken.None.I searched the runtime repo to see if anyone had already filed an issue, and I couldn't find one. If none of us can find an existing issue, I can file a new one. The code I added to work around the issue fixes our tests, but leaves zombie SSE response stream, unlike a real fix to SocketsHttpHandler.I'm guessing this wouldn't be an issue given a real socket, because I didn't see shutdown take 30 seconds until I started using the ConnectCallback.See #225 (comment) for the most up-to-date info.