TypeScript SDK 的 JSON 响应模式:用responseMode: 'json'在 serverless/edge 运行时提供单次 JSON 响应
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
在 Model Context Protocol(MCP)的现代 HTTP 服务路径中,服务器默认以 SSE 流应答请求,但这在无法长期保持流打开的 serverless / edge 运行时(如按请求计费的函数平台)中并不适用。本指南基于官方 TypeScript SDK 中的json-response示例(examples/json-response/README.md),完整讲解createMcpHandler({ responseMode: 'json' })这一单次application/json响应模式:它的语义、适用场景、完整可运行的服务端与客户端代码,以及它在 SDK 源码中的底层实现原理。读完你将能自主决定何时用 JSON 模式替代 SSE 流式应答,并能在自己的 Hono / Workers 等 HTTP 入口中落地同一配置。
一、为什么需要「单次 JSON 响应」模式
MCP 的现代(2026-07-28 协议修订版)per-request HTTP 服务路径中,一次请求-响应交换的默认行为是:如果 handler 在产生结果之前发出了任何 related 消息(如 progress、logging 通知),响应会升级为 SSE 流,把中间消息和最终结果一起推送给客户端。
这个「按需升级为流」的行为对常规部署非常友好,但存在一类部署无法接受:
- Serverless / edge 运行时:函数平台的请求生命周期通常很短,且很多平台无法保持一个流长期打开(超时、网关缓冲、计费模型均不友好);
- 这类环境更希望每个请求对应一个完整的、可以立即返回的 JSON 响应体。
responseMode: 'json'正是为这类场景设计的开关:createMcpHandler收到该选项后,每个请求只返回一个application/json响应体,而不是 SSE 流。代价是调用过程中途发出的通知会被丢弃——这一点在示例文档中明确说明,handler 在构造时也会打印警告。
需要强调的是,该选项本质上是 HTTP-only 的:它塑造的是 HTTP 响应体形态,stdio 路径没有对应物,stdio 传输也不会触及该选项(见 server.ts 头部注释)。
二、三种响应模式一网打尽
responseMode是createMcpHandler的选项之一,其类型定义与完整语义位于 createMcpHandler.ts 的CreateMcpHandlerOptions中,底层类型PerRequestResponseMode定义在 perRequestTransport.ts。三种取值如下:
| 取值 | 行为 | 典型适用场景 |
|---|---|---|
'auto'(默认) | 单个 JSON body;除非handler 在结果前发出 related 消息,此时响应升级为 SSE 流 | 常规部署,兼顾简单与流式能力 |
'sse' | 总是以 SSE 流应答 | 需要确定性流式行为、且 handler 结果前可能不发出任何消息的场景 |
'json' | 永不流式;结果之前发出的 related 消息(progress、logging 等)一律丢弃,只投递终态结果 | serverless / edge 等无法保持流打开的运行时 |
两个重要的通用边界(对三种模式均成立,见选项文档):
subscriptions/listen订阅流总是通过 SSE 提供,与responseMode无关——它本质是一条持续的变更事件流,无法压缩成单次 JSON;- 预分发的校验拒绝(ladder 拒绝)始终带着映射后的 HTTP 状态码直接以 JSON 应答,不会被强制
sse模式框进 200 流(见 perRequestTransport.ts 中「ladder 拒绝在 dispatch window 内以映射状态码结算」的实现说明)。
仅作用于现代(2026-07-28)路径
responseMode只塑造现代 per-request HTTP 路径的响应体。从 package.json 的注释可以确认:
responseModeshapes the modern (2026-07-28) per-request path only; 2025-era traffic goes through the stateless legacy fallback unaffected, so a legacy leg would not exercise the option.
也就是说,未携带 per-request_metaenvelope 的 2025 时代流量仍然走无状态 legacy 回退路径(legacy: 'stateless',默认),完全不受该选项影响。
三、服务端实战:createMcpHandler+responseMode: 'json'
完整的服务端示例代码在 server.ts,核心只有几行:
import { serve } from '@hono/node-server'; import { parseExampleArgs } from '@mcp-examples/shared'; import { createMcpHonoApp } from '@modelcontextprotocol/hono'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import * as z from 'zod/v4'; function buildServer(): McpServer { const server = new McpServer({ name: 'json-response-example', version: '1.0.0' }); server.registerTool( 'greet', { description: 'A simple greeting tool', inputSchema: z.object({ name: z.string() }) }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }) ); return server; } const { port } = parseExampleArgs(); // `responseMode: 'json'` is the point of this story — applies to the modern // (2026-07-28) per-request HTTP path. const handler = createMcpHandler(buildServer, { responseMode: 'json' }); // `createMcpHonoApp()` binds the endpoint behind localhost host/origin // validation by default, matching the framework factories' defaults. const app = createMcpHonoApp(); app.all('/mcp', c => handler.fetch(c.req.raw)); serve({ fetch: app.fetch, port, hostname: '127.0.0.1' }, () => { console.error(`[server] listening on http://127.0.0.1:${port}/mcp`); });逐点拆解:
buildServer工厂:createMcpHandler接受一个McpServerFactory(createMcpHandler.ts),每个 HTTP 请求都会调用工厂生成一个全新的 server 实例(per-request serving 模型)。工厂是跨时代共用的:同一个工具定义既服务现代路径,也服务无状态 legacy 回退。responseMode: 'json':正是本文的主题配置。它被传入createMcpHandler后,最终经由invokeseam(invoke.ts)交给PerRequestHTTPServerTransport,由其决定响应体形态。构造期警告:当选项为
'json'时,createMcpHandler会在构造时打印一条console.warn(createMcpHandler.ts):responseMode: 'json' drops mid-call notifications. subscriptions/listen streams are always served over SSE regardless; other notifications emitted before a result are dropped.挂载方式:示例使用
@modelcontextprotocol/hono的createMcpHonoApp()创建 Hono 应用,再app.all('/mcp', c => handler.fetch(c.req.raw))把 fetch 形状的 handler 绑定到/mcp路由。createMcpHonoApp默认带 localhost host/origin 校验,与框架工厂默认行为一致。handler.fetch是 web-standard 面(Workers/Bun/Deno 均可直接export default),Express/Fastify/原生node:http则用toNodeHandler(handler)(来自@modelcontextprotocol/node)包装一次(见 createMcpHandler.ts 的注释说明)。
四、客户端实战:断言 JSON 响应 + 标准 Client 无缝兼容
示例的客户端(client.ts)做了两件事:先用低层 HTTP 探测断言响应确实是application/json而非text/event-stream,再用未做任何修改的标准Client验证高层兼容性:
import { check, parseExampleArgs } from '@mcp-examples/shared'; import { Client, isJsonContentType, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; const { url } = parseExampleArgs(); const client = new Client({ name: 'json-response-example-client', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } }); await client.connect(new StreamableHTTPClientTransport(new URL(url))); // Low-level: a 2026-07-28 (envelope) request should come back as plain JSON — // the JSON content-type assertion is the point of the story. const probe = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', 'mcp-protocol-version': '2026-07-28', 'mcp-method': 'tools/list' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { 'io.modelcontextprotocol/protocolVersion': '2026-07-28', 'io.modelcontextprotocol/clientInfo': { name: 'probe', version: '1.0.0' }, 'io.modelcontextprotocol/clientCapabilities': {} } } }) }); check.equal(isJsonContentType(probe.headers.get('content-type')), true); check.equal(probe.status, 200); // High-level: the regular Client works unchanged. const result = await client.callTool({ name: 'greet', arguments: { name: 'json' } }); check.equal(result.content?.[0]?.type === 'text' ? result.content[0].text : '', 'Hello, json!'); await client.close();要点:
- 低层探测请求携带现代(2026-07-28)信封的
_meta、mcp-protocol-version与mcp-method头,这是现代 per-request 路径的入口形态;响应断言Content-Type为 JSON(用isJsonContentType做媒体类型判定,而非简单字符串比较)、状态码 200——这正是「JSON 模式」的验证核心; - 高层
Client完全无感知:Client+StreamableHTTPClientTransport照常工作,client.callTool能拿到Hello, json!。也就是说,JSON 响应模式对标准 SDK 客户端是透明的,服务端切换响应形态不需要客户端配合。
五、运行示例
示例位于 pnpm workspace 内,package.json(examples/json-response/package.json)提供了两个脚本:
"scripts": { "server": "tsx server.ts", "client": "tsx client.ts" }典型运行流程(在仓库根目录安装依赖后):
- 启动服务端:
pnpm --filter @mcp-examples/json-response server(或进入 examples/json-response 目录后pnpm server),监听http://127.0.0.1:<port>/mcp; - 另开终端运行客户端:
pnpm --filter @mcp-examples/json-response client,客户端会用上文的低层探测与高层调用双重验证 JSON 响应行为。
parseExampleArgs()来自 workspace 内的 @mcp-examples/shared,负责统一解析端口/URL 参数。
六、底层原理:JSON 模式在 per-request transport 中如何实现
responseMode从选项到行为的完整链路为:
createMcpHandler (选项解析) → serveModern → invoke (invoke.ts) → PerRequestHTTPServerTransport (perRequestTransport.ts) → send() 路径决定响应形态关键实现在 perRequestTransport.ts 的send()方法(第 241-327 行)。发送方(dispatch 层/协议层)发来的每条消息会先经过分类:
- 终态响应(result 或 error):若当前没有打开 SSE 流、且模式不是
sse,则以Response.json(message, { status: 200, headers: { 'Content-Type': 'application/json' } })结算整个交换(第 310-313 行); - 与在途请求相关的非终态消息(mid-call notification 或 server-to-client request):这里就是
'json'模式的分水岭——
// A message related to the in-flight request that is not its terminal // response: a mid-call notification or a server-to-client request // emitted by the handler. if (this._responseMode === 'json') { // JSON responses cannot carry mid-call messages; they are dropped. return; }JSON 响应体没有承载中间消息的通道,因此相关消息被直接丢弃(第 319-322 行);而在'auto'模式下,同一位置会触发upgradeToSse()(第 323-325 行)——这正是「自动模式按需升级为 SSE、JSON 模式永不流式」的底层分叉点。
此外,交换的 HTTP 状态码映射也有讲究:只有 dispatch window 内产生的错误(校验 ladder、era 注册表 gate 等)才按 ladder 表映射 HTTP 状态;handler 产出的错误在 JSON 模式下保持在 HTTP 200 的 JSON-RPC error 体内(第 260-313 行的注释详尽说明了该规则)。
七、测试验证:三种模式行为被单元测试钉死
SDK 在 createMcpHandler.test.ts 中用三个用例钉死了responseMode的行为:
- 默认惰性升级:handler 在结果前发出 related 通知时,响应升级为 SSE 流(
Content-Type含text/event-stream,body 含notifications/progress); 'json'永不流式且丢弃 mid-call 通知:对同一个「先发 progress 再返回结果」的工具调用,responseMode: 'json'下响应Content-Type为application/json、状态 200、body不含notifications/progress但包含最终结果文本——这正是 client.ts 断言逻辑的测试侧印证;'sse'即使 handler 结果前不发任何消息也强制流式:Content-Type为text/event-stream。
如果你的 handler 会在结果前发送 progress/logging 等通知,且这些通知对调用方有业务价值,请避免使用'json'模式,或选择'auto'/'sse';反之,若你的工具是「单次入参、单次出参、无需中间反馈」的形态,且部署在无法保持流的 serverless/edge 运行时,responseMode: 'json'就是官方 SDK 提供的标准解法。
八、总结
responseMode: 'json'让现代(2026-07-28)per-request HTTP 路径每个请求只返回一个application/json响应体,是 serverless / edge 运行时无法保持 SSE 流打开时的官方应对方案;- 代价明确:结果之前发出的相关消息(progress、logging 等)被丢弃,仅投递终态结果;
subscriptions/listen订阅流不受影响、始终走 SSE; - HTTP-only:无 stdio 等价物,且只影响现代路径,2025 时代流量走无状态 legacy 回退不受影响;
- 配置极简:
createMcpHandler(buildServer, { responseMode: 'json' }),标准 SDKClient无需任何改动即可兼容; - 三种模式(
auto/sse/json)的取舍、底层send()分叉实现与单元测试均已在上文给出仓库内源码位置,可自行深入研读。
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考