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:
Yaojia Wang
2025-11-07 19:38:34 +01:00
parent d3ef2c1441
commit 48a8431e4f
43 changed files with 7003 additions and 0 deletions

View 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