--- 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 ExecuteAsync( McpToolCall toolCall, CancellationToken cancellationToken); } public class McpToolCall { public string Name { get; set; } public Dictionary Arguments { get; set; } } public class McpToolResult { public IEnumerable 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 { ["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 _logger; public async Task 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 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(args, "type", required: true), Priority = ParseEnum(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 { ["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 ExecuteAsync( McpToolCall toolCall, CancellationToken ct) { var issueId = ParseGuid(toolCall.Arguments, "issueId"); var newStatus = ParseEnum(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 { ["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