Skip to content

Commit b3ecd90

Browse files
authored
fix(backend): 接口适配 MCP 2.0 (#2752)
## 为什么改 MCP 2.0 移除了旧版 FastMCP 接口,导致后端服务无法正常启动。 ## 改动要点 - 迁移到 MCPServer 及新版 ToolAnnotations - 将 MCP 依赖约束为 2.x - 更新相关开发文档 - 通过测试 - 改动量不大 Closes #2729
1 parent c16372a commit b3ecd90

11 files changed

Lines changed: 56 additions & 54 deletions

File tree

docs/develop/zzz/backend/design-principles.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ server 给「**事实**」,智能体做「**决策 + 通用理解**」。server
5555
### P3. 操作 vs 观察分离
5656
操作类 tool 改状态(进游戏 / 停止 / reload / 改配置),观察类只读(窗口 / 截图 / 运行态)。副作用两种标注:
5757
- **docstring** 文字说明(给智能体读);
58-
- **MCP tool annotations**(`ToolAnnotations(readOnlyHint=...)` 等,**字段名 camelCase**(mcp sdk 与 JSON 线一致;⚠️ 用 snake_case 会被 pydantic 当 extra 忽略、静默失效);机器可读,官方推荐)。
58+
- **MCP tool annotations**(`ToolAnnotations(read_only_hint=...)` 等,Python 使用 snake_case;序列化到 MCP JSON 时 SDK 自动转成 `readOnlyHint` 等 camelCase 字段;机器可读,官方推荐)。
5959

6060
具体怎么标(分类判据 + 代码写法)见 [mcp-implementation.md](mcp-implementation.md) 第 1 节。
6161

docs/develop/zzz/backend/entry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ GUI 的「开发工具 -> MCP 服务」页面提供本机 server 管理:
7979

8080
## 依赖(dev 组)
8181

82-
- `mcp`:FastMCP / streamable-http。
82+
- `mcp>=2,<3`:MCPServer / streamable-http。
8383
- `uvicorn`:ASGI server。
8484

8585
## 远程 SSH

docs/develop/zzz/backend/mcp-implementation.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
>
55
> **通用 MCP tool 设计方法论**(tool 命名 / description 契约 / tight input·output schema / annotations / 结构化返回 / actionable error / 读写分离 / token 经济)遵循 **Anthropic 官方 `mcp-server-dev` plugin 的 [`tool-design.md`](https://github.com/anthropics/claude-plugins-official/blob/main/plugins/mcp-server-dev/skills/build-mcp-server/references/tool-design.md)** —— 写 / 改 MCP tool 前先装它(Claude Code 安装见 [setup/ai_coding.md](../../setup/ai_coding.md))。**本文只讲本项目特化的写法与约束**,不重复通用知识。
66
>
7-
> 适用范围:`src/zzz_od/backend/mcp/`(`app.py` / `service_app.py` / `prompts.py`)下所有 `@mcp.tool`。SDK 基线:官方 `mcp` 包内置 `FastMCP`(`from mcp.server.fastmcp import FastMCP`),非第三方 gofastmcp
7+
> 适用范围:`src/zzz_od/backend/mcp/`(`app.py` / `service_app.py` / `prompts.py`)下所有 `@mcp.tool`。SDK 基线:官方 `mcp>=2,<3` 包内置 `MCPServer`(`from mcp.server import MCPServer`)
88
99
## 1. annotations:本项目 tool 怎么分类标
1010

@@ -16,21 +16,21 @@
1616

1717
本项目分类:
1818

19-
| 类别 | `readOnlyHint` | `destructiveHint` | 本项目 tool |
19+
| 类别 | `read_only_hint` | `destructive_hint` | 本项目 tool |
2020
|---|:---:|:---:|---|
2121
| 纯观察(只读,不改状态) | `True` || `check_game_window` / `capture_game_screen` / `analyze_screen` / `get_run_status` / `list_applications` / `list_operations` / `describe_operation` / `list_mcp_usage_guides` / `get_mcp_usage_guide` |
2222
| 操作游戏 / 触发运行 / 改配置(改状态非破坏) | 不标(默认非 read_only) || `click_game` / `key_tap` / `drag` / `input_text` / `open_game` / `run_one_dragon` / `run_standalone_app` / `run_operation` / `stop_run` / `upsert_screen_area` |
2323
| 不可逆 / 破坏性 | 不标 | `True` | `delete_screen_area`(删 screen_info area) / `close_game`(关游戏) |
2424

2525
**本项目特化**:`open_world_hint` / `idempotent_hint` **默认不标** —— 本项目 tool 都操作**本地游戏运行时**(属「外部世界」交互,`open_world` 保持 MCP 默认语义;`idempotent` 标了也无决策价值,click 幂等无需声明、run 不幂等)。判据:只在「客户端会因此改变确认 / 缓存策略」时才标。
2626

27-
导入:`from mcp.types import ToolAnnotations`(**字段名 camelCase**:`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` / `title`,与 JSON wire 一致;⚠️ snake_case 会被 pydantic 当 extra 忽略、静默失效)。工厂注册的 tool 同理:`mcp.tool(annotations=...)(make_xxx(backend))`
27+
导入:`from mcp.types import ToolAnnotations`。Python 参数和属性使用 snake_case:`read_only_hint` / `destructive_hint` / `idempotent_hint` / `open_world_hint` / `title`;SDK 序列化到 MCP JSON 时自动转成 camelCase。工厂注册的 tool 同理:`mcp.tool(annotations=...)(make_xxx(backend))`
2828

2929
## 2. Field 参数描述:本项目哪些参数必须加
3030

3131
(通用「用 Pydantic `Field` 加 description / 约束 / `Literal` 枚举」见官方 mcp-builder Phase 2。这里只讲本项目选哪些参数。)
3232

33-
**FastMCP 把函数签名转 JSON schema,但不解析 docstring 的 `Args:` 段成字段 description** —— 参数级说明只能靠 `Annotated[type, Field(description=...)]`
33+
**MCPServer 把函数签名转 JSON schema,但不解析 docstring 的 `Args:` 段成字段 description** —— 参数级说明只能靠 `Annotated[type, Field(description=...)]`
3434

3535
本项目**必须加 Field description** 的:智能体**不靠参数名 + 类型就懂不了**的 ——
3636
- 布尔开关的隐式语义(`save_image` / `pc_alt` / `block` / `enter` / `use_clipboard`);
@@ -87,11 +87,12 @@ def click_game(
8787
改 / 加 MCP tool 后逐条对照:
8888

8989
- [ ] docstring 三要素齐 + 首句「观察 / 操作」标注;
90-
- [ ] `annotations` 按第 1 节分类标了(观察 `readOnlyHint=True` / 破坏 `destructiveHint=True`,**字段名 camelCase**);
90+
- [ ] `annotations` 按第 1 节分类标了(观察 `read_only_hint=True` / 破坏 `destructive_hint=True`,Python 字段使用 snake_case);
9191
- [ ] **读写分离**:单 tool 不混观察 + 操作(通用硬要求);相似操作 tool(`click_game` / `key_tap` / `drag`)描述**互指**何时用另一个(disambiguate);
9292
- [ ] `title` annotation(可选):Anthropic Directory 提交时每个 tool 必须有 title;本项目不提交 Directory,按需作 UI 显示名;
9393
- [ ] 难懂参数加了 `Field description`;
9494
- [ ] 返回结构化(非裸字符串,单值 ack 除外)+ 错误兜底按第 3 节;
95+
- [ ] 同步 tool 可在 MCPServer 的工作线程中并行执行:不依赖固定线程;GPU session 继续经 `gpu_executor` 串行;
9596
- [ ] **同步 `mcp.md` 工具表**(签名 / 参数 / 返回 / tool 总数)—— 文档与实现脱节是最高频坑;
9697
- [ ] HTTP 对称能力是否也要补(两个适配器消费者都用 → 放 backend 共享,见 design P11);
9798
- [ ] 测试(`zzz-od-test/test/zzz_od/backend/`)断言对齐新返回;

docs/develop/zzz/backend/mcp.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
要点:
3535

3636
- `app.py` 放 MCP server 创建、基础 game tool 和总注册入口;`service_app.py` 放应用运行 tool 与自定义 op tool 工厂。
37-
- backend 实例通过闭包注入 tool,不使用全局单例,也不让 FastMCP lifespan 管 backend 生命周期。
38-
- `capture_game_screen` 落盘返回路径;`analyze_screen` 返回结构化 dataclass,由 FastMCP 序列化。
37+
- backend 实例通过闭包注入 tool,不使用全局单例,也不让 MCPServer lifespan 管 backend 生命周期。
38+
- `capture_game_screen` 落盘返回路径;`analyze_screen` 返回结构化 dataclass,由 MCPServer 序列化。
39+
- MCPServer 2 会在 AnyIO 工作线程中执行同步 tool;同步 backend 方法不能依赖固定调用线程,OCR / YOLO 仍须通过项目的 `gpu_executor` 串行调用。异步 tool 继续在事件循环中执行。
3940
- `analyze_screen(save_image=True)`(实时模式)把已截的内存图顺手存盘 + 回传 `screenshot_path`,供调用方喂 vision double-check;默认 `false` 不落盘,离线模式忽略。
4041
- `analyze_screen` 成功时返回 `vision_hint`:提醒本结果仅含 OCR + 模板匹配的部分识别,不等同完整视觉理解,需要全面判断画面时配合视觉工具 / 多模态再看(能力边界提示,[design-principles.md](design-principles.md) P14;防智能体把部分识别当画面全貌)。失败时为 `null`
4142
- 所有运行(`open_game` / 一条龙 / 独立应用 / 自定义 op)经**同一个 `RunSlot`** 派发:op 路径(`open_game` / `run_operation`)槽自管 `start_running/execute/stop_running`,app 路径(`run_one_dragon` / `run_standalone_app`)委托 `run_application`(复用 GUI/CLI 共享入口)。`block=True``asyncio.wrap_future(future)` 阻塞 await 取结果,`block=False` 立刻返回已启动状态,后续用 `get_run_status` 查进度。
@@ -51,7 +52,7 @@
5152

5253
## Instructions
5354

54-
`FastMCP(name, instructions=...)` 传 server 级 `instructions`,握手时返回;客户端**通常注入** system prompt(协议 Optional/MAY,Claude Code 会注入;非协议强制)。放两边共通的操作哲学(保持精炼):工具分类(观察/操作)、操作三件套(`analyze_screen` → 操作 → 等 ~1s 后验)、实机约束(`pc_alt`)、出错查 log、安全边界。
55+
`MCPServer(name, instructions=...)` 传 server 级 `instructions`,握手时返回;客户端**通常注入** system prompt(协议 Optional/MAY,Claude Code 会注入;非协议强制)。放两边共通的操作哲学(保持精炼):工具分类(观察/操作)、操作三件套(`analyze_screen` → 操作 → 等 ~1s 后验)、实机约束(`pc_alt`)、出错查 log、安全边界。
5556

5657
## 引导内容三通道
5758

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dev = [
3535
"pytest-asyncio>=1.4.0",
3636
"ruff>=0.15.18",
3737
"pyuac==0.0.3",
38-
"mcp>=1.0.0",
38+
"mcp>=2.0.0,<3",
3939
"uvicorn>=0.30.0",
4040
"pyright>=1.1.411",
4141
]

src/zzz_od/backend/entry/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""后端服务入口:装配 backend + MCP + HTTP,由 uvicorn 运行。
22
33
本模块把 Task 4(MCP 适配器)与 Task 5(HTTP ``/game/*`` 适配器)装配到同一个
4-
``FastMCP`` 实例上,并通过 ``streamable_http_app()`` 得到一个 Starlette app,
4+
``MCPServer`` 实例上,并通过 ``streamable_http_app()`` 得到一个 Starlette app,
55
最终交给 uvicorn 在单进程内并行对外提供 MCP(``/mcp``)与 HTTP(``/game/*``)服务。
66
"""
77

@@ -25,7 +25,7 @@
2525

2626

2727
def create_app(backend: ZzzBackendContext) -> "Starlette":
28-
"""装配应用:同一 FastMCP 同时挂 MCP tool 与 ``/game/*`` custom_route。
28+
"""装配应用:同一 MCPServer 同时挂 MCP tool 与 ``/game/*`` custom_route。
2929
3030
先创建 MCP 服务器(注册 4 个 game 工具),再把 ``/game/*`` HTTP 端点挂到
3131
同一实例上,最后返回 ``streamable_http_app()`` 产生的 Starlette app。

src/zzz_od/backend/http/routes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""HTTP 适配器:``/game/*`` 端点,把 ``ZzzBackendContext`` 暴露给 web/skill。
22
33
本模块在后端 game 切片(``ZzzBackendContext``)之上架设一层 HTTP 传输适配:
4-
- ``register_http_routes`` 通过 FastMCP 的 ``custom_route`` 挂 7 个端点
4+
- ``register_http_routes`` 通过 MCPServer 的 ``custom_route`` 挂 7 个端点
55
(``window``/``capture``/``analyze``/``enter``/``status``/``stop``/``close``),与 MCP ``/mcp``
66
端点同进程共存。
77
- 7 个处理器函数(``handle_game_*``)为模块级、可独立调用,便于直接测试,
@@ -15,7 +15,7 @@
1515
import asyncio
1616
from dataclasses import asdict
1717

18-
from mcp.server.fastmcp import FastMCP
18+
from mcp.server import MCPServer
1919
from starlette.requests import Request
2020
from starlette.responses import JSONResponse, Response
2121

@@ -211,14 +211,14 @@ async def handle_game_close(backend: ZzzBackendContext, _request: Request | None
211211
return JSONResponse({"result": msg})
212212

213213

214-
def register_http_routes(mcp: FastMCP, backend: ZzzBackendContext) -> None:
215-
"""把 ``/game/*`` 端点挂到 FastMCP
214+
def register_http_routes(mcp: MCPServer, backend: ZzzBackendContext) -> None:
215+
"""把 ``/game/*`` 端点挂到 MCPServer
216216
217217
使用 ``custom_route``(装饰器工厂二次调用)在 Starlette 层挂载 7 个端点,
218218
与 MCP ``/mcp`` 同进程共存。通过闭包将 ``backend`` 注入到各 lambda 处理器。
219219
220220
Args:
221-
mcp: 目标 ``FastMCP`` 实例。
221+
mcp: 目标 ``MCPServer`` 实例。
222222
backend: 已就绪的 ``ZzzBackendContext``,提供 game 切片能力。
223223
"""
224224
register_service_routes(mcp, backend)

src/zzz_od/backend/http/service_routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
from dataclasses import asdict
55

6-
from mcp.server.fastmcp import FastMCP
6+
from mcp.server import MCPServer
77
from starlette.requests import Request
88
from starlette.responses import JSONResponse, Response
99

@@ -209,7 +209,7 @@ def op_factory(ctx): # noqa: ANN202 闭包签名固定 Callable[[ZContext], Ope
209209
return JSONResponse({'result': msg})
210210

211211

212-
def register_service_routes(mcp: FastMCP, backend: ZzzBackendContext) -> None:
212+
def register_service_routes(mcp: MCPServer, backend: ZzzBackendContext) -> None:
213213
"""注册应用运行服务端点。"""
214214
@mcp.custom_route("/health", methods=["GET"])
215215
async def _health(request: Request) -> Response:

0 commit comments

Comments
 (0)