构建 MCP 服务端:Python + TypeScript SDK 本节摘要:多数 MCP 教程只演示 stdio 的 hello-world。一个真实的服务端要同时暴露 tools、resources、prompts,处理能力协商,发出结构化错误,并在不同 SDK 间行为一致。本节端到端构建一个笔记服务端:标准库 stdio 传输、JSON-RPC 分发、三大服务端原语,外加一种纯函数风格——让你毕业时能无缝换成 Python SDK 的 FastMCP 或 TypeScript SDK,而无需重写工具逻辑。 学习目标 阅读完本节,你应当能够: 实现 、 、 、 、 、 、 方法。 写一个分发循环:从 stdin 读 JSON-RPC 消息,往 stdout 写响应。
本节摘要:多数 MCP 教程只演示 stdio 的 hello-world。一个真实的服务端要同时暴露 tools、resources、prompts,处理能力协商,发出结构化错误,并在不同 SDK 间行为一致。本节端到端构建一个笔记服务端:标准库 stdio 传输、JSON-RPC 分发、三大服务端原语,外加一种纯函数风格——让你毕业时能无缝换成 Python SDK 的 FastMCP 或 TypeScript SDK,而无需重写工具逻辑。
阅读完本节,你应当能够:
initialize、tools/list、tools/call、resources/list、resources/read、prompts/list、prompts/get 方法。在你用上远程传输(第 09 节)或鉴权层(第 16 节)之前,你需要一个干净的本地服务端。本地即 stdio:服务端被客户端作为子进程拉起,消息按换行分隔流过 stdin/stdout。
2025-11-25 规范规定,stdio 消息编码为带显式 \n 分隔符的 JSON 对象。这里没有 SSE;SSE 是旧的远程模式,2026 年中正在被移除(Atlassian 的 Rovo MCP 服务端 2026-06-30 弃用、Keboola 2026-04-01 弃用)。对 stdio,「一行一个 JSON 对象」就是全部线格式。
笔记服务端是好样本,因为它同时练到三大服务端原语:Tools 做变更(notes_create)、Resources 暴露数据(notes://{id})、Prompts 发模板(review_note)。本节的形状可泛化到任意领域。
loop: line = stdin.readline() msg = json.loads(line) if 有 id: 处理请求 -> 写响应 else: 处理通知 -> 不写响应
三条铁律:
id 的响应匹配。initializedef initialize(params): return { "protocolVersion": "2025-11-25", "capabilities": { "tools": {"listChanged": True}, "resources": {"listChanged": True, "subscribe": False}, "prompts": {"listChanged": False}, }, "serverInfo": {"name": "notes", "version": "1.0.0"}, }
只声明你支持的——客户端靠能力集来给特性上门控。
tools/list 与 tools/calltools/list 返回 {tools:[...]},每个条目带 name、description、inputSchema。tools/call 取 {name, arguments},返回 {content:[块], isError:bool}。
内容块是类型化的,最常见三种:
{"type": "text", "text": "找到 2 条笔记"} {"type": "resource", "resource": {"uri": "notes://14", "text": "..."}} {"type": "image", "data": "<base64>", "mimeType": "image/png"}
工具错误有两种形态:协议级错误(未知方法、坏参数)是 JSON-RPC 错误;工具级错误(调用合法但工具失败)以 {content:[...], isError:true} 返回——这让模型能在自己的上下文里看到这次失败。
def tools_call(params): name, args = params["name"], params.get("arguments", {}) handler = TOOL_HANDLERS.get(name) if handler is None: return error(-32601, f"未知工具: {name}") # JSON-RPC 错误 try: blocks = handler(**args) # 执行器 return {"content": blocks, "isError": False} except Exception as e: # 工具级错误:把失败喂回模型,让它自纠 return {"content": [{"type":"text","text":f"工具失败: {e}"}], "isError": True}
Resources 设计上只读。resources/list 返回清单;resources/read 返回内容。URI 可以是 file://...、http://... 或自定义 scheme 如 notes://。
把数据作为 resource 而非 tool 暴露时:
ui:// 把它扩展到交互式资源。Prompts 是带命名参数的模板。host 把它们呈现为斜杠命令。一个 review_note prompt 可能取一个 note_id 参数,产出一条多消息的提示模板,客户端把它喂给自己的模型。
sys.stdout.flush()。每个工具可携带 annotations,描述安全属性:
readOnlyHint:true——纯读,可安全重试。destructiveHint:true——不可逆副作用,客户端应确认。idempotentHint:true——相同输入产出相同输出。openWorldHint:true——与外部系统交互。客户端用这些决定 UX(确认对话框、状态指示)与路由(第 17 节)。
code/main.py 的标准库服务端约 180 行。FastMCP(Python)把同样逻辑塌缩成装饰器风格:
from fastmcp import FastMCP app = FastMCP("notes") @app.tool() def notes_search(query: str, limit: int = 10) -> list[dict]: """按关键词搜索笔记。limit 控制返回条数。""" ...
TypeScript SDK 形状等价。毕业路径在你准备好时是插入式替换——能力、分发、内容块这些概念都不变。
| 维度 | 标准库手写 | FastMCP(Python) | TypeScript SDK |
|---|---|---|---|
| 行数 | ~180 | <80 | <80 |
| 分发循环 | 手写 readline/dispatch | 框架内置 | 框架内置 |
| 概念透明度 | 高(每行可见) | 中(装饰器抽象) | 中 |
| 适合 | 学习、调试 | 生产 | 生产(TS 栈) |
💡 心法:先用标准库手写一遍,吃透分发循环与内容块;再毕业到 FastMCP/TS SDK,工具逻辑零改动。线格式行为应完全一致——用同一套 JSON-RPC 测试脚手架验证。
本节产出 outputs/skill-mcp-server-scaffolder.md——给定一个领域(笔记、工单、文件、数据库),它脚手架出一个 MCP 服务端,带正确的 tools/resources/prompts 划分与 SDK 毕业路径。
code/main.py 是一个完整的笔记 MCP 服务端,stdio、仅标准库。它处理 initialize、三个工具(notes_list、notes_search、notes_create)的 tools/list 与 tools/call、每条笔记的 resources/list 与 resources/read、以及一个 review_note prompt。你可以通过管道喂数 JSON-RPC 来驱动它:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python main.py
手驱动:运行 code/main.py,用手构造的 JSON-RPC 消息驱动它。先跑 notes_create,再 resources/read 取回新笔记。
加删除工具:加一个 notes_delete 工具,带 annotations:{destructiveHint:true}。验证客户端(真实 host,Claude Desktop 可用)会弹出确认对话框。
实现订阅:实现 resources/subscribe,使服务端在笔记被改时推 notifications/resources/updated,并加一个 keepalive 任务。
移植到 FastMCP:把服务端移植到 FastMCP,Python 文件应缩到 80 行以内。线格式行为必须一致,用同一套 JSON-RPC 测试脚手架验证。
读规范补字段:读规范的 server/tools 节,找出本节服务端未实现的一个工具定义字段(有几个候选,挑一个加上)。
{content,isError:true} 喂回模型。下一节,我们站到桌子的另一边——构建一个 MCP 客户端:发现服务端工具、管理会话、把工具调用桥接进 LLM。