MCP TypeScript SDK Sampling 示例详解:一个工具同时服务 2025 与 2026 双协议纪元

MCP TypeScript SDK Sampling 示例详解:一个工具同时服务 2025 与 2026 双协议纪元

【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk

导读

examples/sampling/是 MCP TypeScript SDK 官方示例库中的一个最小可运行示例,演示了"服务端工具向宿主 LLM 请求一次补全(sampling)"这一完整链路。它的核心价值在于:同一份服务端代码、同一个客户端 handler,可以同时工作在 2025 协议纪元(push 式sampling/createMessage)与 2026-07-28 协议纪元(inputRequired.createMessage嵌入式请求)之下——协议载体不同,但用户感知完全相同。读完本文,你将掌握ctx.mcpReq.requestSampling与inputRequired.createMessage两种 API 的写法差异、客户端自动履行(auto-fulfilment)机制,以及 push 式 sampling 的弃用背景与迁移方向。


一、示例定位:采样(Sampling)是什么

Sampling 是 MCP 协议中的一项服务器→客户端(server-to-client)能力:服务端在工具 handler 内部不直接调用任何 LLM 的 API,而是把消息列表发给已连接的客户端(宿主),由宿主用它所掌控的模型完成推理,再把补全结果返回给服务端继续执行。这样服务端从不持有模型密钥——密钥在宿主手里。

该示例围绕一个summarize工具展开:它接收一段文本,构造一条"请简洁总结"的用户消息,通过 sampling 机制请求宿主模型的补全,再把模型返回的摘要作为工具结果返回给客户端。运行链路(客户端 → 服务端 → 宿主模型 → 服务端 → 客户端)是 sampling 场景最典型的闭环演示。

# 服务端与客户端源码 examples/sampling/server.ts examples/sampling/client.ts

二、双协议纪元对比:同一目标,两种 API

原 README 用一张表格点明了本示例的核心设计——"一个工厂(factory),两个协议纪元":

2025 纪元(--legacy,push 式)2026-07-28(多轮往返)
await ctx.mcpReq.requestSampling({ messages, maxTokens })—— 服务端主动推送一条sampling/createMessage请求,并原地(in-line)等待应答return inputRequired({ inputRequests: { summary: inputRequired.createMessage({ messages, maxTokens }) } })—— 客户端履行内嵌请求后,把响应附加在重试调用上一并返回

两种方式在协议层面走向完全不同:

  • 2025 纪元(push 式):存在一条服务器→客户端的请求通道。handler 通过ctx.mcpReq.requestSampling(...)把请求推给客户端,客户端调用自己的模型并应答,handler 阻塞直至收到应答后继续。
  • 2026-07-28(多轮往返):协议移除了服务器→客户端请求通道。handler 不再推送请求,而是返回一个input_required结果,其中内嵌sampling/createMessage请求;客户端的自动履行驱动(auto-fulfilment driver)取出内嵌请求、调用模型、然后把模型应答附加在重试的tools/call上,handler 再次进入并从ctx.mcpReq.inputResponses中读取应答。

而关键的一致性在于:客户端只注册一个sampling/createMessagehandler(见 examples/sampling/client.ts);在 2026-07-28 这条腿上,自动履行驱动把内嵌请求分发到同一个handler 上,因此两种纪元下"客户端如何应答"的代码完全复用,无需编写两套。

⚠️弃用提示:push 式 sampling 自协议修订版 2026-07-28(SEP-2577)起被弃用(deprecated),但在弃用窗口期内(至少 12 个月)仍然可用,见 docs/servers/sampling.md。


三、服务端实现解析:一个工具,两个纪元分支

完整服务端代码在 examples/sampling/server.ts。核心是buildServer工厂函数——它接收McpRequestContext,据此判断当前连接协商出的协议纪元,返回一个注册了summarize工具的McpServer:

function buildServer(reqCtx: McpRequestContext): McpServer { const server = new McpServer({ name: 'sampling-example', version: '1.0.0' }); server.registerTool( 'summarize', { description: 'Summarize text using the host LLM', inputSchema: z.object({ text: z.string() }) }, async ({ text }, ctx): Promise => { const messages = [ { role: 'user' as const, content: { type: 'text' as const, text: `Please summarize the following text concisely:\n\n${text}` } } ]; // ... 纪元分支见下文 } ); return server; }

工具 handler 的类型签名值得注意:Promise——返回值既可能是普通工具结果,也可能是input_required结果。这是 2026-07-28 纪元下"用返回值代替推送"的协议要求。

3.1 2025 纪元分支:push 式requestSampling

if (reqCtx.era === 'legacy') { // 2025-era: push a server→client `sampling/createMessage` request // and await the model's answer in-line. const response = await ctx.mcpReq.requestSampling({ messages, maxTokens: 500 }); // `content` is a single block when no tools were passed. const content = response.content; const summary = !Array.isArray(content) && content.type === 'text' ? content.text : 'Unable to generate summary'; return { content: [{ type: 'text', text: summary }] }; }

几个要点:

  • ctx.mcpReq.requestSampling({ messages, maxTokens })的参数对应CreateMessageRequest:messages是消息数组,maxTokens: 500限制模型最大输出 token 数(示例中的具体值,可按需调整)。
  • 返回的response是CreateMessageResult:由客户端决定由哪个模型履行,携带model(模型名)、role: 'assistant'与一个content内容块。由于请求未附带工具(tools),content是单一内容块而非数组,示例据此做了!Array.isArray(content)判断。
  • handler 会阻塞直到客户端应答,因此服务端自始至终不持有模型密钥——密钥与推理都发生在宿主一侧。

3.2 2026-07-28 分支:返回inputRequired内嵌请求

// 2026-07-28: return inputRequired with an embedded // `sampling/createMessage` — the client's auto-fulfilment driver // dispatches it to the same `sampling/createMessage` handler and // retries this call with the model's response attached. const response = ctx.mcpReq.inputResponses?.['summary'] as { content?: { type: string; text?: string } } | undefined; if (!response) { return inputRequired({ inputRequests: { summary: inputRequired.createMessage({ messages, maxTokens: 500 }) } }); } const summary = response.content?.type === 'text' ? (response.content.text ?? '') : 'Unable to generate summary'; return { content: [{ type: 'text', text: summary }] };

这是典型的"write-once"多轮 handler 写法:

  1. 第一轮进入:ctx.mcpReq.inputResponses中还没有summary键,于是返回inputRequired({ inputRequests: { summary: inputRequired.createMessage({ messages, maxTokens: 500 }) } })。inputRequests是一个 map,summary是自定义的请求键名,值由inputRequired.createMessage(...)构建。
  2. 客户端履行:客户端的自动履行驱动取出内嵌的sampling/createMessage请求,交给之前注册的同一个 handler 应答。
  3. 重试进入:客户端把模型应答附加在重试的tools/call上,handler 再次进入时从ctx.mcpReq.inputResponses['summary']读到模型应答,生成最终工具结果并返回。

从仓库源码看,inputRequired.createMessage的实现非常直接——它只是把方法名与参数打包成内嵌请求结构体(见 packages/core-internal/src/shared/inputRequired.ts):

createMessage(params: CreateMessageRequestParams): InputRequest { return { method: 'sampling/createMessage', params }; }

而inputRequired(spec)本身要求spec至少携带inputRequests(且至少一项)或requestState之一,否则抛出TypeError(同一文件的第 96 行附近有该校验逻辑)。

3.3 双纪元共用同一工厂

server.ts末尾根据传输类型装配服务:stdio 走serveStdio(buildServer),HTTP 走createMcpHandler(buildServer)+ Hono 应用(createMcpHonoApp()默认绑定 localhost 主机/源校验,app.all('/mcp', c => handler.fetch(c.req.raw))挂载到/mcp路径)。服务端日志只写 stderr——stdio 模式下 stdout 是 JSON-RPC 流,绝不能混入日志。


四、客户端实现解析:一个 handler 服务两个纪元

完整客户端代码在 examples/sampling/client.ts。客户端通过parseExampleArgs()(来自 examples/shared/src/args.ts)解析传输与纪元:--http切到 HTTP,--legacy切到 2025 纪元,默认是 stdio + 2026-07-28。

const client = new Client( { name: 'sampling-example-client', version: '1.0.0' }, { versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' }, capabilities: { sampling: {} } } );
  • versionNegotiation.mode:modern(默认)自动协商 2026-07-28,legacy强制 2025 纪元。
  • capabilities: { sampling: {} }:客户端必须声明sampling能力,服务端的requestSampling/inputRequired才可能被满足。

随后客户端连接传输(stdio 下通过npx -y tsx拉起同目录的server.ts,HTTP 下直连 URL),并只注册一个sampling/createMessagehandler:

client.setRequestHandler('sampling/createMessage', async () => ({ role: 'assistant', content: { type: 'text', text: '[canned summary]' }, model: 'stub', stopReason: 'endTurn' }));

真实宿主会在这里调用自己的 LLM;示例出于确定性测试的目的,直接返回固定文本[canned summary],并用stopReason: 'endTurn'表示自然结束。这个 handler 在两条腿上都生效:

  • 2025 腿:服务端 push 的sampling/createMessage直接到达此 handler。
  • 2026-07-28 腿:服务端返回的input_required中的内嵌请求,由客户端的自动履行驱动分发到这同一个handler,应答后被附加到重试调用上。

4.1 调用工具并断言往返结果

const result = await client.callTool({ name: 'summarize', arguments: { text: 'hello world' } }); check.equal(result.content?.[0]?.type === 'text' ? result.content[0].text : '', '[canned summary]');

客户端调用summarize工具后,用check(来自node:assert/strict的别名,见 examples/shared/src/args.ts)断言工具结果中的文本正是 handler 返回的[canned summary]——证明 sampling 请求经历了"服务端 → 客户端 handler → 服务端"的完整往返。这是典型的自校验示例:跑通即验证。


五、传输与纪元的组合约束

原 README 明确指出了 push 式采样在各传输上的可用性差异:

Push-style sampling 仅在stdio/legacy上被完整演练(createMcpHandler的 stateless-legacy 姿态没有客户端响应 POST 的返回路径——会话化组合见 examples/legacy-routing/);http/legacy 这一条腿只验证 initialize 握手。2026-07-28 的inputRequired.createMessage在两种传输上都运行。

原因在于:2025 纪元下客户端的sampling/createMessage应答是一条独立的 POST 请求,必须到达发送请求的那同一个服务端实例。createMcpHandler默认的 stateless-legacy 姿态没有这样的会话返回路径,因此 push 式流程只在 stdio/legacy 上被完整测试;http/legacy 腿在client.ts中仅断言getServerCapabilities()?.tools存在(即 initialize 握手成功),不再调用工具。

而 2026-07-28 纪元走"返回input_required+ 客户端重试"的请求-响应闭环,天然兼容 stdio 与 HTTP 两种传输。


六、运行方式

示例脚本定义在 examples/sampling/package.json:

# 2026-07-28 纪元(inputRequired 内嵌请求) pnpm --filter @mcp-examples/sampling client # 2025 纪元(push 式 sampling) pnpm --filter @mcp-examples/sampling client -- --legacy

说明:

  • 两个命令运行的都是tsx client.ts,客户端默认通过 stdio 拉起同目录的server.ts。
  • 追加--http可切换到 HTTP 传输(服务端监听http://127.0.0.1:3000/mcp),并可通过--port自定义端口;追加--legacy强制 2025 纪元。这些 flag 由 examples/shared/src/args.ts 统一解析,示例运行器 scripts/examples/run-examples.ts 也会用同样的 flag 驱动每个传输/纪元组合。
  • package.json的example.era字段标记为"dual",即该示例面向双纪元设计。

七、能力强制校验与迁移方向

7.1 客户端必须声明 sampling 能力

requestSampling只对声明了sampling能力并注册了sampling/createMessagehandler 的客户端生效。若在McpServer构造时传入enforceStrictCapabilities: true,SDK 会在发送任何服务器发起请求前校验客户端声明的能力;对未声明sampling的客户端,requestSampling会在 handler 内抛出,工具调用以isError: true的结果返回:

{ content: [ { type: 'text', text: 'Client does not support sampling (required for sampling/createMessage)' } ], isError: true }

这一行为有仓库测试与配套示例佐证,见 examples/guides/servers/sampling.examples.ts(bareServer用enforceStrictCapabilities: true构造,对未声明能力的客户端调用工具后打印被拒绝的结果)。

7.2 弃用与迁移:直接调用 LLM 提供方 API

如 docs/servers/sampling.md 所述,sampling 自协议版本 2026-07-28(SEP-2577)起弃用。原因很实际:sampling 把一次 LLM 调用路由到已连接的客户端,而 2026-07-28 修订移除了承载它的服务器→客户端请求通道。

迁移路径同样简单:在服务端引入 LLM 提供方的 SDK,在工具 handler 内用自己的 API key直接调用模型。handler 的骨架保持不变,唯一变化的是把requestSampling调用替换为直接提供方调用——从此不再依赖客户端支持什么。需要指出的是,2026-07-28 纪元下requestSampling会直接抛错;该纪元的替代形式正是本示例展示的"从 handler 返回内嵌createMessage请求",该形式由input_required机制承载,其完整说明(包括嵌入式请求的四种构建器elicit/elicitUrl/createMessage/listRoots、acceptedContent校验、requestState跨轮状态与 HMAC 编解码保护等)见 docs/servers/input-required.md。


八、小结

examples/sampling/用约 80 行服务端代码 + 50 行客户端代码,完整演示了 MCP sampling 能力的双纪元实现:

  1. 一个工厂,两个纪元:buildServer依据reqCtx.era分支——2025 纪元用 push 式ctx.mcpReq.requestSampling({ messages, maxTokens })原地等待;2026-07-28 纪元返回inputRequired({ inputRequests: { summary: inputRequired.createMessage({ messages, maxTokens }) } })并读取重试后的ctx.mcpReq.inputResponses。
  2. 一个客户端 handler 通吃:客户端只注册一个sampling/createMessagehandler,2026-07-28 腿由自动履行驱动把内嵌请求分发到同一 handler,再重试工具调用。
  3. 传输约束:push 式采样仅在 stdio/legacy 上完整演练;inputRequired.createMessage在 stdio 与 HTTP 上都可用。
  4. 弃用与迁移:push 式 sampling 已随 2026-07-28(SEP-2577)弃用,迁移目标是服务端直接调用 LLM 提供方 API;enforceStrictCapabilities: true可强制校验客户端能力。
  5. 可直接运行验证:pnpm --filter @mcp-examples/sampling client(2026)与加-- --legacy(2025)两条命令均以自断言方式验证采样闭环。

无论你正在实现依赖宿主模型的服务端工具,还是维护需要应答采样请求的宿主客户端,这个示例都是理解两种协议纪元下 sampling 行为差异的起点。

【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考