Administrator
发布于 2025-12-03 / 4362 阅读
54

MCP 协议详解:AI 应用的 USB-C 接口

同事问:MCP 不就是 Function Calling 换了个名字吗

上个月内部分享会,我讲完 MCP 之后有同事问了这个问题。当时我答得不太好,说了些「更标准化」「生态更好」之类的空话。后来我认真想了想,也去读了规范原文,这篇算是个正经的回答。

结论是:不是换名字,是两个层次的东西。Function Calling 是模型的一项能力,MCP 是一个进程间通信协议。这个区别决定了它们解决完全不同的问题。

先把两者的定位摆清楚

维度Function CallingMCP
本质模型 API 的一个参数开放通信协议(JSON-RPC 2.0)
谁定义各模型厂商(OpenAI、Anthropic、通义…)Anthropic 发起的开放规范
解决什么让模型能表达「我想调用某个函数」让工具提供方和消费方解耦
绑定关系工具定义绑定在某次 API 调用上工具由独立进程提供,可被多方消费
方向单向(应用 → 模型)双向(服务端也能反向请求客户端)

最直白的说法:Function Calling 是「模型告诉我它想干什么」,MCP 是「我去哪里找到能干这件事的东西」。前者是意图表达,后者是能力供给。

一个具体例子。我们用 Function Calling 接公司的 CMDB:

// 方式一:Function Calling,每个应用都要写一遍
ChatCompletionRequest req = ChatCompletionRequest.builder()
    .model("qwen-max")
    .messages(messages)
    .tools(List.of(
        Tool.builder().function(Function.builder()
            .name("query_hosts")
            .description("按服务名查询主机信息")
            .parameters(JsonSchema.builder()
                .addProperty("service", Property.of("string", "服务名"))
                .required(List.of("service"))
                .build())
            .build()).build()
    ))
    .build();

这段代码里,工具的定义、调用的执行、结果的格式化,全在同一个进程里。如果有三个应用要用 CMDB,就要写三遍(或者抽成公共库,但那还是代码级复用)。

换成 MCP 之后,CMDB 的接入方实现一次 MCP Server,其他应用只是「连上它」:

// 方式二:MCP,应用侧只需要连接
@Bean
McpSyncClient cmdbClient() {
    return McpClient.sync(transport)
        .requestTimeout(Duration.ofSeconds(30))
        .build();
}
cmdbClient.initialize();
// 工具定义由 server 提供,client 直接拉取
var tools = cmdbClient.listTools();

关键差异不是代码行数,是能力的提供者和消费者可以完全独立演进。CMDB 团队加一个新工具,不用通知任何消费方,他们的 client 下次 listTools 就自动看到了。

MCP 的架构:三个角色

规范里定义了 Host、Client、Server 三个角色,这个划分经常被忽略但很重要。

┌──────────────────────────────────────────┐
│  Host(宿主应用)                          │
│  例:Claude Desktop、IDE 插件、我们的 Agent │
│                                          │
│  ┌────────┐  ┌────────┐  ┌────────┐     │
│  │Client 1│  │Client 2│  │Client 3│     │
│  └───┬────┘  └───┬────┘  └───┬────┘     │
└──────┼───────────┼───────────┼──────────┘
       │ 1:1       │ 1:1       │ 1:1
   ┌───┴────┐  ┌───┴────┐  ┌───┴────┐
   │Server A│  │Server B│  │Server C│
   │ (CMDB) │  │(GitLab)│  │(监控)  │
   └────────┘  └────────┘  └────────┘

Client 和 Server 是 1:1 的关系,一个 Client 只连一个 Server。Host 负责管理多个 Client。这个设计的好处是隔离——某个 Server 出问题、有安全边界要求、或者需要不同的认证方式,都不会影响其他连接。

我们内部的做法是一个 Host(Agent 运行时)挂了 4 个 Client:CMDB、监控系统、发布平台、知识库。每个 Server 有不同的权限模型和部署位置。

传输层:从 SSE 到 Streamable HTTP

这部分是今年变化最大的地方,也是很多人认知滞后的地方。

早期的 MCP 用 HTTP+SSE 传输,需要两个端点:

GET  /sse    → 服务端推送事件流(长连接)
POST /message → 客户端发送请求

这个设计有几个明显问题:连接断开后无法恢复、服务端必须维护长连接(无法无状态部署)、不支持断线重连。

现在的规范用 Streamable HTTP,单端点:

POST /mcp    → 客户端发 JSON-RPC 请求
               服务端可返回:
                 application/json        (普通响应)
                 text/event-stream       (升级为 SSE 流)
GET  /mcp    → 可选,客户端监听服务端主动推送
DELETE /mcp  → 显式终止会话
POST /mcp HTTP/1.1
Host: cmdb.internal
Content-Type: application/json
Accept: application/json, text/event-stream     ← 两个都要写
Mcp-Session-Id: 8f3a-4b2c-9d1e                 ← 可选,服务端分配

{"jsonrpc":"2.0","id":7,"method":"tools/call",
 "params":{"name":"query_hosts","arguments":{"service":"order-service"}}}

这个改动带来两个重要能力:一是服务端可以完全无状态(不分配 Session-Id,每个请求独立处理,随便水平扩展);二是断线可恢复(规范建议用 SSE 事件 ID 支持 Last-Event-ID 重连)。

我们在生产用的是无状态模式。原因很简单:我们的 MCP Server 要部署在 K8s 上,节点随时可能被调度走,无状态是最省心的。代价是没法做服务端主动推送(sampling、进度通知这类需要反向通信的能力用不了),但我们的场景不需要。

顺便说一句,很多老文章和老的 MCP Server 实现还在用 HTTP+SSE 传输。如果你在选库,注意看它支持的是哪个版本。Spring AI 的 MCP 模块两边都支持,通过 mcp-endpoint 配置区分。

能力协商:不是所有 Server 都一样

MCP 有个设计我觉得很聪明——初始化时双向声明能力,双方各自说明自己支持什么。

// Server 声明
{
  "capabilities": {
    "tools":     { "listChanged": true },
    "resources": { "subscribe": true, "listChanged": true },
    "prompts":   { "listChanged": false },
    "logging":   {},
    "completion": {}
  }
}

// Client 声明
{
  "capabilities": {
    "roots":    { "listChanged": true },    // 能提供文件系统根目录
    "sampling": {},                          // 能替 Server 调用 LLM
    "elicitation": {}                        // 能向用户提问(较新的能力)
  }
}

rootssampling 是 Function Calling 完全没有的概念,值得单独说。

roots 是客户端告诉服务端「你可以访问这些目录」。比如 IDE 插件把当前项目根目录作为 root 传给文件系统 MCP Server,Server 就知道自己只能在这个范围内操作。这是一种声明式的权限边界。

sampling 更反直觉:服务端可以反过来请求客户端调用大模型。流程是 Server 说「我需要用 LLM 处理一下这段数据」,Client 拿到请求后用自己的模型和额度去调用,把结果返回给 Server。

// Server → Client:请求采样
{"jsonrpc":"2.0","id":42,"method":"sampling/createMessage","params":{
  "messages":[{"role":"user","content":{"type":"text",
               "text":"把以下日志压缩成一句话:..."}}],
  "maxTokens": 200,
  "systemPrompt": "你是日志分析助手"}}

// Client → Server:返回结果
{"jsonrpc":"2.0","id":42,"result":{
  "role":"assistant",
  "content":{"type":"text","text":"订单服务 3 台实例在 14:02 出现连接池耗尽"},
  "model":"qwen-plus","stopReason":"endTurn"}}

这个设计解决了两个实际问题:一是 MCP Server 不需要自己持有 API Key(用客户端的额度);二是最终控制权在客户端——用户可以审核、修改、拒绝服务端的采样请求。这比给 Server 一个 API Key 安全得多。

我们内部还没用 sampling,主要因为它要求有状态连接(服务端要记住请求上下文)。但架构上我认可这个设计。

Server 的三个能力:不只是工具

很多人以为 MCP Server 就是暴露工具。实际上规范定义了三类能力,区别很大:

能力谁控制语义方法
Tools模型执行动作,可能有副作用tools/listtools/call
Resources应用/用户读取数据,幂等resources/listresources/readresources/subscribe
Prompts用户预置的提示词模板prompts/listprompts/get

「谁控制」这一列是关键。Tools 由模型主动调用,Resources 和 Prompts 由人或应用主动选择。这个区分在 Function Calling 里完全不存在——Function Calling 只有工具。

Resource 的价值在于它把「知识」和「动作」分开了。我们的 SOP 文档如果做成工具,模型得知道「有个工具能读 SOP」;做成 Resource,可以在客户端界面上展示成可附件的文档,用户点一下就加载进上下文,不需要模型猜。

Prompt 模板则是把人类经验固化。我们写了几个运维场景的标准排查流程模板,新同事直接用,效果和老同事基本一致。这是 Function Calling 完全覆盖不到的场景。

回到那个问题:到底该用哪个

现在可以正经回答了。我的判断标准:

  • 工具只被一个应用用、且短期内不会变 → Function Calling 就够了。多一层协议就是多一层复杂度和故障点。我们有个一次性的数据清洗脚本,用的就是 Function Calling,200 行搞定。
  • 工具要被多个应用/多个 Agent 复用 → MCP。我们的 CMDB 被三个 Agent 客户端使用,MCP 省掉了三份重复实现。
  • 需要把能力开放给外部生态(比如让 Claude Desktop 或 IDE 接入)→ 必须 MCP。因为 Function Calling 没有跨应用的标准,你没法给 Claude Desktop 塞一段 Java 代码。
  • 需要资源订阅、提示模板、反向采样 → 只能 MCP

还有个现实考量:MCP 的调试成本更高。Function Calling 出问题,看一次 API 请求响应就知道;MCP 出问题,要区分是传输层、协议层还是应用层的问题。我们大概有 30% 的 MCP 相关工时花在调试上,Function Calling 大概是 5%。

现在的生态和我们的实践

到这个月,MCP 的生态已经比较成熟了。我们自己跑了 4 个 Server,社区和厂商提供的 Server 也很多(数据库、Git、文件系统、各类 SaaS)。Spring AI 的 MCP 支持已经稳定,我们用了三个月,没遇到协议层面的 bug。

但也有几个地方还不够成熟,提一下:

  • 鉴权规范还在演进。早期的 MCP 没有定义认证方式,大家各搞各的(有的用 Header 传 token,有的用环境变量)。现在规范里引入了基于 OAuth 2.1 的授权框架,但实现程度参差不齐。我们内部用的是简单的 Header token + 网关层校验,够用但不标准。
  • 工具数量多了之后选择准确率下降。这个我在另一篇里提过,我们的经验是单个 Server 不超过 40 个工具。MCP 协议本身没有提供工具分组或路由的机制,需要自己在架构层解决。
  • 无状态模式下的权限校验。有 Session 时可以一次认证多次调用,无状态下每次请求都要重新校验。我们的做法是在网关层做一次认证,把用户身份放进内部 Header,MCP Server 信任这个 Header。

就写到这。如果哪天你也被《MCP 协议详解:AI 应用的 USB-C 接口》里同一个坑绊住,回来翻这篇,能省半小时。

参考