MCP 是什么
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 推出的开放协议,让 AI 应用与外部工具服务器对话。它建立在 JSON-RPC 2.0 之上——如果你学过 M03 提示词模板、或在 pi-pro 里见过 JSON 用法,会觉得它很熟悉。
三个角色要分清:
| 角色 | 是什么 | 例子 |
|---|---|---|
| MCP Host | 宿主应用,模型真正运行的地方 | Pi |
| MCP Client | 协议客户端,负责连接与转发 | 你写的扩展 |
| MCP Server | 提供工具的外部程序 | filesystem 服务器、数据库服务器 |
传输方式有两种:
- stdio:本地启动一个子进程,通过标准输入输出通信。适合本地工具,
command: "npx"直接拉起 - HTTP(Streamable HTTP):连接远程服务器端点。适合云端服务、团队共享的工具
无论哪种传输,流程都只有三步:
1. initialize —— 握手,协商协议版本与能力
2. tools/list —— 问服务器「你有哪些工具」
3. tools/call —— 带参数调用某个工具
一句话:MCP 服务器是「工具提供方」,协议是「统一点菜单」。
为什么 Pi 不内置 MCP
还记得 M01-1 吗?Pi 刻意省略了 MCP、子 Agent、计划模式等,设计哲学就是「少即是多,缺的按需加」。
更关键的是:MCP 不是一个「内置功能」,而是一套协议。接入方式取决于你的项目需要哪个服务器、哪种传输、什么权限。这种「按需定制」的事,恰好是扩展系统该干的——扩展 = 写 TypeScript 程序 = 你可以在里面启动一个 MCP Client。
所以正确的姿势不是等官方加内置 MCP,而是用扩展自己桥接。
思路:扩展做「桥」
桥接的套路非常固定,把协议三步翻译成 Pi 扩展的三个动作:
启动连接(session_start)
└─► client.connect(transport) # 连上服务器
└─► client.listTools() # 拿到工具清单
└─► 逐个 pi.registerTool(...) # 每个 MCP 工具 = 一个 Pi 工具
调用时(模型调用 mcp_xxx)
└─► client.callTool({ name, arguments }) # 转发给服务器
└─► 返回结果映射成 Pi 工具格式
关闭(session_shutdown)
└─► client.close() # 04-3 学的:清理在 shutdown
注意你已经在 04-3 学过「session_start 重建、session_shutdown 清理」——这里就是它的实战应用。
最小实现:用官方 SDK
写 MCP Client 不需要从零实现 JSON-RPC,官方有 TypeScript SDK:@modelcontextprotocol/sdk。
第一步:准备环境(扩展目录需要是一个能装依赖的 npm 项目):
# 在扩展目录(如 ~/.pi/agent/extensions/)初始化并安装依赖
npm init -y
npm i @modelcontextprotocol/sdk @earendil-works/pi-coding-agent typebox
第二步:写桥接扩展。以官方 filesystem 服务器为例:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
const client = new Client({ name: "pi-mcp-bridge", version: "0.1.0" });
let transport: StdioClientTransport | null = null;
// 启动/重载时连接,把服务器工具注册成 Pi 工具
pi.on("session_start", async () => {
transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", process.cwd()],
});
await client.connect(transport);
const { tools } = await client.listTools();
for (const t of tools) {
pi.registerTool({
// 加前缀避免和 Pi 内置工具混淆
name: `mcp_${t.name}`,
label: `MCP · ${t.name}`,
description: t.description ?? "",
// MCP 的 inputSchema 是 JSON Schema;Pi 用 TypeBox。
// 简单做法:收任意扁平参数后原样透传。
parameters: Type.Record(Type.String(), Type.Unknown()),
async execute(_toolCallId, params) {
const r = await client.callTool({ name: t.name, arguments: params });
// MCP 返回 { content, isError },Pi 工具返回 { content, details },
// content 都是 [{ type: "text", text }] 结构,可直接映射。
return { content: r.content, details: {} };
},
});
}
});
// 会话结束/切换时关闭连接,别让它挂在后台
pi.on("session_shutdown", async () => {
await client.close();
transport = null;
});
}
三个容易出错的地方,先记住:
- SDK 的 import 要带
.js后缀(client/index.js、client/stdio.js),它是 ESM 包 - 参数 schema 要转换:MCP 用 JSON Schema,Pi 的
registerTool要 TypeBox。教学例用Type.Record收任意参数;生产环境可以写一个 JSON Schema → TypeBox 的小转换器,或为常用工具手写字段 - 连接只建一次:在
session_start里连、复用,别在每次工具调用里new Client
远程服务器怎么接
stdio 只适用于本地命令。接远程 MCP 服务器时,把 StdioClientTransport 换成 HTTP 传输:
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("https://your-mcp-endpoint.example.com/mcp")
);
其余代码完全一样:connect → listTools → 逐个 registerTool。同一份桥接代码,换一行传输就切换了数据来源。
落地练习:接一个文件系统服务器
- 在扩展目录建 npm 项目,装好 SDK(上面第一步)
- 把桥接扩展写进
~/.pi/agent/extensions/mcp-bridge.ts - 启动 pi,输入
/tools或直接问模型「你现在能用哪些工具」 - 让模型用
mcp_read_file/mcp_list_directory读一个文件,确认桥接通了
怎么判断做对了?——模型的工具列表里出现
mcp_前缀的工具;调用后能返回真实文件内容;/reload后重新连接正常、session_shutdown后没有残留进程。
卡住了怎么办? 工具没出现 → 确认 session_start 里 connect 成功、listTools 有返回。调用报错 → 看服务器 stderr(stdio 模式报错会打到扩展进程日志)。npx 拉不动 → 先手动跑一遍 npx -y @modelcontextprotocol/server-filesystem 确认网络与 Node 版本。
常见坑:把 MCP 服务器当成「没权限的东西」
最容易忽略的是信任边界:MCP 服务器跑在真实环境里,文件系统服务器能读你整台电脑的文件,数据库服务器能执行真实查询,浏览器服务器能操作真实页面。它等于给 Pi 的工具箱接上了「真刀真枪」。
安全习惯:
- 只连可信服务器,先看
tools/list的清单再决定要不要开放 - 服务器别用 root 跑;给 stdio 服务器传限定目录(比如
server-filesystem ./projects/而不是/) - 需要确认的高危操作,用 04-1 学的
tool_call事件拦截,在mcp_*调用前弹确认
小结
- MCP = 基于 JSON-RPC 2.0 的开放协议,流程只有三步:initialize → tools/list → tools/call
- Pi 不内置 MCP 是设计选择,扩展就是天然的 MCP Client 位置
- 桥接套路:
session_start连接 → 逐个registerTool→execute里转发callTool→session_shutdown关闭 - 本地用
StdioClientTransport,远程用StreamableHTTPClientTransport,桥接代码不变 - MCP 服务器有真实权限,信任边界要自己把关
到这里,扩展开发的四个主题(事件、工具、生命周期、MCP)全部学完。下一节进入 M05 贯穿项目——把 md-tools 完整封装成 Skill。