feat(worker): add external MCP endpoint with API-key auth

A second WebApplication runs the external MCP server on its own port (default 47822) so it can expose a different tool set under different auth than the internal /mcp endpoint. Shared singletons (config, broadcaster, queue, db factory) are injected by instance so both apps share runtime state. ExternalMcpAuthMiddleware enforces an optional X-ClaudeDo-Key header; loopback-only trust when no key is configured.

Tools: ListTaskLists, ListTasks, GetTask, AddTask, UpdateTaskStatus, RunTaskNow, CancelTask.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-25 09:36:46 +02:00
parent 16e1ddd129
commit 45320427e8
4 changed files with 280 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Runner;
@@ -72,6 +73,7 @@ builder.Services.AddScoped<PlanningMcpContextAccessor>();
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddMcpServer()
.WithHttpTransport()
@@ -108,4 +110,44 @@ app.MapMcp("/mcp");
app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})",
cfg.SignalRPort, cfg.DbPath);
app.Run();
// Build the external MCP endpoint as a separate WebApplication on its own port.
// Rationale: ModelContextProtocol.AspNetCore registers one server per DI container,
// so we need a second app to expose a different tool set under different auth.
// Shared singletons (QueueService, HubBroadcaster, WorkerConfig, db factory) are
// injected by instance so both apps operate on the same runtime state.
WebApplication? externalApp = null;
if (cfg.ExternalMcpPort > 0)
{
var externalBuilder = WebApplication.CreateBuilder();
externalBuilder.Services.AddSingleton(cfg);
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<HubBroadcaster>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<QueueService>());
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>());
externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
externalBuilder.Services.AddScoped<TaskRepository>();
externalBuilder.Services.AddScoped<ListRepository>();
externalBuilder.Services.AddScoped<ExternalMcpService>();
externalBuilder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<ExternalMcpService>();
externalBuilder.WebHost.UseUrls($"http://127.0.0.1:{cfg.ExternalMcpPort}");
externalApp = externalBuilder.Build();
externalApp.UseMiddleware<ExternalMcpAuthMiddleware>();
externalApp.MapMcp("/mcp");
externalApp.Logger.LogInformation(
"ClaudeDo.Worker external MCP listening on http://127.0.0.1:{Port} (auth: {Auth})",
cfg.ExternalMcpPort,
string.IsNullOrEmpty(cfg.ExternalMcpApiKey) ? "loopback-only" : "X-ClaudeDo-Key");
}
if (externalApp is null)
{
await app.RunAsync();
}
else
{
await Task.WhenAll(app.RunAsync(), externalApp.RunAsync());
}