MCP TypeScript SDK Wire Schemas 实战:用 @modelcontextprotocol/core 直接校验 JSON 协议与 OAuth/OpenID 负载
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
导读
在 Model Context Protocol(MCP)开发中,网关、代理、测试夹具以及无状态 worker 集群这类代码路径上,常常没有Client或Server帮你把 JSON 自动解析成类型化对象——你手里只有原始字符串。本文基于官方 TypeScript SDK 的@modelcontextprotocol/core包,讲解如何用 SDK 内部同源的 Zod 校验常量(wire schemas)直接对协议帧、工具调用结果、OAuth 授权服务器元数据做解析与验证。读完你会掌握:*Schema常量的命名规律与使用场景、如何用JSONRPCMessageSchema校验未解码的 JSON-RPC 信封、如何在代理中按method路由原始请求,以及为什么类型、守卫函数和错误类要改从@modelcontextprotocol/server/@modelcontextprotocol/client导入。
什么是 wire schemas
MCP TypeScript SDK 内部对协议负载(protocol payload)和 OAuth/OpenID 负载的校验,全部建立在一组 Zod 常量之上。这组常量被集中导出为@modelcontextprotocol/core包——它是“Model Context Protocol 规范 + OAuth/OpenID Zod schemas 的公开官方入口”(见 packages/core/src/index.ts 的头部注释)。
换句话说:SDK 自己在运行时校验什么 schema,@modelcontextprotocol/core就导出什么 schema。当你持有的是未经 SDK 对象包装的原始 JSON(例如上游服务器直接返回的tools/call响应体),就可以用同一个 schema 常量做.parse()或.safeParse(),保证你的校验结果与 SDK 内部的判断完全一致。
该包导出且仅导出Zod 值,没有任何别的东西。导出面严格分成两组(与 SDK 自身的 spec-vs-auth 拆分一一对应):
- SPEC 组:来自 packages/core/src/schemas.ts 的每一个
export const *Schema(内部辅助常量除外),例如CallToolResultSchema、ListToolsResultSchema、InitializeRequestSchema、JSONRPCMessageSchema; - OAUTH/OPENID 组:来自 packages/core/src/auth.ts 的
OAuthMetadataSchema、OAuthTokensSchema、OAuthProtectedResourceMetadataSchema、OpenIdProviderDiscoveryMetadataSchema、IdJagTokenExchangeResponseSchema等 12 个 auth schema。
源码层面有一个“漂移防护”测试保证这两组与源头模块严格同步:packages/core/test/coreSchemas.test.ts 直接读取src/schemas.ts和core-internal/src/types/specTypeSchema.ts中的authSchemas注册表,断言Object.keys(core)与两者并集双向完全相等——新增一个 spec/auth schema 若忘记在 core 中再导出,测试就会失败;内部辅助常量若泄漏到公开面,同样失败。测试同时断言 spec 组不少于 154 个、auth 组恰好 12 个。
安装与包定位
@modelcontextprotocol/core的运行时中立性是其设计核心:
- 唯一依赖是
zod(见 packages/core/package.json 的dependencies字段,"zod": "catalog:runtimeShared"); - 要求 Node.js >= 20;
type: "module",但通过exports同时提供 ESM(dist/index.mjs)与 CommonJS(dist/index.cjs)构建,import和require均可直接解析; - 包内另有
./internal子路径导出,用于内部消费,普通用户从根路径导入即可。
安装命令:
npm install @modelcontextprotocol/core有一个常见疑问需要澄清:我如果已经在用@modelcontextprotocol/server或@modelcontextprotocol/client,还需要单独装 core 吗?
不需要。server和client在公开 API 面上刻意保持“无 Zod”(Zod-free),但它们在运行时解析的共享 schema 图正是从这个包解析出来的——也就是说,core已经作为它们的传递依赖出现在你的依赖树里。只有当你直接importcore 里的常量时,才需要把它加进自己的dependencies。
校验一条线上负载:CallToolResultSchema 实战
最典型的场景:你的网关把客户端发来的tools/call转发给上游服务器,上游返回的响应体是一段未知的 JSON。转发回客户端之前,先验证它是否符合协议。
import { CallToolResultSchema } from '@modelcontextprotocol/core'; // The body an upstream server returned for a tools/call you forwarded. const body: unknown = JSON.parse('{"content":[{"type":"text","text":"Travel mug"}]}'); const parsed = CallToolResultSchema.safeParse(body); if (!parsed.success) { throw new Error(`upstream returned an invalid tools/call result: ${parsed.error.message}`); } console.log(parsed.data.content);这里选用safeParse(而非parse)是刻意的:校验失败时它返回失败结果而不是抛异常,方便网关把错误包装成自己的错误响应。校验成功后,parsed.data是完整类型化的结果:
[ { type: 'text', text: 'Travel mug' } ]CallToolResultSchema在源码中的定义(packages/core/src/schemas.ts#L1401-L1440)值得细读,它揭示了几个容易踩坑的兼容细节:
content是z.array(ContentBlockSchema).default([])—— 线上协议要求该字段存在,但解析是容错的:上游若省略content(与 v1 时代已部署服务器省略structuredContent的行为一致),会默认成空数组而不是报错;structuredContent是z.unknown().optional()—— 自 SEP-2106 起允许任意 JSON 值(数组、原始值、null),取属性前需要先做收窄;而 2025-11-25 旧线协议版本仍保留 object-only 约束(冻结在wire/rev2025-11-25/schemas.ts中),本公开 schema 是放宽后的中性版本;isError为可选布尔,缺省视为false——协议规定工具自身的错误应放进结果体并置isError: true,而不是抛协议级错误响应,否则 LLM 看不到错误无法自纠。
畸形输入时 safeParse 返回什么
把同一个 schema 交给畸形 body,safeParse返回失败结果而不是抛异常:
const malformed = CallToolResultSchema.safeParse({ content: 'Travel mug' }); console.log(malformed.error?.issues);错误会精确点名违约字段:
[ { expected: 'array', code: 'invalid_type', path: [ 'content' ], message: 'Invalid input: expected array, received string' } ]path、code、expected、message这些 Zod issue 字段可以直接用于生成对调用方友好的错误提示。上述示例代码并非纸面推演——它们来自仓库中可运行、带自校验的配套示例 examples/guides/advanced/wire-schemas.examples.ts,该文件末尾的自检逻辑会在“畸形content竟然解析成功”等前提被破坏时抛异常(非零退出),保证文档引用的输出与真实运行结果永远一致。可以用以下命令亲自跑一遍:
pnpm --filter @modelcontextprotocol/examples typecheck npx tsx guides/advanced/wire-schemas.examples.ts # 在 examples/ 目录下执行v1 用户迁移提示
如果你从 v1 升级:这些*Schema常量正是 v1 时代@modelcontextprotocol/sdk/types.js导出的那些 schema(spec 组),OAuth/OpenID 组则对应 v1 的@modelcontextprotocol/sdk/shared/auth.js。v2 的迁移只是 import 路径的替换,.parse()/.safeParse()调用方式完全不变:
// v1 import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; // v2 —— 同一个 Zod schema,新包 import { CallToolResultSchema } from '@modelcontextprotocol/core';官方 codemod(npx @modelcontextprotocol/codemod@latest v1-to-v2 .)会自动重写这类导入路径,详见 升级指南 v1 → v2(特别是其中 “Zod*Schemaconstants moved to@modelcontextprotocol/core” 一节)。同时注意:@modelcontextprotocol/client和@modelcontextprotocol/server都不再重新导出这些 schema 常量,v2 中两者都保持公开面无 Zod。
你需要这个包吗:先判断你的代码形态
不是所有 MCP 代码都需要 wire schemas。官方文档给出的判断标准非常清晰:
- 如果你用
McpServer或Client构建应用,跳过这个包:服务端 handler 收到的工具调用(服务端工具)已经被校验过,客户端发起的工具调用(客户端调用)返回的也是类型化结果——SDK 已经把 JSON 和类型之间的转换做完了,你不需要看到原始字节。 - 只有当你和 JSON 之间没有任何东西挡着时,才需要
@modelcontextprotocol/core。典型的适用者包括:- 网关 / 代理(gateway / proxy):转发原始 JSON-RPC 帧,需要在校验后决定放行还是拦截;
- 测试夹具(test harness):构造或断言线上格式的报文;
- worker 集群(worker fleets):无状态实例之间传递裸 JSON(参见 网关与 worker 集群)。
一句话总结这个包的定位:它是写给“手持原始 JSON”的代码的,而不是写给Client/Server用户的。
挑选与你手中报文匹配的 schema
规范中每一个具名类型都有一个同名常量,命名规律是<SpecType>Schema:
- 请求:
CallToolRequestSchema、ListToolsRequestSchema、InitializeRequestSchema、GetPromptRequestSchema…… - 结果:
ListToolsResultSchema、ReadResourceResultSchema、CompleteResultSchema…… - 通知:
ProgressNotificationSchema、ResourceUpdatedNotificationSchema、LoggingMessageNotificationSchema…… - 参数:当你手里只有
params对象时,用*ParamsSchema形态,如CallToolRequestParamsSchema、SubscribeRequestParamsSchema。
拿不准是哪种消息:先校验未解码的信封
当你还不知道手里这条消息是哪一种时,用JSONRPCMessageSchema校验完整的、未解码的 JSON-RPC 信封:
import { JSONRPCMessageSchema } from '@modelcontextprotocol/core'; const frame = '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"mug"}}}'; const message = JSONRPCMessageSchema.parse(JSON.parse(frame));message的类型会被收窄到四种 JSON-RPC 形态之一:请求(request)、通知(notification)、成功响应(result response)、错误响应(error response);非法帧会抛出ZodError。
从源码看(packages/core/src/schemas.ts#L136-L196),JSONRPCMessageSchema只是这四者的并集:
export const JSONRPCMessageSchema = z.union([ JSONRPCRequestSchema, JSONRPCNotificationSchema, JSONRPCResultResponseSchema, JSONRPCErrorResponseSchema ]);而这四个成员 schema 都使用.strict()构建,且jsonrpc字段被z.literal(JSONRPC_VERSION)锁定为协议版本号字面量——多余字段、错误版本号都会被拒绝。JSONRPCErrorResponseSchema的error对象要求整数code、字符串message,data为可选 unknown;JSONRPCRequestSchema的id使用RequestIdSchema(string 或 int)。顺带一提,JSONRPCResponseSchema(成功 + 错误响应的并集)也已导出,适用于明确知道是“响应”的场景。
在代理中路由原始 JSON-RPC
拿到手头场景最常见的组合拳是:信封只解析一次,按method分支,再用每个方法对应的请求 schema 做细粒度校验,最后转发。
import { CallToolRequestSchema } from '@modelcontextprotocol/core'; if ('method' in message) { switch (message.method) { case 'tools/call': { const call = CallToolRequestSchema.parse(message); console.log(`forward tools/call for ${call.params.name} upstream`); break; } default: console.log(`forward ${message.method} unchanged`); } }这里的两个关键点:
- 先用
'method' in message做判别收窄,把通知/响应排除在分支之外; - 对
tools/call分支再用CallToolRequestSchema做严格校验——call.params.name是类型化的string,call.params.arguments也有完整类型。整条路径上没有任何Client或Server实例:
forward tools/call for search upstreamCallToolRequestParamsSchema(packages/core/src/schemas.ts#L1454 起)继承自TaskAugmentedRequestParamsSchema并追加name: z.string()与arguments字段——这解释了为什么call.params.name能直接作为字符串使用。
边界提醒:wire schemas 只负责“这帧 JSON 是否合法”。校验之外的一切——会话管理、能力协商(capability negotiation)、请求关联(request correlation)——需要状态与协议状态机,那正是 低层服务器(low-level server) 的领域。代理若需要这些能力,应当基于 SDK 构建而不是手写。
校验 OAuth 与发现元数据
core 的第二组导出覆盖 OAuth 与 OpenID 发现元数据,命名约定与 spec 组完全一致。最常用的是OAuthMetadataSchema——校验授权服务器(authorization server)的元数据文档(RFC 8414):
import { OAuthMetadataSchema } from '@modelcontextprotocol/core'; // In production this body comes from GET <issuer>/.well-known/oauth-authorization-server. const response = new Response( JSON.stringify({ issuer: 'https://auth.example.com', authorization_endpoint: 'https://auth.example.com/authorize', token_endpoint: 'https://auth.example.com/token', response_types_supported: ['code'] }) ); const metadata = OAuthMetadataSchema.parse(await response.json()); console.log(metadata.token_endpoint);缺失必需端点(如token_endpoint)的文档解析失败;合法的文档返回类型化对象:
https://auth.example.com/token从 packages/core/src/auth.ts 的源码看,OAuthMetadataSchema的必需字段是issuer、authorization_endpoint、token_endpoint、response_types_supported,其余(registration_endpoint、scopes_supported、grant_types_supported、code_challenge_methods_supported、dpop_signing_alg_values_supported等)全部可选。值得注意的细节:
- 端点和 URI 字段用的是内部
SafeUrlSchema——它在z.url()基础上额外拒绝javascript:、data:、vbscript:协议(见 packages/core/src/auth.ts#L6-L25),这是 SDK 对 SSRF/注入类攻击的面包防御; authorization_response_iss_parameter_supported这类布尔字段使用了.optional().catch(undefined),即使上游返回非布尔值也不会让整个文档解析崩溃(容错设计)。
同组还有:
OAuthTokensSchema—— OAuth 2.1 token 响应(access_token、token_type必填;expires_in用z.coerce.number()宽容处理字符串化数字;refresh_token、scope、id_token可选;.strip()剔除多余字段);OAuthProtectedResourceMetadataSchema—— 受保护资源元数据(RFC 9728),resource必填;OpenIdProviderDiscoveryMetadataSchema—— OpenID Provider 发现元数据,从源码看它是OpenIdProviderMetadataSchema与OAuthMetadataSchema.pick({ code_challenge_methods_supported: true })的合并——这是对现实世界中 OIDC 供应商“混排 OIDC 与 OAuth 字段”的针对性兼容;- 此外还有
IdJagTokenExchangeResponseSchema(RFC 8693 ID-JAG token 交换响应,issued_token_type锁定为urn:ietf:params:oauth:token-type:id-jag)、OAuthClientMetadataSchema(RFC 7591 动态客户端注册)、OAuthErrorResponseSchema、OAuthClientInformationSchema等。
类型、守卫与错误类从 SDK 包导入
@modelcontextprotocol/core只导出 Zod 值,没有别的。以下三类东西是@modelcontextprotocol/server和@modelcontextprotocol/client的公开 API,按你已有的依赖包从它们导入:
- 规范 TypeScript 类型:如
CallToolResult、ListToolsResult; isJSONRPCRequest风格的守卫函数;- 错误类。
import type { CallToolResult } from '@modelcontextprotocol/client'; import * as z from 'zod/v4'; // The SDK's spec type and the schema's own inferred output describe the same value. const relayed: CallToolResult = parsed.data; type CallToolResultFromCore = z.infer<typeof CallToolResultSchema>;这个赋值能通过类型检查并非巧合:core schema 解析出来的东西,正是 SDK 包所类型化的东西——同一份 schema 图,两种消费方式。只依赖 core 的第三方包也可以用z.infer派生同样的类型,从而与 SDK 的类型定义保持同步,不需要引入 server/client。
zod/v4子路径的引入方式是仓库内的既有惯例(core 自身的 src/schemas.ts 第一行就是import * as z from 'zod/v4'),z.infer<typeof CallToolResultSchema>即标准用法。
免 Zod 依赖的替代方案:isSpecType 守卫
如果你的项目想完全避免引入 Zod 依赖,只想检查一个值的形状,使用@modelcontextprotocol/client和@modelcontextprotocol/server导出的isSpecType守卫:
import { isSpecType } from '@modelcontextprotocol/client'; isSpecType.CallToolResult(value);从 v1→v2 迁移指南(docs/migration/upgrade-to-v2.md)可知,isSpecType与specTypeSchemas均按SpecTypeName(MCP 规范中所有具名类型的字面量联合)索引,所以有完整自动补全、拼错立即编译报错。specTypeSchemas.X实现的是同步的StandardSchemaV1Sync接口:validate()返回{ value }或{ issues },永不抛异常——与.parse()的抛异常行为不同,适合对未知数据做无副作用的形状检查。
小结
@modelcontextprotocol/core导出的是SDK 自己使用的规范与 OAuth/OpenID Zod schemas,除此之外什么都没有;- 它的目标受众是手持原始 JSON的代码——网关、代理、测试夹具、worker 集群——而不是
Client/Server用户; - 每个规范具名类型都有
<Name>Schema常量;拿不准类型时用JSONRPCMessageSchema校验未解码的信封(它会在请求、通知、成功响应、错误响应四种形态间收窄); - 类型、守卫函数和错误类不在 core 里,从
@modelcontextprotocol/server或@modelcontextprotocol/client导入; - 该包运行时中立,
zod是它唯一的依赖,并且会作为 server/client 的传递依赖自动进入你的依赖树。
进一步阅读:升级到 v2 指南(*Schema导入路径迁移)、网关与 worker 集群(wire schemas 的主要应用场景)、低层服务器(校验之外的状态与协商能力)、服务端工具 与 客户端调用(使用高层 API 时为何不需要本包)。
【免费下载链接】typescript-sdkThe official TypeScript SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-sdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考