1.2 最小可运行服务端:三大装饰器初体验


1.2 最小可运行服务端:三大装饰器初体验

本节摘要:环境装好后,我们用十几行代码写出第一个 MCP 服务端。核心是体验 SDK 最关键的哲学——类型即契约:你不写一行 JSON Schema,SDK 从函数的名字、文档字符串、类型注解里替你读出全部元数据。我们会构造一个 MCPServer 实例,用 @mcp.tool() 注册一个「两数相加」工具,再用 @mcp.resource(uri) 注册一个「问候」资源模板。写完后,你会理解为什么这三个装饰器能替代传统 RPC 框架里那一大套手动声明。读完本节,你手里就有一个能被客户端连接的服务端了。

一、构造 MCPServer:一个能力容器

一切从一个构造调用开始:

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()

现在往这个容器里注册第一个工具:

@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(uri)

工具是「模型决定调用」的原语。现在看另一类原语——资源:

@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} 是路径参数。这个类比贯穿整个资源体系。

四、装饰器背后:SDK 替你省了什么

现在退一步,看看这三个装饰器到底替你省了多少事。如果用传统方式(手写 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}!"

这个服务端现在还不能「跑」——它只是定义了能力,还没有被传输层驱动。要让它真正能用,有两条路:

  1. 内存连接(下一节):用 SDK 自带的 Client 在进程内直接连,适合测试验证。
  2. 传输启动(第 8 章):用 mcp.run() 跑成 stdio 子进程或 HTTP 服务。

本节我们先不启动它,下一节用内存客户端连上去验证。

本节要点回顾

  1. MCPServer("名字") 创建的是一个能力容器,组织工具/资源/提示词管理器,本身不做协议。
  2. 导入路径是 from mcp.server import MCPServer,客户端则是 from mcp import Client
  3. @mcp.tool() 从函数读出四样东西:名字、描述、输入 Schema、输出 Schema,你无需手写。
  4. 文档字符串会被原样发给模型,写好它等于帮模型理解工具。
  5. URI 带 {param} 是资源模板,参数化数据;无参数是具体资源,固定数据。
  6. 三个装饰器替代了传统 RPC 的六个步骤,这个比例是 SDK 存在的全部理由。

服务端写好了但还没跑起来,下一节我们用内存客户端连上去,亲手调一次工具,验证「真的能用」。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U