MCP

近期 MCP 协议越来越爆火,很多开发者都投身参与 MCP Server/Client 的开发,各个大厂也纷纷推出自己的 MCP 集成平台或开放 MCP 接口。也有一些朋友读者在技术群讨论 MCP 技术,很多人对 MCP 的机制不清楚,也有一些文章讲解 MCP 时不够清晰甚至误导了读者,所以笔者在这个周末在学习 MCP 时,写下该笔记,尽可能提供更多的示例和讲解,帮助读者理清楚 MCP 和 LLM 之间的关系,已经如何实际落地使用 MCP。

MCP 协议

MCP 协议文档地址:https://modelcontextprotocol.io/introduction

中文版文档地址:https://mcp-docs.cn/introduction


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

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

image-20250419095413432


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


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


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


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


MCP 核心概念

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

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

由于其它的概念跟服务端开发有关,笔者就不讲解了,详细 MCP 开发服务端实践可以看笔者另一篇文章:

https://www.cnblogs.com/whuanle/p/18837493


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

  • 优点:

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

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

加载 MCP

不管 MCP 服务是哪种传输方式,客户端代码都是同一个三步走的模板。先记住这个骨架,后面的差异只是第一步换成不同的 Transport

安装依赖包:
ModelContextProtocol

// ① 创建并连接 McpClient —— 告诉它去哪儿、怎么连
await using var mcpClient = await McpClient.CreateAsync(/* 某种 Transport */);

// ② 列出工具 —— 把服务端对外提供的 tools 拉成清单
var mcpTools = await mcpClient.ListToolsAsync();

// ③ 当作普通工具交给 agent —— 剩下的交给第九章那套引擎
AIAgent agent = chatClient.AsAIAgent(
    model: deploymentName,
    instructions: "...",
    tools: [.. mcpTools.Cast<AITool>()]);

Console.WriteLine(await agent.RunAsync("你的问题"));


MCP 的使用,基本就这三步,主要差别在于使用何种 Transport。下面分别看三种最常见的连接方式。

笔者注,MCP SSE 方式目前已经被抛弃使用。


stdio 连接本地子进程

最经典的 MCP 场景:一个 npm 包(比如官方的 GitHub 工具)通过子进程跑起来,客户端用 stdin/stdout 跟它收发 JSON-RPC。

image-20260725091942320


using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;

// ① 用 StdioClientTransport 拉起一个子进程,通过它的标准输入/输出收发 JSON-RPC
await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
{
    Name = "MCPServer",
    Command = "npx",                                              // 启动命令
    Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-github"],  // 拉官方 GitHub MCP 服务
}));

// ② 列出这个 MCP 服务对外暴露的工具
var mcpTools = await mcpClient.ListToolsAsync();

// ③ 工具清单整张塞给 agent —— 它们就是普通的 AITool
AIAgent agent = new OpenAIClient(
    credential: new ApiKeyCredential("1234"),
    options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
    .GetChatClient("qwen/qwen3.5-9b").AsAIAgent(
        instructions: "You answer questions related to GitHub repositories only.",
        tools: [.. mcpTools.Cast<AITool>()]);

Console.WriteLine(await agent.RunAsync("Summarize the last four commits to the microsoft/semantic-kernel repository?"));


笔者注:stdio 通道很"脆"。 JSON-RPC 走的是子进程的 stdout,任何写进 stdout 的内容都会污染协议流——包括 Console.WriteLine、日志、进度条。所以如果你反过来要写一个 stdio MCP 服务端(见后面"agent 反过来当 MCP 服务"),必须把日志重定向到 stderr、清空默认的 console 日志 provider,否则客户端一连接就报解析错误。


Streamable HTTP

如果 MCP 服务挂在网上,就用 HttpClientTransport。它底层走的是 MCP 1.x 推荐的 Streamable HTTP 传输:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel; 

// ① 用 HttpClientTransport 连一个远程 MCP 服务
await using McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
{
    Endpoint = new Uri("https://mcp.amap.com/mcp?key=你自己的高德key"),   // 高德应用 key
    Name = "高德地图",
}));


// ② 列出工具,打印出来确认连上了
IList<McpClientTool> mcpTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");

// ③ 工具清单整张塞给 agent —— 它们就是普通的 AITool
AIAgent agent = new OpenAIClient(
    credential: new ApiKeyCredential("1234"),
    options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
    .GetChatClient("qwen/qwen3.5-9b")
    .AsAIAgent(
    name: "Map",
    instructions: "Y你是地图助手.",
    tools: [.. mcpTools.Cast<AITool>()]);

Console.WriteLine(await agent.RunAsync("规划深圳去惠州旅游的自驾路线和景点"));

image-20260814100941825


这就是 MCP 协议标准化的好处,同一套 McpClient API,底下接的是完全不同的传输层,代码几乎差不多。

当然,我们还要注意几个事情:

  • HttpClientTransport 默认会走 HttpTransportMode.AutoDetect:先尝试 Streamable HTTP,不行再降级到老的 SSE 传输。所以你一般不用关心服务端到底支持哪一种。不过老 SSE 方式已经逐渐抛弃了。
  • 需要鉴权时(比如连企业内部的 MCP 服务),可以传一个自己配好的 HttpClient,里面挂 DelegatingHandler 注入 Authorization: Bearer ... 头。
  • HTTP 方式的连接生命周期比 stdio 靠谱一些。

Hosted MCP

前两种方式里,工具的发现和调用都发生在你的进程里McpClient 在本地连上服务、本地发起 tools/call、本地拿结果。MAF 框架还有一种完全不同的模式,托管 MCP:你只声明"有这么个 MCP 服务、地址在哪、允许调哪些工具",真正的连接、发现、调用全交给模型供应商(比如 Azure OpenAI Responses API)在云端做

这种方式不依赖 McpClient,而是用一个标记类 HostedMcpServerTool

// 来自 Microsoft.Agents.AI 命名空间 —— 这是一个"声明性"的工具
AITool serverTool = new HostedMcpServerTool(
    serverName: "microsoft_learn_hosted",
    serverAddress: "https://learn.microsoft.com/api/mcp")
{
    AllowedTools = ["microsoft_docs_search"],                       // 白名单:只许调这几个
    ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire     // 是否每次调用都要人批
};

// 直接当普通工具塞进 agent,不需要 McpClient、不需要 ListToolsAsync
List<AITool> allTools = [serverTool];
AIAgent agent = ...AsAIAgent(model, instructions, name, tools: allTools);

让长任务工具"轮询到底"

有些工具一次调用要跑很久(数据集分析、模型训练、大规模检索),直接 tools/call 会卡死在 HTTP/stdio 通道上。MCP 1.2 引入了 Tasks 机制 ,客户端先发一个 tools/call 的任务(tasks/call),拿到一个 taskId,然后轮询 tasks/get 直到任务进入终态,再取结果。

这种发任务→轮询→取结果的套路如果交给业务代码写很啰嗦,所以 MAF 提供了专门的扩展方法,把它整套透明化。这就是 Microsoft.Agents.AI.Mcp 这个包的核心价值之一:

# 长任务支持在这个包里,需要单独引用
Microsoft.Agents.AI.Mcp
using Microsoft.Agents.AI.Mcp;   // ListAgentToolsWithTaskSupportAsync 在这里

await using var mcpClient = await McpClient.CreateAsync(new StdioClientTransport(new()
{
    Name = "DatasetAnalyzer",
    Command = "dotnet",
    Arguments = [thisAssemblyPath, "--server"],
}));

// 关键这一步:用 ListAgentToolsWithTaskSupportAsync 替代普通的 ListToolsAsync
var taskOptions = new McpTaskOptions
{
    DefaultTimeToLive = TimeSpan.FromMinutes(5),          // 任务在服务端最多存活多久
    CancelRemoteTaskOnLocalCancellation = true,          // 本地取消时,顺手取消远端任务
};
var mcpTools = await mcpClient.ListAgentToolsWithTaskSupportAsync(taskOptions);

AIAgent agent = ...AsAIAgent(
    instructions: "...",
    tools: [.. mcpTools.Cast<AITool>()]);

// 对调用方完全透明:和普通工具用起来一模一样
Console.WriteLine(await agent.RunAsync("Analyze the dataset named 'sales-2025-q1'..."));

agent 把自己暴露成 MCP 服务

前面都是 Agent 当 MCP 客户端,去消费别人的工具。MCP 协议其实是双向的,你也能把一个 Agent 本身包成一个 MCP 工具,让别的客户端(Claude Desktop、Cursor、另一个 agent)来调。


using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;

AIAgent agent = new OpenAIClient(
    credential: new ApiKeyCredential("1234"),
    options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
    .GetChatClient("qwen/qwen3.5-9b")
    .AsAIAgent(
    name: "Map",
    instructions: "你是地图助手.");

McpServerTool tool = McpServerTool.Create(agent.AsAIFunction());

HostApplicationBuilder builder = Host.CreateEmptyApplicationBuilder(settings: null);
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithTools([tool]);

await builder.Build().RunAsync();