MCP 协议设计与实现
第8章 TypeScript Server 实现剖析
第8章 TypeScript Server 实现剖析
在前面的章节中,我们已经理解了 MCP 协议的整体架构与传输层机制。从本章开始,我们将深入 MCP TypeScript SDK 的服务端实现,逐行剖析源码中最核心的设计决策。MCP 的服务端是整个生态系统的基石——它决定了开发者如何定义工具、暴露资源、管理提示词,以及如何将这些能力安全地交付给 AI 客户端。
SDK 的服务端采用了一个精妙的双层架构:底层的 Server 类负责协议级别的通信细节,上层的 McpServer 类则提供开发者友好的注册式 API。这种分层设计既保证了协议实现的严谨性,又让日常开发变得简洁直观。本章将从这个架构出发,逐步揭示工具注册、请求分发、输入输出校验、自动补全、中间件防护以及多框架集成的完整实现链路。
8.1 双层服务端架构:设计哲学与职责划分
MCP TypeScript SDK 的服务端由两个核心类构成,分别位于不同的抽象层次。理解它们的职责边界,是理解整个服务端实现的前提。
graph TB
subgraph "开发者视角"
Dev[应用代码]
Dev -->|"server.registerTool()"| McpServer
Dev -->|"server.registerResource()"| McpServer
Dev -->|"server.registerPrompt()"| McpServer
Dev -->|"server.connect(transport)"| McpServer
end
subgraph "McpServer 层 (mcp.ts)"
McpServer["McpServer<br/>高层 API 封装"]
McpServer -->|"内部持有"| RegTools["_registeredTools<br/>工具注册表"]
McpServer -->|"内部持有"| RegRes["_registeredResources<br/>资源注册表"]
McpServer -->|"内部持有"| RegPrompts["_registeredPrompts<br/>提示词注册表"]
end
subgraph "Server 层 (server.ts)"
Server["Server extends Protocol<br/>协议级实现"]
Server -->|"管理"| ReqHandlers["请求处理器映射<br/>_requestHandlers"]
Server -->|"管理"| NotifHandlers["通知处理器映射<br/>_notificationHandlers"]
Server -->|"管理"| Capabilities["能力声明<br/>_capabilities"]
end
McpServer -->|"this.server"| Server
Server -->|"connect()"| Transport["Transport<br/>传输层"]
style McpServer fill:#e1f5fe
style Server fill:#fff3e0
8.1.1 Server 类:协议层的忠实执行者
Server 类定义在 packages/server/src/server/server.ts(截至 2026-04-22 main 共 714 行),它继承自 Protocol<ServerContext> 基类。Protocol 是 MCP SDK 中客户端和服务端共享的通信基础设施,提供了 JSON-RPC 消息的收发、请求-响应匹配、超时管理、取消通知等底层能力。
Server 在 Protocol 之上增加了服务端特有的职责:
// server.ts 第99-141行
export class Server extends Protocol<ServerContext> {
private _clientCapabilities?: ClientCapabilities;
private _clientVersion?: Implementation;
private _capabilities: ServerCapabilities;
private _instructions?: string;
private _jsonSchemaValidator: jsonSchemaValidator;
constructor(
private _serverInfo: Implementation,
options?: ServerOptions
) {
super({
...options,
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
});
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
this._instructions = options?.instructions;
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
// 自动处理初始化握手
this.setRequestHandler('initialize', request => this._oninitialize(request));
this.setNotificationHandler('notifications/initialized', () => this.oninitialized?.());
if (this._capabilities.logging) {
this._registerLoggingHandler();
}
}
}
Server 类的核心职责包括:
- 初始化握手:自动处理
initialize请求,协商协议版本,交换能力声明 - 能力管理:通过
registerCapabilities()动态注册服务端能力,并在assertRequestHandlerCapability()中校验能力与请求方法的匹配关系 - 请求校验:覆写
setRequestHandler()方法,对tools/call请求施加额外的 Schema 验证(第225-272行) - 通知权限检查:在
assertNotificationCapability()中确保服务端只发送已声明能力范围内的通知 - 客户端交互:提供
createMessage()(采样请求)、elicitInput()(用户输入获取)、listRoots()等与客户端交互的方法
特别值得注意的是 Server 对 tools/call 的特殊处理。在 setRequestHandler 的覆写中(第225-272行),当 method 为 tools/call 时,SDK 会用一个包装函数替换原始 handler,对请求参数和返回结果分别做 Schema 校验:
// server.ts 第225-268行
public override setRequestHandler<M extends RequestMethod>(
method: M,
handler: (request: RequestTypeMap[M], ctx: ServerContext) => ResultTypeMap[M] | Promise<ResultTypeMap[M]>
): void {
if (method === 'tools/call') {
const wrappedHandler = async (request, ctx): Promise<ServerResult> => {
const validatedRequest = parseSchema(CallToolRequestSchema, request);
if (!validatedRequest.success) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Invalid tools/call request: ${errorMessage}`);
}
const result = await Promise.resolve(handler(request, ctx));
// 对非任务请求,验证返回值符合 CallToolResultSchema
const validationResult = parseSchema(CallToolResultSchema, result);
if (!validationResult.success) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Invalid tools/call result: ${errorMessage}`);
}
return validationResult.data;
};
return super.setRequestHandler(method, wrappedHandler);
}
return super.setRequestHandler(method, handler);
}
这层防线确保了即使开发者直接使用底层 Server 类注册工具处理器,协议层面的数据完整性也能得到保障。
8.1.2 McpServer 类:开发者友好的门面
McpServer 类定义在 packages/server/src/server/mcp.ts(截至 2026-04-22 main 共 1095 行),它并不继承 Server,而是组合了一个 Server 实例:
// mcp.ts 第61-77行
export class McpServer {
public readonly server: Server;
private _registeredResources: { [uri: string]: RegisteredResource } = {};
private _registeredResourceTemplates: { [name: string]: RegisteredResourceTemplate } = {};
private _registeredTools: { [name: string]: RegisteredTool } = {};
private _registeredPrompts: { [name: string]: RegisteredPrompt } = {};
constructor(serverInfo: Implementation, options?: ServerOptions) {
this.server = new Server(serverInfo, options);
}
}
组合优于继承——这个设计选择使得 McpServer 能够完全控制暴露给开发者的 API 表面,而不必担心 Server 或 Protocol 的内部方法泄漏到公开接口中。同时,开发者仍然可以通过 server.server 属性访问底层 Server 实例,用于发送通知或设置自定义请求处理器等高级操作。
connect() 和 close() 方法直接委托给内部 Server 实例,传输层的绑定完全透明:
// mcp.ts 第107-116行
async connect(transport: Transport): Promise<void> {
return await this.server.connect(transport);
}
async close(): Promise<void> {
await this.server.close();
}
8.2 工具注册的完整链路
工具是 MCP 服务端最重要的能力之一。让我们追踪一次 registerTool() 调用的完整执行路径,从开发者的 API 调用一直到请求被处理并返回结果。
sequenceDiagram
participant Dev as 开发者代码
participant McpS as McpServer
participant RegT as _registeredTools
participant S as Server (Protocol)
participant Client as MCP 客户端
Dev->>McpS: registerTool("calc", config, callback)
McpS->>McpS: 检查名称冲突
McpS->>McpS: _createRegisteredTool()
McpS->>McpS: validateAndWarnToolName("calc")
McpS->>McpS: createToolExecutor(inputSchema, handler)
McpS->>RegT: 存入 _registeredTools["calc"]
McpS->>McpS: setToolRequestHandlers() [首次调用]
McpS->>S: registerCapabilities({ tools: ... })
McpS->>S: setRequestHandler("tools/list", ...)
McpS->>S: setRequestHandler("tools/call", ...)
McpS->>S: sendToolListChanged()
Note over Client,S: 客户端发起工具调用
Client->>S: tools/call { name: "calc", arguments: {...} }
S->>McpS: 调用已注册的 tools/call handler
McpS->>RegT: 查找 _registeredTools["calc"]
McpS->>McpS: validateToolInput(tool, args)
McpS->>McpS: executeToolHandler(tool, args, ctx)
McpS->>McpS: validateToolOutput(tool, result)
McpS-->>Client: CallToolResult
8.2.1 注册阶段:从 API 到存储
registerTool() 方法(第865-894行)接收三个参数:工具名称、配置对象和回调函数。它首先检查名称是否已被占用,然后委托给内部的 _createRegisteredTool() 方法:
// mcp.ts 第865-894行
registerTool<OutputArgs extends StandardSchemaWithJSON,
InputArgs extends StandardSchemaWithJSON | undefined = undefined>(
name: string,
config: {
title?: string;
description?: string;
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
annotations?: ToolAnnotations;
_meta?: Record<string, unknown>;
},
cb: ToolCallback<InputArgs>
): RegisteredTool {
if (this._registeredTools[name]) {
throw new Error(`Tool ${name} is already registered`);
}
return this._createRegisteredTool(
name, title, description, inputSchema, outputSchema,
annotations, { taskSupport: 'forbidden' }, _meta, cb
);
}
_createRegisteredTool() 方法(第767-837行)执行以下关键步骤:
- 名称验证:调用
validateAndWarnToolName(name)检查工具名是否符合规范 - 创建执行器:调用
createToolExecutor(inputSchema, handler)将原始回调包装为统一的执行器接口 - 构建注册对象:创建
RegisteredTool对象,包含enable()、disable()、remove()、update()等生命周期方法 - 触发首次初始化:调用
setToolRequestHandlers()注册协议级处理器(仅首次执行) - 发送变更通知:调用
sendToolListChanged()通知已连接的客户端
createToolExecutor 函数(第1125-1154行)是一个精巧的适配层,它解决了”有参数”和”无参数”两种工具回调签名的统一调用问题:
// mcp.ts 第1125-1154行
function createToolExecutor(
inputSchema: StandardSchemaWithJSON | undefined,
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
): ToolExecutor {
const isTaskHandler = 'createTask' in handler;
if (isTaskHandler) {
// 任务型工具的执行路径
const taskHandler = handler as TaskHandlerInternal;
return async (args, ctx) => {
if (inputSchema) {
return taskHandler.createTask(args, taskCtx);
}
return (taskHandler.createTask as (ctx: CreateTaskServerContext) => ...)(taskCtx);
};
}
if (inputSchema) {
// 有参数的普通工具: callback(args, ctx)
const callback = handler as ToolCallbackInternal;
return async (args, ctx) => callback(args, ctx);
}
// 无参数的普通工具: callback(ctx)
const callback = handler as (ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
return async (_args, ctx) => callback(ctx);
}
8.2.2 调用阶段:校验、执行、再校验
当客户端发送 tools/call 请求时,处理流程经过三道关卡。
第一关:输入校验。validateToolInput() 方法(第240-261行)使用 Standard Schema 协议验证传入参数:
// mcp.ts 第240-261行
private async validateToolInput(tool, args, toolName) {
if (!tool.inputSchema) {
return undefined; // 无 schema 则跳过验证
}
const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
if (!parseResult.success) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`);
}
return parseResult.data;
}
这里使用的是 Standard Schema 协议(而非直接绑定 Zod),这意味着开发者可以使用 Zod、Valibot、ArkType 等任何兼容 Standard Schema 的库来定义输入约束。
第二关:执行处理器。executeToolHandler() 方法(第300-303行)通过之前创建的 executor 调用实际回调:
private async executeToolHandler(tool, args, ctx) {
return tool.executor(args, ctx);
}
第三关:输出校验。validateToolOutput() 方法(第266-295行)验证返回结果是否符合 outputSchema:
// mcp.ts 第266-295行
private async validateToolOutput(tool, result, toolName) {
if (!tool.outputSchema) return;
if (!('content' in result)) return; // CreateTaskResult 不校验
if (result.isError) return; // 错误结果不校验
if (!result.structuredContent) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Output validation error: Tool ${toolName} has an output schema but no structured content`);
}
const parseResult = await validateStandardSchema(tool.outputSchema, result.structuredContent);
if (!parseResult.success) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Output validation error: Invalid structured content for tool ${toolName}: ${parseResult.error}`);
}
}
异常兜底。整个 tools/call 处理器被 try-catch 包裹(第170-213行),除了 UrlElicitationRequired 这一特殊错误外,所有异常都会被转换为带 isError: true 标记的 CallToolResult,而不是让 JSON-RPC 层面的错误响应直接暴露给客户端:
// mcp.ts 第225-234行
private createToolError(errorMessage: string): CallToolResult {
return {
content: [{ type: 'text', text: errorMessage }],
isError: true
};
}
8.2.3 动态管理:enable/disable/update/remove
每个注册对象都内建了完整的生命周期管理能力。以 RegisteredTool 为例(第798-830行),update() 方法支持动态修改名称、描述、Schema、回调等任何属性,修改后自动重建 executor 并通知客户端:
update: updates => {
if (updates.name !== undefined && updates.name !== name) {
validateAndWarnToolName(updates.name);
delete this._registeredTools[name];
if (updates.name) this._registeredTools[updates.name] = registeredTool;
}
// ...更新各属性...
if (needsExecutorRegen) {
registeredTool.executor = createToolExecutor(registeredTool.inputSchema, currentHandler);
}
this.sendToolListChanged(); // 通知客户端重新获取工具列表
}
remove() 实际上是 update({ name: null }) 的快捷方式——将 name 设为 null 会从注册表中删除该工具,但不会销毁对象本身,这允许后续通过 update({ name: 'newName' }) 重新注入。
8.3 资源与提示词的注册机制
资源和提示词的注册遵循与工具类似的模式,但各有其独特的设计考量。
8.3.1 资源注册:静态 URI vs 动态模板
registerResource() 方法(第576-621行)通过方法重载区分两种资源类型:
- 静态资源:传入字符串 URI,直接存入
_registeredResources字典 - 动态资源模板:传入
ResourceTemplate对象,存入_registeredResourceTemplates字典
当客户端发送 resources/read 请求时(第481-502行),处理器先精确匹配静态资源,未命中则遍历所有模板尝试 URI 模式匹配:
// mcp.ts 第481-502行
this.server.setRequestHandler('resources/read', async (request, ctx) => {
const uri = new URL(request.params.uri);
// 精确匹配
const resource = this._registeredResources[uri.toString()];
if (resource) {
if (!resource.enabled) throw new ProtocolError(...);
return resource.readCallback(uri, ctx);
}
// 模板匹配
for (const template of Object.values(this._registeredResourceTemplates)) {
const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
if (variables) {
return template.readCallback(uri, variables, ctx);
}
}
throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`);
});
ResourceTemplate 类(第1021-1063行)封装了 URI 模板和两种可选回调:list(枚举所有匹配资源)和 complete(URI 变量自动补全)。list 回调被显式要求必须声明(即便为 undefined),这是为了避免开发者意外遗漏资源枚举功能。
8.3.2 提示词注册:Schema 驱动的参数处理
提示词注册的特殊之处在于 createPromptHandler() 函数(第1265-1287行),它创建了一个闭包来封装 Schema 解析和回调调用:
// mcp.ts 第1265-1287行
function createPromptHandler(name, argsSchema, callback): PromptHandler {
if (argsSchema) {
const typedCallback = callback as (args, ctx) => GetPromptResult | Promise<GetPromptResult>;
return async (args, ctx) => {
const parseResult = await validateStandardSchema(argsSchema, args);
if (!parseResult.success) {
throw new ProtocolError(ProtocolErrorCode.InvalidParams,
`Invalid arguments for prompt ${name}: ${parseResult.error}`);
}
return typedCallback(parseResult.data, ctx);
};
} else {
const typedCallback = callback as (ctx) => GetPromptResult | Promise<GetPromptResult>;
return async (_args, ctx) => typedCallback(ctx);
}
}
这个设计与工具的 createToolExecutor 异曲同工:通过闭包捕获类型信息,避免了在调用点进行运行时类型断言。
8.4 自动补全机制 (completable.ts)
自动补全是 MCP 协议中一个提升用户体验的精巧功能。它允许客户端在用户输入提示词参数或资源 URI 变量时,实时获取补全建议。整个机制的实现集中在 completable.ts(75行)和 McpServer 的补全处理器中。
8.4.1 Completable Schema:用 Symbol 标记可补全字段
completable() 函数(第51-59行)使用 Symbol 属性为 Schema 附加补全元数据,这是一种零侵入的标记方式:
// completable.ts 第3-58行
export const COMPLETABLE_SYMBOL: unique symbol = Symbol.for('mcp.completable');
export function completable<T extends StandardSchemaWithJSON>(
schema: T,
complete: CompleteCallback<T>
): CompletableSchema<T> {
Object.defineProperty(schema as object, COMPLETABLE_SYMBOL, {
value: { complete } as CompletableMeta<T>,
enumerable: false, // 不出现在 JSON 序列化中
writable: false, // 不可修改
configurable: false // 不可删除
});
return schema as CompletableSchema<T>;
}
使用 Symbol.for('mcp.completable') 而非普通 Symbol 是有意为之——Symbol.for 创建的是全局 Symbol,即使跨模块引用也能保持同一性,这对于 Schema 对象可能在不同包之间传递的场景至关重要。
属性被设置为 enumerable: false,这确保了补全元数据不会干扰 Schema 的正常序列化(例如转换为 JSON Schema 时)。
8.4.2 补全请求的处理流程
当 McpServer 检测到注册的提示词或资源模板包含可补全字段时,会自动调用 setCompletionRequestHandler() 启用 completion/complete 请求处理器。处理器根据引用类型分发到不同的处理逻辑:
// mcp.ts 第352-371行
this.server.setRequestHandler('completion/complete', async (request) => {
switch (request.params.ref.type) {
case 'ref/prompt':
return this.handlePromptCompletion(request, request.params.ref);
case 'ref/resource':
return this.handleResourceCompletion(request, request.params.ref);
default:
throw new ProtocolError(ProtocolErrorCode.InvalidParams, ...);
}
});
提示词补全的处理过程(第373-399行)特别值得关注:它需要从 Zod schema 的 shape 中提取出指定参数名对应的字段,解包可能的 Optional 包装,然后检查该字段是否被 completable() 标记过:
// mcp.ts 第373-399行
private async handlePromptCompletion(request, ref) {
const prompt = this._registeredPrompts[ref.name];
if (!prompt?.enabled || !prompt.argsSchema) return EMPTY_COMPLETION_RESULT;
const promptShape = getSchemaShape(prompt.argsSchema);
const field = unwrapOptionalSchema(promptShape?.[request.params.argument.name]);
if (!isCompletable(field)) return EMPTY_COMPLETION_RESULT;
const completer = getCompleter(field);
if (!completer) return EMPTY_COMPLETION_RESULT;
const suggestions = await completer(request.params.argument.value, request.params.context);
return createCompletionResult(suggestions);
}
补全结果最多返回 100 个建议(第1289-1298行),超出部分通过 hasMore: true 标记告知客户端存在更多结果:
function createCompletionResult(suggestions: readonly unknown[]): CompleteResult {
const values = suggestions.map(String).slice(0, 100);
return {
completion: {
values,
total: suggestions.length,
hasMore: suggestions.length > 100
}
};
}
8.5 传输层绑定:从 McpServer 到网络
McpServer.connect(transport) 的调用链最终抵达 Protocol 基类的 connect() 方法(protocol.ts 第455行)。这个方法执行三个关键操作:
- 接管传输层回调:替换 transport 的
onclose、onerror、onmessage回调 - 消息路由:根据消息类型(请求/响应/通知)分发到对应的处理器
- 启动传输:调用
transport.start()开始接收消息
// protocol.ts 第455-485行
async connect(transport: Transport): Promise<void> {
this._transport = transport;
this._transport.onclose = () => { /* 清理 */ };
this._transport.onerror = (error) => { /* 错误报告 */ };
this._transport.onmessage = (message, extra) => {
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
this._onresponse(message); // 响应 -> 匹配等待中的请求
} else if (isJSONRPCRequest(message)) {
this._onrequest(message, extra); // 请求 -> 查找并执行处理器
} else if (isJSONRPCNotification(message)) {
this._onnotification(message); // 通知 -> 查找并执行通知处理器
}
};
await this._transport.start();
}
Server 类覆写了 buildContext() 方法(第156-176行),为每个请求构建 ServerContext 对象,包含日志、用户输入获取、采样请求等服务端特有的能力:
// server.ts 第156-176行
protected override buildContext(ctx: BaseContext, transportInfo?: MessageExtraInfo): ServerContext {
return {
...ctx,
mcpReq: {
...ctx.mcpReq,
log: (level, data, logger) => this.sendLoggingMessage({ level, data, logger }),
elicitInput: (params, options) => this.elicitInput(params, options),
requestSampling: (params, options) => this.createMessage(params, options)
},
http: hasHttpInfo ? {
req: transportInfo?.request,
closeSSE: transportInfo?.closeSSEStream,
closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream
} : undefined
};
}
这意味着在任何工具回调中,开发者都可以通过 ctx.mcpReq.log() 发送日志、通过 ctx.mcpReq.elicitInput() 向用户请求输入,这些能力是在传输层绑定阶段就注入到上下文中的。
8.6 框架集成适配器
MCP TypeScript SDK 为三大主流 Node.js Web 框架提供了开箱即用的适配器,它们位于 packages/middleware/ 目录下。
graph LR
subgraph "packages/middleware/"
Express["express/<br/>createMcpExpressApp()"]
Fastify["fastify/<br/>createMcpFastifyApp()"]
Hono["hono/<br/>createMcpHonoApp()"]
end
Express -->|"express.json()"| JSON1[JSON Body 解析]
Fastify -->|"内建 JSON 解析"| JSON2[JSON Body 解析]
Hono -->|"手动 clone + json()"| JSON3[JSON Body 解析]
Express -->|"app.use()"| DNS1[DNS 重绑定防护]
Fastify -->|"addHook('onRequest')"| DNS2[DNS 重绑定防护]
Hono -->|"app.use('*')"| DNS3[DNS 重绑定防护]
DNS1 --> HV[hostHeaderValidation]
DNS2 --> HV
DNS3 --> HV
style Express fill:#d4edda
style Fastify fill:#cce5ff
style Hono fill:#fff3cd
三个适配器遵循完全一致的设计模式,核心差异仅在于框架特有的中间件注册方式。
8.6.1 Express 适配器
createMcpExpressApp()(packages/middleware/express/src/express.ts 第62-88行)创建预配置的 Express 应用:
export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express {
const { host = '127.0.0.1', allowedHosts, jsonLimit } = options;
const app = express();
app.use(express.json(jsonLimit ? { limit: jsonLimit } : undefined));
if (allowedHosts) {
app.use(hostHeaderValidation(allowedHosts));
} else {
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
if (localhostHosts.includes(host)) {
app.use(localhostHostValidation());
}
}
return app;
}
8.6.2 Fastify 适配器
Fastify 无需额外的 JSON 解析中间件(框架内建),DNS 防护通过 addHook('onRequest', ...) 注入:
export function createMcpFastifyApp(options = {}): FastifyInstance {
const app = Fastify();
// Fastify parses JSON by default - no middleware needed
if (allowedHosts) {
app.addHook('onRequest', hostHeaderValidation(allowedHosts));
} else if (localhostHosts.includes(host)) {
app.addHook('onRequest', localhostHostValidation());
}
return app;
}
8.6.3 Hono 适配器
Hono 适配器最为特殊——它需要手动实现 JSON body 解析,因为 Hono 的 Web Standard API 风格不提供自动解析:
// hono.ts 第47-68行
app.use('*', async (c: Context, next) => {
if (c.get('parsedBody') !== undefined) return await next();
const ct = c.req.header('content-type') ?? '';
if (!ct.includes('application/json')) return await next();
try {
const parsed = await c.req.raw.clone().json(); // 克隆请求以避免消耗流
c.set('parsedBody', parsed);
} catch {
return c.text('Invalid JSON', 400);
}
return await next();
});
注意 c.req.raw.clone().json() 的使用——Hono 基于 Web Standard Request API,请求体是一次性流,必须先 clone() 才能安全读取,否则后续中间件将无法再次访问请求体。
8.6.4 DNS 重绑定防护
三个适配器共享同一个安全策略:当服务器绑定到 localhost 地址时,自动启用 Host 头验证。核心验证逻辑位于 hostHeaderValidation.ts(每个适配器包有自己的副本,但逻辑相同):
// hostHeaderValidation.ts (server/src/server/middleware/)
export function validateHostHeader(
hostHeader: string | null | undefined,
allowedHostnames: string[]
): HostHeaderValidationResult {
if (!hostHeader) {
return { ok: false, errorCode: 'missing_host', message: 'Missing Host header' };
}
let hostname: string;
try {
hostname = new URL(`http://${hostHeader}`).hostname;
} catch {
return { ok: false, errorCode: 'invalid_host_header', ... };
}
if (!allowedHostnames.includes(hostname)) {
return { ok: false, errorCode: 'invalid_host', ... };
}
return { ok: true, hostname };
}
使用 new URL(http://${hostHeader}).hostname 来解析 Host 头是一个巧妙的做法,它利用浏览器标准的 URL 解析器来正确处理 IPv4、IPv6(如 [::1]:3000)和带端口的主机名,避免了手动解析的复杂性。
8.7 初始化握手与能力协商
MCP 的初始化握手是一个严格的两步过程,完全由 Server 类自动处理。
Server 构造函数中注册了 initialize 请求处理器,其实现如下(第426-444行):
// server.ts 第426-444行
private async _oninitialize(request: InitializeRequest): Promise<InitializeResult> {
const requestedVersion = request.params.protocolVersion;
this._clientCapabilities = request.params.capabilities;
this._clientVersion = request.params.clientInfo;
const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion)
? requestedVersion
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
this.transport?.setProtocolVersion?.(protocolVersion);
return {
protocolVersion,
capabilities: this.getCapabilities(),
serverInfo: this._serverInfo,
...(this._instructions && { instructions: this._instructions })
};
}
关键设计决策:
-
版本协商策略:如果客户端请求的版本在支持列表中,直接使用;否则回退到服务端支持的最新版本(列表第一项),而不是直接拒绝连接。这种”尽力兼容”的策略确保了前向兼容性。
-
能力的延迟注册:
McpServer的setToolRequestHandlers()等方法在第一次注册工具/资源/提示词时才调用registerCapabilities(),而不是在构造函数中就声明所有能力。这意味着如果一个服务端没有注册任何工具,它的能力声明中就不会包含tools字段,客户端也不会发送tools/list请求。 -
能力注册的时序约束:
registerCapabilities()方法(第211-220行)会检查传输层是否已连接,如果已连接则抛出异常。这确保了能力声明只在初始化之前发生——但McpServer的注册方法通过setToolRequestHandlers()中的幂等检查(_toolHandlersInitialized标志)巧妙地绕过了这个限制,允许在连接后继续注册新工具(此时只更新内部注册表,不修改已声明的能力)。
8.8 错误处理的多层防线
MCP TypeScript SDK 的错误处理设计体现了”纵深防御”的思想,从 Schema 验证到协议层,每一层都有独立的错误捕获机制。
Schema 层:validateStandardSchema() 在输入/输出不符合预期时返回 { success: false, error: string },上层将其转换为 ProtocolError。
工具执行层:tools/call 处理器(mcp.ts:161 起)的 try-catch 将所有异常包装为 CallToolResult,只有 UrlElicitationRequired(mcp.ts:209 的分支)被允许直接传播——因为 URL Elicitation 需要客户端执行重定向操作,不能被当作普通的工具执行错误处理。
协议层:Server.setRequestHandler 的覆写(第225-272行)对 tools/call 的请求和响应都做 Schema 验证,确保即使开发者直接使用底层 API 也不会发送格式错误的数据。
能力层:assertRequestHandlerCapability()(第369-416行)和 assertNotificationCapability()(第307-366行)在注册处理器和发送通知时检查能力声明,在开发阶段就暴露配置错误。
传输层:Protocol 基类的 onmessage 回调对无法识别的消息类型调用 _onerror(),确保畸形消息不会导致静默失败。
这种多层防线的设计意味着:即使某一层的校验被绕过(例如开发者直接操作底层 Server 类),其他层仍然能够拦截错误。这是构建可靠分布式系统的重要原则。
8.8.1 ts-server 核心文件清单与行数(截至 2026-04-22 main)
先把”server 包到底有多大、每个文件占多少”这件事用真实可复核的行数钉死。以下数据通过 github.com/modelcontextprotocol/typescript-sdk 的 packages/server/src/server/ 直读 main 分支源码得出,行号标注一律写”截至 2026-04-22 main”,读者自行 git checkout 主干即可对齐。
| 文件 | 行数 | 核心职责 | 本章定位 |
|---|---|---|---|
mcp.ts | 1095 | McpServer 门面类(registerTool/Resource/Prompt、补全分发、能力延迟声明) | §8.1.2 / §8.2 / §8.3 主线 |
server.ts | 714 | Server extends Protocol(initialize 握手、tools/call 双向校验、ServerContext 构建、能力断言) | §8.1.1 / §8.7 主线 |
streamableHttp.ts | 1224 | WebStandardStreamableHTTPServerTransport(session / SSE / 响应流 / Last-Event-Id 恢复) | ch13 延伸 |
stdio.ts | 127 | StdioServerTransport(stdin/stdout line-delimited JSON) | ch12 延伸 |
completable.ts | 67 | completable() Symbol 标记 + isCompletable / getCompleter 提取器 | §8.4 主线 |
三条值得钉住的物理事实:
streamableHttp.ts1224 行 /stdio.ts127 行 ≈ 9.6×——HTTP 传输要管 session 路由、SSE 双向流、DNS 重绑定防护、CORS、响应流收尾、OAuth 配合;stdio 只负责按行读写 JSON。网络传输的工程复杂度远超本地 IPC,这条比例比 Python SDK(13.9×,见 ch10 §10.10.5)略低,因为 TS 版把部分中间件外置到packages/middleware/。- 本章双层合计
mcp.ts 1095 + server.ts 714 = 1809 行——占 server 包源码约 55%(含 transport 与 completable 总计约 3300 行),余下 45% 是传输层。mcp.ts是server.ts的 1.53 倍:门面类承载的注册表、生命周期、补全分发、executor 闭包远多于协议层本身的代码量。 completable.ts67 行——全章 §8.4 讨论的自动补全机制只用 67 行就把”Symbol 元数据 + 类型守卫 + 解包 Optional”三件事写完。对比types/spec.types.ts3247 行(规范生成的 TS 类型),TS SDK 在”薄壳能封装的地方绝不膨胀”这条原则上非常克制。
8.8.2 Server vs McpServer:两套 API 的能力投影
Server 与 McpServer 并不是”低级 vs 高级”的简单包装关系,它们暴露的 API 在粒度和语义上有根本差异。下表以方法为单位对齐:
| 关注点 | Server(server.ts 714 行) | McpServer(mcp.ts 1095 行) |
|---|---|---|
| 注册接口 | setRequestHandler(method, handler) 按 JSON-RPC method 名直接挂钩子 | registerTool / registerResource / registerPrompt 按语义注册 |
| 参数校验 | 仅对 tools/call 的请求与响应做 CallToolRequestSchema / CallToolResultSchema 包裹(第 273-316 行) | 三段式:validateToolInput → executor → validateToolOutput(§8.2.2),Standard Schema 协议可接 Zod / Valibot / ArkType |
| 能力声明 | registerCapabilities({...}) 要求显式传字段,连接后调用直接抛异常 | setToolRequestHandlers 等方法在首次注册时才声明能力,内部用 _toolHandlersInitialized 幂等标志规避”连接后注册”限制 |
| 生命周期 | 只处理 initialize + notifications/initialized(构造函数直接挂钩) | RegisteredTool.enable/disable/update/remove 动态管理,update 触发 sendToolListChanged |
| 客户端反向交互 | createMessage / elicitInput / listRoots / sendLoggingMessage / sendToolListChanged 等原子方法 | 不直接暴露,通过 server.server.xxx 穿透调用;工具回调里走 ctx.mcpReq.log / elicitInput / requestSampling |
| 补全 | 无——需要开发者自己调用 setRequestHandler('completion/complete', ...) | completable() 标记 + handlePromptCompletion / handleResourceCompletion 自动分发(§8.4.2) |
| 错误模型 | 协议级 ProtocolError 原样抛出 | tools/call 异常被 createToolError 包装成 { isError: true, content: [...] };仅 UrlElicitationRequired 直通 |
| 适用场景 | 需要自定义 method(如私有扩展 x-company/foo)、直接写协议层调试工具 | 99% 的业务开发——工具/资源/提示词三件套 |
两套 API 的关键不对称:Server 是方法级 API(method 是第一公民),McpServer 是能力级 API(tool / resource / prompt 是第一公民)。这也解释了为什么 McpServer 不用继承——如果继承,setRequestHandler 就会出现在子类公开面上,语义层的 registerTool 和协议层的 setRequestHandler('tools/call', ...) 同时暴露,开发者很容易同时操作两套入口导致状态分裂。组合方案下,底层只通过 .server 属性”主动穿透”才可访问,分层边界清晰。
8.8.3 TS Server (本章 ch8) ↔ Py Client (ch11 首批扩写) 对称对比
ch11 作为 Python SDK 客户端首批深度扩写的章节(见 ch11 §11.8 真实源码账本),已把 mcp/client/session.py 等文件钉到行号。把本章 TS Server 与 ch11 Py Client 做”语言×角色”交叉对比,能帮助读者理解MCP 协议的双语言实现在哪些地方趋同、在哪些地方分叉:
| 维度 | TS Server(本章) | Py Client(ch11 §11.8) | 趋同 or 分叉 |
|---|---|---|---|
| 核心类分层 | Protocol → Server → McpServer 三层 | BaseSession → ClientSession 两层 | 趋同——都把 JSON-RPC 收发抽离基类 |
| 语言原生特性利用 | TypeScript 条件类型 + Standard Schema 协议适配多校验库 | Python anyio 结构化并发 + TaskGroup + memory stream | 分叉——TS 走类型,Py 走并发 |
| 初始化握手 | Server._oninitialize(server.ts 第 363-378 行)自动回应 | ClientSession.initialize 主动发起 + 发送 initialized 通知 | 趋同——双端严格遵循规范 |
| 版本协商 | ”尽力兼容”:不支持则回退到服务端首选版本 | 严格等值——版本不匹配直接抛 RuntimeError | 分叉——服务端宽松、客户端严格 |
| 错误处理哲学 | 工具异常包装成 isError: true 不中断会话 | 协议错误 McpError / 传输错误 anyio.BrokenResourceError 直接传播 | 分叉——服务端兜底、客户端透传 |
| 取消支持 | Protocol 基类 CancellationToken(server.ts 继承) | anyio cancel scope + TaskGroup 级联取消 | 趋同——都对 JSON-RPC notifications/cancelled 响应 |
| 补全能力 | completable() Symbol 标记 + 服务端主动分发 | 客户端仅发 completion/complete 请求,收结果渲染 | 互补——一端标记、一端消费 |
| 代码体量 | server 包 3300+ 行(本节 §8.8.1) | mcp/client/ 全部约 2500 行(ch11 §11.8.1) | 趋同——服务端略厚于客户端,因为要管能力声明与注册表 |
这张表要传递的工程观念:MCP 协议是双向对称的(服务端可向客户端发起 sampling/createMessage、客户端可向服务端发起 tools/call),但两个 SDK 在相同协议下仍做了语言特色的取舍——TS 用类型系统做规范约束,Py 用结构化并发做生命周期管理;服务端倾向”兜底”(因为要对不可信客户端鲁棒),客户端倾向”透传”(因为上层业务需要感知错误做决策)。
读者若要构建跨语言 MCP 系统(例如 Rust 服务端 + TS 客户端),本节对比可作为”协议理解的参照系”——任何偏离这两端现状的设计决策都需要额外论证。
8.8.4 Server.setRequestHandler 双向校验的边界情况
本章 §8.1.1 已经提到 Server.setRequestHandler 对 tools/call 做了包装,但源码里还有两个易被忽视但会导致 bug 的边界情况(截至 2026-04-22 main 的 server.ts 第 273-316 行):
边界 1:任务型工具(task handler)不校验 result。当请求是”创建任务”(CreateTaskRequest)时,返回的是 CreateTaskResult(包含 taskId),而不是普通的 CallToolResult({ content: [...] })。包装函数对 result 的校验只针对非任务路径——源码里写作 if (!('taskId' in result)) 之后再跑 parseSchema(CallToolResultSchema, result)。如果开发者误把任务型工具当普通工具使用(返回了 { content: [...] } 但没有 taskId),校验会通过;但客户端拉取任务状态时会立即失败。定位这种 bug 必须同时看两端日志——单看服务端不报错。
边界 2:pass-through 模式无法绕过。有开发者为了性能尝试注册”裸” tools/call handler(不走 McpServer,直接调 server.server.setRequestHandler('tools/call', fn)),期望绕过双向校验。但 Server.setRequestHandler 的覆写无条件生效——只要 method 等于 tools/call,包装就会套上。如果确实需要绕过(比如代理模式转发给下游 MCP Server),唯一办法是向更底层的 Protocol 类传递已校验的结果——这在 SDK 公开 API 里没暴露,属于”被设计成不能绕过”。这条约束是有意为之,因为代理模式下如果不校验,畸形响应会污染整个工具链。
8.8.5 McpServer 的三种常见误用与源码级应对
读完前面的源码剖析,以下三类误用在社区 issue 里反复出现,本节给出 SDK 内部的”防御代码”定位:
误用 1:在 connect() 之后调用 registerCapabilities。用户代码如 await server.connect(transport); server.server.registerCapabilities({ logging: {} }) 会直接抛 Error('Cannot register capabilities after connecting to transport')——见 server.ts 第 211-220 行。但通过 McpServer.registerTool 在连接后继续注册新工具是合法的,因为 setToolRequestHandlers 内部的 _toolHandlersInitialized 幂等标志确保 capability 只声明一次,之后的注册只更新 _registeredTools 字典并发 notifications/tools/list_changed。这条不对称的设计常被误解成”bug”,实际上是刻意的——capability 属于协议握手契约、必须冻结,但工具集合属于运行时状态、允许演进。
误用 2:忘记 await registerTool 中回调返回的 Promise。createToolExecutor 中对所有回调统一 async 包裹(mcp.ts 第 1125-1154 行),所以即使开发者的 callback 写成同步函数,SDK 也会通过 async (args, ctx) => callback(args, ctx) 把返回值 Promise 化。但如果 callback 内部用了 setTimeout / setImmediate 这类脱离 Promise 链的异步逻辑,SDK 无法帮你兜底——这些 callback 必须手动返回 new Promise((resolve) => setTimeout(() => resolve({content:[...]}), n)) 才能被正确 await。这条教训在”流式采样”场景(见 ch17 sampling)特别容易踩。
误用 3:用 update({name: …}) 改名后继续用旧引用。RegisteredTool 的 update 方法(mcp.ts 第 800 多行)在改名时执行 delete this._registeredTools[oldName]; this._registeredTools[newName] = registeredTool——注意它把同一个 registeredTool 对象重新挂到新 key 上。也就是说,开发者手里持有的 registeredTool 变量仍然有效,继续调用 .update({ enabled: false }) 仍然生效。但如果开发者之前用 _registeredTools['oldName'] 这种”按 key 取值”的方式拿引用(这种写法本身就不推荐),改名后旧 key 就是 undefined 了。SDK 的 API 鼓励”拿 registerTool 返回值”这种引用语义,而不是”从字典里反向查找”。
8.8.6 Transport 层与 Server 的协作时序
最后把 §8.5 的传输层绑定和 §8.7 的初始化握手拼成一张端到端时序图,帮助读者把”连接建立 → 初始化 → 工具调用 → 关闭”四个阶段串起来:
sequenceDiagram
autonumber
participant App as 应用代码
participant MS as McpServer
participant S as Server (Protocol)
participant T as Transport
participant C as Client
App->>MS: new McpServer(info, options)
App->>MS: registerTool("calc", ...)
Note over MS,S: setToolRequestHandlers 首次触发<br/>registerCapabilities({tools: {listChanged: true}})
App->>MS: connect(transport)
MS->>S: this.server.connect(transport)
S->>T: transport.start()
T-->>C: 传输层就绪(stdio/HTTP)
C->>T: initialize { protocolVersion, capabilities, clientInfo }
T->>S: onmessage
S->>S: _oninitialize(server.ts 363-378)
S-->>C: { protocolVersion, capabilities, serverInfo, instructions? }
C->>T: notifications/initialized
T->>S: onmessage → 通知处理器
C->>T: tools/call { name: "calc", arguments: {...} }
T->>S: onmessage → _onrequest
S->>S: setRequestHandler 包装:校验 Request Schema
S->>MS: 调 tools/call handler
MS->>MS: validateToolInput
MS->>MS: executeToolHandler
MS->>MS: validateToolOutput
MS-->>S: CallToolResult
S->>S: setRequestHandler 包装:校验 Result Schema
S-->>C: 响应
App->>MS: server.close()
MS->>S: this.server.close()
S->>T: transport.close()
T-->>C: 连接关闭通知
时序图要点:
registerTool必须在connect之前完成——否则首次调用会在registerCapabilities处抛异常。之后的增量注册走”只更新字典 + 发 listChanged 通知”路径。initialize与notifications/initialized是两条消息:服务端响应完initialize后必须等客户端发notifications/initialized才算握手完成。这期间如果客户端提前发tools/call,Protocol基类会按正常 JSON-RPC 处理(没有强制阻塞),但这违反规范。close()沿McpServer → Server → Transport逐层下沉,底层 transport 负责给对端发送关闭信号(stdio 直接关闭 stdin 流,HTTP 关闭 SSE 连接或返回 405)。
这张时序图也能帮读者在写集成测试时定位挂起——如果测试在 initialize 后超时,多半是客户端漏发了 notifications/initialized;如果在 tools/call 后超时,多半是 executor 里的 Promise 没被正确 resolve。
8.8.7 Standard Schema 协议:为什么 TS SDK 不绑定 Zod
McpServer.validateToolInput 使用的 validateStandardSchema 并不直接依赖 Zod。Standard Schema 是 2024 年由 Zod / Valibot / ArkType 三家维护者联合提出的接口规范(github.com/standard-schema/standard-schema),核心约定非常简洁——任何 schema 实现在自己对象上挂一个 ~standard 属性(注意波浪号前缀是为了避开用户字段冲突),里面暴露 version / vendor / validate 三个字段。MCP SDK 只调 validate(value) 这一个方法,因此校验库可插拔。
为什么这个选择对 MCP 很重要:服务端代码常常需要把工具的 inputSchema 同时”运行时校验”和”序列化为 JSON Schema 给客户端看”。Zod 早期没有 JSON Schema 导出,得靠 zod-to-json-schema 第三方包;Valibot 的体积比 Zod 小一个数量级;ArkType 则在类型推导速度上更快。TS SDK 把选择权留给用户——StandardSchemaWithJSON 类型(mcp.ts 第 110 行附近)在 ~standard 之上追加了一个 jsonSchema 属性或 toJSON() 方法,用于拿到给客户端看的 schema 文本。
与 Python SDK 的对称对比(ch10 §10.2 讨论过):Python SDK 硬依赖 Pydantic v2(所有工具的参数校验走 BaseModel.model_validate),没有 Python 版的 Standard Schema 协议。结果就是 Python 用户想换成 attrs / msgspec 必须自己写 adapter。这也反映了两个生态的文化差异——Python 习惯”官方钦定一个库”,TypeScript 习惯”靠接口解耦”。
8.8.8 本章源码账本汇总
把本章引用过的所有”截至 2026-04-22 main”源码位置集中列一张表,读者复核时按图索骥:
| 文件 | 行号范围 | 本章章节 | 内容 |
|---|---|---|---|
packages/server/src/server/server.ts | 99-141 | §8.1.1 | Server 构造函数、initialize 处理器挂载 |
packages/server/src/server/server.ts | 211-220 | §8.7 / §8.8.5 | registerCapabilities 连接后拒绝 |
packages/server/src/server/server.ts | 273-316 | §8.1.1 / §8.8.4 | setRequestHandler 覆写、tools/call 双向校验 |
packages/server/src/server/server.ts | 363-378 | §8.7 | _oninitialize 版本协商 |
packages/server/src/server/mcp.ts | 61-77 | §8.1.2 | McpServer 组合 Server |
packages/server/src/server/mcp.ts | 107-116 | §8.1.2 | connect / close 委托 |
packages/server/src/server/mcp.ts | 240-261 | §8.2.2 | validateToolInput |
packages/server/src/server/mcp.ts | 266-295 | §8.2.2 | validateToolOutput |
packages/server/src/server/mcp.ts | 352-399 | §8.4.2 | 补全请求分发 |
packages/server/src/server/mcp.ts | 481-502 | §8.3.1 | resources/read 精确+模板匹配 |
packages/server/src/server/mcp.ts | 789-834 | §8.3.1 | registerResource |
packages/server/src/server/mcp.ts | 891-926 | §8.2.1 | registerTool |
packages/server/src/server/mcp.ts | 928-963 | §8.3.2 | registerPrompt |
packages/server/src/server/mcp.ts | 1125-1154 | §8.2.1 / §8.8.5 | createToolExecutor |
packages/server/src/server/completable.ts | 3-58 | §8.4.1 | completable() Symbol 标记 |
packages/core/src/shared/protocol.ts | 455-485 | §8.5 | Protocol.connect 消息路由 |
若社区后续重构(例如 mcp.ts 拆分)——读者按”方法名”搜索仍可定位(git grep 'registerTool('、git grep 'validateToolInput'),因为本章刻意选用这些命名作为锚点。若整个文件被重命名,则第 8.8.1 节的行数表会一起失效,届时以 SDK CHANGELOG 为准。
8.9 总结
本章深入剖析了 MCP TypeScript SDK 服务端的完整实现。核心要点如下:
双层架构是整个设计的基石。Server 类(继承 Protocol)处理协议层面的一切——初始化握手、能力协商、请求验证、消息路由;McpServer 类通过组合 Server 实例,提供了 registerTool()、registerResource()、registerPrompt() 等直觉式 API,将协议复杂性完全封装在内部。
工具注册的链路从 API 调用开始,经过名称验证、executor 创建、注册表存储、协议处理器注册(首次)、客户端通知,形成一条完整的初始化管线。工具调用则经过输入校验、executor 执行、输出校验的三段式流程,每一段都有独立的错误处理。
自动补全机制通过 Symbol 属性标记实现零侵入的元数据附加,结合 Schema shape 解析和 Optional 解包,为提示词参数和资源 URI 变量提供实时补全建议。
框架集成采用工厂函数模式,为 Express、Fastify、Hono 三大框架提供预配置的应用实例,内建 DNS 重绑定防护和 JSON body 解析,开发者只需关注 MCP 路由的挂载。
错误处理的纵深防御——从 Standard Schema 验证、工具执行异常捕获、协议级 Schema 校验到能力声明检查——确保了在任何异常场景下,系统都能给出明确的错误信息而不是静默失败。
理解了这些服务端内部机制,你不仅能更高效地使用 SDK 构建 MCP 服务器,更能在遇到问题时快速定位根因——因为你知道每一个请求从进入到返回所经过的每一道关卡。