你当初说不要 MCP!

Earendil 曾公开声明 Pi 不支持 MCP,如今却把它做进了核心。文章解释一年里 MCP 变了什么、为什么它不再适合只当扩展,以及他们用 Codemode 和 Jev 处理 GitHub issue 的新玩法。

中文
复制
米色纸纹背景的封面,上方是 Earendil 的黑色标志与名称,下方是文章标题「"You Said No MCP!"」

日期:2026 年 9 月 29 日,星期二 发件人:Earendil Engineering 收件人:你 主题:“你说过不要 MCP!”

如果你以前访问过 pi.dev,你会看到一句骄傲的声明:Pi 不支持 MCP。如果你听过我们聊 Pi 的播客,你会不止一次听到我们对 MCP 不以为然。包括 Mario 写的一篇帖子。然而,如果你升级 Pi,你会发现 MCP 如今已是受支持的功能之一。发生了什么?

事情会变

首先要记住的是,世界不是静止的。过去一年我们一直在关注 MCP,今天的 MCP 已不是当年的 MCP。但仅凭这一点,还不足以成为把它放进核心的理由。如你所知,Pi 有一个很棒的扩展生态,MCP 完全可以做成一个扩展,对吧?甚至可以是 Earendil 官方背书的扩展。你说得没错,MCP 确实可以做成扩展,它本来也是。MCP 如今进入核心,是我们一起重新思考后的结果。

到底变了什么?

我们把 MCP 放进核心,原因不只是 MCP 本身变了,还因为我们发现,为此所需的改动本身就有普遍价值。比如,我们为 MCP 所做的改动,也让 Jev 在 Pi 里更容易使用。归根结底,Pi 需要的东西和 MCP 需要的东西很像:一个可以折腾的沙箱,形式就是解释器。

MCP 有很多地方变好了,但也有不少地方没变。它最大的问题依然是难以组合。即便有了 codemode——那不过是个用来组合工具调用的小巧沙箱——MCP 也没能完全解决这一点。但到了这一步,问题已经不太在 MCP 本身,而在于外面的那些 MCP 服务器,以及各种 harness 与它们协作的不同方式。

许多 MCP 服务器仍然是为那种把工具直接倒进上下文的 harness 而构建的,并试图在自己这一侧通过返回文本来优化 token 效率。我们现在对 MCP 的看法是,它应该更接近 OpenAPI,加上智能的工具发现。也就是说,工具应当返回结构化数据,工具应当能通过其文档和描述被发现。

CLI 之所以如此好用,是因为 agent 和模型用高效的 bash 手法把东西拼起来就行。但从根本上说,用 MCP 也完全可以做到这一点。Pi 里的 MCP 就是把这些工具暴露给一个 JavaScript 沙箱,和其他 harness(比如 Codex)的做法一样。

现代 LLM 中的 MCP

这自然会引出一个问题:为什么我们不干脆抛开 MCP,只做 Codemode?部分答案与 Pi 目前表达工具的方式有关。近几个月我们做了大量工作,让 Pi 能适配那些支持延迟加载工具、对话中途插入 system 消息以及调整推理等级的新模型。但我们的工具配置还没有升级,尚未更好地扩展到这些新能力。

在 Codemode 的世界里,需要决定一个工具是可供 LLM 使用,还是只供 LLM 中负责 Codemode 的那部分使用。普通的 MCP 扩展从 Pi 的工具配置里拿不到足够的元数据,没法把这件事做好。所以我们必须确保工具可以配置成仅延迟加载,或者只属于 Codemode。

我们本可以直接把元数据接好,让 MCP 扩展更好用,但我们同样认为,MCP 加上 Codemode 能解决它长期以来存在的不少问题。想对一件事产生正面影响,最好的办法就是拥抱它。我们认为如今的 MCP 比过去任何时候都更好,但服务器和模式仍有改进空间。所以我们想加入这场讨论,帮助它在小巧的 harness 里也能良好运作,而不是站在一旁干看着。

什么是 Codemode?

既然我们已经聊了这么多 Codemode,也许该解释一下它到底是什么。harness 执行工具时,大体上分两侧:可以在 bash 运行的地方执行,也可以在 harness 的 agent loop 运行的地方执行。两侧的信任级别差别很大。harness loop 往往跑在受信任的环境里,而它执行的工具通常跑在并不那么受信任的沙箱中。

Codemode 的特殊之处在于,它运行在 harness 所在的一侧。把它理解为一种编排和协调工具调用的机制最合适。它是一个沙箱,让 agent 能以更灵活的方式发起这些工具调用——自行决定调用顺序,并用 JavaScript 把它们组合起来。由于 Codemode 也运行在 harness 一侧,它的状态作为会话记录的一部分保存,而不是存在文件系统里。

理论上任何语言都行,但 JavaScript 相当有吸引力:小体积的 JavaScript 可以打包成 WASM 二进制文件,还能提供一定程度的保护。

在 Pi 中,配置了 MCP 后 Codemode 会自动加载,也可以作为默认工具加进配置。直接让 pi 重新配置自己来启用 codemode 就行!之后你可以用它做很多有意思的事,不限于 MCP。比如,当你登录的 provider 提供“Jev”时,你可以这样做:

用 codemode 调用 typesafe/jev,找出我们 issue tracker 上最抓狂的 20 位评论者

它会聪明地把 Linear MCP 和 Jev 之类的东西组合起来完成这项分析,直接在 Pi 里跑,完全不浪费上下文。

> Use typesafe/jev via codemode to find the most frustrated people on our issue tracker

I'll pull the open issues from Linear and let Jev rate the tone of each thread, four at a time.

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: "Judge ONLY the emotional tone of the people writing. " +
 "Ignore how severe the bug is.",
 criteria: {
 none: "Neutral, factual, or friendly, even about a serious bug",
 mild: "Explicit annoyance, impatience, or disappointment",
 high: "Clearly angry, exasperated, sarcastic, or fed up",
 },
 },
};

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 earlier calls)
✓ 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 TUI after /reload does not follows custom tool's re…",
 "PI-9566 context size defaults to 128k despite the real size…",
 "PI-10031 Pi sporadically stuck in \"Working...\" when thinking…",
 "PI-4714 [feature] /update TUI command instead of pi update",
 "PI-5916 Support provider extensions with model aliases and…",
 "PI-7547 [Windows] [sink-thread] How do you use Pi on window…",
 "PI-6907 Add an Installation section to the README",
 "PI-6212 Proposal: Bedrock path should honor `compat.forceAd…",
 "PI-9932 before_agent_start: forced system prompt keeps tool…",
 "PI-7730 High CPU usage on Mac OS with long session",
 "PI-6600 pi update --extensions and npm scripts: npm 11.16.0…"
 ]
}

Most of the tracker is calm. Jev rated 156 of the 167 open issues as neutral, 11
as mildly frustrated and none as highly frustrated.

The clearest cases:

- PI-6907 no installation section in the README (“It's frustrating”) - PI-10031
Pi gets stuck on “Working...” after pressing Esc during thinking - PI-4714 a
/update command request (“a pain in the butt”) - PI-7730 high CPU usage on macOS
in long sessions

The per-issue verdicts are stored in codemode under frustration, so I can dig
into any of them without fetching the issues again.

Pi 中这样一次会话的精简回放。

关于 Jev 和 Codemode 这类东西,我们后面还会细说,但我们希望这篇文章能作为一个例子,说明随着世界不断演进,我们会如何审慎地调整和更新 Pi。

来源: Earendil← 返回首页