73 KiB
ColaFlow MCP Server Architecture Design
Version: 1.0 Date: 2025-11-04 Author: System Architect Status: Design Document
Table of Contents
- Background & Goals
- Architecture Overview
- Module Design
- Resources & Tools Design
- Diff Preview & Approval Mechanism
- Security & Permission Model
- Audit & Observability
- Database Design
- Technology Stack & Rationale
- Implementation Roadmap
- Risks & Mitigation
1. Background & Goals
1.1 Business Context
ColaFlow aims to be an AI-native project management system where AI tools (ChatGPT, Claude, Gemini) can:
- Read project data (projects, issues, sprints, documents)
- Write project data with human approval via diff preview
- Automate workflows while maintaining human oversight and safety
1.2 Current System State
M1 Achievements:
- ✅ Identity Module (User, Tenant, Multi-tenancy)
- ✅ JWT Authentication & RBAC (TenantOwner, TenantAdmin, TenantMember, TenantGuest, AIAgent)
- ✅ Clean Architecture (.NET 9.0, PostgreSQL, EF Core)
- ✅ Domain-Driven Design with Aggregates, Value Objects, Domain Events
Current Tech Stack:
- Backend: .NET 9.0, ASP.NET Core
- Database: PostgreSQL + EF Core
- Authentication: JWT Bearer
- Architecture: Clean Architecture (Domain, Application, Infrastructure, API)
1.3 Technical Objectives
- MCP Server Integration: Expose ColaFlow resources and tools to AI clients via MCP protocol
- Safety First: All AI write operations require diff preview → human approval → commit
- Auditability: Complete audit trail for all AI actions
- Scalability: Designed for horizontal scaling, multi-tenant isolation
- Extensibility: Support multiple AI models and future integrations
1.4 Constraints
- Must integrate with existing Clean Architecture
- Must reuse Identity Module (User, Tenant, TenantRole)
- Must maintain multi-tenant data isolation
- Must comply with GDPR and enterprise security standards
2. Architecture Overview
2.1 High-Level Architecture
┌──────────────────────────────────────────────────────────────────┐
│ AI Client Layer │
│ ChatGPT, Claude, Gemini (via MCP SDK) │
└────────────────────────────┬─────────────────────────────────────┘
│ MCP Protocol (JSON-RPC over stdio/SSE)
┌────────────────────────────┴─────────────────────────────────────┐
│ ColaFlow MCP Server │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ MCP Protocol Handler (JSON-RPC Endpoints) │ │
│ │ - initialize, resources/list, resources/read │ │
│ │ - tools/list, tools/call │ │
│ │ - prompts/list, prompts/get │ │
│ └──────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴─────────────────────────────────────┐ │
│ │ MCP Application Services │ │
│ │ - ResourceService (read-only access) │ │
│ │ - ToolInvocationService (write with diff preview) │ │
│ │ - DiffPreviewService (generate, store, approve) │ │
│ │ - PromptTemplateService (AI prompt management) │ │
│ └──────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴─────────────────────────────────────┐ │
│ │ MCP Security Layer │ │
│ │ - AIAgent Authentication (API Key, JWT) │ │
│ │ - Permission Validation (field-level, tenant-scoped) │ │
│ │ - Rate Limiting │ │
│ └──────────────────────┬─────────────────────────────────────┘ │
└─────────────────────────┼──────────────────────────────────────┘
│
┌─────────────────────────┴──────────────────────────────────────┐
│ ColaFlow Application Layer │
│ - ProjectManagement.Application (Epic, Story, Task, Sprint) │
│ - Identity.Application (User, Tenant, Role) │
└─────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────────────┴──────────────────────────────────────┐
│ Domain Layer │
│ - ProjectManagement.Domain (Aggregates, Entities, VOs) │
│ - Identity.Domain (User, Tenant, Role) │
└─────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────────────┴──────────────────────────────────────┐
│ Infrastructure Layer │
│ - PostgreSQL (Tenant-isolated data) │
│ - MCP Audit Logs (mcp_audit_logs) │
│ - Diff Preview Storage (mcp_diff_previews) │
└────────────────────────────────────────────────────────────────┘
2.2 MCP Protocol Overview
MCP (Model Context Protocol) is a standardized protocol for AI-application communication:
Key Concepts:
- Resources: Read-only data exposures (e.g.,
project://12345,issue://67890) - Tools: AI-invokable functions (e.g.,
create_issue,update_status) - Prompts: Reusable AI prompt templates
- Sampling: AI model invocation (future phase)
Transport:
- stdio: Standard input/output for local processes (MCP SDK default)
- SSE (Server-Sent Events): For web-based AI clients
- JSON-RPC 2.0: Request/response protocol
Example Tool Call Flow:
1. AI Client → MCP Server: tools/list
2. MCP Server → AI Client: [create_issue, update_status, ...]
3. AI Client → MCP Server: tools/call { name: "create_issue", arguments: {...} }
4. MCP Server → AI Client: { diffPreview: {...}, approvalRequired: true }
5. Human approves diff
6. AI Client → MCP Server: tools/call { previewId: "abc123", approve: true }
7. MCP Server → AI Client: { success: true, issueId: "12345" }
2.3 Module Boundaries
New Modules:
-
ColaFlow.Modules.Mcp.Domain
- Aggregates: McpAgent, DiffPreview, AuditLog
- Repositories: IMcpAgentRepository, IDiffPreviewRepository, IAuditLogRepository
-
ColaFlow.Modules.Mcp.Application
- Services: IResourceService, IToolInvocationService, IDiffPreviewService
- Commands: ApproveDiffCommand, RejectDiffCommand, RollbackOperationCommand
- Queries: ListResourcesQuery, GetDiffPreviewQuery
-
ColaFlow.Modules.Mcp.Infrastructure
- Persistence: McpDbContext, Repositories
- Protocol: JsonRpcHandler, SseTransportHandler
- Security: ApiKeyAuthenticationHandler
-
ColaFlow.Modules.Mcp.API
- Controllers: McpProtocolController (JSON-RPC endpoints)
- Middleware: McpAuthenticationMiddleware, McpAuditMiddleware
3. Module Design
3.1 MCP.Domain Module
3.1.1 McpAgent Aggregate
namespace ColaFlow.Modules.Mcp.Domain.Aggregates.McpAgents;
/// <summary>
/// Represents an AI Agent registered to access ColaFlow via MCP
/// </summary>
public sealed class McpAgent : AggregateRoot
{
// Identity
public Guid Id { get; private set; }
public TenantId TenantId { get; private set; }
public string AgentName { get; private set; } // e.g., "ChatGPT for PM Team"
public string AgentType { get; private set; } // e.g., "Claude", "ChatGPT", "Gemini"
// Authentication
public string ApiKeyHash { get; private set; } // Hashed API key
public DateTime ApiKeyExpiresAt { get; private set; }
public bool IsActive { get; private set; }
// Permissions
public McpPermissionLevel PermissionLevel { get; private set; }
public List<string> AllowedResources { get; private set; } // ["projects.*", "issues.read"]
public List<string> AllowedTools { get; private set; } // ["create_issue", "update_status"]
// Audit
public DateTime CreatedAt { get; private set; }
public Guid CreatedBy { get; private set; }
public DateTime? LastUsedAt { get; private set; }
public int RequestCount { get; private set; }
// Factory Method
public static McpAgent Create(
TenantId tenantId,
string agentName,
string agentType,
string apiKeyHash,
DateTime apiKeyExpiresAt,
Guid createdBy)
{
// ... validation & domain event
}
// Business Methods
public void RecordUsage() { /* ... */ }
public void UpdatePermissions(McpPermissionLevel level, List<string> resources, List<string> tools) { /* ... */ }
public void RevokeAccess() { /* ... */ }
public void RegenerateApiKey(string newApiKeyHash, DateTime expiresAt) { /* ... */ }
}
public enum McpPermissionLevel
{
ReadOnly = 1, // Can only read resources
WriteWithPreview = 2, // Can write with diff preview (requires approval)
DirectWrite = 3 // Can write directly (dangerous, TenantOwner only)
}
3.1.2 DiffPreview Aggregate
namespace ColaFlow.Modules.Mcp.Domain.Aggregates.DiffPreviews;
/// <summary>
/// Represents a diff preview for AI-initiated write operations
/// </summary>
public sealed class DiffPreview : AggregateRoot
{
public Guid Id { get; private set; }
public TenantId TenantId { get; private set; }
public Guid AgentId { get; private set; } // McpAgent.Id
// Operation Details
public string ToolName { get; private set; } // e.g., "create_issue"
public string InputParametersJson { get; private set; } // JSON serialized
// Diff Details
public DiffOperation Operation { get; private set; } // Create, Update, Delete
public string EntityType { get; private set; } // e.g., "Issue", "Project"
public Guid? EntityId { get; private set; } // null for Create
public string BeforeStateJson { get; private set; } // null for Create
public string AfterStateJson { get; private set; }
public string DiffJson { get; private set; } // Structured diff
// Risk Assessment
public RiskLevel RiskLevel { get; private set; } // Low, Medium, High
public List<string> RiskReasons { get; private set; } // ["Deletes 50 tasks", "Changes sprint deadline"]
// Approval Workflow
public DiffPreviewStatus Status { get; private set; }
public Guid? ApprovedBy { get; private set; }
public DateTime? ApprovedAt { get; private set; }
public Guid? RejectedBy { get; private set; }
public DateTime? RejectedAt { get; private set; }
public string? RejectionReason { get; private set; }
// Rollback
public bool IsCommitted { get; private set; }
public Guid? CommittedEntityId { get; private set; }
public DateTime? CommittedAt { get; private set; }
public string? RollbackToken { get; private set; } // For event sourcing rollback
// Timestamps
public DateTime CreatedAt { get; private set; }
public DateTime ExpiresAt { get; private set; } // Auto-reject after 24 hours
// Factory & Methods
public static DiffPreview Create(/* ... */) { /* ... */ }
public void Approve(Guid approvedBy) { /* ... */ }
public void Reject(Guid rejectedBy, string reason) { /* ... */ }
public void MarkAsCommitted(Guid entityId) { /* ... */ }
}
public enum DiffOperation { Create, Update, Delete }
public enum RiskLevel { Low, Medium, High, Critical }
public enum DiffPreviewStatus { Pending, Approved, Rejected, Expired, Committed }
3.1.3 McpAuditLog Aggregate
namespace ColaFlow.Modules.Mcp.Domain.Aggregates.AuditLogs;
/// <summary>
/// Complete audit trail for all MCP operations
/// </summary>
public sealed class McpAuditLog : AggregateRoot
{
public Guid Id { get; private set; }
public TenantId TenantId { get; private set; }
public Guid AgentId { get; private set; }
// Request Details
public string OperationType { get; private set; } // "resources/read", "tools/call"
public string ResourceUri { get; private set; } // e.g., "project://12345"
public string ToolName { get; private set; }
public string InputParametersJson { get; private set; }
// Response Details
public bool IsSuccess { get; private set; }
public string? ErrorMessage { get; private set; }
public int? HttpStatusCode { get; private set; }
// Diff Preview (if applicable)
public Guid? DiffPreviewId { get; private set; }
public DiffPreviewStatus? DiffStatus { get; private set; }
// Performance
public int DurationMs { get; private set; }
// Context
public string ClientIpAddress { get; private set; }
public string UserAgent { get; private set; }
public DateTime Timestamp { get; private set; }
public static McpAuditLog Create(/* ... */) { /* ... */ }
}
3.2 MCP.Application Module
3.2.1 Resource Service
namespace ColaFlow.Modules.Mcp.Application.Services;
public interface IResourceService
{
/// <summary>
/// List all available resources for the current AI Agent
/// </summary>
Task<List<ResourceDescriptor>> ListResourcesAsync(
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default);
/// <summary>
/// Read a specific resource
/// </summary>
Task<ResourceContent> ReadResourceAsync(
string resourceUri, // e.g., "project://abc-123"
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default);
}
public record ResourceDescriptor(
string Uri,
string Name,
string Description,
string MimeType); // "application/json", "text/plain"
public record ResourceContent(
string Uri,
string Content, // JSON or text
string MimeType);
Implementation:
- Reads from ProjectManagement module (via Application layer)
- Applies field-level permissions (hide sensitive fields)
- Tenant-scoped queries
- Returns JSON representations
3.2.2 Tool Invocation Service
namespace ColaFlow.Modules.Mcp.Application.Services;
public interface IToolInvocationService
{
/// <summary>
/// List all available tools for the current AI Agent
/// </summary>
Task<List<ToolDescriptor>> ListToolsAsync(
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default);
/// <summary>
/// Invoke a tool (generates diff preview for write operations)
/// </summary>
Task<ToolInvocationResult> InvokeToolAsync(
string toolName,
Dictionary<string, object> arguments,
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default);
}
public record ToolDescriptor(
string Name,
string Description,
JsonSchema InputSchema);
public record ToolInvocationResult
{
public bool RequiresApproval { get; init; }
public Guid? DiffPreviewId { get; init; }
public DiffPreviewDto? DiffPreview { get; init; }
public object? Result { get; init; } // For read-only tools
public bool IsSuccess { get; init; }
public string? ErrorMessage { get; init; }
}
3.2.3 Diff Preview Service
namespace ColaFlow.Modules.Mcp.Application.Services;
public interface IDiffPreviewService
{
/// <summary>
/// Generate a diff preview for a proposed write operation
/// </summary>
Task<DiffPreview> GenerateDiffAsync(
string toolName,
Dictionary<string, object> arguments,
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default);
/// <summary>
/// Get diff preview by ID
/// </summary>
Task<DiffPreview?> GetDiffPreviewAsync(
Guid previewId,
TenantId tenantId,
CancellationToken cancellationToken = default);
/// <summary>
/// Approve and commit a diff preview
/// </summary>
Task<CommitResult> ApproveAndCommitAsync(
Guid previewId,
Guid approvedBy,
TenantId tenantId,
CancellationToken cancellationToken = default);
/// <summary>
/// Reject a diff preview
/// </summary>
Task RejectAsync(
Guid previewId,
Guid rejectedBy,
string reason,
TenantId tenantId,
CancellationToken cancellationToken = default);
}
public record CommitResult(
bool IsSuccess,
Guid? EntityId,
string? ErrorMessage);
Diff Generation Algorithm:
- Deserialize tool arguments
- Load current state (if updating)
- Apply arguments to domain model (dry-run)
- Generate JSON diff (before/after)
- Calculate risk level:
- Low: Single field updates, comments
- Medium: Status changes, assignments
- High: Deletions, sprint changes, bulk operations
- Critical: Data exports, permission changes
- Store DiffPreview entity
- Return preview to AI client
3.3 MCP.Infrastructure Module
3.3.1 JSON-RPC Protocol Handler
namespace ColaFlow.Modules.Mcp.Infrastructure.Protocol;
public class JsonRpcHandler
{
private readonly IResourceService _resourceService;
private readonly IToolInvocationService _toolService;
public async Task<JsonRpcResponse> HandleRequestAsync(
JsonRpcRequest request,
TenantId tenantId,
Guid agentId,
CancellationToken cancellationToken = default)
{
return request.Method switch
{
"initialize" => await HandleInitializeAsync(request),
"resources/list" => await HandleResourcesListAsync(tenantId, agentId),
"resources/read" => await HandleResourceReadAsync(request, tenantId, agentId),
"tools/list" => await HandleToolsListAsync(tenantId, agentId),
"tools/call" => await HandleToolCallAsync(request, tenantId, agentId),
_ => JsonRpcResponse.Error(request.Id, -32601, "Method not found")
};
}
}
3.3.2 Database Schema (EF Core Configurations)
// McpAgentConfiguration.cs
public class McpAgentConfiguration : IEntityTypeConfiguration<McpAgent>
{
public void Configure(EntityTypeBuilder<McpAgent> builder)
{
builder.ToTable("mcp_agents", "mcp");
builder.HasKey(a => a.Id);
builder.Property(a => a.AgentName).IsRequired().HasMaxLength(200);
builder.Property(a => a.ApiKeyHash).IsRequired().HasMaxLength(512);
// Tenant isolation
builder.HasQueryFilter(a => a.TenantId == /* current tenant */);
// Indexes
builder.HasIndex(a => new { a.TenantId, a.IsActive });
}
}
4. Resources & Tools Design
4.1 Resource Definitions
4.1.1 Project Resources
{
"resources": [
{
"uri": "projects://list",
"name": "All Projects",
"description": "List all accessible projects",
"mimeType": "application/json"
},
{
"uri": "project://{projectId}",
"name": "Project Details",
"description": "Get detailed information about a specific project",
"mimeType": "application/json"
},
{
"uri": "project://{projectId}/issues",
"name": "Project Issues",
"description": "List all issues in a project",
"mimeType": "application/json"
}
]
}
Example Resource Response:
{
"uri": "project://abc-123",
"content": {
"id": "abc-123",
"name": "ColaFlow MVP",
"description": "Build initial MVP version",
"status": "Active",
"owner": {
"id": "user-456",
"name": "John Doe",
"email": "john@example.com"
},
"startDate": "2025-11-01",
"endDate": "2025-12-31",
"issueCount": 45,
"completedIssueCount": 12
},
"mimeType": "application/json"
}
4.1.2 Issue Resources
{
"resources": [
{
"uri": "issues://search?query={query}",
"name": "Search Issues",
"description": "Search issues by title, description, or tags"
},
{
"uri": "issue://{issueId}",
"name": "Issue Details",
"description": "Get detailed information about a specific issue"
}
]
}
4.1.3 Sprint Resources
{
"resources": [
{
"uri": "sprints://current",
"name": "Current Sprint",
"description": "Get the currently active sprint"
},
{
"uri": "sprint://{sprintId}",
"name": "Sprint Details",
"description": "Get detailed information about a specific sprint"
}
]
}
4.2 Tool Definitions
4.2.1 create_issue Tool
{
"name": "create_issue",
"description": "Create a new issue in a project",
"inputSchema": {
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "The project ID where the issue will be created"
},
"title": {
"type": "string",
"description": "Issue title (required)"
},
"description": {
"type": "string",
"description": "Detailed description of the issue"
},
"issueType": {
"type": "string",
"enum": ["Story", "Task", "Bug", "Epic"],
"description": "Type of issue"
},
"priority": {
"type": "string",
"enum": ["Low", "Medium", "High", "Critical"],
"default": "Medium"
},
"assigneeId": {
"type": "string",
"description": "User ID to assign the issue to (optional)"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Tags for categorization"
}
},
"required": ["projectId", "title", "issueType"]
}
}
Diff Preview Example:
{
"previewId": "preview-123",
"operation": "create",
"entityType": "Issue",
"changes": {
"before": null,
"after": {
"title": "Implement MCP Server authentication",
"description": "Add API key authentication for AI agents",
"issueType": "Task",
"priority": "High",
"projectId": "project-abc",
"assigneeId": "user-456",
"tags": ["backend", "security"]
}
},
"riskLevel": "Low",
"riskReasons": [],
"requiresApproval": true,
"expiresAt": "2025-11-05T10:00:00Z"
}
4.2.2 update_issue_status Tool
{
"name": "update_issue_status",
"description": "Update the status of an existing issue",
"inputSchema": {
"type": "object",
"properties": {
"issueId": {
"type": "string",
"description": "The issue ID to update"
},
"status": {
"type": "string",
"enum": ["ToDo", "InProgress", "Review", "Done"],
"description": "New status"
},
"comment": {
"type": "string",
"description": "Optional comment explaining the status change"
}
},
"required": ["issueId", "status"]
}
}
Diff Preview Example:
{
"previewId": "preview-456",
"operation": "update",
"entityType": "Issue",
"entityId": "issue-789",
"changes": {
"before": {
"status": "InProgress",
"updatedAt": "2025-11-03T15:30:00Z"
},
"after": {
"status": "Done",
"updatedAt": "2025-11-04T10:00:00Z"
}
},
"diff": [
{
"field": "status",
"oldValue": "InProgress",
"newValue": "Done"
}
],
"riskLevel": "Medium",
"riskReasons": ["Marks issue as complete without code review"],
"requiresApproval": true
}
4.2.3 assign_issue Tool
{
"name": "assign_issue",
"description": "Assign an issue to a team member",
"inputSchema": {
"type": "object",
"properties": {
"issueId": { "type": "string" },
"assigneeId": { "type": "string" },
"notifyAssignee": { "type": "boolean", "default": true }
},
"required": ["issueId", "assigneeId"]
}
}
4.2.4 log_decision Tool
{
"name": "log_decision",
"description": "Log an architectural or product decision",
"inputSchema": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"title": { "type": "string" },
"decision": { "type": "string" },
"rationale": { "type": "string" },
"alternatives": {
"type": "array",
"items": { "type": "string" }
},
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["projectId", "title", "decision"]
}
}
4.3 Prompt Templates
{
"prompts": [
{
"name": "daily_standup",
"description": "Generate daily standup report",
"arguments": [
{
"name": "date",
"description": "Report date (YYYY-MM-DD)",
"required": false
}
],
"template": "Generate a daily standup report for {{date}}. Include:\n1. Completed tasks\n2. In-progress tasks\n3. Blockers\n4. Upcoming priorities"
},
{
"name": "sprint_planning",
"description": "Generate sprint planning summary",
"template": "Analyze the backlog and generate sprint planning recommendations:\n1. Suggested stories for next sprint\n2. Estimated story points\n3. Team capacity analysis\n4. Risk assessment"
}
]
}
5. Diff Preview & Approval Mechanism
5.1 Diff Generation Flow
┌──────────────┐
│ AI Client │
│ (ChatGPT) │
└──────┬───────┘
│ tools/call { name: "create_issue", arguments: {...} }
▼
┌────────────────────────────────────────────────────────┐
│ ToolInvocationService │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Validate permissions (can use create_issue?) │ │
│ │ 2. Validate arguments (schema validation) │ │
│ │ 3. Call DiffPreviewService.GenerateDiffAsync() │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ DiffPreviewService.GenerateDiffAsync() │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Load current state (if update/delete) │ │
│ │ 2. Create domain entity (dry-run, no persist) │ │
│ │ 3. Generate JSON diff (JsonDiffPatch library) │ │
│ │ 4. Calculate risk level │ │
│ │ - Check deletion count │ │
│ │ - Check critical field changes │ │
│ │ - Check impact scope │ │
│ │ 5. Create DiffPreview aggregate │ │
│ │ 6. Persist to mcp_diff_previews table │ │
│ │ 7. Return DiffPreview to client │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Response to AI Client │
│ { │
│ "requiresApproval": true, │
│ "previewId": "preview-123", │
│ "diffPreview": { │
│ "operation": "create", │
│ "entityType": "Issue", │
│ "changes": { before: null, after: {...} }, │
│ "riskLevel": "Low" │
│ } │
│ } │
└──────────────────────────────────────────────────────┘
5.2 Approval Workflow
┌──────────────────────────────────────────────────────────────┐
│ Human User (via Web UI or CLI) │
│ - Views diff preview in Admin Dashboard │
│ - Reviews changes (before/after comparison) │
│ - Decides: Approve or Reject │
└────────────────────────┬─────────────────────────────────────┘
│
▼ (Approve)
┌────────────────────────────────────────────────────────────┐
│ DiffPreviewService.ApproveAndCommitAsync() │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. Load DiffPreview by ID │ │
│ │ 2. Validate status == Pending │ │
│ │ 3. Validate not expired │ │
│ │ 4. Mark as Approved │ │
│ │ 5. Execute actual operation: │ │
│ │ - Call Application layer command │ │
│ │ - e.g., CreateIssueCommand │ │
│ │ 6. Persist entity to database │ │
│ │ 7. Update DiffPreview.IsCommitted = true │ │
│ │ 8. Generate rollback token (if needed) │ │
│ │ 9. Create McpAuditLog entry │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
▼ (Reject)
┌────────────────────────────────────────────────────────────┐
│ DiffPreviewService.RejectAsync() │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. Load DiffPreview by ID │ │
│ │ 2. Mark as Rejected │ │
│ │ 3. Store rejection reason │ │
│ │ 4. Create McpAuditLog entry │ │
│ │ 5. Notify AI agent (optional) │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
5.3 Risk Level Calculation
public class RiskCalculator
{
public RiskLevel CalculateRisk(DiffOperation operation, string entityType, object changes)
{
var riskScore = 0;
var reasons = new List<string>();
// High risk: Deletions
if (operation == DiffOperation.Delete)
{
riskScore += 50;
reasons.Add("Deletion operation");
}
// Medium risk: Status changes
if (changes.ContainsField("status"))
{
riskScore += 20;
reasons.Add("Status change");
}
// High risk: Critical entities
if (entityType == "Sprint" || entityType == "Epic")
{
riskScore += 30;
reasons.Add($"Critical entity type: {entityType}");
}
// High risk: Bulk operations
if (changes.AffectedEntityCount > 10)
{
riskScore += 40;
reasons.Add($"Affects {changes.AffectedEntityCount} entities");
}
return riskScore switch
{
>= 80 => RiskLevel.Critical,
>= 50 => RiskLevel.High,
>= 20 => RiskLevel.Medium,
_ => RiskLevel.Low
};
}
}
5.4 Rollback Mechanism
Strategy: Event Sourcing + Snapshot
public interface IRollbackService
{
/// <summary>
/// Rollback a committed diff preview
/// </summary>
Task<RollbackResult> RollbackAsync(
Guid previewId,
Guid requestedBy,
string reason,
CancellationToken cancellationToken = default);
}
public class RollbackService : IRollbackService
{
public async Task<RollbackResult> RollbackAsync(/* ... */)
{
// 1. Load DiffPreview
var preview = await _repository.GetByIdAsync(previewId);
// 2. Validate can rollback
if (!preview.IsCommitted)
return RollbackResult.Failure("Not committed yet");
if (preview.Operation == DiffOperation.Delete)
return RollbackResult.Failure("Cannot rollback deletions");
// 3. Load entity
var entity = await LoadEntityAsync(preview.EntityType, preview.CommittedEntityId);
// 4. Apply reverse changes
if (preview.Operation == DiffOperation.Create)
{
// Soft delete
entity.Delete();
}
else if (preview.Operation == DiffOperation.Update)
{
// Restore from BeforeStateJson
var beforeState = JsonSerializer.Deserialize(preview.BeforeStateJson);
entity.ApplyState(beforeState);
}
// 5. Persist
await _unitOfWork.CommitAsync();
// 6. Audit
await _auditLog.LogRollbackAsync(previewId, requestedBy, reason);
return RollbackResult.Success();
}
}
6. Security & Permission Model
6.1 Authentication Mechanisms
6.1.1 API Key Authentication (Primary)
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 1. Extract API key from header
if (!Request.Headers.TryGetValue("X-MCP-API-Key", out var apiKeyHeaderValues))
return AuthenticateResult.Fail("Missing API Key");
var apiKey = apiKeyHeaderValues.FirstOrDefault();
// 2. Hash and lookup in database
var hashedKey = HashApiKey(apiKey);
var agent = await _agentRepository.GetByApiKeyHashAsync(hashedKey);
if (agent == null || !agent.IsActive)
return AuthenticateResult.Fail("Invalid or inactive API Key");
// 3. Check expiration
if (agent.ApiKeyExpiresAt < DateTime.UtcNow)
return AuthenticateResult.Fail("API Key expired");
// 4. Create claims principal
var claims = new[]
{
new Claim("agent_id", agent.Id.ToString()),
new Claim("tenant_id", agent.TenantId.Value.ToString()),
new Claim("agent_type", agent.AgentType),
new Claim("permission_level", agent.PermissionLevel.ToString()),
new Claim(ClaimTypes.Role, "AIAgent")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
// 5. Record usage
agent.RecordUsage();
await _agentRepository.UpdateAsync(agent);
return AuthenticateResult.Success(ticket);
}
}
API Key Format:
mcp_<environment>_<random32chars>
Example: mcp_prod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
API Key Storage:
- Never store plain text API keys
- Use BCrypt hashing (like passwords)
- Store:
ApiKeyHash,ApiKeyExpiresAt,IsActive
6.1.2 JWT Authentication (Fallback for Web Clients)
// Reuse existing JWT authentication from Identity module
// Add "AIAgent" role claim for AI-authenticated users
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "ApiKeyOrJwt";
})
.AddPolicyScheme("ApiKeyOrJwt", "API Key or JWT", options =>
{
options.ForwardDefaultSelector = context =>
{
if (context.Request.Headers.ContainsKey("X-MCP-API-Key"))
return "ApiKey";
return JwtBearerDefaults.AuthenticationScheme;
};
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", null)
.AddJwtBearer(/* ... existing JWT config ... */);
6.2 Authorization & Permissions
6.2.1 Permission Levels
public enum McpPermissionLevel
{
/// <summary>
/// Read-only access to resources
/// - Can call resources/list, resources/read
/// - Cannot call any tools
/// </summary>
ReadOnly = 1,
/// <summary>
/// Read + Write with preview (DEFAULT for AI Agents)
/// - Can read all resources
/// - Can call tools, but generates diff preview
/// - All writes require human approval
/// </summary>
WriteWithPreview = 2,
/// <summary>
/// Direct write (DANGEROUS, TenantOwner approval required)
/// - Can read all resources
/// - Can write directly without approval
/// - Should be used only for trusted automation
/// </summary>
DirectWrite = 3
}
6.2.2 Field-Level Permissions
public class FieldLevelPermissionFilter
{
private static readonly HashSet<string> SensitiveFields = new()
{
"passwordHash", "apiKeyHash", "salary", "ssn", "creditCard"
};
public object FilterSensitiveFields(object entity, TenantRole role)
{
// AIAgent role: Hide all sensitive fields
if (role == TenantRole.AIAgent)
{
var json = JsonSerializer.Serialize(entity);
var document = JsonDocument.Parse(json);
foreach (var sensitiveField in SensitiveFields)
{
RemoveField(document, sensitiveField);
}
return JsonSerializer.Deserialize(document.RootElement);
}
return entity; // Human users: no filtering
}
}
6.2.3 Resource-Level Permissions
public class ResourcePermissionValidator
{
public bool CanAccessResource(string resourceUri, McpAgent agent)
{
// Check if agent has explicit permission
if (agent.AllowedResources.Contains("*"))
return true; // Full access
// Check wildcard patterns
foreach (var pattern in agent.AllowedResources)
{
if (MatchesPattern(resourceUri, pattern))
return true;
}
return false;
}
private bool MatchesPattern(string uri, string pattern)
{
// Support wildcards:
// "projects.*" matches "projects://list", "project://123"
// "issues.read" matches "issue://456" (read-only)
// "issues.write" matches tool calls
var regex = new Regex(pattern.Replace("*", ".*"));
return regex.IsMatch(uri);
}
}
6.3 Rate Limiting
public class McpRateLimiter
{
private readonly IDistributedCache _cache; // Redis
public async Task<bool> CheckRateLimitAsync(Guid agentId, string operation)
{
var key = $"ratelimit:agent:{agentId}:{operation}";
var current = await _cache.GetStringAsync(key);
var limits = operation switch
{
"resources/read" => (100, TimeSpan.FromMinutes(1)), // 100 req/min
"tools/call" => (10, TimeSpan.FromMinutes(1)), // 10 req/min
_ => (50, TimeSpan.FromMinutes(1))
};
var count = int.Parse(current ?? "0");
if (count >= limits.Item1)
return false; // Rate limit exceeded
await _cache.SetStringAsync(
key,
(count + 1).ToString(),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = limits.Item2
});
return true;
}
}
6.4 Tenant Isolation
// Global query filter (already exists in Identity module)
public class McpDbContext : DbContext
{
private readonly ITenantContext _tenantContext;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply tenant filter to all MCP entities
modelBuilder.Entity<McpAgent>()
.HasQueryFilter(e => e.TenantId == _tenantContext.TenantId);
modelBuilder.Entity<DiffPreview>()
.HasQueryFilter(e => e.TenantId == _tenantContext.TenantId);
modelBuilder.Entity<McpAuditLog>()
.HasQueryFilter(e => e.TenantId == _tenantContext.TenantId);
}
}
7. Audit & Observability
7.1 Audit Log Schema
-- mcp.mcp_audit_logs
CREATE TABLE mcp.mcp_audit_logs (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
agent_id UUID NOT NULL,
-- Request details
operation_type VARCHAR(100) NOT NULL, -- 'resources/read', 'tools/call'
resource_uri VARCHAR(500),
tool_name VARCHAR(200),
input_parameters_json JSONB,
-- Response details
is_success BOOLEAN NOT NULL,
error_message TEXT,
http_status_code INTEGER,
-- Diff preview (if applicable)
diff_preview_id UUID,
diff_status VARCHAR(50), -- 'Pending', 'Approved', 'Rejected'
-- Performance
duration_ms INTEGER NOT NULL,
-- Context
client_ip_address VARCHAR(50),
user_agent TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
-- Indexes
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id),
CONSTRAINT fk_agent FOREIGN KEY (agent_id) REFERENCES mcp.mcp_agents(id)
);
CREATE INDEX idx_audit_tenant_timestamp ON mcp.mcp_audit_logs(tenant_id, timestamp DESC);
CREATE INDEX idx_audit_agent_timestamp ON mcp.mcp_audit_logs(agent_id, timestamp DESC);
CREATE INDEX idx_audit_operation ON mcp.mcp_audit_logs(operation_type, timestamp DESC);
7.2 Audit Middleware
public class McpAuditMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context, IMcpAuditLogRepository auditRepo)
{
var startTime = Stopwatch.GetTimestamp();
// Capture request
var agentId = context.User.FindFirst("agent_id")?.Value;
var tenantId = context.User.FindFirst("tenant_id")?.Value;
var requestBody = await CaptureRequestBodyAsync(context.Request);
// Execute request
Exception? exception = null;
try
{
await _next(context);
}
catch (Exception ex)
{
exception = ex;
throw;
}
finally
{
// Calculate duration
var elapsedMs = (int)((Stopwatch.GetTimestamp() - startTime) * 1000.0 / Stopwatch.Frequency);
// Create audit log
var auditLog = McpAuditLog.Create(
tenantId: Guid.Parse(tenantId),
agentId: Guid.Parse(agentId),
operationType: context.Request.Path,
resourceUri: ExtractResourceUri(requestBody),
toolName: ExtractToolName(requestBody),
inputParametersJson: requestBody,
isSuccess: exception == null && context.Response.StatusCode < 400,
errorMessage: exception?.Message,
httpStatusCode: context.Response.StatusCode,
durationMs: elapsedMs,
clientIpAddress: context.Connection.RemoteIpAddress?.ToString(),
userAgent: context.Request.Headers.UserAgent.ToString());
await auditRepo.AddAsync(auditLog);
}
}
}
7.3 Observability Metrics
Key Metrics to Track:
-
Request Volume
- Total MCP requests per minute
- Requests by operation type (resources/read, tools/call)
- Requests by agent
-
Diff Preview Metrics
- Diff previews generated per hour
- Approval rate (approved / total)
- Rejection rate
- Average time to approval
- Expired previews (not approved in time)
-
Performance
- Average response time by operation
- P50, P95, P99 latencies
- Database query performance
-
Security
- Failed authentication attempts
- Rate limit violations
- Permission denial rate
-
Business Metrics
- Active AI agents per tenant
- Most-used tools
- Most-accessed resources
Implementation (Prometheus + Grafana):
public class McpMetrics
{
private static readonly Counter RequestCounter = Metrics.CreateCounter(
"mcp_requests_total",
"Total MCP requests",
new CounterConfiguration
{
LabelNames = new[] { "operation_type", "agent_type", "status" }
});
private static readonly Histogram RequestDuration = Metrics.CreateHistogram(
"mcp_request_duration_ms",
"MCP request duration in milliseconds",
new HistogramConfiguration
{
LabelNames = new[] { "operation_type" },
Buckets = Histogram.ExponentialBuckets(10, 2, 10)
});
private static readonly Gauge ActiveDiffPreviews = Metrics.CreateGauge(
"mcp_diff_previews_pending",
"Number of pending diff previews",
new GaugeConfiguration
{
LabelNames = new[] { "tenant_id" }
});
public void RecordRequest(string operationType, string agentType, int statusCode, int durationMs)
{
RequestCounter.WithLabels(operationType, agentType, statusCode.ToString()).Inc();
RequestDuration.WithLabels(operationType).Observe(durationMs);
}
}
8. Database Design
8.1 Entity Relationship Diagram
┌─────────────────────────────────────────────────────────────┐
│ identity.tenants │
│ id, name, slug, status, subscription_plan │
└────────────────────────┬────────────────────────────────────┘
│
│ 1:N
▼
┌─────────────────────────────────────────────────────────────┐
│ mcp.mcp_agents │
│ id, tenant_id, agent_name, agent_type, api_key_hash, │
│ permission_level, allowed_resources, allowed_tools, │
│ is_active, created_at, last_used_at │
└────────────────────────┬────────────────────────────────────┘
│
│ 1:N
▼
┌─────────────────────────────────────────────────────────────┐
│ mcp.mcp_diff_previews │
│ id, tenant_id, agent_id, tool_name, input_parameters_json, │
│ operation, entity_type, entity_id, before_state_json, │
│ after_state_json, diff_json, risk_level, status, │
│ approved_by, approved_at, rejected_by, rejected_at, │
│ is_committed, committed_entity_id, rollback_token, │
│ created_at, expires_at │
└─────────────────────────────────────────────────────────────┘
│
│ 1:N
▼
┌─────────────────────────────────────────────────────────────┐
│ mcp.mcp_audit_logs │
│ id, tenant_id, agent_id, operation_type, resource_uri, │
│ tool_name, input_parameters_json, is_success, │
│ error_message, http_status_code, diff_preview_id, │
│ duration_ms, client_ip_address, user_agent, timestamp │
└─────────────────────────────────────────────────────────────┘
8.2 Database Schema (SQL)
-- Schema: mcp
CREATE SCHEMA IF NOT EXISTS mcp;
-- Table: mcp_agents
CREATE TABLE mcp.mcp_agents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_name VARCHAR(200) NOT NULL,
agent_type VARCHAR(100) NOT NULL, -- 'Claude', 'ChatGPT', 'Gemini'
-- Authentication
api_key_hash VARCHAR(512) NOT NULL,
api_key_expires_at TIMESTAMP NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
-- Permissions
permission_level VARCHAR(50) NOT NULL DEFAULT 'WriteWithPreview',
allowed_resources JSONB NOT NULL DEFAULT '[]', -- ["projects.*", "issues.read"]
allowed_tools JSONB NOT NULL DEFAULT '[]', -- ["create_issue", "update_status"]
-- Audit
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL,
last_used_at TIMESTAMP,
request_count INTEGER NOT NULL DEFAULT 0,
-- Constraints
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_created_by FOREIGN KEY (created_by) REFERENCES identity.users(id)
);
-- Indexes
CREATE INDEX idx_agents_tenant ON mcp.mcp_agents(tenant_id, is_active);
CREATE INDEX idx_agents_api_key ON mcp.mcp_agents(api_key_hash) WHERE is_active = true;
CREATE INDEX idx_agents_last_used ON mcp.mcp_agents(last_used_at DESC);
-- Table: mcp_diff_previews
CREATE TABLE mcp.mcp_diff_previews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_id UUID NOT NULL,
-- Operation details
tool_name VARCHAR(200) NOT NULL,
input_parameters_json JSONB NOT NULL,
-- Diff details
operation VARCHAR(50) NOT NULL, -- 'Create', 'Update', 'Delete'
entity_type VARCHAR(100) NOT NULL, -- 'Issue', 'Project', 'Sprint'
entity_id UUID, -- NULL for Create operations
before_state_json JSONB, -- NULL for Create
after_state_json JSONB NOT NULL,
diff_json JSONB NOT NULL, -- Structured diff
-- Risk assessment
risk_level VARCHAR(50) NOT NULL, -- 'Low', 'Medium', 'High', 'Critical'
risk_reasons JSONB NOT NULL DEFAULT '[]',
-- Approval workflow
status VARCHAR(50) NOT NULL DEFAULT 'Pending', -- 'Pending', 'Approved', 'Rejected', 'Expired', 'Committed'
approved_by UUID,
approved_at TIMESTAMP,
rejected_by UUID,
rejected_at TIMESTAMP,
rejection_reason TEXT,
-- Rollback
is_committed BOOLEAN NOT NULL DEFAULT false,
committed_entity_id UUID,
committed_at TIMESTAMP,
rollback_token VARCHAR(500),
-- Timestamps
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL DEFAULT (NOW() + INTERVAL '24 hours'),
-- Constraints
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_agent FOREIGN KEY (agent_id) REFERENCES mcp.mcp_agents(id) ON DELETE CASCADE,
CONSTRAINT fk_approved_by FOREIGN KEY (approved_by) REFERENCES identity.users(id),
CONSTRAINT fk_rejected_by FOREIGN KEY (rejected_by) REFERENCES identity.users(id)
);
-- Indexes
CREATE INDEX idx_diff_previews_tenant_status ON mcp.mcp_diff_previews(tenant_id, status, created_at DESC);
CREATE INDEX idx_diff_previews_agent ON mcp.mcp_diff_previews(agent_id, created_at DESC);
CREATE INDEX idx_diff_previews_expires ON mcp.mcp_diff_previews(expires_at) WHERE status = 'Pending';
CREATE INDEX idx_diff_previews_entity ON mcp.mcp_diff_previews(entity_type, entity_id);
-- Table: mcp_audit_logs
CREATE TABLE mcp.mcp_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_id UUID NOT NULL,
-- Request details
operation_type VARCHAR(100) NOT NULL, -- 'resources/read', 'tools/call'
resource_uri VARCHAR(500),
tool_name VARCHAR(200),
input_parameters_json JSONB,
-- Response details
is_success BOOLEAN NOT NULL,
error_message TEXT,
http_status_code INTEGER,
-- Diff preview (if applicable)
diff_preview_id UUID,
diff_status VARCHAR(50), -- 'Pending', 'Approved', 'Rejected'
-- Performance
duration_ms INTEGER NOT NULL,
-- Context
client_ip_address VARCHAR(50),
user_agent TEXT,
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_agent FOREIGN KEY (agent_id) REFERENCES mcp.mcp_agents(id) ON DELETE CASCADE,
CONSTRAINT fk_diff_preview FOREIGN KEY (diff_preview_id) REFERENCES mcp.mcp_diff_previews(id)
);
-- Indexes (optimized for time-series queries)
CREATE INDEX idx_audit_tenant_timestamp ON mcp.mcp_audit_logs(tenant_id, timestamp DESC);
CREATE INDEX idx_audit_agent_timestamp ON mcp.mcp_audit_logs(agent_id, timestamp DESC);
CREATE INDEX idx_audit_operation_timestamp ON mcp.mcp_audit_logs(operation_type, timestamp DESC);
CREATE INDEX idx_audit_diff_preview ON mcp.mcp_audit_logs(diff_preview_id) WHERE diff_preview_id IS NOT NULL;
-- Partitioning for audit logs (for better performance at scale)
-- Future optimization: Partition by month
-- CREATE TABLE mcp.mcp_audit_logs_2025_11 PARTITION OF mcp.mcp_audit_logs
-- FOR VALUES FROM ('2025-11-01') TO ('2025-12-01');
8.3 Data Retention Policy
-- Automatic cleanup of expired diff previews
CREATE OR REPLACE FUNCTION mcp.cleanup_expired_diff_previews()
RETURNS void AS $$
BEGIN
UPDATE mcp.mcp_diff_previews
SET status = 'Expired'
WHERE status = 'Pending'
AND expires_at < NOW();
END;
$$ LANGUAGE plpgsql;
-- Schedule cleanup job (run every hour)
-- Use pg_cron extension or external scheduler
SELECT cron.schedule('cleanup-expired-diffs', '0 * * * *', 'SELECT mcp.cleanup_expired_diff_previews()');
-- Archive old audit logs (retain 90 days)
CREATE OR REPLACE FUNCTION mcp.archive_old_audit_logs()
RETURNS void AS $$
BEGIN
DELETE FROM mcp.mcp_audit_logs
WHERE timestamp < NOW() - INTERVAL '90 days';
END;
$$ LANGUAGE plpgsql;
9. Technology Stack & Rationale
9.1 Backend Technologies
| Component | Technology | Rationale |
|---|---|---|
| Runtime | .NET 9.0 | Already in use, excellent performance, modern C# features |
| Web Framework | ASP.NET Core | Industry standard, high performance, built-in DI |
| Architecture | Clean Architecture | Already implemented, promotes testability and maintainability |
| ORM | Entity Framework Core | Already in use, excellent LINQ support, migration management |
| Database | PostgreSQL 16+ | Already in use, JSONB for flexible schemas, excellent performance |
| Caching | Redis | Distributed caching for rate limiting, session storage |
| Messaging | (Future) RabbitMQ | For async processing, event-driven workflows |
9.2 MCP Protocol Libraries
| Component | Technology | Rationale |
|---|---|---|
| JSON-RPC | Custom implementation | MCP uses JSON-RPC 2.0, simple to implement in C# |
| JSON Serialization | System.Text.Json | Built-in, high performance, good for JSONB interop |
| JSON Diff | JsonDiffPatch.NET | Library for generating JSON diffs |
| Transport | SSE (Server-Sent Events) | For web-based AI clients, long-lived connections |
| Transport (Future) | WebSocket | For bidirectional communication, streaming |
9.3 Security Libraries
| Component | Technology | Rationale |
|---|---|---|
| API Key Hashing | BCrypt.Net-Next | Already in use for passwords, proven security |
| JWT | Microsoft.IdentityModel.Tokens | Already in use, standard JWT implementation |
| Rate Limiting | Custom + Redis | Distributed rate limiting for multi-instance scenarios |
| CORS | ASP.NET Core CORS | Built-in, easy configuration |
9.4 Observability
| Component | Technology | Rationale |
|---|---|---|
| Metrics | Prometheus + Grafana | Industry standard, rich ecosystem |
| Logging | Serilog | Structured logging, excellent sink support |
| Tracing | OpenTelemetry | Distributed tracing, MCP request flow visibility |
| APM | (Optional) Application Insights | For Azure deployments |
9.5 Why NOT Use MCP SDK (TypeScript)?
Reasons:
- Language Mismatch: ColaFlow backend is .NET, MCP SDK is TypeScript/Node.js
- Performance: .NET offers better performance for backend workloads
- Integration: Easier to integrate with existing Clean Architecture
- Control: Custom implementation gives full control over security and audit
Alternative Approach:
- Implement MCP protocol specification in C# (JSON-RPC over HTTP/SSE)
- Provides compatibility with any MCP-compliant client
- No need to interop between .NET and Node.js
10. Implementation Roadmap
Phase 1: Foundation (2 weeks)
Goal: Basic MCP Server infrastructure
Deliverables:
-
✅ MCP.Domain module
- McpAgent aggregate
- DiffPreview aggregate
- McpAuditLog aggregate
- Repositories
-
✅ MCP.Infrastructure module
- Database schema migrations
- EF Core configurations
- Repository implementations
-
✅ API Key authentication
- ApiKeyAuthenticationHandler
- Agent registration endpoints
- API key generation utility
-
✅ Basic audit logging
- McpAuditMiddleware
- Audit log persistence
Acceptance Criteria:
- Can register an AI agent with API key
- Can authenticate using API key
- All requests are logged to mcp_audit_logs
Phase 2: Resources Implementation (2 weeks)
Goal: Expose read-only resources to AI clients
Deliverables:
-
✅ Resource Service
- IResourceService interface
- ResourceService implementation
- Resource descriptors (projects, issues, sprints)
-
✅ JSON-RPC protocol handler
- JsonRpcHandler for resources/list
- JsonRpcHandler for resources/read
-
✅ Permission validation
- Field-level filtering
- Resource-level access control
-
✅ API endpoints
- POST /api/mcp/jsonrpc (JSON-RPC endpoint)
- Resource URI routing
Acceptance Criteria:
- AI client can list available resources
- AI client can read project data
- AI client can read issue data
- Sensitive fields are filtered out
Testing:
# Test resources/list
curl -X POST http://localhost:5000/api/mcp/jsonrpc \
-H "X-MCP-API-Key: mcp_dev_..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list"
}'
# Test resources/read
curl -X POST http://localhost:5000/api/mcp/jsonrpc \
-H "X-MCP-API-Key: mcp_dev_..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": { "uri": "project://abc-123" }
}'
Phase 3: Tools & Diff Preview (3 weeks)
Goal: Implement write operations with diff preview
Deliverables:
-
✅ Diff Preview Service
- DiffPreviewService implementation
- Diff generation algorithm
- Risk level calculation
-
✅ Tool Invocation Service
- ToolInvocationService implementation
- Tool descriptors (create_issue, update_status, assign_issue)
- Integration with Application layer commands
-
✅ JSON-RPC protocol handler for tools
- tools/list
- tools/call (generates diff preview)
-
✅ Diff approval endpoints
- POST /api/mcp/diffs/{id}/approve
- POST /api/mcp/diffs/{id}/reject
- GET /api/mcp/diffs (list pending diffs)
Acceptance Criteria:
- AI client can list available tools
- AI client can call create_issue (generates diff preview)
- Human can view diff preview in Admin UI
- Human can approve diff (commits to database)
- Human can reject diff (discards preview)
Testing:
# Test tools/call (creates diff preview)
curl -X POST http://localhost:5000/api/mcp/jsonrpc \
-H "X-MCP-API-Key: mcp_dev_..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_issue",
"arguments": {
"projectId": "abc-123",
"title": "Implement MCP authentication",
"issueType": "Task",
"priority": "High"
}
}
}'
# Response: { "requiresApproval": true, "previewId": "preview-456", ... }
# Approve diff
curl -X POST http://localhost:5000/api/mcp/diffs/preview-456/approve \
-H "Authorization: Bearer <human-jwt-token>" \
-H "Content-Type: application/json"
Phase 4: Admin Dashboard (2 weeks)
Goal: UI for managing AI agents and diff previews
Deliverables:
-
✅ AI Agent Management UI
- List agents
- Register new agent
- Regenerate API key
- Configure permissions
-
✅ Diff Preview Dashboard
- List pending diffs
- View diff details (before/after comparison)
- Approve/reject buttons
- Diff history
-
✅ Audit Log Viewer
- Filter by agent, operation, date range
- View request/response details
- Export to CSV
Acceptance Criteria:
- Admin can register AI agents via UI
- Admin can view pending diff previews
- Admin can approve/reject diffs with visual diff viewer
- Admin can view audit logs
Phase 5: Advanced Features (2 weeks)
Goal: Rollback, rate limiting, advanced tools
Deliverables:
-
✅ Rollback Service
- Rollback committed diffs
- Event sourcing integration
-
✅ Rate Limiting
- Redis-based distributed rate limiter
- Per-agent rate limits
- Per-operation rate limits
-
✅ Advanced Tools
- log_decision
- add_comment
- create_sprint
-
✅ Prompt Templates
- Prompt template management
- prompts/list
- prompts/get
Acceptance Criteria:
- Admin can rollback committed changes
- Rate limiting prevents abuse
- AI clients can use advanced tools
- AI clients can retrieve prompt templates
Total Timeline: 11 weeks (~2.5 months)
Milestones:
- Week 2: Basic MCP Server running
- Week 4: AI clients can read resources
- Week 7: AI clients can create issues with approval
- Week 9: Admin UI complete
- Week 11: Production-ready with all features
11. Risks & Mitigation
11.1 Technical Risks
Risk 1: MCP Protocol Compatibility
Description: MCP specification may change, breaking compatibility
Impact: High (requires code changes)
Probability: Medium (MCP is still evolving)
Mitigation:
- Abstract MCP protocol logic into separate layer
- Implement version negotiation in
initializecall - Subscribe to MCP specification updates
- Design protocol handlers to be easily extensible
Risk 2: Diff Preview Accuracy
Description: Generated diffs may not accurately represent changes
Impact: High (incorrect approvals/rejections)
Probability: Medium (complex domain logic)
Mitigation:
- Comprehensive unit tests for diff generation
- Dry-run domain commands before generating diffs
- Use well-tested JSON diff library (JsonDiffPatch.NET)
- Visual diff viewer in Admin UI for human verification
Risk 3: Performance at Scale
Description: Diff generation and audit logging may slow down at scale
Impact: Medium (slow response times)
Probability: Low (with proper optimization)
Mitigation:
- Use async processing for audit logs (fire-and-forget)
- Implement database connection pooling
- Use Redis for caching frequently accessed resources
- Partition audit logs by month (PostgreSQL table partitioning)
- Monitor performance metrics (Prometheus)
Risk 4: Security Vulnerabilities
Description: API key leakage, permission bypass, injection attacks
Impact: Critical (data breach, unauthorized access)
Probability: Medium (without proper security practices)
Mitigation:
-
API Key Security:
- Never log API keys in plain text
- Use BCrypt hashing for storage
- Implement key rotation policy (expire after 90 days)
- Rate limiting to prevent brute force
-
Permission Validation:
- Validate permissions at every layer (Controller, Service, Repository)
- Use global query filters for tenant isolation
- Field-level filtering for sensitive data
-
Injection Prevention:
- Use parameterized queries (EF Core)
- Validate all input with FluentValidation
- Sanitize JSON input before deserialization
-
Regular Security Audits:
- Dependency scanning (Snyk, Dependabot)
- Penetration testing
- Code review for security issues
11.2 Business Risks
Risk 5: User Adoption
Description: Users may not trust AI-generated changes
Impact: High (low adoption)
Probability: Medium (change management challenge)
Mitigation:
- Start with read-only mode (no writes) to build trust
- Provide transparent diff previews with clear before/after
- Implement risk indicators (color-coded: green/yellow/red)
- Offer rollback capability for safety net
- User training and documentation
Risk 6: Regulatory Compliance
Description: AI operations may violate GDPR, SOC2, or industry regulations
Impact: Critical (legal issues, fines)
Probability: Low (with proper design)
Mitigation:
- Complete audit trail (who, what, when, why)
- Right to be forgotten (GDPR Article 17)
- Soft delete AI-generated data
- Provide data export/deletion endpoints
- Data minimization (only expose necessary fields to AI)
- Consent management (users opt-in to AI features)
- Data residency (support region-specific data storage)
11.3 Operational Risks
Risk 7: Database Growth
Description: Audit logs and diff previews grow unbounded
Impact: Medium (storage costs, slow queries)
Probability: High (without retention policy)
Mitigation:
- Data retention policy:
- Archive audit logs after 90 days
- Delete expired diff previews after 7 days
- Compress historical data
- Partitioning:
- Partition audit logs by month (PostgreSQL)
- Use TimescaleDB for time-series data (future)
- Monitoring:
- Alert when table sizes exceed thresholds
- Regular database maintenance (VACUUM, ANALYZE)
Risk 8: AI Agent Abuse
Description: Malicious AI agents spam requests, attempt unauthorized access
Impact: Medium (resource exhaustion, data exfiltration)
Probability: Medium (if API keys are leaked)
Mitigation:
- Rate Limiting:
- 100 requests/min for resources/read
- 10 requests/min for tools/call
- IP-based rate limiting (fallback)
- Anomaly Detection:
- Alert on unusual request patterns
- Automatic suspension of suspicious agents
- API Key Rotation:
- Force rotation every 90 days
- Revoke keys on suspicious activity
- CAPTCHA for Registration:
- Human verification when registering new agents
12. Appendix
12.1 MCP Protocol Reference
JSON-RPC 2.0 Request Format:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_issue",
"arguments": { ... }
}
}
JSON-RPC 2.0 Response Format:
{
"jsonrpc": "2.0",
"id": 1,
"result": { ... }
}
Error Response:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32600,
"message": "Invalid Request",
"data": { "details": "..." }
}
}
12.2 Example Tool Schemas
create_project:
{
"name": "create_project",
"description": "Create a new project",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"ownerId": { "type": "string" },
"startDate": { "type": "string", "format": "date" },
"endDate": { "type": "string", "format": "date" }
},
"required": ["name", "ownerId"]
}
}
update_sprint:
{
"name": "update_sprint",
"description": "Update sprint details",
"inputSchema": {
"type": "object",
"properties": {
"sprintId": { "type": "string" },
"name": { "type": "string" },
"startDate": { "type": "string", "format": "date" },
"endDate": { "type": "string", "format": "date" },
"goal": { "type": "string" }
},
"required": ["sprintId"]
}
}
12.3 Configuration Examples
appsettings.Mcp.json:
{
"Mcp": {
"ApiKeyExpirationDays": 90,
"DiffPreviewExpirationHours": 24,
"RateLimit": {
"ResourcesRead": 100,
"ToolsCall": 10,
"WindowMinutes": 1
},
"AuditLog": {
"RetentionDays": 90,
"EnablePartitioning": true
},
"DefaultPermissions": {
"Level": "WriteWithPreview",
"AllowedResources": ["projects.*", "issues.*", "sprints.*"],
"AllowedTools": ["create_issue", "update_status", "assign_issue"]
}
}
}
Summary
This architecture design provides a comprehensive, secure, and scalable MCP Server for ColaFlow that:
- Integrates seamlessly with existing Clean Architecture and Identity Module
- Prioritizes safety via diff preview and human approval for all AI writes
- Ensures auditability with complete audit logs and observability
- Supports multi-tenancy with tenant-scoped data isolation
- Enables extensibility for future AI models and integrations
Key Design Decisions:
- Custom MCP protocol implementation in C# (not TypeScript SDK)
- BCrypt API key authentication (reuses existing security patterns)
- Diff preview workflow (safety-first approach)
- PostgreSQL JSONB for flexible diff storage
- Redis for distributed rate limiting
- Prometheus + Grafana for observability
Next Steps:
- Review and approve this architecture document
- Begin Phase 1 implementation (Foundation)
- Set up CI/CD pipeline for MCP module
- Create integration tests for MCP protocol
Document Status: Ready for Review Reviewers: Product Manager, Backend Team Lead, Security Team Approval Required: Yes
Revision History:
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2025-11-04 | System Architect | Initial architecture design |