本节摘要:服务端主线开始。本节先把
MCPServer这个高层容器的构造与生命周期讲透。MCPServer("名称")创建的不只是一个对象,而是一个能力容器——它把工具管理器、资源管理器、提示词管理器组织起来,等待你注册函数,也等待被传输层驱动。我们会讲清它的构造参数、它内部管理的三大子管理器、以及「定义能力」与「真正运行」两个阶段的分离。理解这个容器,你才能理解后续每一章往里「放东西」时到底放到了哪。
最简构造只需要一个参数——服务端名字:
from mcp.server import MCPServer mcp = MCPServer("Demo")
这一行做了什么?它在内存里建起了一个空的能力容器,内部已经组织好三大子管理器:
MCPServer("Demo") 内部结构 ┌─────────────────────────────────────────┐ │ MCPServer(能力容器) │ │ ├── 工具管理器(ToolManager) │ ← @mcp.tool 往这放 │ ├── 资源管理器(ResourceManager) │ ← @mcp.resource 往这放 │ ├── 提示词管理器(PromptManager) │ ← @mcp.prompt 往这放 │ └── (内部状态:能力声明、版本等) │ └─────────────────────────────────────────┘
此时这个容器还是空的——没注册任何工具/资源/提示词,也没有被传输驱动。它只是一个「等待被填充、等待被运行」的容器。
MCPServer 的构造参数,常用的有这几个:
| 参数 | 类型 | 作用 | 必填 |
|---|---|---|---|
| 名称 | 字符串 | 服务端名字,出现在能力声明与日志里 | 是 |
| 依赖/配置 | (可选) | 注入额外配置,如自定义中间件 | 否 |
| 提示 | (可选) | 控制指令提示等高级选项 | 否 |
第 1 章我们只用了名字。后续章节会逐步用到其他参数——比如第 12 章会讲如何注入自定义中间件。本节记住一点:构造期只决定「这个容器长什么样」,不决定「它现在在跑什么」。
💡 技巧:服务端名字虽然只是个标识,但起得好能省很多调试麻烦。建议起一个能体现用途的名字,如
"database-tools"、"file-resources",而不是"server"、"mcp"这种无信息量的名字。这个名字会出现在客户端的能力声明、检查器界面、日志里。
理解 MCPServer 的关键,是认清它的生命有两个独立阶段:
阶段一:定义能力(注册期) ┌─────────────────────────────────────────┐ │ mcp = MCPServer("Demo") │ │ @mcp.tool() │ │ def add(...): ... │ ← 往容器里放能力 │ @mcp.resource(...) │ │ def greeting(...): ... │ └─────────────────────────────────────────┘ ↑ 此时尚未运行,只是定义 阶段二:真正运行(运行期) ┌─────────────────────────────────────────┐ │ mcp.run() # 跑成 stdio 服务 │ ← 被传输驱动 │ # 或 │ │ mcp.run("streamable-http") │ ← 跑成 HTTP 服务 └─────────────────────────────────────────┘ ↑ 现在才真正开始接受客户端连接
这两个阶段的分离,是 SDK 设计的一个关键。它带来三个好处:
| 好处 | 说明 |
|---|---|
| 可测试 | 阶段一定义的容器,可在不启动传输的情况下用内存客户端测(第 1.3 节) |
| 可组合 | 同一个容器定义,可分别用不同传输跑(stdio/HTTP),业务代码不变 |
| 可检查 | 阶段一就能用检查器看能力清单,不用真启动 |
阶段一(注册期)里,你用三个装饰器往容器放能力:
mcp = MCPServer("Demo") @mcp.tool() # 放进工具管理器 def add(a: int, b: int) -> int: ... @mcp.resource("greeting://{name}") # 放进资源管理器 def greeting(name: str) -> str: ... @mcp.prompt() # 放进提示词管理器 def summarize(text: str) -> str: ...
三个装饰器背后是同一套哲学:函数即元数据来源。它们都从函数身上读名字、描述、参数,差别只在「读出来当什么」:
| 装饰器 | 函数名当 | 文档字符串当 | 类型注解当 |
|---|---|---|---|
@mcp.tool() |
工具名 | 工具描述(给模型) | 输入 Schema(给模型) |
@mcp.resource(uri) |
处理器名(内部) | 资源描述 | URI 参数(从 URI 占位符) |
@mcp.prompt() |
提示词名 | 提示词描述 | 扁平字符串参数列表 |
注意资源稍微特别——它的参数既来自 URI 占位符({name}),也来自函数签名,SDK 会把两者对齐。第 5 章详讲。
阶段二(运行期)才让容器真正工作。最常见的两种启动方式:
# 方式一:跑成 stdio 服务(本地默认) mcp.run() # 方式二:跑成流式 HTTP 服务(生产) mcp.run("streamable-http")
run() 这个方法会阻塞(进入事件循环),开始监听传输、接受客户端连接、分发请求。在第 8 章会详讲每种传输的具体配置,这里先建立直觉:run() 之前是「定义」,run() 之后是「运行」。
⚠️ 注意:内存连接(第 1.3 节的
Client(mcp))是这条规律的例外——它不调用mcp.run(),而是直接把容器对象喂给客户端,在进程内驱动。这正是它适合测试的原因:跳过传输、跳过运行期启动,直接验证阶段一定义的能力。
这里先剧透一点第 3.3 节的内容,帮你建立整体感。MCPServer 在客户端连接时,会根据已注册的能力,自动推断并声明 capabilities:
| 你注册了 | 服务端自动声明 |
|---|---|
任意 @mcp.tool() |
tools 能力 |
任意 @mcp.resource() |
resources 能力 |
任意 @mcp.prompt() |
prompts 能力 |
| 一个补全处理器 | completions 能力 |
| 都没注册 | (对应能力不声明) |
这就是第 1.3 节看到的 server_capabilities 字典的来源——它不是你写的,是 MCPServer 从你注册的东西推断出来的。这条「注册即声明」的规则贯穿整个 SDK,第 3.3 节会专门讲。
MCPServer("名称") 创建的是一个能力容器,内部组织工具/资源/提示词三大管理器。mcp.run() 是阶段二的入口,之前是定义,之后是运行;内存连接是这条规律的例外。容器清楚了,下一节我们逐个展开三个装饰器,看它们如何从函数读出元数据。