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,304 @@
---
story_id: story_5_8
sprint_id: sprint_5
phase: Phase 2 - Resources
status: not_started
priority: P1
story_points: 5
assignee: backend
estimated_days: 2
created_date: 2025-11-06
dependencies: [story_5_5]
---
# Story 5.8: Redis Caching Integration
**Phase**: Phase 2 - Resources (Week 3-4)
**Priority**: P1 HIGH
**Estimated Effort**: 5 Story Points (2 days)
## User Story
**As a** System Architect
**I want** Redis caching for frequently accessed MCP Resources
**So that** AI agents get fast responses and database load is reduced
## Business Value
Performance optimization delivers:
- 30-50% faster response times (from 200ms to 80ms)
- Reduced database load (80% cache hit rate target)
- Better scalability (support more concurrent AI agents)
- Improved user experience (faster AI responses)
## Acceptance Criteria
### AC1: Redis Integration
- [ ] Redis client configured (StackExchange.Redis)
- [ ] Connection pooling and retry logic
- [ ] Health checks for Redis availability
### AC2: Cache-Aside Pattern
- [ ] Check cache before database query
- [ ] Populate cache on cache miss
- [ ] Return cached data on cache hit
### AC3: TTL Configuration
- [ ] `projects.list`: 5 minutes TTL
- [ ] `users.list`: 5 minutes TTL
- [ ] `sprints.current`: 2 minutes TTL
- [ ] `issues.search`: 2 minutes TTL (cache by query params)
### AC4: Cache Invalidation
- [ ] Invalidate on data updates (domain events)
- [ ] Manual cache clear API endpoint
- [ ] Tenant-scoped cache keys
### AC5: Performance Metrics
- [ ] Track cache hit rate (target > 80%)
- [ ] Track cache miss rate
- [ ] Track response time improvement
- [ ] Export metrics for monitoring
### AC6: Testing
- [ ] Unit tests for caching logic
- [ ] Integration tests (cache hit/miss scenarios)
- [ ] Performance benchmarks (with/without cache)
## Technical Design
### Cache Service Interface
```csharp
public interface IMcpCacheService
{
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
Task SetAsync<T>(string key, T value, TimeSpan ttl, CancellationToken cancellationToken = default);
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default);
}
public class RedisMcpCacheService : IMcpCacheService
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<RedisMcpCacheService> _logger;
public async Task<T?> GetAsync<T>(string key, CancellationToken ct)
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (!value.HasValue)
{
_logger.LogDebug("Cache miss: {Key}", key);
return default;
}
_logger.LogDebug("Cache hit: {Key}", key);
return JsonSerializer.Deserialize<T>(value!);
}
public async Task SetAsync<T>(string key, T value, TimeSpan ttl, CancellationToken ct)
{
var db = _redis.GetDatabase();
var json = JsonSerializer.Serialize(value);
await db.StringSetAsync(key, json, ttl);
_logger.LogDebug("Cache set: {Key} (TTL: {TTL}s)", key, ttl.TotalSeconds);
}
public async Task RemoveAsync(string key, CancellationToken ct)
{
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(key);
_logger.LogDebug("Cache removed: {Key}", key);
}
public async Task RemoveByPatternAsync(string pattern, CancellationToken ct)
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: pattern);
var db = _redis.GetDatabase();
foreach (var key in keys)
{
await db.KeyDeleteAsync(key);
}
_logger.LogDebug("Cache removed by pattern: {Pattern}", pattern);
}
}
```
### Cached Resource Example
```csharp
public class ProjectsListResource : IMcpResource
{
private readonly IProjectRepository _projectRepo;
private readonly ITenantContext _tenantContext;
private readonly IMcpCacheService _cache;
public async Task<McpResourceContent> GetContentAsync(
McpResourceRequest request,
CancellationToken cancellationToken)
{
var tenantId = _tenantContext.CurrentTenantId;
var cacheKey = $"mcp:{tenantId}:projects.list";
// Try cache first
var cached = await _cache.GetAsync<ProjectListDto[]>(cacheKey, cancellationToken);
if (cached != null)
{
return CreateResponse(cached);
}
// Cache miss - query database
var projects = await _projectRepo.GetAllAsync(tenantId, cancellationToken);
var projectDtos = projects.Select(MapToDto).ToArray();
// Populate cache
await _cache.SetAsync(cacheKey, projectDtos, TimeSpan.FromMinutes(5), cancellationToken);
return CreateResponse(projectDtos);
}
}
```
### Cache Key Format
```
mcp:{tenantId}:{resourceUri}[:{params_hash}]
Examples:
- mcp:00000000-0000-0000-0000-000000000001:projects.list
- mcp:00000000-0000-0000-0000-000000000001:issues.search:abc123
- mcp:00000000-0000-0000-0000-000000000001:sprints.current
```
### Cache Invalidation (Domain Events)
```csharp
public class ProjectUpdatedEventHandler : INotificationHandler<ProjectUpdatedEvent>
{
private readonly IMcpCacheService _cache;
public async Task Handle(ProjectUpdatedEvent e, CancellationToken ct)
{
// Invalidate projects.list for this tenant
await _cache.RemoveAsync($"mcp:{e.TenantId}:projects.list", ct);
// Invalidate specific project
await _cache.RemoveAsync($"mcp:{e.TenantId}:projects.get/{e.ProjectId}", ct);
}
}
```
## Tasks
### Task 1: Redis Client Setup (2 hours)
- [ ] Add StackExchange.Redis NuGet package
- [ ] Configure connection string in `appsettings.json`
- [ ] Create `RedisMcpCacheService` implementation
- [ ] Add health checks
**Files to Create**:
- `ColaFlow.Modules.Mcp/Services/RedisMcpCacheService.cs`
### Task 2: Cache Service Interface (2 hours)
- [ ] Create `IMcpCacheService` interface
- [ ] Implement Get/Set/Remove methods
- [ ] Add logging and metrics
**Files to Create**:
- `ColaFlow.Modules.Mcp/Contracts/IMcpCacheService.cs`
### Task 3: Update Resources with Caching (6 hours)
- [ ] Update `ProjectsListResource` to use cache
- [ ] Update `UsersListResource` to use cache
- [ ] Update `SprintsCurrentResource` to use cache
- [ ] Update `IssuesSearchResource` to use cache (with query hash)
### Task 4: Cache Invalidation Event Handlers (3 hours)
- [ ] ProjectCreated/Updated/Deleted → invalidate projects cache
- [ ] EpicCreated/Updated/Deleted → invalidate issues cache
- [ ] StoryCreated/Updated/Deleted → invalidate issues cache
- [ ] TaskCreated/Updated/Deleted → invalidate issues cache
**Files to Create**:
- `ColaFlow.Modules.Mcp/EventHandlers/CacheInvalidationEventHandlers.cs`
### Task 5: Performance Metrics (2 hours)
- [ ] Track cache hit/miss rates
- [ ] Track response time improvement
- [ ] Log metrics to structured logs
### Task 6: Unit & Integration Tests (4 hours)
- [ ] Test cache hit scenario
- [ ] Test cache miss scenario
- [ ] Test cache invalidation
- [ ] Performance benchmarks (with/without cache)
**Files to Create**:
- `ColaFlow.Modules.Mcp.Tests/Services/RedisMcpCacheServiceTests.cs`
- `ColaFlow.Modules.Mcp.Tests/Integration/CachingIntegrationTests.cs`
## Testing Strategy
### Performance Benchmarks
```
Scenario: projects.list (100 projects)
- Without cache: 180ms (database query)
- With cache: 60ms (67% improvement)
- Cache hit rate: 85%
```
### Integration Tests
- Test cache hit after first request
- Test cache miss on first request
- Test cache invalidation on update
- Test TTL expiration
## Dependencies
**Prerequisites**:
- Story 5.5 (Core Resources) - Resources to cache
- Redis server running (Docker or cloud)
**Optional**: Not blocking for M2 MVP
## Risks & Mitigation
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| Redis unavailable | Medium | Low | Fallback to database, circuit breaker |
| Cache inconsistency | Medium | Medium | Short TTL, event-driven invalidation |
| Memory usage | Low | Low | Set max memory limit in Redis |
## Definition of Done
- [ ] Redis integration working
- [ ] Cache hit rate > 80%
- [ ] Response time improved by 30%+
- [ ] Cache invalidation working
- [ ] All tests passing
- [ ] Performance benchmarks documented
## Notes
### Why This Story Matters
- **Performance**: 30-50% faster response times
- **Scalability**: Reduce database load by 80%
- **Cost**: Lower database resource usage
- **User Experience**: Faster AI responses
### Redis Configuration
```json
{
"Redis": {
"ConnectionString": "localhost:6379",
"InstanceName": "colaflow:",
"DefaultTTL": 300
}
}
```