本节摘要:环境装好后,我们用十几行代码写出第一个 MCP 服务端。核心是体验 SDK 最关键的哲学——类型即契约:你不写一行 JSON Schema,SDK 从函数的名字、文档字符串、类型注解里替你读出全部元数据。我们会构造一个
MCPServer实例,用@mcp.tool()注册一个「两数相加」工具,再用@mcp.resource(uri)注册一个「问候」资源模板。写完后,你会理解为什么这三个装饰器能替代传统 RPC 框架里那一大套手动声明。读完本节,你手里就有一个能被客户端连接的服务端了。
一切从一个构造调用开始:
from mcp.server import MCPServer mcp = MCPServer("Demo")
这一行做了什么?它创建了一个能力容器——mcp 这个对象本身不做协议,它的职责是把「工具管理器」「资源管理器」「提示词管理器」组织起来,等待你往里注册函数,也等待被传输层驱动。
构造参数只有一个必填的:服务端名字("Demo")。这个名字会出现在客户端的能力声明、日志、检查器界面里,起识别作用。
💡 技巧:注意导入路径是
from mcp.server import MCPServer,不是from mcp import MCPServer。这是 SDK 有意区分服务端与客户端的导入入口——客户端是from mcp import Client,服务端多一层.server。这个区分会在第 9 章讲客户端时再次出现。
现在往这个容器里注册第一个工具:
@mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b
就这么几行。@mcp.tool() 装饰器把一个普通函数变成了 MCP 工具。但真正发生的事情远不止「打个标签」——SDK 在装饰的那一刻,从函数身上读出了四样东西:
| 函数身上的东西 | SDK 读出来当什么 | 最终发给模型的形式 |
|---|---|---|
函数名 add |
工具名 | "name": "add" |
文档字符串 """Add two numbers.""" |
工具描述 | "description": "Add two numbers." |
参数注解 a: int, b: int |
输入 Schema | {"a": {"type": "integer"}, "b": {"type": "integer"}} |
返回注解 -> int |
输出 Schema | 结构化输出(第 4 章详讲) |
这就是「类型即契约」的字面含义——你不声明 Schema,SDK 从函数读出来。模型最终看到的,是一份完整的、带类型描述的工具清单,而你没有为此写过一行 JSON Schema。
⚠️ 注意:文档字符串不是装饰,它会被原样发给模型当工具描述。一个清晰的文档字符串(
"""Add two numbers.""")能让模型更准确地判断「这个工具该不该调用」;而一个模糊或缺失的文档字符串,会让模型乱调用。养成写文档字符串的习惯,等于在帮模型理解你的工具。
工具是「模型决定调用」的原语。现在看另一类原语——资源:
@mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!"
这里有个关键细节:URI greeting://{name} 里带了 {name} 这个占位符。它的存在,让这个资源变成了资源模板(Resource Template) 而不是「具体资源」。
两者的差别很重要,第 5 章会详细讲,这里先记住一句:
| 形态 | URI 形式 | 能否直接列举 | 何时用 |
|---|---|---|---|
| 具体资源 | config://app(无参数) |
能,直接出现在资源列表 | 只有一份固定数据 |
| 资源模板 | greeting://{name}(带参数) |
不能,需提供参数实例化 | 同一模式、参数化数据 |
URI 里的 {name} 同时也是函数的参数名——SDK 用这个对应关系,把「读资源」请求里的参数,自动映射成函数调用。所以当客户端请求 greeting://World,SDK 就调用 greeting("World"),拿到 "Hello, World!"。
💡 技巧:把 URI 想象成 Web API 的路径。
greeting://{name}类比GET /greeting/{name},{name}是路径参数。这个类比贯穿整个资源体系。
现在退一步,看看这三个装饰器到底替你省了多少事。如果用传统方式(手写 JSON-RPC + Schema),实现同样的「一个工具」你要做:
传统做法(不用 SDK) ┌──────────────────────────────────────────┐ │ 1. 手写 add 的 JSON Schema(参数、类型) │ │ 2. 注册一个 tools/list 处理器,返回清单 │ │ 3. 注册一个 tools/call 处理器,分发到 add │ │ 4. 手动解析参数、校验类型、转换错误 │ │ 5. 把返回值包装成协议响应 │ │ 6. 声明 tools 能力,告诉客户端「我有工具」│ └──────────────────────────────────────────┘
而用 SDK,你只写了这个:
@mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b
六个步骤被三个装饰器+一套类型推断整套省掉。 这个比例(你写 1 行,SDK 替你做 6 步)就是这个 SDK 存在的全部理由。后面第 2 章会从架构视角讲清楚这套推断机制是怎么组织的,第 4 章会深入工具契约的细节。
把上面两段拼起来,就是全书反复用到的最小服务端:
from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!"
这个服务端现在还不能「跑」——它只是定义了能力,还没有被传输层驱动。要让它真正能用,有两条路:
Client 在进程内直接连,适合测试验证。mcp.run() 跑成 stdio 子进程或 HTTP 服务。本节我们先不启动它,下一节用内存客户端连上去验证。
MCPServer("名字") 创建的是一个能力容器,组织工具/资源/提示词管理器,本身不做协议。from mcp.server import MCPServer,客户端则是 from mcp import Client。@mcp.tool() 从函数读出四样东西:名字、描述、输入 Schema、输出 Schema,你无需手写。{param} 是资源模板,参数化数据;无参数是具体资源,固定数据。服务端写好了但还没跑起来,下一节我们用内存客户端连上去,亲手调一次工具,验证「真的能用」。