Nullable filter/patch params across the External/ MCP tool classes (ListTasks, UpdateTask, AddSubtask, ReviewTask, SetMyDay, SetListConfig/SetTaskConfig, CreateList/UpdateList) lacked C# default values, so the generated tool schema marked them required — MCP clients omitting them (the common case) failed. Gave every such parameter a default value. Also registered a call-tool filter (ExternalMcpExceptionFilter) on the external MCP host that translates InvalidOperationException/ArgumentException into McpException, since the SDK's own catch-all discards ex.Message for any other exception type and returns a generic "An error occurred invoking 'X'." string. Added a reflection-based schema test sweeping every [McpServerToolType] class to guard against reintroducing a required-but-nullable parameter.
76 lines
3.0 KiB
C#
76 lines
3.0 KiB
C#
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 ListSummaryDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
|
|
|
[McpServerToolType]
|
|
public sealed class ListMcpTools
|
|
{
|
|
private readonly ListRepository _lists;
|
|
private readonly HubBroadcaster _broadcaster;
|
|
|
|
public ListMcpTools(ListRepository lists, HubBroadcaster broadcaster)
|
|
{
|
|
_lists = lists;
|
|
_broadcaster = broadcaster;
|
|
}
|
|
|
|
[McpServerTool, Description("Create a new task list. workingDir sets the git repo tasks run against; commitType defaults to 'chore'.")]
|
|
public async Task<ListSummaryDto> CreateList(
|
|
string name, string? workingDir = null, string? commitType = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
throw new InvalidOperationException("name is required.");
|
|
|
|
var entity = new ListEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Name = name,
|
|
WorkingDir = workingDir.NullIfBlank(),
|
|
DefaultCommitType = commitType.NullIfBlank() ?? CommitTypeRegistry.DefaultType,
|
|
CreatedAt = DateTime.UtcNow,
|
|
};
|
|
await _lists.AddAsync(entity, cancellationToken);
|
|
await _broadcaster.ListUpdated(entity.Id);
|
|
return ToDto(entity);
|
|
}
|
|
|
|
[McpServerTool, Description("Rename a list and/or change its working dir and default commit type. Pass null to leave a field unchanged.")]
|
|
public async Task<ListSummaryDto> UpdateList(
|
|
string listId, string? name = null, string? workingDir = null, string? commitType = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var entity = await _lists.GetByIdAsync(listId, cancellationToken)
|
|
?? throw new InvalidOperationException($"List {listId} not found.");
|
|
|
|
if (name is not null && string.IsNullOrWhiteSpace(name))
|
|
throw new InvalidOperationException("name cannot be blank.");
|
|
if (name is not null) entity.Name = name;
|
|
if (workingDir is not null)
|
|
entity.WorkingDir = workingDir.NullIfBlank();
|
|
if (commitType is not null)
|
|
entity.DefaultCommitType = commitType.NullIfBlank() ?? CommitTypeRegistry.DefaultType;
|
|
|
|
await _lists.UpdateAsync(entity, cancellationToken);
|
|
await _broadcaster.ListUpdated(listId);
|
|
return ToDto(entity);
|
|
}
|
|
|
|
[McpServerTool, Description("Delete a list and its tasks. Irreversible.")]
|
|
public async Task DeleteList(string listId, CancellationToken cancellationToken)
|
|
{
|
|
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
|
?? throw new InvalidOperationException($"List {listId} not found.");
|
|
await _lists.DeleteAsync(listId, cancellationToken);
|
|
await _broadcaster.ListUpdated(listId);
|
|
}
|
|
|
|
private static ListSummaryDto ToDto(ListEntity l) =>
|
|
new(l.Id, l.Name, l.WorkingDir, l.DefaultCommitType);
|
|
}
|