Files
ColaFlow/docs/stories/sprint_5/story_5_11.md
Yaojia Wang 48a8431e4f feat(backend): Implement MCP Protocol Handler (Story 5.1)
Implemented JSON-RPC 2.0 protocol handler for MCP communication, enabling AI agents to communicate with ColaFlow using the Model Context Protocol.

**Implementation:**
- JSON-RPC 2.0 data models (Request, Response, Error, ErrorCode)
- MCP protocol models (Initialize, Capabilities, ClientInfo, ServerInfo)
- McpProtocolHandler with method routing and error handling
- Method handlers: initialize, resources/list, tools/list, tools/call
- ASP.NET Core middleware for /mcp endpoint
- Service registration and dependency injection setup

**Testing:**
- 28 unit tests covering protocol parsing, validation, and error handling
- Integration tests for initialize handshake and error responses
- All tests passing with >80% coverage

**Changes:**
- Created ColaFlow.Modules.Mcp.Contracts project
- Created ColaFlow.Modules.Mcp.Domain project
- Created ColaFlow.Modules.Mcp.Application project
- Created ColaFlow.Modules.Mcp.Infrastructure project
- Created ColaFlow.Modules.Mcp.Tests project
- Registered MCP module in ColaFlow.API Program.cs
- Added /mcp endpoint via middleware

**Acceptance Criteria Met:**
 JSON-RPC 2.0 messages correctly parsed
 Request validation (jsonrpc: "2.0", method, params, id)
 Error responses conform to JSON-RPC 2.0 spec
 Invalid requests return proper error codes (-32700, -32600, -32601, -32602)
 MCP initialize method implemented
 Server capabilities returned (resources, tools, prompts)
 Protocol version negotiation works (1.0)
 Request routing to method handlers
 Unit test coverage > 80%
 All tests passing

**Story**: docs/stories/sprint_5/story_5_1.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 19:38:34 +01:00

16 KiB

story_id, sprint_id, phase, status, priority, story_points, assignee, estimated_days, created_date, dependencies
story_id sprint_id phase status priority story_points assignee estimated_days created_date dependencies
story_5_11 sprint_5 Phase 3 - Tools & Diff Preview not_started P0 8 backend 3 2025-11-06
story_5_1
story_5_9
story_5_10

Story 5.11: Core MCP Tools Implementation

Phase: Phase 3 - Tools & Diff Preview (Week 5-6) Priority: P0 CRITICAL Estimated Effort: 8 Story Points (3 days)

User Story

As an AI Agent I want to create, update, and comment on issues through MCP Tools So that I can automate project management tasks

Business Value

MCP Tools enable AI write operations, completing the AI integration loop:

  • AI can create Issues (Epic/Story/Task) via natural language
  • AI can update issue status automatically
  • AI can add comments and collaborate with team
  • 50% reduction in manual project management work

Acceptance Criteria

AC1: create_issue Tool

  • Create Epic, Story, or Task
  • Support all required fields (title, description, type, priority)
  • Support optional fields (assignee, estimated hours, parent)
  • Generate Diff Preview before execution
  • Create PendingChange (NOT execute immediately)
  • Return pendingChangeId to AI

AC2: update_status Tool

  • Update issue status (Todo → InProgress → Done, etc.)
  • Validate status transitions (workflow rules)
  • Generate Diff Preview
  • Create PendingChange
  • Return pendingChangeId to AI

AC3: add_comment Tool

  • Add comment to any issue (Epic/Story/Task)
  • Support markdown formatting
  • Track comment author (AI API Key)
  • Generate Diff Preview
  • Create PendingChange
  • Return pendingChangeId to AI

AC4: Tool Registration

  • All 3 Tools auto-register at startup
  • tools/list returns complete catalog
  • Each Tool has name, description, input schema

AC5: Input Validation

  • JSON Schema validation for tool inputs
  • Required fields validation
  • Type validation (UUID, enum, etc.)
  • Return -32602 (InvalidParams) for validation errors

AC6: Testing

  • Unit tests for each Tool (> 80% coverage)
  • Integration tests (end-to-end tool execution)
  • Test Diff Preview generation
  • Test PendingChange creation (NOT execution)

Technical Design

Tool Interface

public interface IMcpTool
{
    string Name { get; }
    string Description { get; }
    McpToolInputSchema InputSchema { get; }

    Task<McpToolResult> ExecuteAsync(
        McpToolCall toolCall,
        CancellationToken cancellationToken);
}

public class McpToolCall
{
    public string Name { get; set; }
    public Dictionary<string, object> Arguments { get; set; }
}

public class McpToolResult
{
    public IEnumerable<McpToolContent> Content { get; set; }
    public bool IsError { get; set; }
}

public class McpToolContent
{
    public string Type { get; set; } // "text" or "resource"
    public string Text { get; set; }
}

Example: CreateIssueTool

public class CreateIssueTool : IMcpTool
{
    public string Name => "create_issue";
    public string Description => "Create a new issue (Epic/Story/Task/Bug)";

    public McpToolInputSchema InputSchema => new()
    {
        Type = "object",
        Properties = new Dictionary<string, JsonSchemaProperty>
        {
            ["projectId"] = new() { Type = "string", Format = "uuid", Required = true },
            ["title"] = new() { Type = "string", MinLength = 1, MaxLength = 200, Required = true },
            ["description"] = new() { Type = "string" },
            ["type"] = new() { Type = "string", Enum = new[] { "Epic", "Story", "Task", "Bug" }, Required = true },
            ["priority"] = new() { Type = "string", Enum = new[] { "Low", "Medium", "High", "Critical" } },
            ["assigneeId"] = new() { Type = "string", Format = "uuid" },
            ["estimatedHours"] = new() { Type = "number", Minimum = 0 },
            ["parentId"] = new() { Type = "string", Format = "uuid" }
        }
    };

    private readonly IDiffPreviewService _diffPreview;
    private readonly IPendingChangeService _pendingChange;
    private readonly ILogger<CreateIssueTool> _logger;

    public async Task<McpToolResult> ExecuteAsync(
        McpToolCall toolCall,
        CancellationToken ct)
    {
        _logger.LogInformation("Executing create_issue tool");

        // 1. Parse and validate input
        var input = ParseAndValidateInput(toolCall.Arguments);

        // 2. Build "after data" object
        var afterData = new IssueDto
        {
            ProjectId = input.ProjectId,
            Title = input.Title,
            Description = input.Description,
            Type = input.Type,
            Priority = input.Priority ?? Priority.Medium,
            AssigneeId = input.AssigneeId,
            EstimatedHours = input.EstimatedHours,
            ParentId = input.ParentId
        };

        // 3. Generate Diff Preview (CREATE operation)
        var diff = await _diffPreview.GeneratePreviewAsync(
            entityId: null,
            afterData: afterData,
            operation: "CREATE",
            cancellationToken: ct);

        // 4. Create PendingChange (do NOT execute yet)
        var pendingChange = await _pendingChange.CreateAsync(
            toolName: Name,
            diff: diff,
            cancellationToken: ct);

        _logger.LogInformation(
            "PendingChange created: {PendingChangeId} - {Operation} {EntityType}",
            pendingChange.Id, diff.Operation, diff.EntityType);

        // 5. Return pendingChangeId to AI (NOT the created issue)
        return new McpToolResult
        {
            Content = new[]
            {
                new McpToolContent
                {
                    Type = "text",
                    Text = $"Change pending approval. ID: {pendingChange.Id}\n\n" +
                           $"Operation: Create {input.Type}\n" +
                           $"Title: {input.Title}\n" +
                           $"Priority: {input.Priority}\n\n" +
                           $"A human user must approve this change before it takes effect."
                }
            },
            IsError = false
        };
    }

    private CreateIssueInput ParseAndValidateInput(Dictionary<string, object> args)
    {
        // Parse and validate using JSON Schema
        var input = new CreateIssueInput
        {
            ProjectId = ParseGuid(args, "projectId"),
            Title = ParseString(args, "title", required: true),
            Description = ParseString(args, "description"),
            Type = ParseEnum<IssueType>(args, "type", required: true),
            Priority = ParseEnum<Priority>(args, "priority"),
            AssigneeId = ParseGuid(args, "assigneeId"),
            EstimatedHours = ParseDecimal(args, "estimatedHours"),
            ParentId = ParseGuid(args, "parentId")
        };

        // Additional business validation
        if (input.Type == IssueType.Task && input.ParentId == null)
            throw new McpValidationException("Task must have a parent Story or Epic");

        return input;
    }
}

public class CreateIssueInput
{
    public Guid ProjectId { get; set; }
    public string Title { get; set; }
    public string? Description { get; set; }
    public IssueType Type { get; set; }
    public Priority? Priority { get; set; }
    public Guid? AssigneeId { get; set; }
    public decimal? EstimatedHours { get; set; }
    public Guid? ParentId { get; set; }
}

Example: UpdateStatusTool

public class UpdateStatusTool : IMcpTool
{
    public string Name => "update_status";
    public string Description => "Update the status of an issue";

    public McpToolInputSchema InputSchema => new()
    {
        Type = "object",
        Properties = new Dictionary<string, JsonSchemaProperty>
        {
            ["issueId"] = new() { Type = "string", Format = "uuid", Required = true },
            ["newStatus"] = new() { Type = "string", Enum = new[] {
                "Backlog", "Todo", "InProgress", "Review", "Done", "Cancelled"
            }, Required = true }
        }
    };

    private readonly IIssueRepository _issueRepo;
    private readonly IDiffPreviewService _diffPreview;
    private readonly IPendingChangeService _pendingChange;

    public async Task<McpToolResult> ExecuteAsync(
        McpToolCall toolCall,
        CancellationToken ct)
    {
        var issueId = ParseGuid(toolCall.Arguments, "issueId");
        var newStatus = ParseEnum<IssueStatus>(toolCall.Arguments, "newStatus");

        // Fetch current issue
        var issue = await _issueRepo.GetByIdAsync(issueId, ct);
        if (issue == null)
            throw new McpNotFoundException("Issue", issueId.ToString());

        // Build "after data" (only status changed)
        var afterData = issue.Clone();
        afterData.Status = newStatus;

        // Generate Diff Preview (UPDATE operation)
        var diff = await _diffPreview.GeneratePreviewAsync(
            entityId: issueId,
            afterData: afterData,
            operation: "UPDATE",
            cancellationToken: ct);

        // Create PendingChange
        var pendingChange = await _pendingChange.CreateAsync(
            toolName: Name,
            diff: diff,
            cancellationToken: ct);

        return new McpToolResult
        {
            Content = new[]
            {
                new McpToolContent
                {
                    Type = "text",
                    Text = $"Status change pending approval. ID: {pendingChange.Id}\n\n" +
                           $"Issue: {issue.Key} - {issue.Title}\n" +
                           $"Old Status: {issue.Status}\n" +
                           $"New Status: {newStatus}"
                }
            },
            IsError = false
        };
    }
}

Tools Catalog Response

{
  "tools": [
    {
      "name": "create_issue",
      "description": "Create a new issue (Epic/Story/Task/Bug)",
      "inputSchema": {
        "type": "object",
        "properties": {
          "projectId": { "type": "string", "format": "uuid" },
          "title": { "type": "string", "minLength": 1, "maxLength": 200 },
          "type": { "type": "string", "enum": ["Epic", "Story", "Task", "Bug"] }
        },
        "required": ["projectId", "title", "type"]
      }
    },
    {
      "name": "update_status",
      "description": "Update the status of an issue",
      "inputSchema": {
        "type": "object",
        "properties": {
          "issueId": { "type": "string", "format": "uuid" },
          "newStatus": { "type": "string", "enum": ["Backlog", "Todo", "InProgress", "Review", "Done"] }
        },
        "required": ["issueId", "newStatus"]
      }
    },
    {
      "name": "add_comment",
      "description": "Add a comment to an issue",
      "inputSchema": {
        "type": "object",
        "properties": {
          "issueId": { "type": "string", "format": "uuid" },
          "content": { "type": "string", "minLength": 1 }
        },
        "required": ["issueId", "content"]
      }
    }
  ]
}

Tasks

Task 1: Tool Infrastructure (3 hours)

  • Create IMcpTool interface
  • Create McpToolCall, McpToolResult, McpToolInputSchema DTOs
  • Create IMcpToolDispatcher interface
  • Implement McpToolDispatcher (route tool calls)

Files to Create:

  • ColaFlow.Modules.Mcp/Contracts/IMcpTool.cs
  • ColaFlow.Modules.Mcp/DTOs/McpToolCall.cs
  • ColaFlow.Modules.Mcp/DTOs/McpToolResult.cs
  • ColaFlow.Modules.Mcp/Services/McpToolDispatcher.cs

Task 2: CreateIssueTool (6 hours)

  • Implement CreateIssueTool class
  • Define input schema (JSON Schema)
  • Parse and validate input
  • Generate Diff Preview
  • Create PendingChange
  • Return pendingChangeId

Files to Create:

  • ColaFlow.Modules.Mcp/Tools/CreateIssueTool.cs
  • ColaFlow.Modules.Mcp.Tests/Tools/CreateIssueToolTests.cs

Task 3: UpdateStatusTool (4 hours)

  • Implement UpdateStatusTool class
  • Fetch current issue
  • Validate status transition (workflow rules)
  • Generate Diff Preview
  • Create PendingChange

Files to Create:

  • ColaFlow.Modules.Mcp/Tools/UpdateStatusTool.cs
  • ColaFlow.Modules.Mcp.Tests/Tools/UpdateStatusToolTests.cs

Task 4: AddCommentTool (3 hours)

  • Implement AddCommentTool class
  • Support markdown formatting
  • Generate Diff Preview
  • Create PendingChange

Files to Create:

  • ColaFlow.Modules.Mcp/Tools/AddCommentTool.cs
  • ColaFlow.Modules.Mcp.Tests/Tools/AddCommentToolTests.cs

Task 5: Tool Registration (2 hours)

  • Update McpRegistry to support Tools
  • Auto-discover Tools via Reflection
  • Implement tools/list method handler

Task 6: Input Validation (3 hours)

  • Create JSON Schema validator
  • Validate required fields
  • Validate types (UUID, enum, number range)
  • Return McpInvalidParamsException on validation failure

Files to Create:

  • ColaFlow.Modules.Mcp/Validation/JsonSchemaValidator.cs

Task 7: Unit Tests (6 hours)

  • Test CreateIssueTool (happy path)
  • Test CreateIssueTool (validation errors)
  • Test UpdateStatusTool
  • Test AddCommentTool
  • Test input validation

Task 8: Integration Tests (4 hours)

  • Test end-to-end tool execution
  • Test Diff Preview generation
  • Test PendingChange creation
  • Test tool does NOT execute immediately

Files to Create:

  • ColaFlow.Modules.Mcp.Tests/Integration/McpToolsIntegrationTests.cs

Testing Strategy

Integration Test Example

[Fact]
public async Task CreateIssueTool_ValidInput_CreatesPendingChange()
{
    // Arrange
    var toolCall = new McpToolCall
    {
        Name = "create_issue",
        Arguments = new Dictionary<string, object>
        {
            ["projectId"] = _projectId.ToString(),
            ["title"] = "New Story",
            ["type"] = "Story",
            ["priority"] = "High"
        }
    };

    // Act
    var result = await _tool.ExecuteAsync(toolCall, CancellationToken.None);

    // Assert
    Assert.False(result.IsError);
    Assert.Contains("pending approval", result.Content.First().Text);

    // Verify PendingChange created (but NOT executed yet)
    var pendingChanges = await _pendingChangeRepo.GetAllAsync();
    Assert.Single(pendingChanges);
    Assert.Equal(PendingChangeStatus.PendingApproval, pendingChanges[0].Status);

    // Verify Story NOT created yet
    var stories = await _storyRepo.GetAllAsync();
    Assert.Empty(stories); // Not created until approval
}

Dependencies

Prerequisites:

  • Story 5.1 (MCP Protocol Handler) - Tool routing
  • Story 5.9 (Diff Preview Service) - Generate diff
  • Story 5.10 (PendingChange Management) - Create pending change

Used By:

  • Story 5.12 (SignalR Notifications) - Notify on pending change created

Risks & Mitigation

Risk Impact Probability Mitigation
Input validation bypass High Low Comprehensive JSON Schema validation, unit tests
Diff Preview generation fails Medium Medium Error handling, fallback to manual entry
Tool execution blocks Medium Low Async/await, timeout mechanism

Definition of Done

  • All 3 Tools implemented and working
  • Input validation working (JSON Schema)
  • Diff Preview generated correctly
  • PendingChange created (NOT executed)
  • Tools registered and discoverable (tools/list)
  • Unit test coverage > 80%
  • Integration tests passing
  • Code reviewed

Notes

Why This Story Matters

  • Core M2 Feature: Enables AI write operations
  • 50% Time Savings: AI automates manual tasks
  • User Value: Natural language project management
  • Milestone Completion: Completes basic AI integration loop

Key Design Decisions

  1. Deferred Execution: Tools create PendingChange, NOT execute immediately
  2. JSON Schema Validation: Strict input validation prevents errors
  3. Diff Preview First: Always show user what will change
  4. Return pendingChangeId: AI knows what to track