Created
July 21, 2025 09:23
-
-
Save sdcb/80353c3273cd1a89b839c6d40fb1adbc to your computer and use it in GitHub Desktop.
mcp-protocol-dotnet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <Query Kind="Statements"> | |
| <NuGetReference Prerelease="true">ModelContextProtocol</NuGetReference> | |
| <Namespace>ModelContextProtocol</Namespace> | |
| <Namespace>ModelContextProtocol.Client</Namespace> | |
| <Namespace>System.Text.Json.Nodes</Namespace> | |
| </Query> | |
| var clientTransport = new SseClientTransport(new SseClientTransportOptions() | |
| { | |
| Name = "MyServer", | |
| Endpoint = new Uri("http://localhost:5000"), | |
| }); | |
| var client = await McpClientFactory.CreateAsync(clientTransport); | |
| // Print the list of tools available from the server. | |
| (await client.ListToolsAsync()).Select(x => new { x.Name, Desc = JsonObject.Parse(x.JsonSchema.ToString()) }).Dump(); | |
| // Execute a tool (this would normally be driven by LLM tool invocations). | |
| (await client.CallToolAsync( | |
| "echo", | |
| new Dictionary<string, object?>() { ["message"] = ".NET is awesome!" }, | |
| cancellationToken: CancellationToken.None)).Dump(); | |
| (await client.CallToolAsync( | |
| "count", | |
| new Dictionary<string, object?>() { ["n"] = 5 }, | |
| new Reporter(), | |
| cancellationToken: CancellationToken.None)).Dump(); | |
| (await client.CallToolAsync("test_throw", cancellationToken: CancellationToken.None)).Dump(); | |
| (await client.CallToolAsync("not-existing-tool", cancellationToken: CancellationToken.None)).Dump(); | |
| public class Reporter : IProgress<ProgressNotificationValue> | |
| { | |
| public void Report(ProgressNotificationValue value) | |
| { | |
| value.Dump(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <Query Kind="Statements"> | |
| <NuGetReference Prerelease="true">ModelContextProtocol.AspNetCore</NuGetReference> | |
| <Namespace>Microsoft.AspNetCore.Builder</Namespace> | |
| <Namespace>Microsoft.AspNetCore.Http</Namespace> | |
| <Namespace>Microsoft.AspNetCore.Mvc</Namespace> | |
| <Namespace>Microsoft.Extensions.DependencyInjection</Namespace> | |
| <Namespace>Microsoft.Extensions.Hosting</Namespace> | |
| <Namespace>Microsoft.Extensions.Logging</Namespace> | |
| <Namespace>ModelContextProtocol</Namespace> | |
| <Namespace>ModelContextProtocol.Server</Namespace> | |
| <Namespace>System.Collections.Concurrent</Namespace> | |
| <Namespace>System.ComponentModel</Namespace> | |
| <Namespace>System.Text.Json</Namespace> | |
| <Namespace>System.Text.Json.Nodes</Namespace> | |
| <Namespace>System.Text.Json.Serialization</Namespace> | |
| <Namespace>System.Threading.Tasks</Namespace> | |
| <Namespace>Microsoft.AspNetCore.Routing</Namespace> | |
| <Namespace>Microsoft.Extensions.Primitives</Namespace> | |
| <Namespace>Microsoft.AspNetCore.WebUtilities</Namespace> | |
| <IncludeAspNet>true</IncludeAspNet> | |
| </Query> | |
| WebApplicationBuilder builder = WebApplication.CreateBuilder(); | |
| // 1. 注册原生服务和我们的工具类 | |
| builder.Services.AddHttpContextAccessor(); | |
| builder.Services.AddTransient<Tools>(); | |
| WebApplication app = builder.Build(); | |
| // 2. 映射 MCP 端点,自动发现并使用 Tools 类 | |
| app.MapMcpEndpoint<Tools>("/"); | |
| // 3. 启动应用 | |
| await app.RunAsync(QueryCancelToken); | |
| public class Tools(IHttpContextAccessor http) | |
| { | |
| [Description("Echoes the message back to the client.")] | |
| public string Echo(string message) => $"hello {message}"; | |
| [Description("Returns the IP address of the client.")] | |
| public string EchoIP() => http.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; | |
| [Description("Counts from 0 to n, reporting progress at each step.")] | |
| public async Task<int> Count(int n, IProgress<ProgressNotificationValue> progress) | |
| { | |
| for (int i = 0; i < n; ++i) | |
| { | |
| progress.Report(new ProgressNotificationValue() | |
| { | |
| Progress = i, | |
| Total = n, | |
| Message = $"Step {i} of {n}", | |
| }); | |
| await Task.Delay(100); | |
| } | |
| return n; | |
| } | |
| [Description("Throws an exception for testing purposes.")] | |
| public string TestThrow() | |
| { | |
| throw new Exception("This is a test exception"); | |
| } | |
| } | |
| // --- JSON-RPC Base Structures --- | |
| public record JsonRpcRequest( | |
| [property: JsonPropertyName("jsonrpc")] string JsonRpc, | |
| [property: JsonPropertyName("method")] string Method, | |
| [property: JsonPropertyName("params")] object? Params, | |
| [property: JsonPropertyName("id")] int? Id | |
| ); | |
| public record JsonRpcResponse( | |
| [property: JsonPropertyName("jsonrpc")] string JsonRpc, | |
| [property: JsonPropertyName("result")] object? Result, | |
| [property: JsonPropertyName("error")] object? Error, | |
| [property: JsonPropertyName("id")] int? Id | |
| ); | |
| public record JsonRpcError( | |
| [property: JsonPropertyName("code")] int Code, | |
| [property: JsonPropertyName("message")] string Message | |
| ); | |
| // --- MCP Specific Payloads --- | |
| // For initialize method | |
| public record InitializeParams( | |
| [property: JsonPropertyName("protocolVersion")] string ProtocolVersion, | |
| [property: JsonPropertyName("clientInfo")] ClientInfo ClientInfo | |
| ); | |
| public record ClientInfo([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string Version); | |
| public record InitializeResult( | |
| [property: JsonPropertyName("protocolVersion")] string ProtocolVersion, | |
| [property: JsonPropertyName("capabilities")] ServerCapabilities Capabilities, | |
| [property: JsonPropertyName("serverInfo")] ClientInfo ServerInfo | |
| ); | |
| public record ServerCapabilities([property: JsonPropertyName("tools")] object Tools); | |
| // For tools/call method | |
| public record ToolCallParams( | |
| [property: JsonPropertyName("name")] string Name, | |
| [property: JsonPropertyName("arguments")] Dictionary<string, object?> Arguments, | |
| [property: JsonPropertyName("_meta")] ToolCallMeta? Meta | |
| ); | |
| public record ToolCallMeta([property: JsonPropertyName("progressToken")] string ProgressToken); | |
| // For tool call results | |
| public record ToolCallResult( | |
| [property: JsonPropertyName("content")] List<ContentItem> Content, | |
| [property: JsonPropertyName("isError")] bool IsError = false | |
| ); | |
| public record ContentItem([property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("text")] string Text); | |
| // For tools/list results | |
| public record ToolListResult( | |
| [property: JsonPropertyName("tools")] List<ToolDefinition> Tools | |
| ); | |
| public record ToolDefinition( | |
| [property: JsonPropertyName("name")] string Name, | |
| [property: JsonPropertyName("description")] string Description, | |
| [property: JsonPropertyName("inputSchema")] object InputSchema | |
| ); | |
| // For progress notifications | |
| public record ProgressNotification( | |
| [property: JsonPropertyName("jsonrpc")] string JsonRpc, | |
| [property: JsonPropertyName("method")] string Method, | |
| [property: JsonPropertyName("params")] ProgressParams Params | |
| ); | |
| public record ProgressParams( | |
| [property: JsonPropertyName("progressToken")] string ProgressToken, | |
| [property: JsonPropertyName("progress")] int Progress, | |
| [property: JsonPropertyName("total")] int Total, | |
| [property: JsonPropertyName("message")] string Message | |
| ); | |
| // This class is for the IProgress<T> interface in our Tools methods | |
| public class ProgressNotificationValue | |
| { | |
| public int Progress { get; set; } | |
| public int Total { get; set; } | |
| public string Message { get; set; } = string.Empty; | |
| } | |
| public static class McpEndpointExtensions | |
| { | |
| // JSON-RPC Error Codes from your article's findings | |
| private const int InvalidParamsErrorCode = -32602; // Invalid params | |
| private const int MethodNotFoundErrorCode = -32601; // Method not found | |
| private static readonly JsonSerializerOptions s_jsonOptions = new() | |
| { | |
| DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, | |
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
| }; | |
| /// <summary> | |
| /// Maps an endpoint that speaks the Model Context Protocol. | |
| /// </summary> | |
| public static IEndpointRouteBuilder MapMcpEndpoint<TTools>(this IEndpointRouteBuilder app, string pattern) where TTools : class | |
| { | |
| // 预先通过反射发现所有工具方法,并转换为snake_case以匹配MCP命名习惯 | |
| Dictionary<string, MethodInfo> methods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) | |
| .ToDictionary(k => ToSnakeCase(k.Name), v => v); | |
| app.MapPost(pattern, async (HttpContext context, [FromServices] IServiceProvider sp) => | |
| { | |
| JsonRpcRequest? request = await JsonSerializer.DeserializeAsync<JsonRpcRequest>(context.Request.Body, s_jsonOptions); | |
| if (request == null) | |
| { | |
| context.Response.StatusCode = 400; // Bad Request | |
| return; | |
| } | |
| // 核心:处理不同的MCP方法 | |
| switch (request.Method) | |
| { | |
| case "initialize": | |
| await HandleInitialize(context, request); | |
| break; | |
| case "notifications/initialized": | |
| // 在无状态模式下,这个请求只是一个确认,我们返回与initialize类似的信息 | |
| await HandleInitialize(context, request); | |
| break; | |
| case "tools/list": | |
| await HandleToolList<TTools>(context, request); | |
| break; | |
| case "tools/call": | |
| await HandleToolCall<TTools>(context, request, sp, methods); | |
| break; | |
| default: | |
| JsonRpcResponse errorResponse = new("2.0", null, new JsonRpcError(MethodNotFoundErrorCode, "Method not found"), request.Id); | |
| await WriteSseMessageAsync(context.Response, errorResponse); | |
| break; | |
| } | |
| }); | |
| // 旧版SDK会发送GET请求,我们明确返回405 | |
| app.MapGet(pattern, context => | |
| { | |
| context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; | |
| context.Response.Headers.Allow = "POST"; | |
| return Task.CompletedTask; | |
| }); | |
| return app; | |
| } | |
| private static string ToSnakeCase(string name) | |
| { | |
| if (string.IsNullOrEmpty(name)) return name; | |
| var sb = new StringBuilder(name.Length); | |
| for (int i = 0; i < name.Length; i++) | |
| { | |
| char c = name[i]; | |
| if (char.IsUpper(c)) | |
| { | |
| if (sb.Length > 0 && i > 0 && !char.IsUpper(name[i-1])) sb.Append('_'); | |
| sb.Append(char.ToLowerInvariant(c)); | |
| } | |
| else | |
| { | |
| sb.Append(c); | |
| } | |
| } | |
| return sb.ToString(); | |
| } | |
| private static async Task HandleInitialize(HttpContext context, JsonRpcRequest request) | |
| { | |
| // 复用或创建 Session ID | |
| string sessionId = context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues existingSessionId) | |
| ? existingSessionId.ToString() | |
| : WebEncoders.Base64UrlEncode(Guid.NewGuid().ToByteArray()); | |
| context.Response.Headers["Mcp-Session-Id"] = sessionId; | |
| // 构建与抓包一致的响应 | |
| InitializeResult result = new( | |
| "2025-06-18", // Echo the protocol version | |
| new ServerCapabilities(new { listChanged = true }), // Mimic the capabilities | |
| new ClientInfo("PureAspNetCoreMcpServer", "1.0.0") | |
| ); | |
| JsonRpcResponse response = new("2.0", result, null, request.Id); | |
| await WriteSseMessageAsync(context.Response, response); | |
| } | |
| private static async Task HandleToolList<TTools>(HttpContext context, JsonRpcRequest request) where TTools : class | |
| { | |
| EchoSessionId(context); | |
| List<ToolDefinition> toolDefs = []; | |
| MethodInfo[] toolMethods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); | |
| foreach (MethodInfo method in toolMethods) | |
| { | |
| string description = method.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "No description."; | |
| // 简化的动态Schema生成 | |
| Dictionary<string, object> properties = []; | |
| List<string> required = []; | |
| foreach (ParameterInfo param in method.GetParameters()) | |
| { | |
| if (param.ParameterType == typeof(IProgress<ProgressNotificationValue>)) continue; // 忽略进度报告参数 | |
| properties[param.Name!] = new { type = GetJsonType(param.ParameterType) }; | |
| if (!param.IsOptional) | |
| { | |
| required.Add(param.Name!); | |
| } | |
| } | |
| var schema = new { type = "object", properties, required }; | |
| toolDefs.Add(new ToolDefinition(ToSnakeCase(method.Name), description, schema)); | |
| } | |
| ToolListResult result = new(toolDefs); | |
| JsonRpcResponse response = new("2.0", result, null, request.Id); | |
| await WriteSseMessageAsync(context.Response, response); | |
| } | |
| private static async Task HandleToolCall<TTools>(HttpContext context, JsonRpcRequest request, IServiceProvider sp, Dictionary<string, MethodInfo> methods) where TTools : class | |
| { | |
| EchoSessionId(context); | |
| ToolCallParams? toolCallParams = JsonSerializer.Deserialize<ToolCallParams>(JsonSerializer.Serialize(request.Params, s_jsonOptions), s_jsonOptions); | |
| if (toolCallParams == null) return; | |
| string toolName = toolCallParams.Name; | |
| methods.TryGetValue(toolName, out MethodInfo? method); | |
| // 场景1: 调用不存在的工具 -> 返回标准JSON-RPC错误 | |
| if (method == null) | |
| { | |
| JsonRpcError error = new(InvalidParamsErrorCode, $"Unknown tool: '{toolName}'"); | |
| JsonRpcResponse response = new("2.0", null, error, request.Id); | |
| await WriteSseMessageAsync(context.Response, response); | |
| return; | |
| } | |
| // 使用DI容器创建工具类的实例 | |
| using IServiceScope scope = sp.CreateScope(); | |
| TTools toolInstance = scope.ServiceProvider.GetRequiredService<TTools>(); | |
| object? resultValue; | |
| bool isError = false; | |
| try | |
| { | |
| // 通过反射准备方法参数 | |
| ParameterInfo[] methodParams = method.GetParameters(); | |
| object?[] args = new object?[methodParams.Length]; | |
| for (int i = 0; i < methodParams.Length; i++) | |
| { | |
| ParameterInfo p = methodParams[i]; | |
| if (p.ParameterType == typeof(IProgress<ProgressNotificationValue>)) | |
| { | |
| // 创建一个IProgress<T>的实现,它会将进度作为SSE消息发回客户端 | |
| args[i] = new ProgressReporter(context.Response, toolCallParams.Meta!.ProgressToken); | |
| } | |
| else if (toolCallParams.Arguments.TryGetValue(p.Name!, out object? argValue) && argValue is JsonElement element) | |
| { | |
| args[i] = element.Deserialize(p.ParameterType, s_jsonOptions); | |
| } | |
| else if (p.IsOptional) | |
| { | |
| args[i] = p.DefaultValue; | |
| } | |
| else | |
| { | |
| // 场景2a: 缺少必要参数 -> 抛出异常,进入catch块 | |
| throw new TargetParameterCountException($"Tool '{toolName}' requires parameter '{p.Name}' but it was not provided."); | |
| } | |
| } | |
| object? invokeResult = method.Invoke(toolInstance, args); | |
| // 处理异步方法 | |
| if (invokeResult is Task task) | |
| { | |
| await task; | |
| resultValue = task.GetType().IsGenericType ? task.GetType().GetProperty("Result")?.GetValue(task) : null; | |
| } | |
| else | |
| { | |
| resultValue = invokeResult; | |
| } | |
| } | |
| // 场景2b: 工具执行时内部抛出异常 -> isError: true | |
| catch (Exception ex) | |
| { | |
| isError = true; | |
| // 将异常信息包装在result中,而不是顶层error | |
| resultValue = $"An error occurred invoking '{toolName}'. Details: {ex.InnerException?.Message ?? ex.Message}"; | |
| } | |
| List<ContentItem> content = [new("text", resultValue?.ToString() ?? string.Empty)]; | |
| ToolCallResult result = new(content, isError); | |
| JsonRpcResponse finalResponse = new("2.0", result, null, request.Id); | |
| await WriteSseMessageAsync(context.Response, finalResponse); | |
| } | |
| // 手动实现SSE消息写入,告别预览版包 | |
| private static async Task WriteSseMessageAsync(HttpResponse response, object data) | |
| { | |
| if (!response.Headers.ContainsKey("Content-Type")) | |
| { | |
| response.ContentType = "text/event-stream"; | |
| response.Headers.CacheControl = "no-cache,no-store"; | |
| response.Headers.ContentEncoding = "identity"; | |
| response.Headers.KeepAlive = "true"; | |
| } | |
| string json = JsonSerializer.Serialize(data, s_jsonOptions); | |
| string message = $"event: message\ndata: {json}\n\n"; | |
| await response.WriteAsync(message); | |
| await response.Body.FlushAsync(); | |
| } | |
| private static void EchoSessionId(HttpContext context) | |
| { | |
| if (context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues sessionId)) | |
| { | |
| context.Response.Headers["Mcp-Session-Id"] = sessionId; | |
| } | |
| } | |
| private static string GetJsonType(Type type) => Type.GetTypeCode(type) switch | |
| { | |
| TypeCode.String => "string", | |
| TypeCode.Int32 or TypeCode.Int64 or TypeCode.Int16 or TypeCode.UInt32 => "integer", | |
| TypeCode.Double or TypeCode.Single or TypeCode.Decimal => "number", | |
| TypeCode.Boolean => "boolean", | |
| _ => "object" | |
| }; | |
| // 专门用于处理进度报告的辅助类 | |
| private class ProgressReporter(HttpResponse response, string token) : IProgress<ProgressNotificationValue> | |
| { | |
| public void Report(ProgressNotificationValue value) | |
| { | |
| ProgressParams progressParams = new(token, value.Progress, value.Total, value.Message); | |
| ProgressNotification notification = new("2.0", "notifications/progress", progressParams); | |
| // 警告: 在同步方法中调用异步代码,在真实生产环境中需要更优雅的处理 | |
| WriteSseMessageAsync(response, notification).GetAwaiter().GetResult(); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment