Add MCP Streamable HTTP hosting#27
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cfacf6537
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var configureSessionOptions = http.ConfigureSessionOptions; | ||
| http.ConfigureSessionOptions = async (context, serverOptions, cancellationToken) => | ||
| { | ||
| var sessionServices = new CompositeServiceProvider(context.RequestServices, app.Services); |
There was a problem hiding this comment.
Do not capture request-scoped services for stateful sessions
ConfigureSessionOptions is invoked only for the initialize request in stateful mode (the default), so this stores that request's scoped RequestServices for the entire MCP session (see the SDK docs for ConfigureSessionOptions: https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.AspNetCore.HttpServerTransportOptions.html). After the initialize request completes, later tool/resource calls that resolve ASP.NET scoped services such as DbContext or user/request-bound services can resolve from a disposed or stale scope; create a session-owned scope or resolve from the current request/server scope instead of retaining context.RequestServices here.
Useful? React with 👍 / 👎.
|
|
||
| if (!options.Quiet && output is not null) | ||
| { | ||
| await output.WriteLineAsync($"MCP HTTP server listening on {binding.EndpointUrl}").ConfigureAwait(false); |
There was a problem hiding this comment.
Announce the listener only after Kestrel starts
When the configured address cannot actually bind, such as when port 7375 is already in use or the process lacks permission, StartAsync throws after this line has already told the caller that the MCP server is listening. That gives users and scripts a false-ready signal for a server that never started; write the endpoint message only after StartAsync succeeds.
Useful? React with 👍 / 👎.
|
@autocarl Please do a review of this PR. I want to know if the feature makes sense. |
autocarl
left a comment
There was a problem hiding this comment.
Feature direction makes sense: separating ASP.NET Core hosting into Repl.Mcp.AspNetCore and keeping the self-hosted CLI local/trusted-network by default is the right shape. I would fix the inline items before merging, especially the session service lifetime issue.
Validation I ran locally:
dotnet build src/Repl.slnx -c Release— passed, 0 warnings/errorsdotnet test src/Repl.slnx -c Release --no-build— passed, 992/992
Current CI checks are also green.
Non-blocking nit: the new MSTest methods in Given_McpHttpBinding / Given_McpHttpSelfHostSecurity would be more consistent with the existing MCP tests if they had [Description(...)] attributes.
| http.ConfigureSessionOptions = async (context, serverOptions, cancellationToken) => | ||
| { | ||
| var sessionServices = new CompositeServiceProvider(context.RequestServices, app.Services); | ||
| var session = app.CreateMcpServerSession(sessionServices, options.ConfigureServer); |
There was a problem hiding this comment.
This captures context.RequestServices into the MCP session provider. In stateful mode, ConfigureSessionOptions is tied to the initialize request, so later tool/resource calls can run after that request scope has been disposed or become stale. Please create a session-owned IServiceScope from IServiceScopeFactory, use that scope in the session CompositeServiceProvider, and dispose it with the ReplMcpServerSession / session cleanup path. A regression test should dispose the initialize request scope before a later session call resolves a scoped service.
|
|
||
| if (!options.Quiet && output is not null) | ||
| { | ||
| await output.WriteLineAsync($"MCP HTTP server listening on {binding.EndpointUrl}").ConfigureAwait(false); |
There was a problem hiding this comment.
This prints the listener endpoint before webApp.StartAsync(...) succeeds. If Kestrel fails to bind (for example because the port is already in use), users and scripts get a false-ready signal for a server that never started. Move the startup output until after StartAsync completes successfully.
|
|
||
| if (idleTimeoutSeconds is { } idleSeconds) | ||
| { | ||
| runOptions.Http.IdleTimeout = TimeSpan.FromSeconds(idleSeconds); |
There was a problem hiding this comment.
The CLI accepts any integer for --idle-timeout-seconds and --max-idle-sessions and passes those values directly to the transport. Negative or zero values can produce unclear runtime behavior or defer validation to lower layers. Validate these command options explicitly (for example > 0) and return a clear CLI error before starting the server.
|
|
||
| private static void ConfigureRunSessionHandler(HttpServerTransportOptions http) | ||
| { | ||
| #pragma warning disable MCPEXP002 |
There was a problem hiding this comment.
This experimental SDK hook is central to deterministic session cleanup. The suppression is scoped, but it should carry a short justification comment explaining why RunSessionHandler is required and what SDK behavior this depends on; otherwise a future cleanup could remove the hook and reintroduce session leaks.
autocarl
left a comment
There was a problem hiding this comment.
Additional review notes from a second pass. These are separate from the earlier comments and are intended as targeted follow-ups.
| services.TryAddSingleton(app); | ||
|
|
||
| var builder = services.AddMcpServer(); | ||
| if (options.EnableAuthorizationFilters) |
There was a problem hiding this comment.
This makes authorization filters opt-in for the ASP.NET Core integration. In MCP, AddAuthorizationFilters() is what enforces [Authorize]/policy metadata on tools, prompts, and resources; callers might protect the /mcp endpoint generally but accidentally allow any authenticated caller to invoke primitives intended for stricter roles/policies. Consider registering authorization filters by default with an explicit opt-out, or at least make the production sample/docs enable this option.
| /// <summary> | ||
| /// Gets or sets the amount of idle time after which a stateful MCP session expires. | ||
| /// </summary> | ||
| public TimeSpan? IdleTimeout { get; set; } |
There was a problem hiding this comment.
Leaving these hosted HTTP options nullable means AddReplMcpHttp(replApp) inherits the MCP SDK stateful defaults, which are much larger than the self-hosted path's Repl-specific defaults. Since this integration creates Repl/MCP session state per HTTP session, consider giving hosted options conservative defaults too, or requiring callers to explicitly opt into the SDK limits/stateless mode.
| context.Items.Should().NotBeEmpty(); | ||
|
|
||
| #pragma warning disable MCPEXP002 | ||
| await transport.RunSessionHandler!(context, null!, CancellationToken.None); |
There was a problem hiding this comment.
This test stubs RunSessionHandler and passes null!, so it verifies that HttpContext.Items is cleared but not the real Streamable HTTP session lifecycle. It would not catch the request-scope/session-lifetime issue above. Consider adding an integration-style test that initializes a stateful MCP HTTP session, performs a follow-up call on a later request that resolves a scoped ASP.NET service, and then verifies cleanup/disposal.
Summary
Adds ASP.NET Core Streamable HTTP hosting support for Repl MCP servers, plus a self-hosted CLI command for simple local/trusted-network exposure.
This introduces:
Repl.Mcp.AspNetCoreservices.AddReplMcpHttp(replApp)app.MapReplMcp("/mcp")myapp mcp httpservemcp http,mcp http-servehttp://127.0.0.1:7375/mcpSecurity Model
The self-hosted CLI path is intended for local agents and trusted development/container networks.
Defaults:
--allow-remoteis explicitly setHostheadersOriginheadersOriginFor production-grade authentication, TLS, reverse proxy integration, CORS policy, or more rigorous Kestrel configuration, apps should use the hosted ASP.NET Core integration and compose normal ASP.NET Core middleware/endpoint conventions.
Session Model
HTTP MCP sessions are stateful by default and support parallel clients.
This PR also adds deterministic session cleanup through the SDK
RunSessionHandlerhook, so per-session REPL/MCP resources are disposed when the MCP session completes.Self-hosted defaults are conservative:
Notes
The CLI intentionally does not expose a
--statelessswitch in this first slice. Stateless and deeper transport knobs remain available through hosted/advanced configuration.Monitoring/TUI status surfaces are also left for a later slice.
Tests
Validated locally:
Result: