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>
This commit is contained in:
508
docs/stories/sprint_5/story_5_11.md
Normal file
508
docs/stories/sprint_5/story_5_11.md
Normal file
@@ -0,0 +1,508 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user