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:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user