曾经说过不要 MCP 的我们,如今把 MCP 做进了核心
日期:2026年9月29日,星期二
发件人:Earendil Engineering <rfc@earendil.com>
收件人:你
主题:“你说不要 MCP!”
如果你过去访问过 pi.dev,会发现那里有一句豪言壮语,宣称 Pi 不支持 MCP。如果你在播客中听到我们讨论 Pi,会发现我们曾多次对 MCP 不屑一顾,包括 Mario 写过的一篇关于它的文章。然而,一旦升级到新版 Pi,你就会发现 MCP 现在已是受支持的功能。到底发生了什么?
世事变迁
首先要记住的是,世界并非静止不动。过去一年来我们一直密切关注 MCP,如今的 MCP 已非昨日模样。单凭这一点,或许还不足以成为将其纳入核心功能的理由。你也知道,Pi 拥有出色的扩展生态系统,MCP 理应可以作为扩展实现,甚至可以是 Earendil 官方推荐的扩展。没错,它确实可以作为扩展,以前确实如此。如今 MCP 成为核心的一部分,是我们集体深思熟虑后重新权衡的结果。
具体变了什么?
我们将 MCP 引入核心的原因,不仅在于 MCP 自身的演变,更在于我们发现为此所需的改动通常具有通用价值。例如,我们对 MCP 所做的修改,也顺带让 Jev 在 Pi 内部更容易使用。归根结底,Pi 的需求与 MCP 的需求相当相似:都需要一个解释器形式的沙箱来玩耍。
虽然 MCP 在许多方面有所改进,但仍有不少短板。MCP 最大的问题依然是难以组合。即便有了 codemode——一个用于组合工具调用的小型便捷沙箱——MCP 在这一方面也未完全达标。但如今这更多是外部 MCP 服务器的问题,而非 MCP 协议本身,或者是不同框架与其交互方式的问题。
许多 MCP 服务器目前仍是为那些只是将工具堆砌到上下文中的 harness 而构建的,并试图通过返回文本来优化自身的 token 效率。现在我们更倾向于将 MCP 视为一种更接近 OpenAPI 但具备智能工具发现机制的技术。这意味着工具应返回结构化数据,并应能通过文档和描述被发现。
CLI 之所以功能强大,是因为 agent 和模型仅仅使用高效的 bashisms 将各种东西连接起来。但并没有根本性的理由说明你不能用 MCP 做到同样的事。Pi 中的 MCP 只是在向 JavaScript 沙箱暴露这些工具,其他 harness(如 Codex)也是这么做的。
现代 LLM 中的 MCP
这引出了一个问题:我们为什么没有直接做一个不含 MCP 的 Codemode?部分答案与 Pi 中工具当前的表达方式有关。近几个月来,我们做了大量工作,使 Pi 能够适配那些支持延迟加载工具、对话中途插入 system 消息以及动态调整推理级别的新模型。然而,我们尚未升级工具加载策略,以更好地扩展以支持这些新能力。
在 Codemode 的世界里,你需要决定某个工具是对整个 LLM 可用,还是仅对 LLM 的 Codemode 部分可用。普通的 MCP 扩展无法从 Pi 的工具配置中获得足够的元数据来良好地支持这种体验。因此,我们需要确保工具可以被配置为延迟加载,或仅作为 Codemode 专属功能。
虽然我们可以直接集成元数据来改进 MCP 扩展,但我们认为结合 Codemode 的 MCP 能解决其传统上的许多问题。我们相信,积极影响某项技术的最好方式是拥抱它。尽管我们认为现代 MCP 的情况比过去好得多,但现有的服务器和模式仍有改进空间。因此,我们希望参与到这场讨论中,帮助塑造 MCP,使其在小规模 harness 中也能良好运行,而不是站在旁观者位置 merely 看着。
什么是 Codemode?
聊了这么多 Codemode,现在有必要解释一下它到底是什么。Harness 执行工具时,通常有两个位置可选:要么在 bash 运行的地方执行,要么在 harness 的 agent 循环所在处执行。这两侧的信任级别差别很大:harness 循环往往运行在受信任的环境中,而它执行的工具通常跑在一个并不可靠的沙箱里。
Codemode 的特别之处在于它运行在 harness 一侧。最好把它理解为一种编排和协调工具调用的机制:它本身是个沙箱,让 agent 发起工具调用时可以更灵活地决定执行顺序,还能用 JavaScript 把调用组合起来。由于 Codemode 也运行在 harness 一侧,它的状态是作为会话记录的一部分保存的,而不是存在文件系统里。
理论上用什么语言都行,但 JavaScript 特别有吸引力——因为精简版的 JavaScript 可以打包成 WASM 二进制,并提供合理的保护能力。
在 Pi 中,配置了 MCP 时 Codemode 会自动加载,也可以作为默认工具加入配置。直接让 pi 重新配置自己启用 codemode 就行!之后你不仅能用它处理 MCP,还能玩出不少有趣的花样。比如当你用提供 "Jev" 的账号登录后,可以这样操作:
通过 codemode 使用 typesafe/jev,找出我们 issue tracker 上最沮丧的 20 个评论者
它会聪明地把 Linear MCP 和 Jev 组合起来完成分析,全程在 Pi 内部搞定,一点上下文都不浪费。
> 用 codemode 调用 typesafe/jev,找出 issue 追踪器里最烦躁的用户
我会从 Linear 拉取所有 open 状态的 issue,然后让 Jev 按每次 4 个的速度,逐个判断每条线程的情绪强度。
codemode
const { issues } = await tools.mcp__linear__list_issues({
team: "Pi", state: "open", limit: 250,
});
const jev = await models.getModelOfType(
"classifier", "cloudflare-workers-ai", "typesafe/jev",
);
const questions = {
frustration: {
type: "choice",
instructions: "只判断写这段话的人的情绪," +
"忽略 bug 本身有多严重。",
criteria: {
none: "语气中立、陈述事实或友好,即便问题很严重",
mild: "明显的不耐烦、急躁或失望",
high: "明显愤怒、抓狂、讽刺或彻底受够了",
},
},
};
const results = [];
let next = 0;
async function worker() {
while (next < issues.length) {
const issue = issues[next++];
const { comments } = await tools.mcp__linear__list_comments({
issueId: issue.identifier,
});
const c = await models.classify(jev, { state: { ...issue, comments }, questions });
results.push({ id: issue.identifier, title: issue.title, ...c.answers.frustration });
}
}
await Promise.all([worker(), worker(), worker(), worker()]);
store("frustration", results);
const score = (r) => r.probabilities.mild * 0.5 + r.probabilities.high;
const counts = {};
for (const r of results) counts[r.choice] = (counts[r.choice] ?? 0) + 1;
const flagged = results.filter((r) => r.choice !== "none");
flagged.sort((a, b) => score(b) - score(a));
return {
total: results.length,
counts,
flagged: flagged.map((r) => `${r.id} ${r.title}`),
};
... (前面 331 次调用)
✓ mcp__linear__list_comments {"issueId":"PI-4714"} 255ms
✓ mcp__linear__list_comments {"issueId":"PI-3200"} 354ms
✓ models.classify cloudflare-workers-ai/typesafe/jev 756ms
✓ models.classify cloudflare-workers-ai/typesafe/jev 750ms
{
"total": 167,
"counts": {
"none": 156,
"mild": 11
},
"flagged": [
"PI-7740 /reload 后 TUI 没有按自定义工具的行为来…",
"PI-9566 尽管实际 size 更大,context 默认仍设为 128k…",
"PI-10031 思考时 Pi 会间歇性卡在 “Working...”…",
"PI-4714 [feature] 用 /update 指令替代 pi update",
"PI-5916 支持带 model alias 的 provider 扩展…",
"PI-7547 [Windows] [sink-thread] 在 Windows 上怎么用 Pi…",
"PI-6907 给 README 加一个安装章节",
"PI-6212 提案:Bedrock 路径应遵循 `compat.forceAd…",
"PI-9932 before_agent_start:强制系统提示词导致 tool…",
"PI-7730 macOS 长会话下 CPU 占用高",
"PI-6600 pi update --extensions 与 npm scripts:npm 11.16.0…"
]
}
追踪器上整体很平静。Jev 给 167 条 open issue 里 156 条打的是 neutral,11 条 mild 烦躁,没有一条 high 烦躁。
最典型的几条:
- PI-6907:README 里没有安装章节(用户原话:“It's frustrating”)
- PI-10031:思考阶段按 Esc 后 Pi 卡在 “Working...”
- PI-4714:有人提了 /update 指令的需求(原话:“a pain in the butt”)
- PI-7730:macOS 长会话下 CPU 占用高
每条 issue 的判定结果都存在了 codemode 的 frustration 里,之后想深挖哪一条都不用重新拉 issue 了。
A condensed replay of such a session in Pi. 关于 Jev、Codemode 等议题,我们稍后会作进一步讨论。但我们希望本文能作为一个范例,展示随着世界不断演进,我们如何有意识地持续改进并更新 Pi。