mcp-for-beginners 高级服务器用法:用 Low-Level Server 构建可扩展的 MCP 工具架构
mcp-for-beginners 高级服务器用法:用 Low-Level Server 构建可扩展的 MCP 工具架构
导读
在 MCP(Model Context Protocol)开发中,常规服务器(Regular Server)与底层服务器(Low-Level Server)代表了两种截然不同的扩展思路。本篇技术指南以 mcp-for-beginners 课程的“高级服务器用法”章节为核心,对比常规服务器"逐个注册工具"与底层服务器"每类功能只写两个 Handler"两种模式,并通过 Python(Pydantic)与 TypeScript(Zod)双语言实现,手把手带你搭建一个支持工具列表、工具调用与入参校验的低层服务器,最终得到一个只需在 tools/ 目录新增文件即可无限扩展的模块化架构。
为什么要关注 Low-Level Server
MCP SDK 暴露了两类服务器:常规服务器(Regular Server)与低层服务器(Low-Level Server)。常规服务器足以覆盖绝大多数功能扩展需求,但以下两类场景会让你更倾向于使用低层服务器:
- 更好的架构:虽然用常规服务器也能搭建整洁的分层架构,但使用低层服务器时,工具、资源与提示词(Prompt)天然以"目录即模块"的方式组织,架构更易于维护与扩展。
- 功能可用性:部分高级能力只能通过低层服务器使用。课程中提到,后续章节会涉及 Elicitation 与 legacy Sampling 功能,其中 Sampling 在 MCP
2026-07-28规范中已被标记为弃用。
关于 2026-07-28 规范的背景,可以参考仓库中的 01-CoreConcepts/mcp-2026-07-28.md:在该版本中,Roots、Sampling、Logging 与动态客户端注册均进入 Deprecated 生命周期,Sampling 的官方替代方案是"直接集成 LLM 提供方 API"。因此,当你需要使用这些只能在低层服务器上启用的高级能力时,理解低层服务器的编程模型是必要前提。
常规服务器与低层服务器的对比
常规服务器:显式注册每个功能
常规服务器的核心思路是:你想让服务器拥有哪些工具、资源或提示词,就逐个显式注册它们。
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) }]
})
);
这种模式本身没有任何问题,课程前序章节(如 03-GettingStarted/01-first-server/README.md)正是基于 FastMCP 与 @mcp.tool() 装饰器完成第一个服务器构建。但当功能数量增长后,注册逻辑与业务逻辑交织在同一处,架构会逐渐变得难以维护。
低层服务器:每类功能只写两个 Handler
低层服务器要求你换一种思维方式:不再逐个注册工具,而是为每种功能类型(工具、资源或提示词)只创建两个 Handler。以工具为例:
- 列出所有工具:一个函数统一负责所有"列出工具"的请求;
- 调用所有工具:一个函数统一处理所有"调用工具"的请求。
这意味着,新增工具时你不再需要触碰服务器注册代码,只需保证"列出时它会被返回,调用时它会被路由到正确的处理函数"即可。
用低层服务器实现工具列表,代码如下:
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 字段以符合返回类型要求。由于工具定义与服务器 Handler 解耦,你可以把工具与功能定义放到独立目录中,项目结构突然变得非常清爽:
app
--| tools
----| add
----| substract
--| resources
----| products
----| schemas
--| prompts
----| product-description
工具调用同样遵循"一个 Handler 管所有调用"的思路:
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))
]
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 调用工具
return {
content: [{ type: "text", text: `Tool ${name} called with arguments: ${JSON.stringify(input)}, result: ${JSON.stringify(result)}` }]
};
});
如你所见,调用处理器的职责非常清晰:先从请求中解析出"要调用哪个工具、携带什么参数",再把请求路由到对应工具的执行逻辑。
用校验改进架构:Pydantic 与 Zod
仅仅拥有"列表 + 调用"两个 Handler 还不够,你还需要入参校验,确保工具始终以正确的参数被调用。每种运行时都有对应的校验方案:Python 使用 Pydantic,TypeScript 使用 Zod。改进思路是:
- 把创建功能(工具、资源或提示词)的逻辑移到各自的专属目录;
- 为入站请求(例如工具调用请求)增加校验能力。
Python:用 Pydantic 定义 Schema 并校验
先创建 schema.py,用 Pydantic 定义输入模型:
# schema.py
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
再创建 add.py,在 Handler 中尝试把入参解析为 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)}")
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
这段代码完成了三件事:定义名称与描述;把输入模型作为 input_schema;暴露一个持有全部属性的 tool_add 字典。你既可以把解析逻辑放在工具调用处理器中,也可以像这里一样放在 Handler 函数内部,两种位置都可以。
TypeScript:用 Zod 定义 Schema 并校验
在服务器文件中,统一调用处理器现在会先尝试把入参解析为工具定义的 Schema:
// 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 定义放在 schema.ts:
// schema.ts
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
工具定义放在 add.ts,通过 zodToJsonSchema 把 Zod Schema 转换为 MCP 所需的 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;
解析成功后便调用工具本体:
const result = await tool.callback(input);
校验失败则返回 invalid_arguments 错误。这一套组合拳让架构非常清晰:server.ts 是一份极小的文件,只负责把请求 Handler 接线;每个功能都安放在自己的目录(tools/、resources/、prompts/)中。
实战练习:亲手搭建一个 Low-Level Server
下面进入课程练习,目标是:创建一个负责"列出工具 + 调用工具"的低层服务器,落地一套可持续扩展的架构,并加入入参校验。
第 1 步:创建目录架构
一个支持随功能增长而横向扩展的架构如下:
Python
server.py
--| tools
----| __init__.py
----| add.py
----| schema.py
client.py
TypeScript
server.ts
--| tools
----| add.ts
----| schema.ts
client.ts
这套结构保证:新增工具只需在 tools/ 目录中添加文件;资源与提示词可按同样方式建立 resources/、prompts/ 子目录。
第 2 步:创建工具
Python:在 tools/ 子目录中创建工具文件。定义名称、描述、基于 Pydantic 的输入 Schema 与被调用时执行的 Handler,最后以字典 tool_add 暴露全部属性:
# 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)}")
"""Handler function for the add tool."""
return float(input_model.a) + float(input_model.b)
tool_add = {
"name": "add",
"description": "Adds two numbers",
"input_schema": AddInputModel,
"handler": add_handler
}
配套的 schema.py 定义工具输入 Schema:
# schema.py
from pydantic import BaseModel
class AddInputModel(BaseModel):
a: float
b: float
还需要填充 __init__.py,让 tools/ 被当作 Python 模块,并暴露其中的模块:
# __init__.py
from .add import tool_add
tools = {
tool_add["name"] : tool_add
}
后续每新增一个工具,只需往这个字典里追加一条记录。
TypeScript:工具是一个包含四类属性的字典对象:
name:工具名称;rawSchema:Zod Schema,用于校验调用该工具的入站请求;inputSchema:供 Handler 使用的 JSON Schema;callback:实际调用工具的函数。
// 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;
Tool 接口负责把这个字典转换为 MCP 服务器 Handler 可以接受的类型:
// 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 }[] }>;
}
schema.ts 集中存放各工具的输入 Schema,目前只有一个条目,新增工具时继续追加即可:
// schema.ts
import { z } from 'zod';
export const MathInputSchema = z.object({ a: z.number(), b: z.number() });
第 3 步:处理工具列表
在服务器文件中注册"列出工具"的请求处理器。
Python:添加 @server.list_tools 装饰器与 handle_list_tools 实现函数,遍历 tools 字典生成工具列表。每个工具都必须包含名称、描述与 inputSchema:
# server.py(为简洁省略其他代码)
from tools import tools
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
tool_list = []
for tool in tools.values():
tool_list.append(
types.Tool(
name=tool["name"],
description=tool["description"],
inputSchema=pydantic_to_json(tool["input_schema"]),
)
)
return tool_list
TypeScript:在服务器上调用 setRequestHandler,传入 ListToolsRequestSchema:
// index.ts
import addTool from "./add.js";
import subtractTool from "./subtract.js";
import {server} from "../server.js";
import { Tool } from "./tool.js";
export let tools: Array<Tool> = [];
tools.push(addTool);
tools.push(subtractTool);
// server.ts(为简洁省略其他代码)
import { tools } from './tools/index.js';
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
// 返回已注册工具列表
return {
tools: tools
};
});
第 4 步:处理工具调用
Python:使用 @server.call_tool 装饰器实现 handle_call_tool。函数内部分离出工具名与其参数,并确保参数对该工具有效。参数校验可以放在本函数内,也可以下放到实际工具中:
@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))
]
运行机制拆解:
- 工具名已作为输入参数
name传入,参数则存放在arguments字典中; - 通过
result = await tool"handler"调用工具,参数校验发生在handler属性指向的函数内部;若校验失败会抛出异常。
到这里,你已经完整掌握了使用低层服务器"列出工具"与"调用工具"的全部套路。
仓库源码级验证:完整示例对照
课程附带了可直接运行的完整示例,与上文讲解一一对应:
Python 示例(03-GettingStarted/10-advanced/code/python/):
- server.py 中,服务器通过
Server("example-server")创建,handle_list_tools遍历tools字典并借助convert_to_json()把 Pydantic 模型转换为 JSON Schema;handle_call_tool通过tool"handler"路由调用,未知名工具直接抛出ValueError。 - tools/add.py 与 tools/schema.py 实现了"Schema 定义 + Handler + 工具字典"的完整工具模块。
- tools/init.py 把
add工具注册进tools字典,新增工具只需继续追加。 - client.py 通过
stdio_client+ClientSession启动会话,调用session.list_tools()与session.call_tool("add", {"a": 5, "b": 3})验证端到端链路。
TypeScript 示例(03-GettingStarted/10-advanced/code/typescript/):
- src/server.ts 使用
ListToolsRequestSchema与CallToolRequestSchema两个请求 Schema 注册 Handler;调用处理器先经tool.rawSchema.parse()校验参数,再执行tool.callback(input),并区分tool_not_found与invalid_arguments两类错误码。 - src/tools/add.ts 与 src/tools/subtract.ts 展示了"同一 Schema、不同 callback"的工具复用模式。
- src/tools/index.ts 通过
tools.push(addTool)集中登记工具,文件中被注释掉的server.tool(...)调用恰好展示了常规服务器注册写法与低层写法的差异。 - 依赖配置见 package.json,其中
@modelcontextprotocol/sdk版本要求>=1.26.0。
运行与验证
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
TypeScript:按 code/typescript/README.md 操作:
npm install
npm run build
npm start
启动成功会看到 Registering tools... 与 Starting server...。随后可用 MCP Inspector 的 CLI 模式做三类验证:
# 1. 列出工具
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/list
# 2. 正常调用工具
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg b=2
# 3. 调用不存在的工具(应返回 tool_not_found)
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add2 --tool-arg a=1 --tool-arg b=2
# 4. 传入 Schema 拒绝的参数(应返回 invalid_arguments)
npx @modelcontextprotocol/inspector --cli node ./build/app.js --method tools/call --tool-name add --tool-arg a=1 --tool-arg c=2
tools/list 会返回包含 add 与 subtract 两个工具的 JSON Schema(含 required、additionalProperties 等字段);调用 add2 时返回 {"code": "tool_not_found", "message": "Tool add2 not found."};传入非法参数 c 时返回 invalid_arguments 并携带 Zod 的详细校验错误信息。这三条命令完整印证了"列表、路由、校验"三个环节全部生效。
作业:按你的需求扩展
课程的作业设计非常能体现低层服务器的价值:在现有代码基础上扩展一批工具、资源与提示词,并反思——你会发现只需要在 tools/ 目录中添加文件,其他地方一概不用改动。
课程未提供标准答案,这正是刻意留给你独立实践的空间。
本章小结
本章围绕低层服务器展开:
- 理解了低层服务器"每类功能(工具、资源、提示词)仅两个 Handler"的核心编程模型——一个负责列表、一个负责调用;
- 看到了这种模型如何天然催生"目录即模块"的整洁架构,使服务器主文件保持极小,功能按类型各自归位;
- 掌握了用校验库(Python Pydantic / TypeScript Zod)创建输入 Schema、对入站调用请求做参数校验的标准做法;
- 通过完整的 Python 与 TypeScript 示例,走通了从目录设计、工具创建、列表处理到调用处理的全流程。
下一章将在此基础上引入鉴权能力:简单认证(Simple Authentication)。