fix(worker): make external MCP filter params optional, surface tool errors

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.
This commit is contained in:
mika kuns
2026-07-23 18:21:46 +02:00
parent 3d668da65c
commit d7ebafd556
8 changed files with 176 additions and 18 deletions
+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);