第 04 模块 · 4 节

用扩展接入 MCP 服务器

《Pi 进阶实战》04 扩展开发 Extensions · 本节时长 34 分钟

贯穿项目M04 最后一节,把「扩展 = 写程序」的能力接到生态:MCP。学完你就能把任意 MCP 服务器(文件系统、浏览器、数据库、CI 工具……)暴露的工具,变成 Pi 模型可以直接调用的工具。md-tools 的「检查 markdown」以后也可以选择不自己写,而是接一个现成的 MCP 服务器来做。

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.jsclient/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")
);

其余代码完全一样:connectlistTools → 逐个 registerTool同一份桥接代码,换一行传输就切换了数据来源。


落地练习:接一个文件系统服务器

  1. 在扩展目录建 npm 项目,装好 SDK(上面第一步)
  2. 把桥接扩展写进 ~/.pi/agent/extensions/mcp-bridge.ts
  3. 启动 pi,输入 /tools 或直接问模型「你现在能用哪些工具」
  4. 让模型用 mcp_read_file / mcp_list_directory 读一个文件,确认桥接通了

怎么判断做对了?——模型的工具列表里出现 mcp_ 前缀的工具;调用后能返回真实文件内容;/reload 后重新连接正常、session_shutdown 后没有残留进程。

卡住了怎么办? 工具没出现 → 确认 session_startconnect 成功、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_* 调用前弹确认

小结

  1. MCP = 基于 JSON-RPC 2.0 的开放协议,流程只有三步:initialize → tools/list → tools/call
  2. Pi 不内置 MCP 是设计选择,扩展就是天然的 MCP Client 位置
  3. 桥接套路:session_start 连接 → 逐个 registerToolexecute 里转发 callToolsession_shutdown 关闭
  4. 本地用 StdioClientTransport,远程用 StreamableHTTPClientTransport,桥接代码不变
  5. MCP 服务器有真实权限,信任边界要自己把关

到这里,扩展开发的四个主题(事件、工具、生命周期、MCP)全部学完。下一节进入 M05 贯穿项目——把 md-tools 完整封装成 Skill。