工良出品 | 长文讲解 MCP 和案例实战

工良出品 | 长文讲解 MCP 和案例实战

根据 MCP 协议的规定,在 MCP 协议中有以下对象:

  • MCP Hosts: 如 Claude Desktop、IDE 或 AI 工具,希望通过 MCP 访问数据的程序;
  • MCP Clients: 维护与服务器一对一连接的协议客户端;
  • MCP Servers: 轻量级程序,通过标准的 Model Context Protocol 提供特定能力;
  • 本地数据源: MCP 服务器可安全访问的计算机文件、数据库和服务;
  • 远程服务: MCP 服务器可连接的互联网上的外部系统(如通过 APIs);


MCP Host 就是一个 AI 应用,跟用户交互的应用程序,一般是桌面程序,而 MCP Host 跟 MCP Client 可能是放在一起做的,自身即与用户交互,也具有直接调用 MCP Server 的能力。

MCP Server 就是提供 Tool 、资源内容、提示词、对话补全等功能的服务端,MCP Server 的功能或职责是多种多样的,比如高德地图 MCP Server 只提供了 Tool,即接口调用。

本地数据源、远程服务者两个跟 MCP 本身没有关联,而是 MCP Server 自身实现功能的一部分,或者说是支撑 MCP Server 的基础设施和外部依赖。

由于 MCP 概念和功能比较多,因此笔者将一步步使用案例和项目的方式讲解其中的细节,建议读者将示例项目仓库拉下来,根据本文教程尝试自行编写代码以及跑通案例。

核心概念

MCP 协议定义了以下功能模块:

  • Resources
  • Prompts
  • Tools
  • Sampling
  • Roots
  • Transports

由于 Roots 没有多少案例,并且 C# 的 SDK 还没有完善,因此本文只介绍其它功能模块。

本文知识并不是线性讲解以上 MCP 功能。

Transport

Transport 指传输处理消息发送和接收的底层机制,MCP 主要包含两个标准传输实现:

  • 标准输入输出 (stdio):主要对象是本地集成和命令行工具,使用 stdio 传输通过标准输入和输出流进行通信;
  • 服务器发送事件 (SSE):SSE 传输通过 HTTP POST 请求(长连接)实现服务器到客户端的流式通信;

当然,还有一个 Streamable ,但是由于社区支持还不算完善,并且本文也不讲解。

以下是 MCP(Model Context Protocol)协议中stdiossestreamable三者的优缺点和差异的简要说明:

stdio

  • 优点:

    • 平台兼容性高stdio(标准输入输出)是操作系统底层的功能,几乎所有操作系统和编程语言都支持。
    • 简单直接:用于进程间通信,通常是脚本和命令行工具的通信方式,易于实现。
  • 缺点:

    • 缺乏高级功能stdio只能处理简单的文本和二进制数据流,没有内建的消息结构或格式。
    • 不适合在网络环境中的实时交互stdio对于网络通信来说不够灵活和可靠,通常用于本地通信。

sse

  • 优点:

    • 实时更新**:允许服务器通过HTTP连接主动向客户端发送更新消息,适合实时推送的应用场景。

    • 简单实现:基于HTTP协议,不需要复杂的传输层协议,客户端通过 EventSource API 可以很容易地接收。

    • 轻量级:相比WebSocket,SSE更轻量级,适合简单的消息推送场景。

  • 缺点:

    • 单向通信:只能服务器向客户端发送消息,客户端如果需要发送消息,必须通过标准的HTTP请求回服务器。

    • 连接限制:浏览器对同时建立的SSE连接数限制较严格,不适合大量连接的应用场景。

streamable

  • 优点:

    • 效率高:可以处理大数据或连续的数据流,不需要等待整个数据集传输完毕。
    • 实时性好:可以在数据生成时逐步传输,在数据消费时逐步处理,提高实时响应能力。
    • 灵活性高**:支持长时间的连接和传输,适合视频、音频、实时数据库同步等应用。
  • 缺点:

    • 复杂性高:实现和管理流式传输协议、处理数据流的逻辑复杂度较高,需要确保数据的顺序和完整性。
    • 资源消耗:长时间的连接和持续的数据传输可能会消耗较多的服务器和网络资源,需要优化处理。

ModelContextProtocol CSharp 中提供了三种 Transport ,其核心代码在三个类中:

  • StdioClientTransport
  • SseClientTransport
  • StreamClientTransport

下面笔者将会详细讲解 stdio、sse 两种 Transport。

stdio

通过本地进程间通信实现,客户端以子进程形式启动 MCP Server 程序,双方通过stdin/stdout交换 JSON-RPC 消息,传输每条消息时以换行符分隔。

本节示例项目参考 TransportStdioServer、TransportStdioClient。


当使用 stdio 时,McpServer 只需要实现静态方法并配置特性注解即可,然后需要将该程序编译为.exe

TransportStdioServer 添加 Tool :

后面讲解 Tool ,这里先跳过。

[McpServerToolType] public class EchoTool { [McpServerTool, Description("Echoes the message back to the client.")] public static string Echo(string message) => $"hello {message}"; }

然后创建 MCP Server 服务,并使用WithStdioServerTransport()暴露接口能力。

using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using TransportStdioServer; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddMcpServer() .WithStdioServerTransport() .WithTools<EchoTool>(); builder.Logging.AddConsole(options => { options.LogToStandardErrorThreshold = LogLevel.Trace; }); await builder.Build().RunAsync();

编译 TransportStdioServer 项目,在 Windows 下会生成.exe文件,复制.exe文件的绝对路径,在编写 Client 时要用。

C# 编写 Client 时,需要通过命令行参数导入.exe文件,示例如下:

using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; var builder = Host.CreateApplicationBuilder(args); builder.Configuration .AddEnvironmentVariables() .AddUserSecrets<Program>(); var clientTransport = new StdioClientTransport(new() { Name = "Demo Server", // 要使用绝对路径,这里笔者省略了 Command = "E:/../../TransportStdioServer.exe" }); await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport); var tools = await mcpClient.ListToolsAsync(); foreach (var tool in tools) { Console.WriteLine($"Connected to server with tools: {tool.Name}"); }

启动 TransportStdioClient,控制台会打印 TransportStdioServer 中的所有 Mcp tool。


StdioClientTransport 原理是基于命令行参数启动 TransportStdioServer,StdioClient 会将命令行参数拼接起来,然后以子进程方式启动 MCP Server,命令行示例:

cmd.exe/c E:/../TransportStdioServer.exe

StdioClientTransport 核心代码启动子进程:

SSE

本节参考示例项目:TransportSseServer、TransportSseClient。

SSE 是通过 HTTP 长连接实现远程通信的,在使用各种 AI 对话应用时,AI 会像打字机一样逐个输出字符,这种通过 HTTP 长连接、由 HTTP Server 服务器持续推送内容的方式就叫 sse。

SSE Server 需提供两个端点:

  • /sse(GET请求):建立长连接,接收服务器推送的事件流。
  • /messages(POST请求):客户端发送请求至该端点。


在 TransportSseServer 实现简单的 EchoTool。

[McpServerToolType] public sealed class EchoTool { [McpServerTool, Description("Echoes the input back to the client.")] public static string Echo(string message) { return "hello " + message; } }


配置 MCP Server 支持 SSE:

using TransportSseServer.Tools; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMcpServer() .WithHttpTransport() .WithTools<EchoTool>() .WithTools<SampleLlmTool>(); var app = builder.Build(); app.MapMcp(); app.Run("http://0.0.0.0:5000");

TransportSseClient 实现客户端连接 Mcp Server,其代码非常简单,连接到 MCP Server 后将对方支持的 Tool 列出来。

using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; var defaultOptions = new McpClientOptions { ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" } }; var defaultConfig = new SseClientTransportOptions { Endpoint = new Uri($"http://localhost:5000/sse"), Name = "Everything", }; // Create client and run tests await using var client = await McpClientFactory.CreateAsync( new SseClientTransport(defaultConfig), defaultOptions, loggerFactory: NullLoggerFactory.Instance); var tools = await client.ListToolsAsync(); foreach (var tool in tools) { Console.WriteLine($"Connected to server with tools: {tool.Name}"); }