-
Notifications
You must be signed in to change notification settings - Fork 2
Add MCP Streamable HTTP hosting #27
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
Draft
carldebilly
wants to merge
9
commits into
main
Choose a base branch
from
feature/mcp-httpserve
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
47d84a4
Add dynamic MCP server sessions
carldebilly 1c4bbaf
Add ASP.NET Core MCP package shell
carldebilly 4a6ab40
Add hosted MCP HTTP extensions
carldebilly df7e816
Add MCP HTTP self-host command
carldebilly d387c3a
Document MCP HTTP hosting
carldebilly 75c4fcc
Harden MCP HTTP self-host defaults
carldebilly d5dece1
Manage MCP HTTP session lifetimes
carldebilly 004775c
Document MCP HTTP hosting guidance
carldebilly 9cfacf6
Address MCP HTTP review feedback
carldebilly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| namespace Repl.Mcp.AspNetCore; | ||
|
|
||
| internal sealed class CompositeServiceProvider( | ||
| IServiceProvider primary, | ||
| IServiceProvider fallback) : IServiceProvider | ||
| { | ||
| public object? GetService(Type serviceType) => | ||
| primary.GetService(serviceType) ?? fallback.GetService(serviceType); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| namespace Repl.Mcp.AspNetCore; | ||
|
|
||
| internal sealed record McpHttpBinding( | ||
| string Host, | ||
| int Port, | ||
| string Path, | ||
| string ListenUrl, | ||
| string EndpointUrl, | ||
| bool AllowsRemote); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| using System.Globalization; | ||
| using System.Net; | ||
| using System.Net.Sockets; | ||
|
|
||
| namespace Repl.Mcp.AspNetCore; | ||
|
|
||
| internal static class McpHttpBindingFactory | ||
| { | ||
| public static McpHttpBinding Create( | ||
| string? host, | ||
| int port, | ||
| string? path, | ||
| bool allowRemote) | ||
| { | ||
| if (port is < 1 or > 65535) | ||
| { | ||
| throw new InvalidOperationException("MCP HTTP port must be between 1 and 65535."); | ||
| } | ||
|
|
||
| var effectiveHost = string.IsNullOrWhiteSpace(host) | ||
| ? ReplMcpHttpServerOptions.DefaultHost | ||
| : host.Trim(); | ||
|
|
||
| var isLoopback = IsLoopbackHost(effectiveHost); | ||
| if (!allowRemote && !isLoopback) | ||
| { | ||
| throw new InvalidOperationException( | ||
| "Remote MCP HTTP bindings require --allow-remote. Use 127.0.0.1 or localhost for local-only serving."); | ||
| } | ||
|
|
||
| var normalizedPath = NormalizePath(path); | ||
| var urlHost = FormatUrlHost(effectiveHost); | ||
| var listenUrl = string.Concat("http://", urlHost, ":", port.ToString(CultureInfo.InvariantCulture)); | ||
| var endpointUrl = string.Concat(listenUrl, normalizedPath); | ||
|
|
||
| return new McpHttpBinding( | ||
| effectiveHost, | ||
| port, | ||
| normalizedPath, | ||
| listenUrl, | ||
| endpointUrl, | ||
| !isLoopback); | ||
| } | ||
|
|
||
| private static string NormalizePath(string? path) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(path)) | ||
| { | ||
| return ReplMcpHttpServerOptions.DefaultPath; | ||
| } | ||
|
|
||
| var normalized = path.Trim(); | ||
| if (normalized[0] != '/') | ||
| { | ||
| normalized = "/" + normalized; | ||
| } | ||
|
|
||
| return normalized; | ||
| } | ||
|
|
||
| private static bool IsLoopbackHost(string host) | ||
| { | ||
| var normalized = TrimIpv6Brackets(host); | ||
| if (string.Equals(normalized, "localhost", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return IPAddress.TryParse(normalized, out var address) | ||
| && IPAddress.IsLoopback(address); | ||
| } | ||
|
|
||
| private static string FormatUrlHost(string host) | ||
| { | ||
| var normalized = TrimIpv6Brackets(host); | ||
| if (IPAddress.TryParse(normalized, out var address) | ||
| && address.AddressFamily == AddressFamily.InterNetworkV6) | ||
| { | ||
| return $"[{normalized}]"; | ||
| } | ||
|
|
||
| return normalized; | ||
| } | ||
|
|
||
| private static string TrimIpv6Brackets(string host) => | ||
| host.Length > 1 && host[0] == '[' && host[^1] == ']' | ||
| ? host[1..^1] | ||
| : host; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| using Repl.Mcp; | ||
|
|
||
| namespace Repl.Mcp.AspNetCore; | ||
|
|
||
| internal sealed class McpHttpModule : IReplModule | ||
| { | ||
| private readonly ReplApp _app; | ||
| private readonly ReplMcpHttpServerOptions _options; | ||
| private readonly ReplMcpServerOptions _mcpOptions; | ||
|
|
||
| public McpHttpModule(ReplApp app, ReplMcpHttpServerOptions options) | ||
| { | ||
| _app = app; | ||
| _options = options; | ||
| _mcpOptions = new ReplMcpServerOptions(); | ||
| options.Http.ConfigureServer?.Invoke(_mcpOptions); | ||
| } | ||
|
|
||
| public void Map(IReplMap map) | ||
| { | ||
| map.Context(_mcpOptions.ContextName, mcp => | ||
| { | ||
| mcp.Map("httpserve", | ||
| HandleHttpServeAsync) | ||
| .WithDescription("Start MCP Streamable HTTP server for agent integration.") | ||
| .WithAlias("http", "http-serve") | ||
| .Hidden(); | ||
| }) | ||
| .Hidden(); | ||
| } | ||
|
|
||
| private async Task<object> HandleHttpServeAsync( | ||
| IReplIoContext io, | ||
| string? host, | ||
| int? port, | ||
| string? path, | ||
| bool allowRemote, | ||
| int? idleTimeoutSeconds, | ||
| int? maxIdleSessions, | ||
| bool quiet, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| var runOptions = CreateRunOptions( | ||
| host, | ||
| port, | ||
| path, | ||
| allowRemote, | ||
| idleTimeoutSeconds, | ||
| maxIdleSessions, | ||
| quiet); | ||
|
|
||
| try | ||
| { | ||
| await ReplMcpHttpServer.RunAsync( | ||
| _app, | ||
| runOptions, | ||
| io.Output, | ||
| cancellationToken).ConfigureAwait(false); | ||
| return Results.Exit(0); | ||
| } | ||
| catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) | ||
| { | ||
| return Results.Exit(0); | ||
| } | ||
| catch (InvalidOperationException ex) | ||
| { | ||
| ReplMcpHttpDiagnostics.StartupFailures.Add(1); | ||
| await io.Error.WriteLineAsync(ex.ToString()).ConfigureAwait(false); | ||
| return Results.Exit(1); | ||
| } | ||
| } | ||
|
|
||
| private ReplMcpHttpServerOptions CreateRunOptions( | ||
| string? host, | ||
| int? port, | ||
| string? path, | ||
| bool allowRemote, | ||
| int? idleTimeoutSeconds, | ||
| int? maxIdleSessions, | ||
| bool quiet) | ||
| { | ||
| var runOptions = _options.Clone(); | ||
| ApplyEndpointOptions(runOptions, host, port, path); | ||
|
|
||
| runOptions.AllowRemote |= allowRemote; | ||
| runOptions.Quiet |= quiet; | ||
|
|
||
| if (idleTimeoutSeconds is { } idleSeconds) | ||
| { | ||
| runOptions.Http.IdleTimeout = TimeSpan.FromSeconds(idleSeconds); | ||
| } | ||
|
|
||
| if (maxIdleSessions is { } maxSessions) | ||
| { | ||
| runOptions.Http.MaxIdleSessionCount = maxSessions; | ||
| } | ||
|
|
||
| return runOptions; | ||
| } | ||
|
|
||
| private static void ApplyEndpointOptions( | ||
| ReplMcpHttpServerOptions runOptions, | ||
| string? host, | ||
| int? port, | ||
| string? path) | ||
| { | ||
| if (!string.IsNullOrWhiteSpace(host)) | ||
| { | ||
| runOptions.Host = host; | ||
| } | ||
|
|
||
| if (port is { } portValue) | ||
| { | ||
| runOptions.Port = portValue; | ||
| } | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(path)) | ||
| { | ||
| runOptions.Path = path; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # Repl.Mcp.AspNetCore | ||
|
|
||
| ASP.NET Core Streamable HTTP hosting integration for Repl MCP servers. | ||
|
|
||
| ## Hosted in ASP.NET Core | ||
|
|
||
| ```csharp | ||
| builder.Services.AddReplMcpHttp(replApp); | ||
|
|
||
| var app = builder.Build(); | ||
| app.MapReplMcp("/mcp"); | ||
| ``` | ||
|
|
||
| `MapReplMcp()` maps the default `/mcp` endpoint. | ||
|
|
||
| Use ASP.NET Core middleware and endpoint conventions for production concerns | ||
| such as authentication, authorization, CORS, HTTPS, and reverse proxy hosting. | ||
|
|
||
| ```csharp | ||
| builder.Services.AddAuthentication(); | ||
| builder.Services.AddAuthorization(); | ||
| builder.Services.AddReplMcpHttp(replApp); | ||
|
|
||
| var app = builder.Build(); | ||
| app.UseAuthentication(); | ||
| app.UseAuthorization(); | ||
| app.MapReplMcp("/mcp").RequireAuthorization(); | ||
| ``` | ||
|
|
||
| For per-installation transport customization, configure the hosted transport: | ||
|
|
||
| ```csharp | ||
| builder.Services.AddReplMcpHttp(replApp, http => | ||
| { | ||
| http.IdleTimeout = TimeSpan.FromMinutes(15); | ||
| http.MaxIdleSessionCount = 50; | ||
| }); | ||
| ``` | ||
|
|
||
| ## Self-Hosted CLI | ||
|
|
||
| Register the local HTTP command on a Repl app: | ||
|
|
||
| ```csharp | ||
| var replApp = ReplApp.Create() | ||
| .UseMcpHttpServer(); | ||
| ``` | ||
|
|
||
| Then run: | ||
|
|
||
| ```bash | ||
| myapp mcp httpserve | ||
| ``` | ||
|
|
||
| The default endpoint is `http://127.0.0.1:7375/mcp`. The port digits map to | ||
| `repl` on a phone keypad. The self-hosted command is intended for local agents | ||
| and trusted development networks. Non-loopback bindings require an explicit | ||
| opt-in: | ||
|
|
||
| ```bash | ||
| myapp mcp httpserve --host 0.0.0.0 --allow-remote | ||
| ``` | ||
|
|
||
| Loopback self-hosting rejects browser requests with unexpected `Origin` headers | ||
| and restricts the HTTP `Host` header to loopback names. Remote opt-in relaxes | ||
| the Host restriction for trusted networks, but does not add authentication or | ||
| TLS. Use hosted ASP.NET Core when the endpoint needs production-grade auth, | ||
| certificates, reverse proxy integration, or custom network policy. | ||
|
|
||
| Self-hosted apps can expose limited advanced hooks: | ||
|
|
||
| ```csharp | ||
| var replApp = ReplApp.Create() | ||
| .UseMcpHttpServer(options => | ||
| { | ||
| options.ConfigureBuilder = builder => | ||
| builder.Configuration.AddEnvironmentVariables("MYAPP_"); | ||
|
|
||
| options.ConfigureApp = app => | ||
| app.UseForwardedHeaders(); | ||
| }); | ||
| ``` | ||
|
|
||
| The `http` and `http-serve` aliases also resolve to `httpserve`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Description>ASP.NET Core Streamable HTTP hosting integration for Repl MCP servers.</Description> | ||
| <PackageReadmeFile>README.md</PackageReadmeFile> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <InternalsVisibleTo Include="Repl.McpTests" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Repl.Mcp\Repl.Mcp.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
| <PackageReference Include="ModelContextProtocol.AspNetCore" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="README.md" Pack="true" PackagePath="" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
32 changes: 32 additions & 0 deletions
32
src/Repl.Mcp.AspNetCore/ReplMcpEndpointRouteBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| using Microsoft.AspNetCore.Builder; | ||
| using Microsoft.AspNetCore.Routing; | ||
|
|
||
| namespace Repl.Mcp.AspNetCore; | ||
|
|
||
| /// <summary> | ||
| /// Extension methods for mapping Repl MCP endpoints. | ||
| /// </summary> | ||
| public static class ReplMcpEndpointRouteBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Maps the Repl MCP Streamable HTTP endpoint. | ||
| /// </summary> | ||
| /// <param name="endpoints">Endpoint route builder.</param> | ||
| /// <param name="pattern">Route pattern for the Streamable HTTP endpoint.</param> | ||
| /// <returns>An endpoint convention builder.</returns> | ||
| public static IEndpointConventionBuilder MapReplMcp( | ||
| this IEndpointRouteBuilder endpoints, | ||
| string pattern) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(endpoints); | ||
| return endpoints.MapMcp(pattern); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Maps the Repl MCP Streamable HTTP endpoint at the default path. | ||
| /// </summary> | ||
| /// <param name="endpoints">Endpoint route builder.</param> | ||
| /// <returns>An endpoint convention builder.</returns> | ||
| public static IEndpointConventionBuilder MapReplMcp(this IEndpointRouteBuilder endpoints) => | ||
| endpoints.MapReplMcp(ReplMcpHttpServerOptions.DefaultPath); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CLI accepts any integer for
--idle-timeout-secondsand--max-idle-sessionsand 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.