MCP协议设计与实现-第8章 TypeScript Server 实现剖析

《MCP 协议设计与实现》完整目录

第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
高层 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 消息的收发、请求-响应匹配、超时管理、取消通知等底层能力。

ServerProtocol 之上增加了服务端特有的职责:

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 类的核心职责包括:

  1. 初始化握手 :自动处理 initialize 请求,协商协议版本,交换能力声明
  2. 能力管理 :通过 registerCapabilities() 动态注册服务端能力,并在 assertRequestHandlerCapability() 中校验能力与请求方法的匹配关系
  3. 请求校验 :覆写 setRequestHandler() 方法,对 tools/call 请求施加额外的 Schema 验证(第225-272行)
  4. 通知权限检查 :在 assertNotificationCapability() 中确保服务端只发送已声明能力范围内的通知
  5. 客户端交互 :提供 createMessage()(采样请求)、elicitInput()(用户输入获取)、listRoots() 等与客户端交互的方法

特别值得注意的是 Servertools/call 的特殊处理。在 setRequestHandler 的覆写中(第225-272行),当 methodtools/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 表面,而不必担心 ServerProtocol 的内部方法泄漏到公开接口中。同时,开发者仍然可以通过 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 调用一直到请求被处理并返回结果。

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() 方法:

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行)执行以下关键步骤:

  1. 名称验证 :调用 validateAndWarnToolName(name) 检查工具名是否符合规范
  2. 创建执行器 :调用 createToolExecutor(inputSchema, handler) 将原始回调包装为统一的执行器接口
  3. 构建注册对象 :创建 RegisteredTool 对象,包含 enable()disable()remove()update() 等生命周期方法
  4. 触发首次初始化 :调用 setToolRequestHandlers() 注册协议级处理器(仅首次执行)
  5. 发送变更通知 :调用 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行)。这个方法执行三个关键操作:

  1. 接管传输层回调 :替换 transport 的 oncloseonerroronmessage 回调
  2. 消息路由:根据消息类型(请求/响应/通知)分发到对应的处理器
  3. 启动传输 :调用 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/ 目录下。

graph LR subgraph "packages/middleware/" Express["express/
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 })
    };
}

关键设计决策:

  1. 版本协商策略:如果客户端请求的版本在支持列表中,直接使用;否则回退到服务端支持的最新版本(列表第一项),而不是直接拒绝连接。这种"尽力兼容"的策略确保了前向兼容性。

  2. 能力的延迟注册McpServersetToolRequestHandlers() 等方法在第一次注册工具/资源/提示词时才调用 registerCapabilities(),而不是在构造函数中就声明所有能力。这意味着如果一个服务端没有注册任何工具,它的能力声明中就不会包含 tools 字段,客户端也不会发送 tools/list 请求。

  3. 能力注册的时序约束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 服务器,更能在遇到问题时快速定位根因------因为你知道每一个请求从进入到返回所经过的每一道关卡。

相关推荐
是小蟹呀^1 小时前
【总结】LangChain中的中间件Middleware
python·中间件·langchain·agent
去伪存真1 小时前
Superpowers 从“调教提示词”转向“构建工程规范”
前端·agent
loong_XL2 小时前
2026智能体爆发现象级产品:OpenClaw、Hermes Agent、Claude Cowork
大模型·agent·智能体·claw·龙虾
一个处女座的程序猿3 小时前
Agent之Memory:EverOS的简介、安装和使用方法、案例应用之详细攻略
agent·memory·everos
fundroid4 小时前
Google 发布 Android Skill & Android CLI:大幅提升 Android Agent 能力
android·agent·cli·skill
花千树_0104 小时前
Java 实现 ReAct Agent:工具调用与推理循环
agent
Cosolar5 小时前
PageIndex技术全解析:基于推理的无向量RAG框架,重构长文档智能检索范式
llm·agent·chatglm (智谱)
Makoto_Kimur6 小时前
Agent 面试速成清单
java·agent
程序员陆业聪6 小时前
Agent时代的工程师危机:当会写代码不再是护城河
agent
数数科技的数据干货6 小时前
官宣!数数科技正式更名为 ThinkingAI
大数据·人工智能·科技·agent