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.
77 lines
3.5 KiB
C#
77 lines
3.5 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 TaskConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns);
|
|
|
|
[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, cfg.MaxTurns);
|
|
}
|
|
|
|
[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 = 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.");
|
|
|
|
var m = model.NullIfBlank();
|
|
var sp = systemPrompt.NullIfBlank();
|
|
var ap = agentPath.NullIfBlank();
|
|
|
|
if (m is null && sp is null && ap is null && maxTurns is null)
|
|
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
|
else
|
|
await _lists.SetConfigAsync(new ListConfigEntity
|
|
{
|
|
ListId = listId, Model = m, SystemPrompt = sp, AgentPath = ap, MaxTurns = maxTurns,
|
|
}, cancellationToken);
|
|
|
|
await _broadcaster.ListUpdated(listId);
|
|
}
|
|
|
|
[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 = 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.");
|
|
|
|
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, ct: cancellationToken);
|
|
await _broadcaster.TaskUpdated(taskId);
|
|
}
|
|
|
|
[McpServerTool, Description("Get per-task config overrides (model/system prompt/agent path/max turns). Returns null if no override is set on this task.")]
|
|
public async Task<TaskConfigDto?> GetTaskConfig(string taskId, CancellationToken cancellationToken)
|
|
{
|
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
|
if (task.Model is null && task.SystemPrompt is null && task.AgentPath is null && task.MaxTurns is null)
|
|
return null;
|
|
return new TaskConfigDto(task.Model, task.SystemPrompt, task.AgentPath, task.MaxTurns);
|
|
}
|
|
}
|