Appearance
第8章 TypeScript Server 实现剖析
在前面的章节中,我们已经理解了 MCP 协议的整体架构与传输层机制。从本章开始,我们将深入 MCP TypeScript SDK 的服务端实现,逐行剖析源码中最核心的设计决策。MCP 的服务端是整个生态系统的基石——它决定了开发者如何定义工具、暴露资源、管理提示词,以及如何将这些能力安全地交付给 AI 客户端。
SDK 的服务端采用了一个精妙的双层架构:底层的 Server 类负责协议级别的通信细节,上层的 McpServer 类则提供开发者友好的注册式 API。这种分层设计既保证了协议实现的严谨性,又让日常开发变得简洁直观。本章将从这个架构出发,逐步揭示工具注册、请求分发、输入输出校验、自动补全、中间件防护以及多框架集成的完整实现链路。
8.1 双层服务端架构:设计哲学与职责划分
MCP TypeScript SDK 的服务端由两个核心类构成,分别位于不同的抽象层次。理解它们的职责边界,是理解整个服务端实现的前提。
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/ 目录下。
三个适配器遵循完全一致的设计模式,核心差异仅在于框架特有的中间件注册方式。
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://$).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 服务器,更能在遇到问题时快速定位根因——因为你知道每一个请求从进入到返回所经过的每一道关卡。