跳到主要内容

低层 API 与整洁架构 — Server、setRequestHandler 与 tools 目录

这一章讲三件事: 已经有趁手的高层 API 了,为什么还要有「低层」这一条路; 低层服务器到底低在哪、写法差多少; 以及作者真正的落脚点——怎么把 MCP 服务器的代码组织成一个大一点的项目也不散架的结构。 这一章技术密度不高,但它是后面采样(第 08 章)、征询(第 09 章)两章的门票。

1. 顶层全景:分发这件小事,谁来做

高层和低层的全部差别,一句话:「收到请求后该调用谁」这件事,高层替你做,低层你自己做。

高层(McpServer): 低层(Server):
server.tool("add", …) server.setRequestHandler(ListToolsRequestSchema, …)
server.tool("subtract", …) server.setRequestHandler(CallToolRequestSchema, …)
│ 每个工具登记一次 │ 每一「类」请求登记一个处理器
▼ ▼
SDK 替你存表、替你按名分发 你自己写:列出所有工具;按名找工具、
校验参数、执行、组装回应

本章主走查:一个 add 工具,在低层服务器里从「被列出来」到「被调用」再到「参数错了被拒」的全程。

2. 先回答「为什么」:三件事逼你下低层

作者很诚实:高层 API 很好,低层不是必需品,是三件事逼出来的1

第一件:管资源的生命周期。 服务器常常要连数据库、连外部服务—— 这些「资源」要在服务器启动时接上、关闭时断开。高层 API 没给你管这件事的把手; 低层你可以自己包一层上下文管理器(context manager)——一种「进来时配好资源、 出去时一定清理干净」的代码结构,哪怕中途出错也照清2。 书里用二十来行 TypeScript 实现了一个:约定资源管理类有 enter(配好并交出资源) 和 exit(清理)两个方法,再写一个 With(manager, fn) 函数, 用 try/finally 保证 exit 必被调用3:

function With(manager: ContextManager, fn: (assets) => void) {
const iterator = manager.enter();
const { value } = iterator.next(); // enter 交出的资源(比如一个数据库连接)
try { fn(value); } // 用资源
catch (e) { console.error(e); }
finally { iterator.return?.(); manager.exit(); } // 天塌下来也清理
}

书中特别注明:TypeScript SDK 目前没内置这个模式(Python SDK 有), 但没有任何东西拦着你自己写4。顺便说一句,这一节能看出本书的改编痕迹—— 讲上下文管理器时举的 with 语句、contextlib、还有后文的「Pydantic」, 都是 Python 世界的词,移植到 TypeScript 时只做了半套4

第二件:架构自由。 高层 API 要求「工具都注册到 server 实例上」, 于是项目一大,server 这个对象就得被传来传去。低层没有这回事—— 第 4 节会看到,工具可以根本不知道 server 的存在1

第三件:有些功能只有低层写得出。 采样(第 08 章)和征询(第 09 章) 在 SDK 里要靠 setRequestHandler 接,作者直接明说「别无他法」5

3. 低层服务器:从「注册工具」到「注册处理器」

写法对照最直观。高层是每个工具登记一次6:

const server = new McpServer({ name: "Demo", version: "1.0.0" });
server.tool("add", { a: z.number(), b: z.number() },
async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }));

低层是每一类请求登记一个处理器。类换成了 Server, 注册方法换成了 setRequestHandler(schema, 处理函数)—— 第一个参数是「这类请求长什么样」的凭证,第二个是你自己的处理逻辑7:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema }
from "@modelcontextprotocol/sdk/types.js";

const server = new Server({ name: "Demo", version: "1.0.0" },
{ capabilities: { tools: {} } });

server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: [ /* 你自己列出全部工具 */ ] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
/* 你自己:按名找工具 → 校验参数 → 执行 → 组装回应 */
});

注意那两张「凭证」:ListToolsRequestSchemaCallToolRequestSchema—— 它们就是第 02 章线上消息 tools/listtools/call 在 SDK 里的类型化身。 高层是「声明有什么」,低层是「接管每一问」——自由度上来了,活儿也上来了7

4. 整洁架构:工具不知道框架的存在

自由拿来干什么?作者给的答案是把代码摆成这样的目录8:

project/
├── app.ts 入口:只管启动、收尾(比如监听 Ctrl+C)
├── server.ts 只在这里接触框架:建 Server、接传输、注册处理器
└── tools/
├── index.ts 把所有工具收进一个数组
├── tool.ts Tool 接口:name / description / rawSchema / inputSchema / callback
├── schema.ts 共用的 Zod 规则(比如两个数字的 MathInputSchema)
├── add.ts 一个工具一个文件
└── subtract.ts

关键在 add.ts 长什么样9:

export default {
name: "add",
description: "Add two numbers",
rawSchema: MathInputSchema, // Zod 版,运行时校验用
inputSchema: zodToJsonSchema(MathInputSchema), // JSON 版,列给客户端看
callback: async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
} as Tool;

这个文件里没有一个字提到 MCP SDK——它就是一个普通 TypeScript 对象。 书里点破了这份设计的用意:「定义工具的地方没有框架依赖,就是纯 TypeScript, 这让它好测、好推理」10。单元测试它不需要起服务器, 把 callback 当普通函数调就行。

还有一个细节值得拆:rawSchemainputSchema 为什么是两份? 因为运行时的世界和线上的世界说两种语言——校验进来的参数要用 Zod(第 03 章), 而列给客户端的清单要交 JSON Schema; zodToJsonSchema 这个工具库负责把前者翻译成后者9。 高层 API 里 SDK 替你翻了;低层,翻译是你的事。

5. 主走查:add 在低层的一生

跟着一个请求走一遍,看低层服务器每一步干什么。

第 ① 幕,客户端问「你会什么」(tools/list)。 ListToolsRequestSchema 的处理器被触发,遍历 tools/ 目录收上来的数组, 把每个工具的 namedescriptioninputSchema 组装成清单交回去11。 客户端看到的 add,和第 03 章高层 API 列出来的格式一模一样—— 协议层没有高低层之分,差别只在服务器内部怎么组织。

第 ② 幕,客户端说「调 add,参数 {a:1, b:2}」(tools/call)。 CallToolRequestSchema 的处理器拿到 request,按顺序过三关12:

关 1:按名找工具
let tool = tools.find(t => t.name === name);
找不到 → 回 { error: { code: "tool_not_found", message: "Tool xxx not found." } }

关 2:校验参数
const input = Schema.parse(request.params.arguments);
校验失败(比如传了 {a:"x"})→ 回 { error: { code: "invalid_arguments", … } }

关 3:执行
const result = await tool.callback(input); // 1 + 2 = 3
组装成 content 文本块交回

这三关就是高层 API 背着你做的全部工作。 第 03 章你只写了三行声明, 是因为 McpServer 内部就是这套逻辑的实现者—— 我们书架对官方 SDK 的拆解证实:高层 API 的注册方法, 最终也是落成这样的请求处理器13

第 ③ 幕,加新工具。tools/ 里新建 multiply.ts, 照 add.ts 的样子填五个字段,在 tools/index.ts 的数组里加一行——完了。 server.ts 一个字不用改。这就是这套架构要的东西:加能力的成本, 不随项目变大而变大8

6. 作者的判断与证据

有证据的:

  • Server + setRequestHandler 的写法、三关调用链、zodToJsonSchema 的必要性, 书里都有完整代码791112;
  • TypeScript SDK 没有内置上下文管理器,书里写明4

作者的判断:

  • 「低层架构更干净,因为不用把 server 实例传来传去」——这是作者的架构偏好, 他自己也承认「高层一样可以组织好,只是通常更乱一点」1;
  • 上下文管理器用「生成器(generator)」实现(enter 用 yield 交资源), 这是作者从 Python contextlib 移植来的设计,TypeScript 里并非唯一写法3

判断(我们的,不是书里的): 这一章被低估了。它表面讲「另一种写法」, 实际是在回答「MCP 服务器长到几十上百个工具时怎么办」—— 而任何认真做产品的服务器都会到那个规模。 它的真正教训不是「用低层」,而是**「让业务代码不依赖框架」这条老原则在 MCP 上的具体摆法**。 如果错,会错在: 如果你的服务器只有三五个工具、也不打算用采样/征询, 这套目录架构是过度设计——高层的 server.tool() 完全够,别为架构而架构。

7. 边界与局限

  • 书里低层示例的错误回应是自造的({ error: { code: "tool_not_found" } }), 没有走 JSON-RPC 的标准错误格式(第 02 章提过错误码体系)——学结构可以,照抄报错不行;

  • 演示代码里 Schema.parse 用的是 inputSchema 变量(代码里装值的一个名字, 内容可以换)接 Zod 规则,但前文 inputSchema 又指 JSON Schema 那份—— 两个名字装两种东西,是书里的瑕疵,真写代码时建议分别叫 zodSchemajsonSchema;

  • 上下文管理器只处理了「同步清理」;真实场景里数据库断开是异步 (不站在原地等它完事,先干别的、好了再回头收结果)的——书里没有覆盖 (补充,不在书里,来自通用知识);

  • 本章只组织了 tools;resources、prompts 的低层组织法书里没给目录方案,但思路同构。

8. 可带走的

  1. 下低层的三个理由:生命周期、架构自由、采样/征询只有低层;
  2. McpServer 注册工具,Server + setRequestHandler(Schema, fn) 接管每一类请求;
  3. 调用链三关:按名找(tool_not_found)→ 校验(invalid_arguments)→ 执行—— 这也是理解一切「调用工具失败」报错的地图;
  4. 工具定义写成不碰框架的纯对象:好测、好搬、好读;
  5. Zod 与 JSON Schema 各管一头:运行校验 vs 线上描述,zodToJsonSchema 当中间人;
  6. 资源要「有始有终」:enter 配上、exit 必清,try/finally 是最低保障;
  7. 目录即架构:server.ts 垄断框架接触面,其余文件一个 SDK 的字都不认识。

9. 原文地图

主题原书章原文位置
为什么低层Maintaining Clean Architecture with an Advanced Server Approachtext/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:19(搜「Use context managers」)
上下文管理器同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:99(搜「allocate and release resources」)
With 实现同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:291(搜「iterator.next」)
TS SDK 没内置同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:448(搜「aren't used in the TypeScript SDK」)
采样/征询只能低层同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:75(搜「no other way forward」)
Server 与 setRequestHandler同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:541(搜「@modelcontextprotocol/sdk/server/index.js」)
高低层对照同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:517(搜「high-level way of building」)
tools 目录同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:812(搜「project/」)
add.ts 与 zodToJsonSchema同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:759(搜「zodToJsonSchema」)
「无框架依赖」同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:908(搜「framework dependencies」)
调用三关同上text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:1311(搜「tool_not_found」)· :1321(搜「Schema.parse」)

Footnotes

  1. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 51 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:51,搜「Use context managers to manage the lifecycle」)与第 673 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:673,搜「pass the server instance around」)。 2 3

  2. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 99 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:99,搜「allocate and release resources」)。

  3. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 288-301 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:291,搜「iterator.next」)。 2

  4. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 448-454 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:448,搜「aren't used in the TypeScript SDK」);「Pydantic」残留见第 1278 与 1413 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:1278,搜「Pydantic schemas」)。 2 3

  5. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 73-87 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:75,搜「no other way forward」)。

  6. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 496-515 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:502,搜「McpServer」)。

  7. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 541-599 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:545,搜「setRequestHandler」)。 2 3

  8. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 812-854 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:812,搜「project/」)。 2

  9. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 759-796 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:767,搜「zodToJsonSchema」;:790,搜「conform to the MCP specification」)。 2 3

  10. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 908 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:908,搜「framework dependencies」)。

  11. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 1115-1121 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:1117,搜「Return the list of registered tools」)。 2

  12. 出处:「Maintaining Clean Architecture with an Advanced Server Approach」第 1282-1339 段(text/08-fm-maintaining-clean-architecture-with-an-advanced-.txt:1311,搜「tool_not_found」;:1323,搜「Schema.parse」)。 2

  13. 补充(不在书里,依据我们的 protocol 书架):官方 SDK 的高层 McpServer,注册 tool/resource/prompt 最终也是落成请求处理器。 依据: shelf=ai-protocol-reference/mcp-typescript-sdk#01-high-level-server.md 事实=该章讲 McpServer 注册项怎么变成请求处理器。