Merge task branch for: fix(worker): external MCP optional params are generated as required; errors are opaque

This commit is contained in:
mika kuns
2026-07-23 20:08:26 +02:00
8 changed files with 176 additions and 18 deletions
+4 -2
View File
@@ -31,7 +31,8 @@ public sealed class ConfigMcpTools
[McpServerTool, Description("Set a list's default model/system prompt/agent path/max turns. Passing all four as null clears the list config.")]
public async Task SetListConfig(
string listId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
string listId, string? model = null, string? systemPrompt = null, string? agentPath = null,
int? maxTurns = null, CancellationToken cancellationToken = default)
{
_ = await _lists.GetByIdAsync(listId, cancellationToken)
?? throw new InvalidOperationException($"List {listId} not found.");
@@ -53,7 +54,8 @@ public sealed class ConfigMcpTools
[McpServerTool, Description("Set per-task config overrides (model/system prompt/agent path/max turns). Pass null for any field to clear that override.")]
public async Task SetTaskConfig(
string taskId, string? model, string? systemPrompt, string? agentPath, int? maxTurns, CancellationToken cancellationToken)
string taskId, string? model = null, string? systemPrompt = null, string? agentPath = null,
int? maxTurns = null, CancellationToken cancellationToken = default)
{
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
@@ -0,0 +1,32 @@
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace ClaudeDo.Worker.External;
/// <summary>
/// The MCP SDK's own call-tool catch-all only preserves ex.Message for <see cref="McpException"/> —
/// any other exception type is replaced with a generic "An error occurred invoking 'X'." with no detail.
/// This filter translates the expected validation exceptions thrown across the External/ tool classes
/// (task/list not found, bad status, unknown model, etc.) into McpException so callers see why a call failed.
/// </summary>
public static class ExternalMcpExceptionFilter
{
public static McpRequestHandler<CallToolRequestParams, CallToolResult> Wrap(
McpRequestHandler<CallToolRequestParams, CallToolResult> next) =>
async (request, cancellationToken) =>
{
try
{
return await next(request, cancellationToken);
}
catch (InvalidOperationException ex)
{
throw new McpException(ex.Message, ex);
}
catch (ArgumentException ex)
{
throw new McpException(ex.Message, ex);
}
};
}
+13 -13
View File
@@ -107,9 +107,9 @@ public sealed class ExternalMcpService
"Valid status values: Idle, Queued, Running, WaitingForReview, WaitingForChildren, Done, Failed, Cancelled.")]
public async Task<IReadOnlyList<TaskDto>> ListTasks(
string listId,
string? createdBy,
string? status,
CancellationToken cancellationToken)
string? createdBy = null,
string? status = null,
CancellationToken cancellationToken = default)
{
TaskStatus? statusFilter = null;
if (!string.IsNullOrWhiteSpace(status))
@@ -193,10 +193,10 @@ public sealed class ExternalMcpService
[McpServerTool, Description("Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. Refuses if the task is currently Running.")]
public async Task<TaskDto> UpdateTask(
string taskId,
string? title,
string? description,
string? commitType,
CancellationToken cancellationToken)
string? title = null,
string? description = null,
string? commitType = null,
CancellationToken cancellationToken = default)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
@@ -219,8 +219,8 @@ public sealed class ExternalMcpService
public async Task<TaskDto> AddSubtask(
string taskId,
string title,
int? orderNum,
CancellationToken cancellationToken)
int? orderNum = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(title))
throw new InvalidOperationException("title is required.");
@@ -300,8 +300,8 @@ public sealed class ExternalMcpService
public async Task<TaskDto> ReviewTask(
string taskId,
string decision,
string? feedback,
CancellationToken cancellationToken)
string? feedback = null,
CancellationToken cancellationToken = default)
{
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
@@ -619,8 +619,8 @@ public sealed class ExternalMcpService
public async Task<TaskDto> SetMyDay(
string taskId,
bool isMyDay,
int? sortOrder,
CancellationToken cancellationToken)
int? sortOrder = null,
CancellationToken cancellationToken = default)
{
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
+3 -2
View File
@@ -22,7 +22,7 @@ public sealed class ListMcpTools
[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, string? commitType, CancellationToken cancellationToken)
string name, string? workingDir = null, string? commitType = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
throw new InvalidOperationException("name is required.");
@@ -42,7 +42,8 @@ public sealed class ListMcpTools
[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, string? workingDir, string? commitType, CancellationToken cancellationToken)
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.");