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.
This commit is contained in:
@@ -63,7 +63,11 @@ public sealed class PrimeRunner : IPrimeRunner
|
|||||||
private Task<PrimeRunOutcome> RunImportTicketsAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
private Task<PrimeRunOutcome> RunImportTicketsAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!_tickets.IsConfigured)
|
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 =>
|
return RunPrepAsync("Ticket import", async log =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ if (cfg.ExternalMcpPort > 0)
|
|||||||
Share<FindingsStore>();
|
Share<FindingsStore>();
|
||||||
Share<HandoffRoundTracker>();
|
Share<HandoffRoundTracker>();
|
||||||
Share<HandlerUsageGuard>();
|
Share<HandlerUsageGuard>();
|
||||||
|
Share<TicketImportService>();
|
||||||
|
|
||||||
externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
||||||
@@ -395,6 +396,7 @@ if (cfg.ExternalMcpPort > 0)
|
|||||||
externalBuilder.Services.AddScoped<DailyNoteRepository>();
|
externalBuilder.Services.AddScoped<DailyNoteRepository>();
|
||||||
externalBuilder.Services.AddScoped<FindingsMcpTools>();
|
externalBuilder.Services.AddScoped<FindingsMcpTools>();
|
||||||
externalBuilder.Services.AddScoped<NotesMcpTools>();
|
externalBuilder.Services.AddScoped<NotesMcpTools>();
|
||||||
|
externalBuilder.Services.AddScoped<TicketMcpTools>();
|
||||||
externalBuilder.Services.AddMcpServer()
|
externalBuilder.Services.AddMcpServer()
|
||||||
.WithHttpTransport(o => o.Stateless = true)
|
.WithHttpTransport(o => o.Stateless = true)
|
||||||
.WithRequestFilters(f => f
|
.WithRequestFilters(f => f
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ public sealed class TicketImportService
|
|||||||
var identity = await client.GetIdentityAsync(ct);
|
var identity = await client.GetIdentityAsync(ct);
|
||||||
var projects = await client.GetProjectsAsync(ct);
|
var projects = await client.GetProjectsAsync(ct);
|
||||||
var items = new List<TicketCandidate>();
|
var items = new List<TicketCandidate>();
|
||||||
|
string? lastFailure = null;
|
||||||
|
var failed = 0;
|
||||||
|
|
||||||
foreach (var project in projects)
|
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.
|
// 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);
|
_logger.LogWarning("Board von Projekt {project_id} übersprungen: {reason}", project.Id, ex.Message);
|
||||||
|
lastFailure = ex.Message;
|
||||||
|
failed++;
|
||||||
continue;
|
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);
|
return (items.DistinctBy(i => i.Id).ToList(), projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ public class PrimeRunnerTests : IDisposable
|
|||||||
var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
||||||
|
|
||||||
Assert.True(outcome.Success);
|
Assert.True(outcome.Success);
|
||||||
|
Assert.Contains("not configured", outcome.Message);
|
||||||
var only = Assert.Single(claude.AllArguments);
|
var only = Assert.Single(claude.AllArguments);
|
||||||
Assert.Contains(DailyPrepPrompt.SetMyDayTool, only);
|
Assert.Contains(DailyPrepPrompt.SetMyDayTool, only);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,15 @@ public sealed class TicketImportAllTests : IDisposable
|
|||||||
Assert.Contains("Hub Lagertechnik", r.SkippedReason);
|
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<TicketApiException>(() => import.GetCandidatesAsync(CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetCandidates_throws_when_not_configured()
|
public async Task GetCandidates_throws_when_not_configured()
|
||||||
{
|
{
|
||||||
@@ -214,7 +223,8 @@ public sealed class TicketImportAllTests : IDisposable
|
|||||||
return (HttpStatusCode.NotFound, "");
|
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<HttpRequestMessage, (HttpStatusCode, string)>? respond = null)
|
||||||
{
|
{
|
||||||
var db = new DbFixture();
|
var db = new DbFixture();
|
||||||
_dbs.Add(db);
|
_dbs.Add(db);
|
||||||
@@ -240,7 +250,7 @@ public sealed class TicketImportAllTests : IDisposable
|
|||||||
|
|
||||||
var clients = new TicketClientFactory(
|
var clients = new TicketClientFactory(
|
||||||
new TicketSystemConfig(worker, patStore),
|
new TicketSystemConfig(worker, patStore),
|
||||||
new StubHttpClientFactory(new StubHandler(Respond)),
|
new StubHttpClientFactory(new StubHandler(respond ?? Respond)),
|
||||||
NullLogger<BandelTicketClient>.Instance);
|
NullLogger<BandelTicketClient>.Instance);
|
||||||
var import = new TicketImportService(clients, factory, NullLogger<TicketImportService>.Instance);
|
var import = new TicketImportService(clients, factory, NullLogger<TicketImportService>.Instance);
|
||||||
return (import, factory.CreateDbContext());
|
return (import, factory.CreateDbContext());
|
||||||
|
|||||||
Reference in New Issue
Block a user