---
story_id: story_5_11
sprint_id: sprint_5
phase: Phase 3 - Tools & Diff Preview
status: not_started
priority: P0
story_points: 8
assignee: backend
estimated_days: 3
created_date: 2025-11-06
dependencies: [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

```csharp
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

```csharp
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

```csharp
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

```json
{
  "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

```csharp
[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
