用最小作用原理写一个 MCP:模型的那双手,该怎么造
"模型不是不够聪明,而是是否有足够有用的 tools。" —— 上一篇《Harness 即一切》留下的那句话
"自然用最少的动作完成事情。" —— 物理学的最小作用原理
上一篇《Harness 即一切》讲清了一件事:agent 之间真正的差距,越来越不在模型本身,而在你给模型装上的那双手——tools。 而在今天,"给模型造一双手"的标准做法,就叫 MCP(Model Context Protocol)。
这一篇就回答一个具体问题:MCP 到底是什么、它为什么这么设计、以及——你怎么用半小时写出一个能用的 MCP?
全文用本项目 code-hacker 里两个真实的 MCP server(filesystem.py / git_tools.py)当解剖样本,配套一个 skill,让你照着就能写出自己的第一个 MCP。
一、一句话讲清 MCP
LLM 本质是一个文本生成器。它关在自己的脑子里,看不到你的文件、跑不了你的代码、读不到你的 git log。
MCP 就是那条把模型接到真实世界的标准电线:
┌─────────────┐ MCP 协议 ┌──────────────┐
模型 ←→ │ Host / │ ←──── tools/list ────→ │ 你的 MCP │
(脑子) │ Client │ ──── tools/call ─────→ │ Server │
│ (CC / IDE) │ ←──── 结构化结果 ──── │ (你写的代码) │
└─────────────┘ └──────────────┘
│
▼
文件 / git / 数据库 / API / ...
(真实世界)
- 模型 负责"想"——它产出一个意图:"我想读
train.py这个文件"。 - MCP server 负责"做"——它把意图变成真实动作(
open().read()),再把世界的状态结构化地喂回模型。
没有 tools,模型是脱缰的野马,只会"说"; 有了 MCP,模型才长出手脚,能"读世界的状态、把行动作用回去"。
一个 MCP server 就是一组 tool 的集合。写 MCP,本质就是写 tool。所以这篇文章真正的主题是:怎么设计一个好 tool。
一·五、MCP 从哪来:从 function calling 到一个开放协议
"给模型装手"这件事,不是 MCP 发明的。MCP 是这条路走到第三步时,被逼出来的标准。把这段史展开,你才会明白 MCP 解决的到底是什么问题。
第 0 步:prompt 层的 hack(2022—2023 初)
最早大家是纯靠提示词让模型"用工具"的:
- ReAct(2022) —— 让模型按
Thought → Action → Observation的格式输出,人类写代码去解析它那行Action: search("..."),执行,再把结果拼回 prompt。 - Toolformer(Meta, 2023.2) —— 训练模型自己在文本里插入 API 调用。
- LangChain 等框架 —— 把这套"解析模型输出 → 调函数 → 回填"封装成 agent/tool 抽象。
问题很明显:模型输出的是自由文本,解析全靠正则和运气。 模型多打一个空格、少一个引号,整条链就断了。"调用工具"还不是一等公民,只是 prompt 工程的小聪明。
第 1 步:Function Calling —— 工具调用进了 API(2023.6)
2023 年 6 月 13 日,OpenAI 在 gpt-4-0613 / gpt-3.5-turbo-0613 里引入了 function calling:
- 开发者用 JSON Schema 描述一个函数(名字、参数、类型)。
- 模型不再吐自由文本,而是直接输出一段结构化 JSON:
{"name": "get_weather", "arguments": {"city": "Beijing"}}。 - 解析不再靠运气——模型被对齐到"按 schema 说话"。
这是关键一跃:"调用外部工具"第一次成了模型 API 的一等公民。 今天你在 git_tools.py 里写的类型签名会被翻译成 schema 发给模型——这套机制的源头就在这里。
同期还有 ChatGPT Plugins(2023.3):让 ChatGPT 通过 OpenAPI spec 调第三方服务。 方向对,但每家插件各写各的,生态碎片化,很快沉寂。
第 2 步:M×N 的集成噩梦
function calling 让"单个应用接单个模型"变简单了,但放到整个生态就爆炸了:
每个应用 × 每个数据源 / 工具 = 各写一套适配
Claude ─┐ ┌─ 文件系统 M 个模型
GPT ─┼─×─┼─ Git ×
Gemini ─┘ ├─ 数据库 N 个工具/数据源
├─ Slack ‖
└─ ... M × N 套胶水代码
每个模型厂商的 function calling 格式略有不同,每个工具又要为每个应用单独接一遍。M 个模型 × N 个工具 = M×N 的集成噩梦。 这正是 USB 出现之前,每个设备配一根专用线的世界。
第 3 步:Anthropic 发明 MCP —— 把 M×N 变成 M+N(2024.11)
2024 年 11 月 25 日,Anthropic 开源了 Model Context Protocol(MCP):不再让每个应用各自接每个工具,而是定义一个开放协议,规定"模型一侧(client)怎么和工具一侧(server)对话"。
- 工具方只要实现一次 MCP server(像本项目的
filesystem.py),任何 MCP 客户端都能用。 - 客户端只要会说 MCP,就能接入任何 MCP server。
- M×N → M+N。
Anthropic 自己的两个类比最传神:
| 类比 | 含义 |
|---|---|
| AI 应用的 USB-C 接口 | 一个统一插口,插上就能用,不用为每个设备配专用线 |
| LSP(Language Server Protocol)之于编辑器 | 当年 LSP 让"一个语言服务器 × 所有编辑器"成为可能;MCP 想做"一个工具 server × 所有 AI 应用" |
2025 年,OpenAI、Google DeepMind 等相继宣布支持 MCP——一个由竞争对手发明的协议,被全行业采纳,这本身就说明它戳中了真问题。
一句话串起来
prompt hack(解析靠运气)
↓ 把工具调用做进 API
function calling(结构化 JSON,但每家一套)
↓ M×N 集成噩梦
MCP(一个开放协议,M+N,USB-C for AI)
function calling 解决的是"模型怎么说出一次工具调用"。 MCP 解决的是"这次调用怎么跨模型、跨应用地标准化传递"。 前者是语法,后者是协议。本文后面教你写的,就是协议这一侧的 server。
二、把"最小作用原理"搬到 MCP 设计上
物理学里,自然总是用最少的动作完成一件事。这个原则迁移到工程,就是高手的雷达:
- 这个 feature 的最小实现路径是什么?
- 这个 bug 的最小复现是什么?
- 这次能砍掉的最大开销是什么?
写 MCP 时,"最小作用"有一个非常具体、非常省钱的含义:
一次 tool 调用,要让模型本来需要绕 5 个来回才能拼出来的信息,一次性拿到。 这就是上一篇说的——精准 tool 即精准上下文。
模型的每一次 Thought → Action → Observation 来回,都在烧 token、烧延迟、烧它有限的注意力。一个设计得好的 tool,等价于给模型一个更短、更准、更便宜的 prompt。
把 README《Why you not student》里那三件高维武器对照过来,就是 MCP 设计的三条铁律:
| 高维武器 | 写 MCP 时的含义 |
|---|---|
| 最小作用原理 | 一个 tool 一次就把模型要的东西给全,别让它绕来绕去 |
| 最小可运行 | 先写一个 10 行能跑通的 tool,验证链路,再加功能 |
| "为什么不行" | 在暴露 tool 之前先想:模型会怎么用错它、怎么把我的机器搞坏 |
下面我们用真实代码,把这三条落到地面。
三、解剖一个真实的 MCP server
打开 git_tools.py,一个完整的 MCP server 只有三段式:
from mcp.server.fastmcp import FastMCP
# ① 起一个 server
mcp = FastMCP(name="git-tools", host="localhost", port=8002)
# ② 用装饰器把一个普通函数变成 tool
@mcp.tool()
async def git_status(repo_path: str = ".") -> str:
"""Show working tree status: staged, unstaged, and untracked files.
Args:
repo_path: Path to the git repository (default: current directory)
"""
return format_result(run_git(["status"], cwd=repo_path))
# ③ 跑起来
if __name__ == "__main__":
mcp.run(transport="streamable-http")
就这么多。FastMCP 帮你做完了所有协议层的脏活——握手、tools/list、tools/call、序列化。你只需要写函数。
但魔鬼在细节里。上面这个 git_status 短短几行,藏着四个写给模型而不是写给人的设计决定:
1. 函数签名 = 给模型的 schema
async def git_status(repo_path: str = ".") -> str:
你写的类型注解(repo_path: str)和默认值(= "."),FastMCP 会自动翻译成 JSON Schema,发给模型。模型靠这个 schema 知道:"哦,这个 tool 要一个叫 repo_path 的字符串,不给的话默认当前目录。"
原则:类型签名即契约。 参数名要让模型一看就懂——
repo_path比p强一万倍。
2. docstring = 写给模型的 prompt
"""Show working tree status: staged, unstaged, and untracked files.
Args:
repo_path: Path to the git repository (default: current directory)
"""
这段 docstring 不是给人看的注释,是给模型看的说明书。模型读它来决定"什么时候该调这个 tool"。所以它必须:
- 第一句话说清"这个 tool 做什么"——一个核心动词(show status)。
Args里解释每个参数,尤其是默认行为。
原则:docstring 是 tool 的脸。 模型选不选你这个 tool,全看这张脸说清楚没有。
3. 返回值要"自带上下文"
看 filesystem.py 里的 read_file,它没有只 return content,而是:
return f"File: {file_path}\nSize: {len(content)} characters\n\n{content}"
返回里带上了文件名、大小。这样模型在后续推理时,不用再回头问"我刚读的是哪个文件",上下文里已经写着了。这正是最小作用——一次返回,信息给全。
原则:返回值是下一轮的 prompt。 让模型读完就知道发生了什么,而不是只拿到一坨裸数据。
4. 安全边界 = 护城河(这就是"为什么不行")
这是 filesystem.py 最值得学的地方。在真正动手之前,它先问"模型会怎么用错我":
BLOCKED_COMMANDS = {'rm', 'del', 'format', 'mkfs', 'dd', 'shutdown', ...}
def is_safe_path(path: str) -> bool:
"""Check if the path is safe (no directory traversal)."""
return not any(part.startswith('..') for part in Path(path).parts)
每个会改变世界的 tool,动手前都先过一道闸:
@mcp.tool()
async def write_file(file_path: str, content: str, ...) -> str:
if not is_safe_path(file_path):
return f"Error: Unsafe file path: {file_path}"
if not is_allowed_file(file_path):
return f"Error: File type not allowed: {Path(file_path).suffix}"
if len(content.encode(encoding)) > MAX_FILE_SIZE:
return f"Error: Content too large"
# ...真正写入
注意:出错时它返回一句人话给模型,而不是抛异常崩掉。模型读到 Error: Unsafe file path 就会自己纠正。
原则:把模型当成一个聪明但会犯错的实习生。 给它锋利的工具,但在工具上装好护栏。 一个错的动作 × 一个能干的模型 = 把你的机器高效地搞坏。
四、可以"长在身上"的 MCP 设计原则
招式可以查文档,原则必须长在身上。下面这张表,是写任何 MCP 时该在 0.1 秒内调用的反射:
| 表层(招式) | 内核(原则) |
|---|---|
| 给 docstring 写清楚第一句 | docstring 是写给模型的 prompt,不是注释 |
| 参数起个好名字、给默认值 | 类型签名即契约,默认值即最小作用 |
| 返回里带上文件名/数量/状态 | 返回值是下一轮的 prompt,要自带上下文 |
| 一个函数只干一件事 | 一个 tool 一个核心动词 |
| 把 5 次小查询合并成 1 个 tool | 精准 tool 即精准上下文 |
| 动手前检查路径/大小/命令 | 在工具上装护栏——先想"模型会怎么用错" |
| 出错 return 一句话,不抛异常 | 错误信息也是给模型的反馈,要能自我纠正 |
| 先写 10 行能跑的,再加功能 | 最小可运行——先验证链路,别一上来造航母 |
几条原则,少而锋利,所以能在写代码的当下瞬间调用。 这正是 review 一个 AI 写的 tool 时,那 5 秒里你身体调用的东西。
一个反例:别把 tool 写成"面"
模型擅长把一个点扩充成面,人类的价值在点的精度。同理,别把一个 tool 设计成"什么都能干"的瑞士军刀:
# ❌ 坏味道:一个 tool 吞下整个 git
@mcp.tool()
def git(subcommand: str, args: str) -> str:
"""Run any git command.""" # 等于没给 tool,模型还是在猜
# ✅ 好味道:一个 tool 一个核心动词(看 git_tools.py 怎么做的)
@mcp.tool()
def git_status(repo_path: str = ".") -> str: ...
@mcp.tool()
def git_diff(repo_path: str = ".", staged: bool = False) -> str: ...
@mcp.tool()
def git_log(repo_path: str = ".", max_count: int = 20) -> str: ...
拆开之后,每个 tool 的 schema 和 docstring 都在精确地告诉模型它能做什么。这就是"精准 tool 即精准上下文"的反面教材与正面教材。
五、最小可运行:半小时写出你的第一个 MCP
理论讲完,上手。遵循最小可运行——先写一个 10 行能跑通的,验证链路。
第 1 步:装依赖
pip install "mcp[cli]" # 或 uv add "mcp[cli]"
第 2 步:写 server(完整文件,可直接跑)
weather.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name="weather", host="localhost", port=8010)
@mcp.tool()
async def get_weather(city: str) -> str:
"""Get the current weather for a city.
Args:
city: City name, e.g. "Beijing"
"""
# 真实场景这里会调天气 API;先用假数据验证链路
return f"Weather in {city}: Sunny, 23°C, humidity 40%."
if __name__ == "__main__":
mcp.run(transport="streamable-http")
跑起来:
python weather.py # 监听 localhost:8010
第 3 步:接到 Claude Code(或任意 MCP 客户端)
在项目的 .mcp.json / mcp.json 里加一条:
{
"mcpServers": {
"weather": { "url": "http://localhost:8010/mcp" }
}
}
重启客户端,问它"北京天气怎么样",它就会自动调用你的 get_weather。
这一步跑通的那一刻,你就完成了一次完整的 Thought → Action(call get_weather) → Observation 闭环。 链路通了,剩下的只是把假数据换成真 API、把 tool 越加越多。
第 4 步:从"能跑"到"能用"——把第四节那张表过一遍
回头给 get_weather 补上:更准的 docstring、输入校验(空城市名怎么办?)、自带上下文的返回、错误时 return 一句话。一个玩具 tool 就长成了一个生产 tool。
六、配套 skill:让写 MCP 变成"提一嘴"的事
为了让"模型越强,harness 越松"落到实处,本项目配了一个 skill:write-mcp(在 .claude/skills/write-mcp.md)。
它把上面这套流程压缩成了模型可以直接执行的步骤:你只要说一句"帮我写一个查天气的 MCP",Claude(或本项目的 Code Hacker agent)就会:
- 用 FastMCP 三段式生成 server 骨架;
- 按"一个 tool 一个核心动词"拆分工具;
- 自动补上 docstring、类型签名、输入校验、自带上下文的返回;
- 选一个不冲突的端口,给出
mcp.json接入片段; - 给出最小可运行的验证命令。
这就是 harness 的自指之美:我们用一个 skill(harness 的一部分),去更快地造出更多 tool(harness 的另一部分)。
七、收尾:tool 是模型的手,而手怎么造,是你的判断
回到最开始那句话——"模型不是不够聪明,而是是否有足够有用的 tools"。
写 MCP 这件事,代码层面 FastMCP 已经简单到几行。真正的难度、也是真正的价值,从来不在"怎么写",而在那些 AI 替你答不了的判断:
- 这个 tool 该不该存在?(还是模型自己用 Bash 就够了)
- 它的边界画在哪?(给多大的权力,装多厚的护栏)
- 它一次该返回什么?(怎样才算"精准上下文")
这些判断,正是 README 里说的——提出一个高维度的、有效的突破点,再让 AI 把它扩充成面。
FastMCP 把"写 tool"这件事压平了。 但"该给模型造一双什么样的手"——这个问题没有训练数据,只能靠你。
去写你的第一个 MCP 吧。从那个 10 行的 weather.py 开始。
参考
- 本项目源码:
filesystem.py(文件系统 MCP)、git_tools.py(Git MCP) - 配套 skill:
.claude/skills/write-mcp.md - 前文:《Harness 即一切:模型不是不够聪明,而是是否有足够有用的 tools》(
docs/harness-is-everything.md) - 思想来源:《Why you not student?》——最小作用原理 / 最小可运行 / "为什么不行"
- 协议规范:Model Context Protocol(modelcontextprotocol.io)