构建 MCP 服务端:Python + TypeScript SDK


文档摘要

构建 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 服务端:Python + TypeScript SDK

本节摘要:多数 MCP 教程只演示 stdio 的 hello-world。一个真实的服务端要同时暴露 tools、resources、prompts,处理能力协商,发出结构化错误,并在不同 SDK 间行为一致。本节端到端构建一个笔记服务端:标准库 stdio 传输、JSON-RPC 分发、三大服务端原语,外加一种纯函数风格——让你毕业时能无缝换成 Python SDK 的 FastMCP 或 TypeScript SDK,而无需重写工具逻辑。

学习目标

阅读完本节,你应当能够:

  1. 实现 initializetools/listtools/callresources/listresources/readprompts/listprompts/get 方法。
  2. 写一个分发循环:从 stdin 读 JSON-RPC 消息,往 stdout 写响应。
  3. 按 JSON-RPC 2.0 规范与 MCP 的额外错误码,发出结构化错误响应
  4. 把一个标准库实现毕业到 FastMCP(Python SDK)或 TypeScript SDK,而无需重写工具逻辑。

一、问题与直觉

在你用上远程传输(第 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: 处理通知 -> 不写响应

三条铁律:

  • 绝不要往 stdout 打印任何非 JSON-RPC 信封的东西,调试日志走 stderr。
  • 每个请求必须用带相同 id 的响应匹配。
  • 通知绝不能被响应。

实现 initialize

def 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/listtools/call

tools/list 返回 {tools:[...]},每个条目带 namedescriptioninputSchematools/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 设计上只读resources/list 返回清单;resources/read 返回内容。URI 可以是 file://...http://... 或自定义 scheme 如 notes://

把数据作为 resource 而非 tool 暴露时:

  • 模型不「调用」它;客户端可在用户请求时把它注入上下文。
  • 订阅让服务端在资源变化时推送更新(第 10 节)。
  • 第 14 节用 ui:// 把它扩展到交互式资源。

实现 prompts

Prompts 是带命名参数的模板。host 把它们呈现为斜杠命令。一个 review_note prompt 可能取一个 note_id 参数,产出一条多消息的提示模板,客户端把它喂给自己的模型。

stdio 传输的微妙之处

  • 换行分隔的 JSON,无长度前缀帧。
  • 不要缓冲,每次写后 sys.stdout.flush()
  • 客户端控制生命周期,stdin 关闭(EOF)时干净退出。
  • 不要静默处理 SIGPIPE,记录日志后退出。

注解(Annotations)

每个工具可携带 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_listnotes_searchnotes_create)的 tools/listtools/call、每条笔记的 resources/listresources/read、以及一个 review_note prompt。你可以通过管道喂数 JSON-RPC 来驱动它:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python main.py

五、练习

  1. 手驱动:运行 code/main.py,用手构造的 JSON-RPC 消息驱动它。先跑 notes_create,再 resources/read 取回新笔记。

  2. 加删除工具:加一个 notes_delete 工具,带 annotations:{destructiveHint:true}。验证客户端(真实 host,Claude Desktop 可用)会弹出确认对话框。

  3. 实现订阅:实现 resources/subscribe,使服务端在笔记被改时推 notifications/resources/updated,并加一个 keepalive 任务。

  4. 移植到 FastMCP:把服务端移植到 FastMCP,Python 文件应缩到 80 行以内。线格式行为必须一致,用同一套 JSON-RPC 测试脚手架验证。

  5. 读规范补字段:读规范的 server/tools 节,找出本节服务端未实现的一个工具定义字段(有几个候选,挑一个加上)。

本节要点回顾

  1. 本地即 stdio:客户端拉起服务端子进程,stdin/stdout 按换行分隔传 JSON-RPC;SSE 是旧远程模式,2026 年中移除。
  2. 分发三铁律:stdout 只许 JSON-RPC 信封、请求必匹配响应、通知绝不响应。
  3. 三种内容块:text、resource、image;工具结果返回内容块数组而非裸字符串。
  4. 两种错误形态:协议级走 JSON-RPC 错误码;工具级走 {content,isError:true} 喂回模型。
  5. Resources 只读 + URI 寻址:模型不调用,客户端按需注入上下文。
  6. Prompts 是模板:带命名参数,host 呈现为斜杠命令。
  7. stdio 微妙点:换行分隔不缓冲、客户端控生命周期、别静默 SIGPIPE。
  8. Annotations 是安全提示:readOnly/destructive/idempotent/openWorld,客户端据此做 UX 与路由。
  9. 毕业路径:标准库 → FastMCP/TS SDK,装饰器抽象,工具逻辑零改动。

下一节,我们站到桌子的另一边——构建一个 MCP 客户端:发现服务端工具、管理会话、把工具调用桥接进 LLM。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U