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>
16 KiB
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.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/listreturns 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
IMcpToolinterface - Create
McpToolCall,McpToolResult,McpToolInputSchemaDTOs - Create
IMcpToolDispatcherinterface - Implement
McpToolDispatcher(route tool calls)
Files to Create:
ColaFlow.Modules.Mcp/Contracts/IMcpTool.csColaFlow.Modules.Mcp/DTOs/McpToolCall.csColaFlow.Modules.Mcp/DTOs/McpToolResult.csColaFlow.Modules.Mcp/Services/McpToolDispatcher.cs
Task 2: CreateIssueTool (6 hours)
- Implement
CreateIssueToolclass - Define input schema (JSON Schema)
- Parse and validate input
- Generate Diff Preview
- Create PendingChange
- Return pendingChangeId
Files to Create:
ColaFlow.Modules.Mcp/Tools/CreateIssueTool.csColaFlow.Modules.Mcp.Tests/Tools/CreateIssueToolTests.cs
Task 3: UpdateStatusTool (4 hours)
- Implement
UpdateStatusToolclass - Fetch current issue
- Validate status transition (workflow rules)
- Generate Diff Preview
- Create PendingChange
Files to Create:
ColaFlow.Modules.Mcp/Tools/UpdateStatusTool.csColaFlow.Modules.Mcp.Tests/Tools/UpdateStatusToolTests.cs
Task 4: AddCommentTool (3 hours)
- Implement
AddCommentToolclass - Support markdown formatting
- Generate Diff Preview
- Create PendingChange
Files to Create:
ColaFlow.Modules.Mcp/Tools/AddCommentTool.csColaFlow.Modules.Mcp.Tests/Tools/AddCommentToolTests.cs
Task 5: Tool Registration (2 hours)
- Update
McpRegistryto support Tools - Auto-discover Tools via Reflection
- Implement
tools/listmethod 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
- Deferred Execution: Tools create PendingChange, NOT execute immediately
- JSON Schema Validation: Strict input validation prevents errors
- Diff Preview First: Always show user what will change
- Return pendingChangeId: AI knows what to track