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,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>ColaFlow.Modules.Mcp.Contracts</AssemblyName>
<RootNamespace>ColaFlow.Modules.Mcp.Contracts</RootNamespace>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,102 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.JsonRpc;
/// <summary>
/// JSON-RPC 2.0 error object
/// </summary>
public class JsonRpcError
{
/// <summary>
/// A Number that indicates the error type that occurred
/// </summary>
[JsonPropertyName("code")]
public int Code { get; set; }
/// <summary>
/// A String providing a short description of the error
/// </summary>
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
/// <summary>
/// A Primitive or Structured value that contains additional information about the error (optional)
/// </summary>
[JsonPropertyName("data")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Data { get; set; }
/// <summary>
/// Creates a new JSON-RPC error
/// </summary>
public JsonRpcError(JsonRpcErrorCode code, string message, object? data = null)
{
Code = (int)code;
Message = message;
Data = data;
}
/// <summary>
/// Creates a new JSON-RPC error with custom code
/// </summary>
public JsonRpcError(int code, string message, object? data = null)
{
Code = code;
Message = message;
Data = data;
}
/// <summary>
/// Creates a ParseError (-32700)
/// </summary>
public static JsonRpcError ParseError(string? details = null) =>
new(JsonRpcErrorCode.ParseError, "Parse error", details);
/// <summary>
/// Creates an InvalidRequest error (-32600)
/// </summary>
public static JsonRpcError InvalidRequest(string? details = null) =>
new(JsonRpcErrorCode.InvalidRequest, "Invalid Request", details);
/// <summary>
/// Creates a MethodNotFound error (-32601)
/// </summary>
public static JsonRpcError MethodNotFound(string method) =>
new(JsonRpcErrorCode.MethodNotFound, $"Method not found: {method}");
/// <summary>
/// Creates an InvalidParams error (-32602)
/// </summary>
public static JsonRpcError InvalidParams(string? details = null) =>
new(JsonRpcErrorCode.InvalidParams, "Invalid params", details);
/// <summary>
/// Creates an InternalError (-32603)
/// </summary>
public static JsonRpcError InternalError(string? details = null) =>
new(JsonRpcErrorCode.InternalError, "Internal error", details);
/// <summary>
/// Creates an Unauthorized error (-32001)
/// </summary>
public static JsonRpcError Unauthorized(string? details = null) =>
new(JsonRpcErrorCode.Unauthorized, "Unauthorized", details);
/// <summary>
/// Creates a Forbidden error (-32002)
/// </summary>
public static JsonRpcError Forbidden(string? details = null) =>
new(JsonRpcErrorCode.Forbidden, "Forbidden", details);
/// <summary>
/// Creates a NotFound error (-32003)
/// </summary>
public static JsonRpcError NotFound(string? details = null) =>
new(JsonRpcErrorCode.NotFound, "Not found", details);
/// <summary>
/// Creates a ValidationFailed error (-32004)
/// </summary>
public static JsonRpcError ValidationFailed(string? details = null) =>
new(JsonRpcErrorCode.ValidationFailed, "Validation failed", details);
}

View File

@@ -0,0 +1,52 @@
namespace ColaFlow.Modules.Mcp.Contracts.JsonRpc;
/// <summary>
/// JSON-RPC 2.0 error codes
/// </summary>
public enum JsonRpcErrorCode
{
/// <summary>
/// Invalid JSON was received by the server (-32700)
/// </summary>
ParseError = -32700,
/// <summary>
/// The JSON sent is not a valid Request object (-32600)
/// </summary>
InvalidRequest = -32600,
/// <summary>
/// The method does not exist or is not available (-32601)
/// </summary>
MethodNotFound = -32601,
/// <summary>
/// Invalid method parameter(s) (-32602)
/// </summary>
InvalidParams = -32602,
/// <summary>
/// Internal JSON-RPC error (-32603)
/// </summary>
InternalError = -32603,
/// <summary>
/// Authentication failed (-32001)
/// </summary>
Unauthorized = -32001,
/// <summary>
/// Authorization failed (-32002)
/// </summary>
Forbidden = -32002,
/// <summary>
/// Resource not found (-32003)
/// </summary>
NotFound = -32003,
/// <summary>
/// Request validation failed (-32004)
/// </summary>
ValidationFailed = -32004
}

View File

@@ -0,0 +1,62 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.JsonRpc;
/// <summary>
/// JSON-RPC 2.0 request object
/// </summary>
public class JsonRpcRequest
{
/// <summary>
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0"
/// </summary>
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
/// <summary>
/// A String containing the name of the method to be invoked
/// </summary>
[JsonPropertyName("method")]
public string Method { get; set; } = string.Empty;
/// <summary>
/// A Structured value that holds the parameter values to be used during the invocation of the method (optional)
/// </summary>
[JsonPropertyName("params")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Params { get; set; }
/// <summary>
/// An identifier established by the Client. If not included, it's a notification (optional)
/// </summary>
[JsonPropertyName("id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Id { get; set; }
/// <summary>
/// Indicates if this is a notification (no response expected)
/// </summary>
[JsonIgnore]
public bool IsNotification => Id == null;
/// <summary>
/// Validates the JSON-RPC request structure
/// </summary>
public bool IsValid(out string? errorMessage)
{
if (JsonRpc != "2.0")
{
errorMessage = "jsonrpc must be exactly '2.0'";
return false;
}
if (string.IsNullOrWhiteSpace(Method))
{
errorMessage = "method is required";
return false;
}
errorMessage = null;
return true;
}
}

View File

@@ -0,0 +1,100 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.JsonRpc;
/// <summary>
/// JSON-RPC 2.0 response object
/// </summary>
public class JsonRpcResponse
{
/// <summary>
/// A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0"
/// </summary>
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
/// <summary>
/// This member is REQUIRED on success. Must not exist if there was an error
/// </summary>
[JsonPropertyName("result")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public object? Result { get; set; }
/// <summary>
/// This member is REQUIRED on error. Must not exist if there was no error
/// </summary>
[JsonPropertyName("error")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonRpcError? Error { get; set; }
/// <summary>
/// This member is REQUIRED. It MUST be the same as the value of the id member in the Request Object.
/// If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.
/// </summary>
[JsonPropertyName("id")]
public object? Id { get; set; }
/// <summary>
/// Creates a success response
/// </summary>
public static JsonRpcResponse Success(object? result, object? id)
{
return new JsonRpcResponse
{
Result = result,
Id = id
};
}
/// <summary>
/// Creates an error response
/// </summary>
public static JsonRpcResponse CreateError(JsonRpcError error, object? id)
{
return new JsonRpcResponse
{
Error = error,
Id = id
};
}
/// <summary>
/// Creates a ParseError response (id is null because request couldn't be parsed)
/// </summary>
public static JsonRpcResponse ParseError(string? details = null)
{
return CreateError(JsonRpcError.ParseError(details), null);
}
/// <summary>
/// Creates an InvalidRequest response
/// </summary>
public static JsonRpcResponse InvalidRequest(string? details = null, object? id = null)
{
return CreateError(JsonRpcError.InvalidRequest(details), id);
}
/// <summary>
/// Creates a MethodNotFound response
/// </summary>
public static JsonRpcResponse MethodNotFound(string method, object? id)
{
return CreateError(JsonRpcError.MethodNotFound(method), id);
}
/// <summary>
/// Creates an InvalidParams response
/// </summary>
public static JsonRpcResponse InvalidParams(string? details, object? id)
{
return CreateError(JsonRpcError.InvalidParams(details), id);
}
/// <summary>
/// Creates an InternalError response
/// </summary>
public static JsonRpcResponse InternalError(string? details, object? id)
{
return CreateError(JsonRpcError.InternalError(details), id);
}
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.Mcp;
/// <summary>
/// Information about the MCP client
/// </summary>
public class McpClientInfo
{
/// <summary>
/// Name of the client application
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Version of the client application
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.Mcp;
/// <summary>
/// MCP initialize request parameters
/// </summary>
public class McpInitializeRequest
{
/// <summary>
/// Protocol version requested by the client
/// </summary>
[JsonPropertyName("protocolVersion")]
public string ProtocolVersion { get; set; } = string.Empty;
/// <summary>
/// Information about the client
/// </summary>
[JsonPropertyName("clientInfo")]
public McpClientInfo ClientInfo { get; set; } = new();
}

View File

@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.Mcp;
/// <summary>
/// MCP initialize response
/// </summary>
public class McpInitializeResponse
{
/// <summary>
/// Protocol version supported by the server
/// </summary>
[JsonPropertyName("protocolVersion")]
public string ProtocolVersion { get; set; } = "1.0";
/// <summary>
/// Information about the server
/// </summary>
[JsonPropertyName("serverInfo")]
public McpServerInfo ServerInfo { get; set; } = new();
/// <summary>
/// Server capabilities
/// </summary>
[JsonPropertyName("capabilities")]
public McpServerCapabilities Capabilities { get; set; } = McpServerCapabilities.CreateDefault();
}

View File

@@ -0,0 +1,79 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.Mcp;
/// <summary>
/// MCP server capabilities
/// </summary>
public class McpServerCapabilities
{
/// <summary>
/// Resources capability
/// </summary>
[JsonPropertyName("resources")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public McpResourcesCapability? Resources { get; set; }
/// <summary>
/// Tools capability
/// </summary>
[JsonPropertyName("tools")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public McpToolsCapability? Tools { get; set; }
/// <summary>
/// Prompts capability
/// </summary>
[JsonPropertyName("prompts")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public McpPromptsCapability? Prompts { get; set; }
/// <summary>
/// Creates default server capabilities with all features supported
/// </summary>
public static McpServerCapabilities CreateDefault()
{
return new McpServerCapabilities
{
Resources = new McpResourcesCapability { Supported = true },
Tools = new McpToolsCapability { Supported = true },
Prompts = new McpPromptsCapability { Supported = true }
};
}
}
/// <summary>
/// Resources capability
/// </summary>
public class McpResourcesCapability
{
/// <summary>
/// Indicates if resources are supported
/// </summary>
[JsonPropertyName("supported")]
public bool Supported { get; set; }
}
/// <summary>
/// Tools capability
/// </summary>
public class McpToolsCapability
{
/// <summary>
/// Indicates if tools are supported
/// </summary>
[JsonPropertyName("supported")]
public bool Supported { get; set; }
}
/// <summary>
/// Prompts capability
/// </summary>
public class McpPromptsCapability
{
/// <summary>
/// Indicates if prompts are supported
/// </summary>
[JsonPropertyName("supported")]
public bool Supported { get; set; }
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace ColaFlow.Modules.Mcp.Contracts.Mcp;
/// <summary>
/// Information about the MCP server
/// </summary>
public class McpServerInfo
{
/// <summary>
/// Name of the server application
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = "ColaFlow MCP Server";
/// <summary>
/// Version of the server application
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = "1.0.0";
}