CodeGym /课程 /ChatGPT Apps /Handshake 与 capabilities:客户端如何得知服务器的能力

Handshake 与 capabilities:客户端如何得知服务器的能力

ChatGPT Apps
第 6 级 , 课程 2
可用

1. 为什么需要 handshake

如果说 REST 端点像是一扇扇独立的门,只需按 URL 去敲,那么 MCP 更像是通过一条通道的持续对话。客户端不仅仅发送零散的请求,它首先会建立会话。Handshake 就是会话开始时的“见面礼”。

在 MCP 中,这一步由一个特殊请求 initialize 来完成,客户端在传输层建立好之后(STDIO、HTTP/stream、WebSocket——都可以)立刻发送。在请求里它会说明:“我使用某个版本的 MCP,这是我支持的能力,以及我是谁。”服务器回应:“我支持这个版本以及这些能力,很高兴认识你。”

成功交换后,客户端会发送通知 notifications/initialized,并且只在此之后才开始正式工作:tools/listresources/listtools/call 等等。

打个比方,MCP 的 handshake 就像把服务器搬进数据中心之前先签租约。在没有约定好规则之前(协议格式、数据中心提供哪些服务、如何计费)——贸然搬服务器毫无意义。

从实操角度看,handshake 解决三件事:

  1. 检查协议版本的兼容性。
  2. 声明服务器支持哪些 MCP“原语”:tools、resources、prompts、日志、通知等。
  3. 提供关于客户端与服务器的元信息——实现的名称与版本。

2. MCP 连接的生命周期:handshake 位于何处

为避免抽象,我们来看一个典型(且大幅简化)的连接流程(flow):

sequenceDiagram
    participant C as 客户端 (ChatGPT/Inspector)
    participant S as MCP 服务器

    C->>S: (1) 建立传输 (STDIO/HTTP-stream)
    C->>S: (2) Request: "initialize"
    S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Request: "tools/list" / "resources/list"
    S-->>C: (6) Result: 工具/资源列表
    C->>S: (7) Request: "tools/call" 等

从技术步骤看如下:

  1. 传输已建立:例如 ChatGPT 将你的服务器以子进程方式启动并连接 STDIO,或 Inspector 对 /mcp 发起 HTTP/stream 请求。
  2. 客户端发送 JSON-RPC 请求 initialize
  3. 服务器用 JSON-RPC 结果回应,包含 protocolVersioncapabilitiesserverInfo 字段。
  4. 客户端发送通知 notifications/initialized——信号:“我已读完,可以开始工作”。
  5. 客户端根据在服务器 capabilities 中看到的内容,调用 discovery 方法(tools/listresources/listprompts/list)。
  6. 服务器返回工具/资源/提示词(prompts)的元数据。
  7. 随后进入“工作”请求:tools/callresources/read 等。

要点在于,handshake 只是一次普通的 JSON-RPC 方法调用 initialize,毫无魔法。学习过 MCP 消息格式后,你已经能解析这样的请求;唯一不同在于这个方法是唯一且“特殊”的,并且总是第一个执行。

3. 客户端在 initialize 中发送什么

我们把 initialize 请求拆开看。一个最小化(为讲解而简化)的请求可能如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

这个示例与 MCP 官方文档展示的内容非常接近。params 中的核心字段有:

protocolVersion

一条表示 MCP 规范版本的字符串,通常是日期格式,例如 "2025-06-18"。它不是你的应用版本,而是协议本身的版本。客户端表示:“我期望使用这个 MCP 版本进行通信。”服务器要么在响应中确认,要么在不支持该版本时返回错误。

这能防止“客户端以为是这样,服务器实现却是那样”的情况。如果没有找到共同版本,宁可坦诚断开连接,也不要交换不兼容的消息。

capabilities(客户端)

一个对象,用于声明客户端自身支持哪些 MCP 能力。例如,ChatGPT 客户端常会标注 elicitation,表示可以处理用户提问(追加输入、确认等)。

示例:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

服务器可以利用这些信息判断是否有必要使用协议中的高级特性。例如 elicitation 意味着客户端(ChatGPT)可以向用户提出澄清性问题并请求额外数据。

clientInfo

简单的元信息:客户端的名称与版本。

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

对服务器开发者来说,这对日志非常有用:你总能看到究竟是谁连了上来——ChatGPT、MCP Inspector、你自己的测试客户端,以及它们的版本号。

4. 服务器如何响应:initialize result

initialize 的响应是一个普通的 JSON-RPC 结果,id 与请求相同,但在 result 字段中给出服务器的能力描述。

在请求中我们看的是客户端侧的 capabilities——客户端自身支持什么。现在看相应的响应对象:服务器的 capabilities,即服务器能做什么。结构大致如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

类似的结构你也会在协议或 SDK 的官方描述中看到。主要部分包括:

protocolVersion(响应)

服务器要么复述客户端提出的版本,要么(理论上)在存在多个交集时选择另一个共同版本。在典型实现中,如果支持,就直接确认客户端的版本;如果不支持,服务器应返回错误并终止通信。

serverInfo

服务器元信息:名称、版本。

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

听起来平淡无奇,但你日后会靠这些信息在日志里筛选定位:“为什么 ChatGPT 的 X 版本与我们 Y 版本的服务器协商失败?”

capabilities(服务器)

这是最有意思的字段。服务器在这里声明支持哪些 MCP 原语与扩展:是否能处理 tools/*resources/*prompts/*,是否能发送列表变更通知等。

如果 capabilities 中没有 tools 这一节,任何正确实现的客户端都不会调用 tools/listtools/call。同理,没有 resources 就意味着客户端不会发送 resources/listresources/read

因此,capabilities 就像一个轻量级约定:“这台服务器上哪些能做,哪些不能做。”

5. 把 capabilities 看作“超能力清单”

接下来我们只关心服务器的 capabilities——即对 initialize 的响应中决定这台服务器支持哪些 MCP 原语的那个对象。

我们更细一点看它的结构。示例(简化但接近规范):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

这个示例在 MCP 的官方架构文档中也有讲解。逐段解读:

Capabilities.tools

出现 tools 说明:服务器会响应 tools/listtools/call 方法。 若包含标志 listChanged: true,表示当工具集合发生变化时,服务器能发送 tools/list_changed 通知。

对 ChatGPT 来说这很有用:可以缓存工具列表,收到 list_changed 后无需完整重连就能更新。

Capabilities.resources

resources 说明服务器支持资源相关的操作:resources/listresources/read,有时还有检索。内部标志包括:

  • subscribe: true——客户端可以订阅资源变更(例如用于实时日志或文件更新)。
  • listChanged: true——当资源新增或消失时,服务器可发送 resources/list_changed 通知。

这对大型目录或经常变化的“实时”数据尤其重要。

Capabilities.prompts

如果服务器注册了预设提示词(例如与你的业务领域绑定的模型调用模板),那么 capabilities 中会出现 prompts。其中也可能有 listChanged 标志。

客户端看到该节,就知道可用方法 prompts/list,也可能有 prompts/get

Capabilities.logging 以及其他

一些服务器实现还会声明 logging——意味着服务器可以通过 MCP 向客户端发送结构化日志,便于调试。

还可能出现其他部分(例如 sampling 或特定扩展)。重要的是协议从设计之初就是可扩展的:你可以在 capabilities 中添加新键,而老客户端如果不认识就会忽略它们。

Insight

实测发现,ChatGPT App 会忽略发给它的 listChanged 消息。当前在编写应用时你无法先声明一组 tools,再动态增加或删除一些 tools,尽管 MCP 协议支持这样做。

在撰写本课程时的现状是:在你的应用注册到 ChatGPT Store 的那一刻,ChatGPT 会向你的应用请求 tools 与 resources 列表,并将它们永久缓存。该情况在 2026 年内改变的概率较大,但在 2026 年第一季度内改变的概率较低。

6. Handshake 之后的 discovery:如何获取工具与资源列表

Handshake 回答的是“服务器总体上会什么”。 下一步就是所谓的 discovery:客户端通过具体方法拉取细节——有哪些工具、可用哪些资源、内置了哪些提示词。

这要用到 discovery 方法:大致是 tools/listresources/listprompts/list。MCP 架构文档也建议按“handshake → discovery → 工具调用”的顺序来说明。

示例请求 tools/list

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

服务器的响应会包含一个工具数组:名称、描述、参数的 JSON Schema,有时还有元数据(比如分类或图标)。

随后 ChatGPT(或其他客户端)会缓存该列表,并在对话中用它来:

  • 为用户问题匹配合适的工具;
  • 核对工具名是否存在;
  • 在发送 tools/call 前验证参数。

资源方面也类似,只是 resources/list 往往通过游标做分页,以免一次拉回海量记录。这也在 MCP 规范中有所描述,是大目录的典型场景。

7. 以我们的 GiftGen 应用为例讲 handshake 与 capabilities

在前面的模块中我们构建了一个帮助挑选礼物的教学应用。我们已有小部件、后端有 suggest_gifts 工具,也有一些礼物目录。现在想象一下,MCP 服务器 gift-genius 的 handshake 会是什么样。

GiftGen 的 handshake 示例

来自客户端的请求:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

我们服务器的响应:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

本质上我们几乎复用了 MCP 官方架构中的示例,只是把名字替换成了自己的应用。

客户端从该响应中得到的信息:

  • 存在工具(tools),并且列表可能动态变化(listChanged: true)。
  • 存在资源(我们的礼物目录,可能保存在文件或数据库中)。
  • 存在提示词(例如“为用户 N 生成礼物的简短描述”之类的模板)。
  • 服务器可以发送日志(便于 Inspector 与调试)。

接着客户端会执行 tools/list,并看到类似这样的工具:

{
  "name": "suggest_gifts",
  "description": "根据收礼人画像推荐礼物创意。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

此时,当用户说类似“给我姐姐推荐礼物,25 岁,预算不超过 50 美元”的话,模型已经知道:有一个 suggest_gifts 工具,参数结构如何,可以通过 tools/call 来调用它。

8. SDK 如何“隐藏” handshake(以及为何仍需理解它)

在我们下节课将要使用的 TypeScript 版 MCP SDK 中,initializenotifications/initialized 这套流程被封装在 connect 方法里。示例代码:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// 工具注册——SDK 会据此自动配置 capabilities.tools
server.tool(
  "suggest_gifts",
  {
    description: "推荐礼物创意。",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... 礼物推荐逻辑 ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// 这里 SDK 会:
// 1) 接收来自客户端的 initialize;
// 2) 用 serverInfo 和 capabilities 进行响应;
// 3) 等待 notifications/initialized;
// 4) 然后开始处理 tools/* 调用。
await server.connect(transport);

SDK 会根据你注册的内容自动汇总 capabilities:如果至少有一次 server.tool(...),它就会在 capabilities 中加入 tools。如果你注册了资源或提示词,就会出现 resourcesprompts

理解 handshake 与 capabilities 并不是为了让你亲手去写 JSON(千万别这么做),而是为了:

  • 阅读 MCP 日志并理解为什么客户端“看不到”你的工具;
  • 诊断协议版本不兼容的问题;
  • 在必要时实现自定义服务器或非标准传输层。

9. 协议版本与能力演进

Handshake 中的 protocolVersion 不是装饰。MCP 规范明确强调:这是协商兼容协议版本的方式;如找不到共同版本,最好结束连接。

一个典型场景:

  1. 你在生产环境部署了一个使用某 SDK 的 MCP 服务器,该 SDK 实现了 MCP 版本 "2025-06-18"
  2. 过一段时间 MCP 出了新版本,你更新了客户端,但服务器仍是旧版本。
  3. 客户端发送的 protocolVersion"2026-02-01",服务器不认识该版本并返回 invalid protocol version(或类似)错误。

实践表明:开发者常忽视这个字段,随后惊讶于为什么连接建立不了。

对版本的正确态度:

  • 始终清楚你的 SDK 支持哪个 MCP 版本(通常写在文档/发布说明中)。
  • 升级 SDK 时,有意识地更新协议版本。
  • 在日志与监控中明确显示因 protocolVersion 不匹配导致的初始化错误。

通过 capabilities 扩展能力也与演进相关:MCP 的新功能会以 capabilities 中的新键形式加入。老客户端会忽略它们,新客户端则可以使用。官方文档把这视为保持向后兼容的设计模式。

10. 从 ChatGPT 与 Inspector 的视角看 handshake

ChatGPT 在连接 MCP 时会做什么

当你在 Dev Mode 中把 MCP 服务器接到 ChatGPT 上,平台在幕后大致会做:

  1. 打开传输(通常是指向 /mcp 的 HTTP/stream)。
  2. 发送 initialize,携带 protocolVersioncapabilitiesclientInfo(类似“ChatGPT Enterprise,某某版本”)。
  3. 接收响应并缓存服务器的 capabilities。
  4. 根据所见的 capabilities 调用 tools/listresources/listprompts/list
  5. 在对话中,当模型决定调用工具时,会对照这个缓存:是否存在该工具、参数模式是什么、该如何构造调用。

如果服务器的 capabilities 不包含 tools,ChatGPT 甚至不会尝试把你的 App 当作工具来提供。如果有 resources,但没有 listChanged 标志,ChatGPT 可能会缓存资源列表,而不等待变更通知。

Inspector 与 MCP Jam 如何帮助调试

诸如 MCP Jam / MCP Inspector 的工具也做几乎相同的事:建立连接、完成 handshake、展示服务器的 capabilities,并让你手动调用 tools/listtools/call 等。

对开发者而言,这是必备工具:

  • 能看到服务器实际返回的 protocolVersion
  • 能直观看到 capabilities 中是否存在 toolsresourcesprompts
  • 能判断 ChatGPT 看不到工具的原因(是不是没声明 capabilities,或 handshake 未完成)。

在本模块的最后一节课里你会更深入地使用这些工具,但现在理解它们正是工作在我们所讲的 handshake 之上就已经很有帮助。

11. 使用 handshake 与 capabilities 时的常见错误

理论看着很直,但在实践中,恰恰是 handshake 与 capabilities 声明最常成为“低级 bug”的来源——尤其在 Dev Mode 或 MCP Inspector 中。下面列出一些你几乎一定会在自己的代码或同事的日志里遇到的典型错误。

错误 1:initialize 请求格式不正确。
在不借助 SDK 手写 MCP 服务器时非常常见——丢了某个 JSON-RPC 必填字段。例如忘了 jsonrpc: "2.0",把 method 写成 "init" 而不是 "initialize",或把 capabilities 写成布尔值而不是对象。MCP 规范要求严格的格式;任何偏差都会导致解析错误与断连。文档与实操指南都特别建议:先确认你的 initialize 完全符合规范,再看其他问题。

错误 2:忽视 protocolVersion。
有时开发者直接从文档里抄一段示例,放了随便的版本字符串,却没核对 SDK 支持情况。结果客户端与服务器使用了不同版本的 MCP,连接自然建立不了。错误表象可能是“客户端根本连不上”。需要把 protocolVersion 当作真正的契约:由前端/代理平台与 MCP 服务器团队明确协商。

错误 3:忘记声明 capabilities。
经典情况:你在服务器上注册了工具,但在手写 handshake 的响应 initialize 时漏掉了在 capabilities 里加入 "tools": {}。在 inspector 里你能看到工具存在,但 ChatGPT 显示 “No tools available”——因为它诚实地相信 capabilities,如果那里没有 tools 小节,它就不会调用 tools/list。Apps SDK 的排障指南也反复强调:一旦 ChatGPT 看不到你的工具,先检查 capabilities。

错误 4:调用未在 capabilities 中声明的方法。
有同学会尝试给没有 resources 小节的服务器发送 resources/list。形式上服务器可以回 Method not found,但更正确的是根本不要调用这些方法。MCP 专门通过 capabilities 来防止这种情况。客户端应先查看 capabilities 中是否存在对应小节,再决定是否调用方法。

错误 5:在收到 notifications/initialized 之前服务器就开始“说话”。
如果服务器在回应 initialize 后立刻向客户端发送日志或通知,而没有等待 notifications/initialized,某些客户端可能会忽略这些消息甚至断开连接。MCP 官方架构强调:必须先完成 handshake,且只有在收到初始化通知后才进入“工作”阶段。

错误 6:更改工具的 schema 却不告知列表已变更。
当你调整了工具的 JSON Schema(把字段改为必填、重命名参数等),但既没有重启服务器,也没有发送工具列表变化的通知,客户端的缓存可能仍是旧的 schema,从而导致诡异的校验错误。规范建议使用 listChanged 标志与 tools/list_changedresources/list_changed 通知,帮助客户端及时更新缓存。

错误 7:过早优化,给 capabilities 加“魔法”。
有的团队在还没吃透基础机制前,就开始搞复杂的 capabilities 动态生成、按客户端分版本、种种奇技淫巧。起步阶段如实声明服务器会什么就够了:tools、resources、prompts、logging。应当随着真实需求再扩展 capabilities,而不是“为未来预留”。这更像组织层面的反模式,而非纯协议问题,但在实战项目中非常常见。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION