using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Server; using Nexus.Api.Controllers; using Nexus.Api.Data; using Nexus.Api.Models; using Nexus.Api.Services; using Xunit; namespace Nexus.Api.Tests; public sealed class NexusMcpToolsTests { [Fact] public void NexusMcpTools_RegistersExpectedToolNames() { var toolNames = typeof(NexusMcpTools) .GetMethods() .Select(method => method.GetCustomAttributes(typeof(McpServerToolAttribute), inherit: false) .OfType() .FirstOrDefault()) .Where(attribute => attribute is not null) .Select(attribute => attribute!.Name ?? string.Empty) .Order() .ToArray(); Assert.Equal( [ "nexus_agent_overview", "nexus_append_activity", "nexus_create_child_task", "nexus_create_task", "nexus_get_activity", "nexus_get_board", "nexus_get_children", "nexus_get_task", "nexus_handoff", "nexus_update_status" ], toolNames); } [Fact] public void NexusMcpTaskState_OnlyContainsCanonicalStates() { Assert.Equal( [ nameof(NexusMcpTaskState.Backlog), nameof(NexusMcpTaskState.InProgress), nameof(NexusMcpTaskState.Blocked), nameof(NexusMcpTaskState.Done), nameof(NexusMcpTaskState.Review) ], Enum.GetNames()); Assert.DoesNotContain("Delegated", Enum.GetNames()); } [Fact] public async Task McpTools_ReadAndWrite_UseBridgeService() { await using var fixture = await TaskWorkflowFixture.CreateAsync(); fixture.SetCallerAgent("iris"); var tools = CreateTools(fixture); var createResult = await tools.CreateTask( title: "MCP parent", detail: "Created through MCP tool facade", priority: "High", assignedTo: "iris", ct: CancellationToken.None); Assert.True(createResult.Ok); Assert.NotNull(createResult.Data); Assert.Equal("MCP parent", createResult.Data!.Title); var activityResult = await tools.AppendActivity( createResult.Data.Id, "MCP checkpoint", "comment", CancellationToken.None); Assert.True(activityResult.Ok); Assert.Equal("MCP checkpoint", activityResult.Data!.Message); var statusResult = await tools.UpdateStatus( createResult.Data.Id, NexusMcpTaskState.InProgress, CancellationToken.None); Assert.True(statusResult.Ok); Assert.Equal(TaskStateHelper.ToStateString(TaskState.InProgress), statusResult.Data!.State); var board = await tools.GetBoard(CancellationToken.None); Assert.Contains(board.InProgress, task => task.Id == createResult.Data.Id); var taskResult = await tools.GetTask(createResult.Data.Id, CancellationToken.None); Assert.True(taskResult.Ok); Assert.Equal("MCP parent", taskResult.Data!.Title); } [Fact] public async Task McpTools_UpdateStatus_RejectsUnauthorizedAgent() { await using var fixture = await TaskWorkflowFixture.CreateAsync(); var task = await fixture.TaskService.CreateDashboardTaskAsync( "Programmer cannot move", "State changes stay with Iris/Bao.", "iris", "Normal", "programmer", null, CancellationToken.None); fixture.SetCallerAgent("programmer"); var tools = CreateTools(fixture); var result = await tools.UpdateStatus(task.Id, NexusMcpTaskState.Done, CancellationToken.None); Assert.False(result.Ok); Assert.Equal("nexus_update_status", result.Command); Assert.Contains("not authorized", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task McpTools_ServiceKey_ResolvesAsNexusSystem() { await using var fixture = await TaskWorkflowFixture.CreateAsync(); fixture.HttpContextAccessor.HttpContext = TaskWorkflowFixture.CreateHttpContext( headers: new Dictionary { ["X-Nexus-Api-Key"] = "test-service-key" }); var tools = CreateTools(fixture); var result = await tools.CreateTask( title: "System MCP task", assignedTo: "iris", ct: CancellationToken.None); Assert.True(result.Ok); Assert.Equal("bao", result.Data!.Source); } private static NexusMcpTools CreateTools(TaskWorkflowFixture fixture) => new( fixture.TaskBridgeService, fixture.AgentService, fixture.HttpContextAccessor, fixture.Configuration, NullLogger.Instance); }