diff --git a/backend/Services/NexusMcpTools.cs b/backend/Services/NexusMcpTools.cs index 28dd484..bd033cd 100644 --- a/backend/Services/NexusMcpTools.cs +++ b/backend/Services/NexusMcpTools.cs @@ -61,15 +61,24 @@ public sealed class NexusMcpTools( return activity.Select(entry => new ActivityEntryDto(entry.Id, entry.Type, entry.Message, entry.CreatedAt)).ToList(); } - // ── P1c: Mutating MCP Tools ── + // ── P1c: Mutating MCP Tools (TaskBridgeService facade) ── + // + // Each tool delegates directly to ITaskBridgeService without introducing + // new business logic. Authorization, validation, and side-effects + // (notifications, live-update broadcasts) are handled by the bridge. + /// + /// Creates a top-level task on the Nexus board. + /// The caller (derived from X-Agent-Id or JWT) is set as the default + /// assignee and source. Priority defaults to "Normal". + /// [McpServerTool(Name = "nexus_create_task")] [Description("Create a top-level Nexus task.")] public async Task> CreateTask( - string title, - string? detail = null, - string? priority = "Normal", - string? assignedTo = null, + [Description("Task title (required).")] string title, + [Description("Optional long-form description.")] string? detail = null, + [Description("Priority label. Defaults to 'Normal'.")] string? priority = "Normal", + [Description("Agent ID to assign the task to. Defaults to the caller.")] string? assignedTo = null, CancellationToken ct = default) { var caller = await ResolveCallerAsync(ct); @@ -84,16 +93,21 @@ public sealed class NexusMcpTools( return ToResponse(result, "nexus_create_task"); } + /// + /// Creates a visible child task under a Nexus parent for delegation. + /// If the parent is in Backlog, it is automatically moved to In progress + /// to signal that coordination has started. + /// [McpServerTool(Name = "nexus_create_child_task")] [Description("Create a visible child task under a Nexus parent task for delegation.")] public async Task> CreateChildTask( - Guid parentTaskId, - string title, - string? detail = null, - string? priority = "Normal", - string? assignedTo = null, - string? expectedFrom = null, - bool startsInProgress = false, + [Description("ID of the parent task.")] Guid parentTaskId, + [Description("Child task title (required).")] string title, + [Description("Optional long-form description.")] string? detail = null, + [Description("Priority label. Defaults to 'Normal'.")] string? priority = "Normal", + [Description("Agent ID to assign. Defaults to the expected-from agent.")] string? assignedTo = null, + [Description("Agent who is expected to deliver this work.")] string? expectedFrom = null, + [Description("If true, the child starts in 'In progress' instead of Backlog.")] bool startsInProgress = false, CancellationToken ct = default) { var caller = await ResolveCallerAsync(ct); @@ -111,11 +125,21 @@ public sealed class NexusMcpTools( return ToResponse(result, "nexus_create_child_task"); } + /// + /// Updates a task's lifecycle state. + /// The parameter is typed as + /// so the MCP SDK rejects unknown + /// integer values before the tool is ever invoked. Additionally, + /// performs an exhaustive switch with a + /// defensive fallback. + /// The bridge enforces authorization (only iris/bao/nexus-system may + /// change state) and canonical-state validation. + /// [McpServerTool(Name = "nexus_update_status")] [Description("Update a Nexus task status. The schema only exposes canonical task states.")] public async Task> UpdateStatus( - Guid taskId, - NexusMcpTaskState state, + [Description("ID of the task to update.")] Guid taskId, + [Description("New state: Backlog (0), InProgress (1), Blocked (2), Done (3), Review (4).")] NexusMcpTaskState state, CancellationToken ct = default) { var caller = await ResolveCallerAsync(ct); @@ -123,12 +147,16 @@ public sealed class NexusMcpTools( return ToResponse(result, "nexus_update_status"); } + /// + /// Appends an activity entry (comment, status note, or checkpoint) to a task. + /// This triggers a live-update broadcast so dashboards stay current. + /// [McpServerTool(Name = "nexus_append_activity")] [Description("Append an activity/checkpoint entry to a Nexus task.")] public async Task> AppendActivity( - Guid taskId, - string message, - string? type = "comment", + [Description("ID of the task to annotate.")] Guid taskId, + [Description("Activity message text (required).")] string message, + [Description("Activity type: 'comment', 'status', 'agent-note', 'handoff'. Defaults to 'comment'.")] string? type = "comment", CancellationToken ct = default) { await ResolveCallerAsync(ct); @@ -136,12 +164,17 @@ public sealed class NexusMcpTools( return ToActivityResponse(result, "nexus_append_activity"); } + /// + /// Marks a task handoff to another agent. + /// Updates ExpectedFrom (and AssignedTo for standalone tasks), appends + /// a handoff activity entry, and sends a notification to the target agent. + /// [McpServerTool(Name = "nexus_handoff")] [Description("Mark a task handoff to another known agent and append handoff activity.")] public async Task> Handoff( - Guid taskId, - string targetAgent, - string? note = null, + [Description("ID of the task to hand off.")] Guid taskId, + [Description("Target agent ID (must be a known agent).")] string targetAgent, + [Description("Optional handoff note.")] string? note = null, CancellationToken ct = default) { await ResolveCallerAsync(ct); @@ -226,11 +259,38 @@ public sealed class NexusMcpTools( }; } +/// +/// Canonical task states exposed via the MCP tool schema. +/// Integer values 0-4 map to the canonical string representations used +/// by . The MCP SDK rejects +/// out-of-range integers before the tool is invoked. +/// public enum NexusMcpTaskState { - Backlog, - InProgress, - Blocked, - Done, - Review + /// Task is in the backlog (not yet started). + Backlog = 0, + /// Work is actively in progress. + InProgress = 1, + /// Work is blocked by an external dependency. + Blocked = 2, + /// Work is complete. + Done = 3, + /// Work is ready for review. + Review = 4 +} + +/// +/// Static helpers for . +/// The method provides a testable entry point +/// for verifying that update_status rejects invalid state values. +/// +public static class NexusMcpTaskStateHelper +{ + /// + /// Returns true when is one of the five + /// canonical values defined in . + /// Rejects undefined cast values (e.g. (NexusMcpTaskState)99). + /// + public static bool IsDefined(NexusMcpTaskState state) => + Enum.IsDefined(state); }