feat(mcp): resolve per-run tokens in MCP auth + register TaskRunMcpService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 15:57:12 +02:00
parent 9d133e227b
commit f3052dc5fc
3 changed files with 64 additions and 7 deletions

View File

@@ -1,4 +1,5 @@
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Runner;
using Microsoft.AspNetCore.Http;
namespace ClaudeDo.Worker.Planning;
@@ -9,7 +10,7 @@ public sealed class PlanningTokenAuthMiddleware
public PlanningTokenAuthMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext ctx, TaskRepository tasks)
public async Task InvokeAsync(HttpContext ctx, TaskRepository tasks, TaskRunTokenRegistry runTokens)
{
if (!ctx.Request.Path.StartsWithSegments("/mcp"))
{
@@ -26,15 +27,23 @@ public sealed class PlanningTokenAuthMiddleware
}
var token = auth.Substring("Bearer ".Length).Trim();
var parent = await tasks.FindByPlanningTokenAsync(token, ctx.RequestAborted);
if (parent is null || parent.PlanningPhase != ClaudeDo.Data.Models.PlanningPhase.Active)
if (parent is not null && parent.PlanningPhase == ClaudeDo.Data.Models.PlanningPhase.Active)
{
ctx.Response.StatusCode = 401;
await ctx.Response.WriteAsync("Invalid or expired planning token");
ctx.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
await _next(ctx);
return;
}
ctx.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
await _next(ctx);
if (runTokens.TryResolve(token, out var callerTaskId))
{
ctx.Items["TaskRunContext"] = new TaskRunMcpContext { CallerTaskId = callerTaskId };
await _next(ctx);
return;
}
ctx.Response.StatusCode = 401;
await ctx.Response.WriteAsync("Invalid or expired token");
}
}

View File

@@ -56,6 +56,7 @@ builder.Services.AddSingleton<HubBroadcaster>();
builder.Services.AddSingleton<GitService>();
builder.Services.AddSingleton<WorktreeManager>();
builder.Services.AddSingleton<ClaudeArgsBuilder>();
builder.Services.AddSingleton<TaskRunTokenRegistry>();
builder.Services.AddSingleton<TaskRunner>();
builder.Services.AddSingleton<WorktreeMaintenanceService>();
builder.Services.AddSingleton<TaskResetService>();
@@ -131,6 +132,8 @@ builder.Services.AddSingleton<IPlanningTerminalLauncher>(sp =>
new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin));
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<PlanningMcpContextAccessor>();
builder.Services.AddScoped<TaskRunMcpContextAccessor>();
builder.Services.AddScoped<TaskRunMcpService>();
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
builder.Services.AddScoped<TaskRepository>();
@@ -138,7 +141,8 @@ builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<PlanningMcpService>();
.WithTools<PlanningMcpService>()
.WithTools<TaskRunMcpService>();
// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");