diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index d4a8cc6..8ff5255 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -10,7 +10,13 @@ Documentation policy:
## Runtime Model
-`Program.cs` builds a hosted stdio MCP server and auto-discovers tools from the assembly.
+`Program.cs` builds an MCP server and auto-discovers tools from the assembly.
+
+Transport modes:
+- stdio remains the default and owns one process per MCP client;
+- `--http` (or `DECOMPILER_TRANSPORT=http`) hosts one shared Streamable HTTP endpoint at `/mcp`;
+- HTTP mode binds only to a validated loopback origin, defaults to `http://127.0.0.1:30503`, and accepts a different origin through `DECOMPILER_HTTP_URL`;
+- HTTP uses stateless requests for current clients and bounded, ten-minute sessions for initialize-handshake clients, while all clients share the singleton `DecompilerWorkspace`.
Key startup behavior:
- registers `DecompilerWorkspace` plus the legacy singleton services;
diff --git a/DecompilerServer.csproj b/DecompilerServer.csproj
index c7eff3b..659d734 100644
--- a/DecompilerServer.csproj
+++ b/DecompilerServer.csproj
@@ -9,11 +9,15 @@
-
-
+
+
+
+
+
+
diff --git a/Program.cs b/Program.cs
index 8d7d17e..8d54b76 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,13 +1,19 @@
-using Microsoft.Extensions.DependencyInjection;
+using System.Net;
+using DecompilerServer.Services;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using DecompilerServer.Services;
+using ModelContextProtocol.AspNetCore;
using ModelContextProtocol.Server;
namespace DecompilerServer;
public partial class Program
{
+ internal const string DefaultHttpUrl = "http://127.0.0.1:30503";
+
internal const string ServerInstructions = """
DecompilerServer inspects loaded .NET assemblies. Use search_symbols first for fragments; use resolve_member_id first for fully-qualified or XML-doc-like guessed symbols.
Common parameter names: search_types/search_members use query, not pattern; resolve_member_id/get_decompiled_source/find_usages use memberId; find_callers/find_callees/get_overrides use methodId; get_types_in_namespace uses ns.
@@ -22,33 +28,113 @@ internal static void ConfigureMcpServerOptions(McpServerOptions options)
public static async Task Main(string[] args)
{
- var builder = Host.CreateApplicationBuilder(args);
- builder.Logging.ClearProviders();
- builder.Logging.AddProvider(new StderrLoggerProvider());
- builder.Logging.SetMinimumLevel(LogLevel.Information);
- // builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
- // builder.Logging.AddFilter("ModelContextProtocol", LogLevel.Warning);
- builder.Services.AddHostedService();
-
- // Register DecompilerServer services as singletons for state persistence
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ var launchOptions = ParseLaunchOptions(
+ args,
+ Environment.GetEnvironmentVariable("DECOMPILER_TRANSPORT"),
+ Environment.GetEnvironmentVariable("DECOMPILER_HTTP_URL"));
+
+ if (launchOptions.UseHttp)
+ {
+ await RunHttpAsync(launchOptions);
+ return;
+ }
+
+ await RunStdioAsync(launchOptions.HostArguments);
+ }
+
+ internal static LaunchOptions ParseLaunchOptions(
+ string[] args,
+ string? transportEnvironment,
+ string? httpUrlEnvironment)
+ {
+ var useHttpArgument = args.Any(arg => string.Equals(arg, "--http", StringComparison.OrdinalIgnoreCase));
+ var hostArguments = args
+ .Where(arg => !string.Equals(arg, "--http", StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ var useHttp = useHttpArgument || string.Equals(transportEnvironment, "http", StringComparison.OrdinalIgnoreCase);
+ var httpUrl = string.IsNullOrWhiteSpace(httpUrlEnvironment) ? DefaultHttpUrl : httpUrlEnvironment.Trim();
+
+ if (useHttp)
+ ValidateLoopbackHttpUrl(httpUrl);
+
+ return new LaunchOptions(useHttp, httpUrl, hostArguments);
+ }
+ internal static void ValidateLoopbackHttpUrl(string httpUrl)
+ {
+ if (!Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri)
+ || !string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
+ || (!string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase)
+ && (!IPAddress.TryParse(uri.Host, out var address) || !IPAddress.IsLoopback(address)))
+ || uri.AbsolutePath != "/"
+ || !string.IsNullOrEmpty(uri.Query)
+ || !string.IsNullOrEmpty(uri.Fragment)
+ || !string.IsNullOrEmpty(uri.UserInfo))
+ {
+ throw new ArgumentException(
+ "DECOMPILER_HTTP_URL must be an HTTP loopback origin such as http://127.0.0.1:30503.",
+ nameof(httpUrl));
+ }
+ }
+
+ private static async Task RunStdioAsync(string[] args)
+ {
+ var builder = Host.CreateApplicationBuilder(args);
+ ConfigureLogging(builder.Logging);
+ AddDecompilerServices(builder.Services);
builder.Services
- .AddMcpServer(ConfigureMcpServerOptions) // core MCP server services
- .WithStdioServerTransport() // Codex talks to STDIO servers
- .WithToolsFromAssembly(); // auto-discover [McpServerTool]s in this assembly
+ .AddMcpServer(ConfigureMcpServerOptions)
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly();
var app = builder.Build();
-
- // Initialize service locator
ServiceLocator.SetServiceProvider(app.Services);
+ await app.RunAsync();
+ }
+ private static async Task RunHttpAsync(LaunchOptions launchOptions)
+ {
+ var builder = WebApplication.CreateBuilder(launchOptions.HostArguments);
+ ConfigureLogging(builder.Logging);
+ builder.WebHost.UseUrls(launchOptions.HttpUrl);
+ builder.Configuration["AllowedHosts"] = "localhost;127.0.0.1;[::1]";
+ AddDecompilerServices(builder.Services);
+ builder.Services
+ .AddMcpServer(ConfigureMcpServerOptions)
+ .WithHttpTransport(options =>
+ {
+ options.SessionMode = HttpServerSessionMode.StatefulForInitializeClients;
+#pragma warning disable MCP9006
+ options.IdleTimeout = TimeSpan.FromMinutes(10);
+ options.MaxIdleSessionCount = 64;
+#pragma warning restore MCP9006
+ })
+ .WithToolsFromAssembly();
+
+ var app = builder.Build();
+ app.MapMcp("/mcp");
+ ServiceLocator.SetServiceProvider(app.Services);
await app.RunAsync();
}
+
+ private static void ConfigureLogging(ILoggingBuilder logging)
+ {
+ logging.ClearProviders();
+ logging.AddProvider(new StderrLoggerProvider());
+ logging.SetMinimumLevel(LogLevel.Information);
+ }
+
+ private static void AddDecompilerServices(IServiceCollection services)
+ {
+ services.AddHostedService();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ internal sealed record LaunchOptions(bool UseHttp, string HttpUrl, string[] HostArguments);
}
diff --git a/README.md b/README.md
index bddadf9..c683c0b 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,26 @@ Open Cursor Settings → MCP and add a new server, or edit `.cursor/mcp.json` in
On Windows, use the `.exe` extension and escaped backslashes.
+When many Codex tasks use the same project, stdio starts one DecompilerServer
+child process per task. To share one bounded workspace and one process instead,
+start DecompilerServer once in loopback HTTP mode:
+
+```text
+DecompilerServer.exe --http
+```
+
+Then configure Codex to connect to that shared process:
+
+```toml
+[mcp_servers.decompiler]
+url = "http://127.0.0.1:30503/mcp"
+```
+
+The default listener is `http://127.0.0.1:30503`. Set
+`DECOMPILER_HTTP_URL` before starting the server to choose another HTTP
+loopback origin. Non-loopback and HTTPS bindings are rejected; terminate TLS
+at a separately secured reverse proxy if remote access is required.
+
diff --git a/Tests/McpProtocolTests.cs b/Tests/McpProtocolTests.cs
index 6042dc8..6646a97 100644
--- a/Tests/McpProtocolTests.cs
+++ b/Tests/McpProtocolTests.cs
@@ -13,6 +13,51 @@ namespace Tests;
///
public class McpProtocolTests
{
+ [Fact]
+ public void LaunchOptions_DefaultToStdio()
+ {
+ var options = DecompilerServer.Program.ParseLaunchOptions([], null, null);
+
+ Assert.False(options.UseHttp);
+ Assert.Equal(DecompilerServer.Program.DefaultHttpUrl, options.HttpUrl);
+ Assert.Empty(options.HostArguments);
+ }
+
+ [Fact]
+ public void LaunchOptions_EnableHttpWithoutPassingPrivateSwitchToHost()
+ {
+ var options = DecompilerServer.Program.ParseLaunchOptions(
+ ["--http", "--environment", "Development"],
+ null,
+ null);
+
+ Assert.True(options.UseHttp);
+ Assert.Equal(["--environment", "Development"], options.HostArguments);
+ }
+
+ [Theory]
+ [InlineData("http://localhost:30503")]
+ [InlineData("http://127.0.0.1:30503")]
+ [InlineData("http://[::1]:30503")]
+ public void LaunchOptions_AcceptLoopbackHttpOrigins(string httpUrl)
+ {
+ var options = DecompilerServer.Program.ParseLaunchOptions([], "http", httpUrl);
+
+ Assert.True(options.UseHttp);
+ Assert.Equal(httpUrl, options.HttpUrl);
+ }
+
+ [Theory]
+ [InlineData("https://127.0.0.1:30503")]
+ [InlineData("http://0.0.0.0:30503")]
+ [InlineData("http://example.com:30503")]
+ [InlineData("http://127.0.0.1:30503/mcp")]
+ public void LaunchOptions_RejectNonLoopbackOrNonOriginUrls(string httpUrl)
+ {
+ Assert.Throws(() =>
+ DecompilerServer.Program.ParseLaunchOptions([], "http", httpUrl));
+ }
+
[Fact]
public void McpServer_Should_Expose_Concise_ServerInstructions()
{