diff --git a/src/ClaudeDo.Worker/External/ExternalMcpService.cs b/src/ClaudeDo.Worker/External/ExternalMcpService.cs index bbe13ed..b1a188d 100644 --- a/src/ClaudeDo.Worker/External/ExternalMcpService.cs +++ b/src/ClaudeDo.Worker/External/ExternalMcpService.cs @@ -10,6 +10,8 @@ namespace ClaudeDo.Worker.External; public sealed record TaskListDto(string Id, string Name, string? WorkingDir); +public sealed record TagDto(long Id, string Name); + public sealed record TaskDto( string Id, string ListId, @@ -29,17 +31,20 @@ public sealed class ExternalMcpService private readonly ListRepository _lists; private readonly QueueService _queue; private readonly HubBroadcaster _broadcaster; + private readonly TagRepository _tags; public ExternalMcpService( TaskRepository tasks, ListRepository lists, QueueService queue, - HubBroadcaster broadcaster) + HubBroadcaster broadcaster, + TagRepository tags) { _tasks = tasks; _lists = lists; _queue = queue; _broadcaster = broadcaster; + _tags = tags; } [McpServerTool, Description("List all task lists available in ClaudeDo.")] @@ -183,6 +188,13 @@ public sealed class ExternalMcpService return cancelled; } + [McpServerTool, Description("List all known tags. Useful for discovering existing tag names (including 'agent' which marks tasks for auto-execution) before tagging.")] + public async Task> ListTags(CancellationToken cancellationToken) + { + var tags = await _tags.GetAllAsync(cancellationToken); + return tags.Select(t => new TagDto(t.Id, t.Name)).ToList(); + } + private static TaskDto ToDto(TaskEntity t) => new( t.Id, t.ListId, diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index cfb768e..2e19c36 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -127,6 +127,7 @@ if (cfg.ExternalMcpPort > 0) sp.GetRequiredService>().CreateDbContext()); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport() diff --git a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs index a05c3a9..5617859 100644 --- a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs @@ -1,11 +1,16 @@ using ClaudeDo.Data; +using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Config; using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; +using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Services; using ClaudeDo.Worker.Tests.Infrastructure; +using ClaudeDo.Worker.Tests.Services; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging.Abstractions; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.External; @@ -91,6 +96,27 @@ public sealed class ExternalMcpServiceTests : IDisposable private ExternalMcpService BuildSut(QueueService queue) => new(_tasks, _lists, queue, _broadcaster, _tags); + private QueueService CreateQueue() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"claudedo_ext_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + var cfg = new WorkerConfig + { + SandboxRoot = Path.Combine(tempDir, "sandbox"), + LogRoot = Path.Combine(tempDir, "logs"), + QueueBackstopIntervalMs = 50, + }; + var fake = new FakeClaudeProcess(); + var hubCtx = new FakeHubContext(); + var broadcaster = new HubBroadcaster(hubCtx); + var dbFactory = _db.CreateFactory(); + var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger.Instance); + var argsBuilder = new ClaudeArgsBuilder(); + var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, cfg, + NullLogger.Instance); + return new QueueService(dbFactory, runner, cfg, NullLogger.Instance); + } + [Fact] public async Task SeededListAndTask_AreRetrievable() { @@ -98,4 +124,20 @@ public sealed class ExternalMcpServiceTests : IDisposable var task = await SeedTaskAsync(listId); Assert.NotNull(await _tasks.GetByIdAsync(task.Id)); } + + [Fact] + public async Task ListTags_ReturnsSeededAndCustomTags() + { + var listId = await SeedListAsync(); + var task = await SeedTaskAsync(listId); + await _tasks.SetTagsAsync(task.Id, new[] { "agent", "custom-tag" }); + + var queue = CreateQueue(); + var sut = BuildSut(queue); + + var tags = await sut.ListTags(CancellationToken.None); + + Assert.Contains(tags, t => t.Name == "agent"); + Assert.Contains(tags, t => t.Name == "custom-tag"); + } }