mcp-for-beginners 进阶指南:用 MCP 低层服务器(Low-Level Server)打造可扩展、可验证的工具架构
mcp-for-beginners 进阶指南:用 MCP 低层服务器(Low-Level Server)打造可扩展、可验证的工具架构
本篇文章是 mcp-for-beginners 开源课程《Getting Started》系列第 10 章的技术实战指南,核心主题是 MCP SDK 中两种服务器形态的取舍:常规服务器(Regular Server)与低层服务器(Low-Level Server)。文中将通过 Python 与 TypeScript 双语言对照,演示如何用"每种能力仅两个请求处理器"的低层模式,把工具(tools)、资源(resources)、提示词(prompts)按目录组织成清晰可扩展的架构,并借助 Pydantic 与 Zod 完成输入校验。读完本文,你将掌握低层服务器的 tools/list 与 tools/call 请求处理机制,并能够基于仓库提供的完整示例搭建自己的低层 MCP 服务器。
为什么需要低层服务器
MCP SDK 向开发者暴露了两种服务器类型:常规服务器(Regular Server)与低层服务器(Low-Level Server)。日常开发中,常规服务器已经足够好用——你只需通过装饰器或注册方法逐条声明工具、资源与提示词即可。但存在两类场景,你会更希望依赖低层服务器:
- 更好的架构:虽然用常规服务器同样可以写出整洁的分层代码,但低层服务器在架构组织上要更自然、更省事。
- 高级特性可用性:部分高级特性(如后续章节将要讲解的 Elicitation 与旧版 Sampling 特性,后者在 MCP
2026-07-28规范中已被标记为废弃)只有在低层服务器上才能使用。
理解两种模式的差异,是正确选择实现路径的前提。
常规服务器:逐条注册能力
先看常规服务器如何创建一个 MCP 服务器。在 03-GettingStarted/10-advanced/README.md 中给出了两种语言的对照写法。
Python(基于 FastMCP)
mcp = FastMCP("Demo")
# 添加一个加法工具
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
TypeScript(基于 McpServer)
const server = new McpServer({
name: "demo-server",
version: "1.0.0"
});
// 添加一个加法工具
server.registerTool("add",
{
title: "Addition Tool",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() }
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
两种写法的共同点非常明确:你显式地逐条添加服务器希望拥有的每个工具、资源或提示词。这种模式直观、适合原型与小规模服务,但当能力数量增长后,所有注册代码会堆积在入口文件里,架构边界逐渐模糊。
低层服务器:每个能力类型只写两个处理器
低层服务器的思维方式完全不同——不再逐条注册工具,而是为每种能力类型(工具、资源、提示词)各准备两个处理器。以工具为例:
- 列出所有工具:一个函数负责响应所有
tools/list请求。 - 处理所有工具调用:同样只有一个函数处理任何
tools/call请求。
换言之,你不再"注册"某个工具,而是确保两件事:当列出工具时它出现在清单里;当有调用请求进来时它能够被正确调用。这种思路立刻让工作量看起来更少、更集中。
第一个处理器:列出工具
Python
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""List available tools."""
return [
types.Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
},
)
]
TypeScript
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// 返回已注册的工具列表
return {
tools: [{
name: "add",
description: "Add two numbers",
inputSchema: {
"type": "object",
"properties": {
"a": {"type": "number", "description": "number to add"},
"b": {"type": "number", "description": "number to add"}
},
"required": ["query"],
}
}]
};
});
注意两个细节:
- 返回的工具列表中的每一项都包含
name、description、inputSchema字段,这与 MCP 协议中Tool类型的返回结构完全一致; - 示例中的
"required": <a href="https://link.gitcode.com/i/fe2cbddf62fa916bb07c3ca149f63f66" target="_blank">"query"]是原文文档遗留的笔误——对照仓库完整实现(如 [python/server.py 与 TypeScript 端由 Zod 生成的 JSON Schema),真正的必填参数应为["a", "b"]。
工具可以放到任意位置
由于列工具只是"遍历一份清单",工具的物理存放位置就不再受限于服务器入口文件。我们可以把每个工具、资源和提示词都放到自己的目录中,项目结构随之变得非常规整:
app
--| tools
----| add
----| substract
--| resources
----| products
----| schemas
--| prompts
----| product-description
第二个处理器:调用工具
调用任意工具同样只有一个处理器。处理器负责从请求中解析出"要调用哪个工具、带哪些参数",再去真正执行调用。
Python
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict[str, str] | None
) -> list[types.TextContent]:
# tools 是以工具名为键的字典
if name not in tools.tools:
raise ValueError(f"Unknown tool: {name}")
tool = tools.tools[name]
result = "default"
try:
result = await tool"handler"
except Exception as e:
raise ValueError(f"Error calling tool {name}: {str(e)}")
return [
types.TextContent(type="text", text=str(result))
]
说明:原文文档此处将
tool"handler"误渲染为一段残缺的相对链接,仓库中的真实实现即为 python/server.py 所示,参数原样传入处理器函数。
TypeScript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if(!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
// args: request.params.arguments
// TODO call the tool,
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
});
到这里,我们已经看到低层服务器的核心机制:注册工作被"清单维护 + 统一分发"两个处理器取代。
用校验(Validation)改进调用流程
仅有分发还不够——工具必须用正确的参数被调用。因此我们要做两件事:
- 把创建特性(工具、资源、提示词)的逻辑移动到各自的专属目录;
- 为收到的调用请求增加参数校验。
每种运行时都有自己的校验方案:Python 用 Pydantic,TypeScript 用 Zod。校验逻辑既可以放在统一的调用处理器里,也可以下沉到具体工具的 handler 函数中,两种做法各有利弊,下文分别展示。
Python:Pydantic 定义 Schema 并校验
在 schema.py 中用 Pydantic 定义输入模型:
# schema.py
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
在 add.py 中尝试把请求参数解析为 AddInputModel,参数不匹配时抛错:
# add.py
from .schema import AddInputModel
async def add_handler(args) -> float:
try:
# 使用 Pydantic 模型校验输入
input_model = AddInputModel(**args)
except Exception as e:
raise ValueError(f"Invalid input: {str(e)}")
# 处理器函数:执行实际加法
return float(input_model.a) + float(input_model.b)
然后以字典形式对外暴露工具的全部元信息:
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
你可以自主决定把这段解析校验逻辑放在调用处理器里,还是放在 handler 函数里。仓库示例选择的是后者——校验在 add_handler 内完成,统一处理器则负责分发与兜底(见 python/tools/add.py)。
TypeScript:Zod 定义 Schema 并在处理器中校验
server.ts 中的调用处理器引入 rawSchema 解析参数,成功后再调用实际回调:
// server.ts
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { params: { name } } = request;
let tool = tools.find(t => t.name === name);
if (!tool) {
return {
error: {
code: "tool_not_found",
message: `Tool ${name} not found.`
}
};
}
const Schema = tool.rawSchema;
try {
const input = Schema.parse(request.params.arguments);
// @ts-ignore
const result = await tool.callback(input);
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
} catch (error) {
return {
error: {
code: "invalid_arguments",
message: `Invalid arguments for tool ${name}: ${error instanceof Error ? error.message : String(error)}`
}
};
}
});
schema.ts 集中存放各工具的 Zod 输入模式:
// schema.ts
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
add.ts 组装工具定义,借助 zod-to-json-schema 同时产出协议层可用的 JSON Schema:
// add.ts
import { Tool } from "./tool.js";
import { MathInputSchema } from "./schema.js";
import { zodToJsonSchema } from "zod-to-json-schema";
export default {
name: "add",
rawSchema: MathInputSchema,
inputSchema: zodToJsonSchema(MathInputSchema),
callback: async ({ a, b }) => {
return {
content: [{ type: "text", text: String(a + b) }]
};
}
} as Tool;
- 处理器先用
Schema.parse(request.params.arguments)校验入参; - 校验通过后调用
await tool.callback(input)执行真实逻辑; - 校验失败则返回
invalid_arguments错误码。
这样的架构让一切各归其位:server.ts 只做请求处理器的装配(非常短小),每个特性则位于 tools/、resources/ 或 prompts/ 各自的目录中。
练习:动手构建一个低层服务器
接下来我们基于仓库 03-GettingStarted/10-advanced/code 中的完整示例,逐步构建一个可运行的、带校验的低层服务器。练习目标:
- 创建一个低层服务器,处理工具列表与工具调用;
- 实现一个可继续扩展的架构;
- 添加校验,确保工具调用参数被正确验证。
-1- 搭建项目架构
Python 目录结构:
server.py
--| tools
----| __init__.py
----| add.py
----| schema.py
client.py
TypeScript 目录结构:
server.ts
--| tools
----| add.ts
----| schema.ts
client.ts
这套架构保证了"新增工具只需往 tools 目录加文件"。你也可以照此思路为资源和提示词建立子目录。
-2- 创建工具
Python:工具文件 add.py 定义名称、描述、Pydantic 输入模式与处理器,并导出字典 tool_add;schema.py 定义输入模式;init.py 让 tools 目录成为模块并集中导出工具字典,之后每加一个新工具只需在 __init__.py 追加一条:
# tools/__init__.py
from .add import tool_add
tools = {
tool_add["name"] : tool_add
}
仓库实现与此一致(见 python/tools/init.py、python/tools/add.py、python/tools/schema.py)。
TypeScript:每个工具默认导出一个包含四类属性的对象,并通过 Tool 接口将其转换为 MCP 处理器可接受的类型(见 typescript/src/tools/tool.ts):
import { z } from 'zod';
export interface Tool {
name: string;
inputSchema: any;
rawSchema: z.ZodTypeAny;
callback: (args: z.infer<z.ZodTypeAny>) => Promise<{ content: { type: string; text: string }[] }>;
}
各属性的分工:
name:工具名称;rawSchema:Zod 模式,用于校验调用该工具的入参;inputSchema:由zodToJsonSchema生成的 JSON Schema,供处理器 / 协议层使用;callback:真正调用工具时执行的函数。
schema.ts 集中存放各工具的输入模式,随着工具增多持续追加条目(见 typescript/src/tools/schema.ts)。
-3- 处理工具列表
Python:使用 @server.list_tools() 装饰器实现 handle_list_tools,遍历工具字典并转换为 types.Tool。注意每个工具都需要 name、description 与 inputSchema。仓库完整实现还包含一个 convert_to_json 辅助函数,负责把 Pydantic 模型的 schema() 转换为协议的 JSON Schema 结构(见 python/server.py)。
TypeScript:在服务器上通过 setRequestHandler(ListToolsRequestSchema, ...) 注册列表处理器。工具清单由 tools/index.ts 集中维护:它导入 addTool、subtractTool 并 push 进 tools 数组(见 typescript/src/tools/index.ts),server.ts 只需返回该数组(见 typescript/src/server.ts)。
-4- 处理工具调用
Python:用 @server.call_tool() 装饰器实现 handle_call_tool。函数内先校验工具名是否存在于工具字典,找不到即抛出 Unknown tool;随后以参数调用 handler,异常被包装为 Error calling tool 抛出让客户端感知(见 python/server.py)。
TypeScript:注册 CallToolRequestSchema 处理器。流程是:按 name 在工具数组中查找 → 不存在则返回 tool_not_found 错误 → 用 rawSchema 解析参数 → 调用 callback → 成功返回文本内容,失败返回 invalid_arguments 错误(见 typescript/src/server.ts)。
至此,你已经完整掌握了低层服务器的工具列表与工具调用机制。
运行与验证完整示例
Python 端
按照 code/python/README.md 的步骤:
python -m venv venv
source ./venv/bin/activate
pip install "mcp[cli]"
python client.py
预期输出:
Available tools: ['add']
Result of add tool: meta=None content=[TextContent(type='text', text='8.0', annotations=None, meta=None)] structuredContent=None isError=False
客户端 client.py 通过 stdio 启动 server.py,建立 ClientSession 后依次调用 session.list_tools() 与 session.call_tool("add", { "a": 5, "b": 3 }),直接验证了低层服务器的列表与调用两个处理器。
TypeScript 端
按照 code/typescript/README.md 的步骤:
npm install
npm run build
npm start
预期看到:
Registering tools...
Starting server...
随后可用 MCP Inspector 进行交互式或命令行测试:
npx @modelcontextprotocol/inspector node build/app.js
CLI 模式列出工具:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/list
返回结果中 add 与 subtract 两个工具的 inputSchema 均由 Zod 自动转换为 JSON Schema(含 required: ["a","b"] 与 additionalProperties: false),这正是 zod-to-json-schema 与 rawSchema 协作的产物。
CLI 模式调用工具:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg b=2
预期看到结果文本中回显参数与计算结果。
调用不存在的工具:
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add2 --tool-arg a=1 --tool-arg b=2
预期返回:
{
"content": [],
"error": {
"code": "tool_not_found",
"message": "Tool add2 not found."
}
}
传入非法参数(缺少 b、多出 c):
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg c=2
预期返回 invalid_arguments 错误,并附带 Zod 给出的详细失败原因(例如 b 字段缺失),这直观证明了校验逻辑已生效。
作业:扩展你的服务器
尝试在现有代码基础上新增若干工具、资源与提示词,并观察一个事实:你只需要在 tools(以及对应的 resources / prompts)目录中新增文件,无需改动服务器入口文件——因为列表与调用都由统一处理器驱动。例如 Python 端只需在 tools/__init__.py 追加 from .subtract import tool_subtract,TypeScript 端只需在 tools/index.ts 中 import subtractTool 并 tools.push(subtractTool)。
本练习不提供标准答案,因为不同的扩展方向各有合理的实现。
总结
本章围绕低层服务器展开:我们看到了它与常规服务器的本质差异——以"每类能力两个处理器"替代逐条注册;掌握了用 Pydantic 与 Zod 构建输入校验模式、保证工具以正确参数被调用的方法;并通过 Python / TypeScript 双语言完整示例,实践了可扩展目录架构的搭建与 MCP Inspector 的验证流程。
下一步建议进入简单认证(Simple Authentication)章节,为低层服务器接入鉴权能力,进一步靠近生产级 MCP 服务。