手写一个最小 MCP Server:stdio 传输、两个 tools、在 Cursor 里验通调用链

手写一个最小 MCP Server:stdio 传输、两个 tools、在 Cursor 里验通调用链

会「接入现成 MCP」和会「自己写一个」是两件事。前者解决生产力,后者解决当工具列表里没有你要的能力、或你必须把数据留在内网时 怎么办。最小 Server 不需要十个工具、也不需要远程部署:一个 stdio 进程、两个 tools、能在 Cursor 里把调用链跑通,就够建立正确心智模型。

本文带你走完:协议直觉 → 目录与依赖 → 两个工具实现要点 → mcp.json → 验通清单 。代码以「可放进 AtomGit 的教学仓」为目标:去密钥、可复制、可改。字段名与 SDK API 随版本会变,以你安装的 MCP SDK / Cursor 文档为准;下面用稳定的概念名描述,避免绑死某次小版本号。

摘要

  1. 先懂调用链:配置 → 握手 → tools/list → tools/call → 回传。
  2. 两个工具够用 :echo_note(纯函数)+ list_workspace_txt(限定目录列举)。
  3. stdio 优先:本地子进程,好调试,适合内网与教学。
  4. 安全默认:根目录收窄、只读列举、密钥不进仓库。
  5. 验通标准: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」文的差异:那边选别人的服务器;这边你是服务器作者。

原理:调用链长什么样

  1. 配置:客户端知道用什么命令启动你的 Server。
  2. 握手:initialize,交换能力。
  3. 发现:tools/list,把名称、描述、输入 schema 交给模型侧。
  4. 调用:tools/call,传入参数;你的 handler 返回 content。
  5. 呈现:模型或 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:验通调用链

按顺序做,不要跳:

  1. MCP 面板里 min-demo 为已连接。
  2. 能看到 echo_note 与 list_workspace_txt。
  3. 在 Ask 中明确:「调用 echo_note,text=ping」。应返回可辨认回显。
  4. 在 playground 放 a.txt,调用 list,应看到文件名。
  5. 传入试图逃逸的 subdir(如 ../),应失败且错误可读。
  6. 故意缺省必填参数,应看到 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 之前,先确认进程本身健康:

  1. 导出 MCP_ROOT 到空沙箱目录,放入两个 txt。
  2. 用 MCP 官方 inspector 或最小客户端发 tools/list。
  3. 再发两次 tools/call。
  4. 把 stderr 日志级别调到可读。

只有当「离开 IDE 也能 call 通」,你才适合排查 Cursor 配置问题。否则你会在错误的层打转:改了三遍 mcp.json,其实是 Python 虚拟环境没激活。

从两个工具扩到「团队用得上」的路径

验通后的扩容顺序建议:

  1. 保持只读;加 read_workspace_txt(仍限根目录、限大小)。
  2. 加显式开关 ALLOW_WRITE=1 才暴露写工具。
  3. 为写工具做审计日志(谁、何时、改了哪)。
  4. 写集成测试: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)

  1. Fork 最小仓,改 echo_note 增加 uppercase 布尔参数。
  2. 增加第三个只读工具 count_txt,返回文件数。
  3. 写一页「我的 MCP_ROOT 边界」说明。
  4. 打开 PR(即使是练习仓)描述验通步骤。

完成作业的标准仍是调用链清单,而不是功能炫技。

结束前的自我提问

若你只能带走一句:我会不会审查别人的 MCP README?若会,这篇就够本;若仍只想复制粘贴星标项目,请先把 safe_join 写会。

小结

手写最小 MCP Server,是把 Agent 的「手」从黑盒变成你仓库里的一个普通进程。stdio、两个 tools、Cursor 验通------这三件事做完,你就具备改现成服务器与自建内网工具的基础。下一步不是堆功能,而是:收紧默认权限,并把教学仓干净地放到 AtomGit。


草稿未发布 · 作者 梧桐秋海 · 活动:九月创作之星、AtomGit秋季、工具实践

相关推荐
EatFan2 小时前
当 CubeMX 遇上 AI Agent:用 MCP 让 AI 直接生成 STM32 HAL 工程
人工智能·stm32·嵌入式硬件·cubemx·stm32cubemx·hal·mcp
用户539418729072 小时前
Cursor 别只用来按 Tab:我每天高频在用的几个功能,附快捷键和踩坑
ai编程·cursor
可乐ea19 小时前
GitHub Security Lab 开源 Fuzzing Taskflow:让 LLM 智能体自己跑完 C/C++ 模糊测试
jvm·c++·github·模糊测试·mcp·代码agent·智能体自动化
网络毒刘19 小时前
Token 账单的「隐形税」:系统提示、工具定义与历史滚动为何比生成贵
agent·token·cursor·成本·mcp
吃饱了得干活20 小时前
Agent 的记忆与工具:从上下文窗口到 MCP
python·agent·mcp
VIP_CQCRE21 小时前
让 Claude 实时联网搜索:Ace Data Cloud Serp MCP 接入指南
ai·claude·搜索·mcp·acedatacloud
AIGC大时代1 天前
OpenAI Agents SDK 工程笔记:MCP 工具接入与生产禁区
mcp·生产禁区·openai agents sdk·mcpserverstdio·hostedmcptool
网络毒刘1 天前
Ask 模式做设计评审:提示词模板 + 检查清单,让 Agent 先读后改
agent·cursor·ask·工具实践·设计评审
code2cat1 天前
【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
java·后端·开发工具·ai agent·mcp