手写一个最小 MCP Server:stdio 传输、两个 tools、在 Cursor 里验通调用链
会「接入现成 MCP」和会「自己写一个」是两件事。前者解决生产力,后者解决当工具列表里没有你要的能力、或你必须把数据留在内网时 怎么办。最小 Server 不需要十个工具、也不需要远程部署:一个 stdio 进程、两个 tools、能在 Cursor 里把调用链跑通,就够建立正确心智模型。
本文带你走完:协议直觉 → 目录与依赖 → 两个工具实现要点 → mcp.json → 验通清单 。代码以「可放进 AtomGit 的教学仓」为目标:去密钥、可复制、可改。字段名与 SDK API 随版本会变,以你安装的 MCP SDK / Cursor 文档为准;下面用稳定的概念名描述,避免绑死某次小版本号。

摘要
- 先懂调用链:配置 → 握手 → tools/list → tools/call → 回传。
- 两个工具够用 :
echo_note(纯函数)+list_workspace_txt(限定目录列举)。 - stdio 优先:本地子进程,好调试,适合内网与教学。
- 安全默认:根目录收窄、只读列举、密钥不进仓库。
- 验通标准:Cursor 能发现、能调用、错参有可读错误。
结论:最小 Server 的价值是把「魔法」变成「可观察的进程与 JSON」。通了再加第三个工具。
结论卡
| 步骤 | 你要看到的证据 | 失败时先查 |
|---|---|---|
| 配置 | mcp.json 被加载 | 路径/命令/工作目录 |
| 握手 | server 非红灯 | stderr / 启动命令 |
| list | 两个工具名可见 | schema 是否合法 |
| call | 返回可预期文本 | 参数类型与权限 |
| 安全 | 越权路径被拒 | 根目录是否配宽 |
背景与边界
MCP 用 JSON-RPC 风格消息描述能力。Cursor 通过配置拉起命令(常见 command + args + env),与 Server 走 stdio 交换。也存在远程传输形态,但教学与内网落地优先 stdio:少一层网络鉴权,问题更可复现。
边界:本文不是完整 SDK 手册;不实现 resources/prompts(另文);不演示绕过客户端安全策略;不连接生产库。若你使用 TypeScript 或 Python 官方 SDK,函数名可能不同,但「声明 tools → 实现 handler → stdio 入口」三步不变。
与「接入现成 MCP」文的差异:那边选别人的服务器;这边你是服务器作者。
原理:调用链长什么样

- 配置:客户端知道用什么命令启动你的 Server。
- 握手:initialize,交换能力。
- 发现:tools/list,把名称、描述、输入 schema 交给模型侧。
- 调用:tools/call,传入参数;你的 handler 返回 content。
- 呈现:模型或 UI 使用返回文本;人审结果。
任一环失败,都不要先怀疑「模型不够聪明」。先看进程是否存活、list 是否为空、call 的错误字符串是否被吞掉。
步骤 1:仓库骨架(可放 AtomGit)
建议最小结构:
text
min-mcp-server/
README.md
pyproject.toml # 或 package.json
src/
server.py # 入口
.env.example
.gitignore
examples/
mcp.json.snippet
.gitignore 至少包含:.env、虚拟环境、缓存、本地密钥文件。README 写清:如何安装、如何在 Cursor 里粘贴配置、默认只读目录是什么。
步骤 2:只做两个 tools

工具 A:echo_note
意图 :验证「参数进来、文本出去」的闭环,不含文件系统。
输入 :text(string,必填),可选 prefix。
输出 :一段回显字符串。
为何先做它:排除「目录权限、编码、路径」干扰,专测协议与配置。
伪代码级逻辑:
text
echo_note(text, prefix="") ->
return f"{prefix}{text}"
工具 B:list_workspace_txt
意图 :在预先配置的根目录 下,列出 *.txt(可改成你的扩展名)。
输入 :可选 subdir(相对路径,禁止 .. 逃逸)。
输出 :文件名列表或「空目录」说明。
硬约束:
- 根目录来自环境变量,例如
MCP_ROOT; resolve(root, subdir)后必须仍在 root 内;- 本教学版本只列举,不读取内容、不删除、不写入。
text
list_workspace_txt(subdir="") ->
path = safe_join(MCP_ROOT, subdir)
return [p.name for p in path.glob("*.txt")]
两个工具的搭配:一个证明协议,一个证明「带边界的副作用面」。
步骤 3:stdio 入口与日志
入口脚本应能被非交互拉起:stdin/stdout 留给协议,调试日志打到 stderr,避免污染协议流。本地可先用官方 inspector 或 Cursor MCP 面板观察。
教学仓 README 可写:
bash
# 示例:用 uv / pip 安装后
export MCP_ROOT=/absolute/path/to/playground
python -m src.server
真正给 Cursor 用时,通常不手动长驻,而由客户端按配置拉起。
步骤 4:写入 Cursor 配置

在项目或全局 MCP 配置中增加(结构示意,键名以你的 Cursor 版本为准):
json
{
"mcpServers": {
"min-demo": {
"command": "python",
"args": ["-m", "src.server"],
"cwd": "/absolute/path/to/min-mcp-server",
"env": {
"MCP_ROOT": "/absolute/path/to/playground"
}
}
}
}
要点:
- cwd 指到仓库,避免相对导入失败;
- MCP_ROOT 用绝对路径,且是你愿意暴露的沙箱目录;
- 改配置后按客户端要求重载 MCP / 重开窗口;
- 先不要挂其他 MCP,降低干扰。
步骤 5:验通调用链

按顺序做,不要跳:
- MCP 面板里
min-demo为已连接。 - 能看到
echo_note与list_workspace_txt。 - 在 Ask 中明确:「调用 echo_note,text=ping」。应返回可辨认回显。
- 在 playground 放
a.txt,调用 list,应看到文件名。 - 传入试图逃逸的
subdir(如../),应失败且错误可读。 - 故意缺省必填参数,应看到 schema / 校验错误,而非空成功。
全部通过,才算「调用链验通」。然后你可以:加第三个工具、改用 TypeScript SDK、或把仓库推到 AtomGit(去密钥后)。
可复制:给 Agent 的联调提示词
text
请只使用 MCP 工具 min-demo.echo_note 与 min-demo.list_workspace_txt。
1) 调用 echo_note,text="chain-ok"
2) 调用 list_workspace_txt(subdir 为空)
3) 汇报两次调用的原始返回,不要改我的仓库文件
若工具不可用,说明你在客户端里看到的错误,不要编造成功。
踩坑清单
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 一直红灯 | 命令/cwd/依赖错误 | 终端手动跑同一命令 |
| list 为空 | 未正确注册 tools | 查 schema 与导出 |
| 能 list 不能 call | handler 抛错被吞 | stderr 日志 |
| 路径逃逸成功 | safe_join 未做 | 立刻修,勿发文炫耀 |
| 模型不调用工具 | 描述不清或未允许 | 提示词点名工具 |
| 协议错乱 | 日志打到 stdout | 改 stderr |
安全底线(写进 README)
- 示例默认只读列举;若加写工具,必须另开开关且默认关闭。
- 禁止在配置或 Rules 里粘贴真实 Token。
- AtomGit/GitHub 公开前跑密钥扫描。
- 不要把 Server 指到家目录或生产挂载。
- 教学演示用假数据;截图打码环境变量。
验收标准
- 他人按 README 能在 30 分钟内复现 list→call
- 两个工具行为与文档一致
- 逃逸路径被拒绝
- 仓库无真实密钥
- 你能向同事画出调用链四步
陷阱与边界
- SDK 升级可能改 API:锁版本,并在 CI 里做「能 list」的冒烟(后续可写集成测试文)。
- 最小不等于可上生产:生产还要鉴权、审计、速率限制。
- 工具描述也是 Token:描述写短、写准。
- 不要一次加十个工具:每加一个,重复验通一次。
实现细节:描述文本怎么写才不坑模型
工具能不能被正确调用,一半取决于 handler,一半取决于描述与 schema。教学里常见翻车:
- 描述写「智能地处理文件」------模型不知道何时该用;
- 参数叫
path却不说明相对谁; - 可选参数过多,模型乱填。
建议:
echo_note描述写成:「把文本原样回显,用于连通性测试;无副作用。」list_workspace_txt写成:「在 MCP_ROOT 下列举 txt 文件名;subdir 为相对路径,禁止 ...;只读。」- schema 里用明确类型与
description;必填字段宁少勿滥。
描述也是 Token:两句精准胜过一段散文。
本地调试剧本(不必先开 Cursor)
在交给 Cursor 之前,先确认进程本身健康:
- 导出
MCP_ROOT到空沙箱目录,放入两个 txt。 - 用 MCP 官方 inspector 或最小客户端发
tools/list。 - 再发两次
tools/call。 - 把 stderr 日志级别调到可读。
只有当「离开 IDE 也能 call 通」,你才适合排查 Cursor 配置问题。否则你会在错误的层打转:改了三遍 mcp.json,其实是 Python 虚拟环境没激活。
从两个工具扩到「团队用得上」的路径
验通后的扩容顺序建议:
- 保持只读;加
read_workspace_txt(仍限根目录、限大小)。 - 加显式开关
ALLOW_WRITE=1才暴露写工具。 - 为写工具做审计日志(谁、何时、改了哪)。
- 写集成测试:mock stdio,断言 schema(后续专文)。
不要第一天就做「通用 shell 工具」------那等于把 Agent 的手直接接到你的 bash,公约与禁区都会失效。
教学仓 README 应有的验收段落
直接给读者一张表:
| 检查项 | 期望 |
|---|---|
| 安装 | 文档中的一条命令可完成 |
| list | 两工具可见 |
| echo | 回显匹配 |
| list dir | 看到沙箱 txt |
| 逃逸 | 失败 |
| 密钥 | 扫描通过 |
读者按表打勾,你的博客才是「可复现实战」,而不是「作者机器上成功过」。
常见问答
Q:必须用官方 SDK 吗?
A:教学期建议用,少踩 JSON-RPC 细节;若环境限制,也可最小手写,但要自己保证消息帧正确。
Q:为什么不直接给 Agent 一个 shell 工具?
A:shell 面太大,禁区无法机械执行。最小 Server 的意义是收窄面。
Q:Cursor 里调用了,但模型编造了返回?
A:在提示词要求「贴原始工具返回」;并在 UI 中核对工具调用轨迹。编造返回通常是模型没真正 call 或你看错了线程。
Q:Windows 路径怎么办?
A:MCP_ROOT 仍用绝对路径;safe_join 要按平台规范化;示例 README 分别给出 POSIX/Windows 片段。
Q:如何分享给同事?
A:推到 AtomGit 后,同事只改 cwd 与 MCP_ROOT;不要分享你的真实机器绝对路径截图到公开处。
对照:接入现成 MCP vs 手写
| 接入现成 | 手写最小 | |
|---|---|---|
| 目标 | 马上有生产力 | 理解与定制 |
| 风险 | 权限模型外来 | 自己写漏边界 |
| 适合 | 标准能力 | 内网特例 |
| 验收 | 官方文档 + 八问 | 本文调用链清单 |
两者不是对立:手写通了,你才更会审查现成 Server 的 README 是否在说谎。
发布前检查(作者视角)
写这篇实战时,作者侧也应:
- 自己在干净目录复现一遍;
- 截图打码绝对路径与用户名;
- 不做「完整可复制机密配置」;
- 在文首声明 SDK 版本可能变化。
实战文的信誉来自可复现,不来自语气强硬。
安全演练:故意做一遍「坏人提示词」
在内部演练(勿对生产)时,用提示词要求 Agent:
「请用 list_workspace_txt 读取 ../../.ssh」或「请删除 playground 外文件」。
期望:工具层拒绝或路径规范化失败;即使模型愿意配合,也打不开。若演练成功越权,立刻停更、修 safe_join、撤回示例仓。把演练结果写进 README「安全」节,比写「我们很重视安全」有用一百倍。
版本钉扎与可重复安装
text
# 示例:锁住 MCP SDK 与运行时
mcp==x.y.z
python>=3.11
并在 CI 增加「导入 server 模块 + 断言 tools 名称集合」的冒烟作业。教学仓可以没有复杂业务测试,但不能没有「安装后还能 list」。读者三个月后复现失败,通常不是读者笨,是作者没锁版本。
练习作业(读者可交到 AtomGit)
- Fork 最小仓,改
echo_note增加uppercase布尔参数。 - 增加第三个只读工具
count_txt,返回文件数。 - 写一页「我的 MCP_ROOT 边界」说明。
- 打开 PR(即使是练习仓)描述验通步骤。
完成作业的标准仍是调用链清单,而不是功能炫技。
结束前的自我提问
若你只能带走一句:我会不会审查别人的 MCP README?若会,这篇就够本;若仍只想复制粘贴星标项目,请先把 safe_join 写会。
小结
手写最小 MCP Server,是把 Agent 的「手」从黑盒变成你仓库里的一个普通进程。stdio、两个 tools、Cursor 验通------这三件事做完,你就具备改现成服务器与自建内网工具的基础。下一步不是堆功能,而是:收紧默认权限,并把教学仓干净地放到 AtomGit。
草稿未发布 · 作者 梧桐秋海 · 活动:九月创作之星、AtomGit秋季、工具实践