From 2b40ea2d2bc6fe9808c2d93312b383176efda007 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 1 Oct 2026 08:49:31 +0200 Subject: [PATCH] fix(mcp): register ticket import tools in the external MCP container The external MCP server has its own DI container; TicketMcpTools could not resolve TicketImportService there, so both tools failed at runtime. Also surface a skipped import in the prime outcome and treat a sweep where every project board fails as an error instead of an empty backlog. --- src/ClaudeDo.Worker/Prime/PrimeRunner.cs | 6 +++++- src/ClaudeDo.Worker/Program.cs | 2 ++ src/ClaudeDo.Worker/Tickets/TicketImportService.cs | 8 ++++++++ .../Prime/PrimeRunnerTests.cs | 1 + .../Tickets/TicketImportAllTests.cs | 14 ++++++++++++-- 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ClaudeDo.Worker/Prime/PrimeRunner.cs b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs index eaa2f035..f73aeec7 100644 --- a/src/ClaudeDo.Worker/Prime/PrimeRunner.cs +++ b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs @@ -63,7 +63,11 @@ public sealed class PrimeRunner : IPrimeRunner private Task RunImportTicketsAsync(PrimeScheduleDto schedule, CancellationToken ct) { if (!_tickets.IsConfigured) - return RunPrepAsync("Daily prep", log => FillMyDayStepAsync(schedule, log, ct)); + return RunPrepAsync("Daily prep", async log => + { + var day = await FillMyDayStepAsync(schedule, log, ct); + return day with { Message = $"Ticket system not configured, import skipped; {day.Message}" }; + }); return RunPrepAsync("Ticket import", async log => { diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index d8838fda..7e356985 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -367,6 +367,7 @@ if (cfg.ExternalMcpPort > 0) Share(); Share(); Share(); + Share(); externalBuilder.Services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); @@ -395,6 +396,7 @@ if (cfg.ExternalMcpPort > 0) externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport(o => o.Stateless = true) .WithRequestFilters(f => f diff --git a/src/ClaudeDo.Worker/Tickets/TicketImportService.cs b/src/ClaudeDo.Worker/Tickets/TicketImportService.cs index 7cc2e578..5b05324e 100644 --- a/src/ClaudeDo.Worker/Tickets/TicketImportService.cs +++ b/src/ClaudeDo.Worker/Tickets/TicketImportService.cs @@ -200,6 +200,8 @@ public sealed class TicketImportService var identity = await client.GetIdentityAsync(ct); var projects = await client.GetProjectsAsync(ct); var items = new List(); + string? lastFailure = null; + var failed = 0; foreach (var project in projects) { @@ -212,6 +214,8 @@ public sealed class TicketImportService { // Ein unlesbares Board darf den Sweep über die übrigen Projekte nicht abbrechen. _logger.LogWarning("Board von Projekt {project_id} übersprungen: {reason}", project.Id, ex.Message); + lastFailure = ex.Message; + failed++; continue; } @@ -233,6 +237,10 @@ public sealed class TicketImportService } } + // Scheitern ALLE Boards (z.B. Scope fehlt), ist das ein Fehler — kein leerer Backlog. + if (projects.Count > 0 && failed == projects.Count) + throw new TicketApiException($"Kein Projekt-Board lesbar: {lastFailure}"); + return (items.DistinctBy(i => i.Id).ToList(), projects); } diff --git a/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs b/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs index 70cb5b17..c4a4134c 100644 --- a/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs @@ -311,6 +311,7 @@ public class PrimeRunnerTests : IDisposable var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None); Assert.True(outcome.Success); + Assert.Contains("not configured", outcome.Message); var only = Assert.Single(claude.AllArguments); Assert.Contains(DailyPrepPrompt.SetMyDayTool, only); } diff --git a/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportAllTests.cs b/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportAllTests.cs index 378f2ee6..67b5d47e 100644 --- a/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportAllTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Tickets/TicketImportAllTests.cs @@ -156,6 +156,15 @@ public sealed class TicketImportAllTests : IDisposable Assert.Contains("Hub Lagertechnik", r.SkippedReason); } + [Fact] + public async Task GetCandidates_throws_when_no_board_is_readable() + { + var (import, _) = await HarnessAsync(respond: r => + r.RequestUri!.AbsolutePath.Contains("/Board/project/") ? (HttpStatusCode.Forbidden, "") : Respond(r)); + + await Assert.ThrowsAsync(() => import.GetCandidatesAsync(CancellationToken.None)); + } + [Fact] public async Task GetCandidates_throws_when_not_configured() { @@ -214,7 +223,8 @@ public sealed class TicketImportAllTests : IDisposable return (HttpStatusCode.NotFound, ""); } - private async Task<(TicketImportService Import, ClaudeDoDbContext Ctx)> HarnessAsync(bool configured = true) + private async Task<(TicketImportService Import, ClaudeDoDbContext Ctx)> HarnessAsync( + bool configured = true, Func? respond = null) { var db = new DbFixture(); _dbs.Add(db); @@ -240,7 +250,7 @@ public sealed class TicketImportAllTests : IDisposable var clients = new TicketClientFactory( new TicketSystemConfig(worker, patStore), - new StubHttpClientFactory(new StubHandler(Respond)), + new StubHttpClientFactory(new StubHandler(respond ?? Respond)), NullLogger.Instance); var import = new TicketImportService(clients, factory, NullLogger.Instance); return (import, factory.CreateDbContext());