《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析(当前)
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling
- 第18章 Elicitation、Roots 与配置管理
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server
- 第21章 设计模式与架构决策
第8章 TypeScript Server 实现剖析
在前面的章节中,我们已经理解了 MCP 协议的整体架构与传输层机制。从本章开始,我们将深入 MCP TypeScript SDK 的服务端实现,逐行剖析源码中最核心的设计决策。MCP 的服务端是整个生态系统的基石------它决定了开发者如何定义工具、暴露资源、管理提示词,以及如何将这些能力安全地交付给 AI 客户端。
SDK 的服务端采用了一个精妙的双层架构 :底层的 Server 类负责协议级别的通信细节,上层的 McpServer 类则提供开发者友好的注册式 API。这种分层设计既保证了协议实现的严谨性,又让日常开发变得简洁直观。本章将从这个架构出发,逐步揭示工具注册、请求分发、输入输出校验、自动补全、中间件防护以及多框架集成的完整实现链路。
8.1 双层服务端架构:设计哲学与职责划分
MCP TypeScript SDK 的服务端由两个核心类构成,分别位于不同的抽象层次。理解它们的职责边界,是理解整个服务端实现的前提。
高层 API 封装"] McpServer -->|"内部持有"| RegTools["_registeredTools
工具注册表"] McpServer -->|"内部持有"| RegRes["_registeredResources
资源注册表"] McpServer -->|"内部持有"| RegPrompts["_registeredPrompts
提示词注册表"] end subgraph "Server 层 (server.ts)" Server["Server extends Protocol
协议级实现"] Server -->|"管理"| ReqHandlers["请求处理器映射
_requestHandlers"] Server -->|"管理"| NotifHandlers["通知处理器映射
_notificationHandlers"] Server -->|"管理"| Capabilities["能力声明
_capabilities"] end McpServer -->|"this.server"| Server Server -->|"connect()"| Transport["Transport
传输层"] style McpServer fill:#e1f5fe style Server fill:#fff3e0
8.1.1 Server 类:协议层的忠实执行者
Server 类定义在 packages/server/src/server/server.ts,它继承自 Protocol<ServerContext> 基类。Protocol 是 MCP SDK 中客户端和服务端共享的通信基础设施,提供了 JSON-RPC 消息的收发、请求-响应匹配、超时管理、取消通知等底层能力。
Server 在 Protocol 之上增加了服务端特有的职责:
typescript
// 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 校验:
typescript
// 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,它并不继承 Server,而是组合 了一个 Server 实例:
typescript
// 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 实例,传输层的绑定完全透明:
typescript
// 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 调用一直到请求被处理并返回结果。
8.2.1 注册阶段:从 API 到存储
registerTool() 方法(第865-894行)接收三个参数:工具名称、配置对象和回调函数。它首先检查名称是否已被占用,然后委托给内部的 _createRegisteredTool() 方法:
typescript
// 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行)是一个精巧的适配层,它解决了"有参数"和"无参数"两种工具回调签名的统一调用问题:
typescript
// 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 协议验证传入参数:
typescript
// 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 调用实际回调:
typescript
private async executeToolHandler(tool, args, ctx) {
return tool.executor(args, ctx);
}
第三关:输出校验 。validateToolOutput() 方法(第266-295行)验证返回结果是否符合 outputSchema:
typescript
// 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 层面的错误响应直接暴露给客户端:
typescript
// 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 并通知客户端:
typescript
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 模式匹配:
typescript
// 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 解析和回调调用:
typescript
// 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 附加补全元数据,这是一种零侵入的标记方式:
typescript
// 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 请求处理器。处理器根据引用类型分发到不同的处理逻辑:
typescript
// 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() 标记过:
typescript
// 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 标记告知客户端存在更多结果:
typescript
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()开始接收消息
typescript
// 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 对象,包含日志、用户输入获取、采样请求等服务端特有的能力:
typescript
// 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/ 目录下。
createMcpExpressApp()"] Fastify["fastify/
createMcpFastifyApp()"] Hono["hono/
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 应用:
typescript
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', ...) 注入:
typescript
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 风格不提供自动解析:
typescript
// 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(每个适配器包有自己的副本,但逻辑相同):
typescript
// 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行):
typescript
// 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 处理器的 try-catch(第170-213行)将所有异常包装为 CallToolResult,只有 UrlElicitationRequired 这一特殊错误被允许直接传播。这是因为 URL Elicitation 需要客户端执行重定向操作,不能被当作普通的工具执行错误处理。
协议层 :Server.setRequestHandler 的覆写(第225-272行)对 tools/call 的请求和响应都做 Schema 验证,确保即使开发者直接使用底层 API 也不会发送格式错误的数据。
能力层 :assertRequestHandlerCapability()(第369-416行)和 assertNotificationCapability()(第307-366行)在注册处理器和发送通知时检查能力声明,在开发阶段就暴露配置错误。
传输层 :Protocol 基类的 onmessage 回调对无法识别的消息类型调用 _onerror(),确保畸形消息不会导致静默失败。
这种多层防线的设计意味着:即使某一层的校验被绕过(例如开发者直接操作底层 Server 类),其他层仍然能够拦截错误。这是构建可靠分布式系统的重要原则。
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 服务器,更能在遇到问题时快速定位根因------因为你知道每一个请求从进入到返回所经过的每一道关卡。