feat(worker): add external MCP list/task config tools
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
66
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
Normal file
66
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Hub;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.External;
|
||||||
|
|
||||||
|
public sealed record TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath);
|
||||||
|
|
||||||
|
[McpServerToolType]
|
||||||
|
public sealed class ConfigMcpTools
|
||||||
|
{
|
||||||
|
private readonly ListRepository _lists;
|
||||||
|
private readonly TaskRepository _tasks;
|
||||||
|
private readonly HubBroadcaster _broadcaster;
|
||||||
|
|
||||||
|
public ConfigMcpTools(ListRepository lists, TaskRepository tasks, HubBroadcaster broadcaster)
|
||||||
|
{
|
||||||
|
_lists = lists;
|
||||||
|
_tasks = tasks;
|
||||||
|
_broadcaster = broadcaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Get a list's default config (model, system prompt, agent path). Returns null if no config is set.")]
|
||||||
|
public async Task<TaskConfigDto?> GetListConfig(string listId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cfg = await _lists.GetConfigAsync(listId, cancellationToken);
|
||||||
|
return cfg is null ? null : new TaskConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Set a list's default model/system prompt/agent path. Passing all three as null clears the list config.")]
|
||||||
|
public async Task SetListConfig(
|
||||||
|
string listId, string? model, string? systemPrompt, string? agentPath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"List {listId} not found.");
|
||||||
|
|
||||||
|
var m = Nullify(model);
|
||||||
|
var sp = Nullify(systemPrompt);
|
||||||
|
var ap = Nullify(agentPath);
|
||||||
|
|
||||||
|
if (m is null && sp is null && ap is null)
|
||||||
|
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
||||||
|
else
|
||||||
|
await _lists.SetConfigAsync(new ListConfigEntity
|
||||||
|
{
|
||||||
|
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap,
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
await _broadcaster.ListUpdated(listId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path). Pass null to clear a field.")]
|
||||||
|
public async Task SetTaskConfig(
|
||||||
|
string taskId, string? model, string? systemPrompt, string? agentPath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
|
await _tasks.UpdateAgentSettingsAsync(taskId, Nullify(model), Nullify(systemPrompt), Nullify(agentPath), cancellationToken);
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
||||||
|
}
|
||||||
80
tests/ClaudeDo.Worker.Tests/External/ConfigMcpToolsTests.cs
vendored
Normal file
80
tests/ClaudeDo.Worker.Tests/External/ConfigMcpToolsTests.cs
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.External;
|
||||||
|
using ClaudeDo.Worker.Hub;
|
||||||
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.External;
|
||||||
|
|
||||||
|
public sealed class ConfigMcpToolsTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DbFixture _db = new();
|
||||||
|
private readonly ClaudeDoDbContext _ctx;
|
||||||
|
private readonly ListRepository _lists;
|
||||||
|
private readonly TaskRepository _tasks;
|
||||||
|
private readonly ConfigMcpTools _sut;
|
||||||
|
|
||||||
|
public ConfigMcpToolsTests()
|
||||||
|
{
|
||||||
|
_ctx = _db.CreateContext();
|
||||||
|
_lists = new ListRepository(_ctx);
|
||||||
|
_tasks = new TaskRepository(_ctx);
|
||||||
|
_sut = new ConfigMcpTools(_lists, _tasks, new HubBroadcaster(new CapturingHubContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { _ctx.Dispose(); _db.Dispose(); }
|
||||||
|
|
||||||
|
private async Task<string> SeedListAsync()
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid().ToString();
|
||||||
|
await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetAndGetListConfig_RoundTrips()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
|
||||||
|
await _sut.SetListConfig(listId, "sonnet", "be terse", null, CancellationToken.None);
|
||||||
|
var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(cfg);
|
||||||
|
Assert.Equal("sonnet", cfg!.Model);
|
||||||
|
Assert.Equal("be terse", cfg.SystemPrompt);
|
||||||
|
Assert.Null(cfg.AgentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetListConfig_AllNull_ClearsConfig()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
await _sut.SetListConfig(listId, "sonnet", null, null, CancellationToken.None);
|
||||||
|
|
||||||
|
await _sut.SetListConfig(listId, null, null, null, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Null(await _sut.GetListConfig(listId, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTaskConfig_PersistsOverrides()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = new TaskEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
ListId = listId,
|
||||||
|
Title = "t",
|
||||||
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CommitType = "chore",
|
||||||
|
};
|
||||||
|
await _tasks.AddAsync(task);
|
||||||
|
|
||||||
|
await _sut.SetTaskConfig(task.Id, "opus", null, null, CancellationToken.None);
|
||||||
|
|
||||||
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.Equal("opus", loaded!.Model);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user