内容
# pi-mcp-server
MCP 服务器,用于连接 Claude Code 和 pi 编程代理。
## 为什么存在
Claude Code 内置的 shell 工具在沙盒中运行,没有外部 DNS,因此任何需要访问 OpenRouter、Anthropic、Google 或其他 LLM 提供商的东西都必须在沙盒外运行。pi 编程代理([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent))就是这样一个工具:它执行实际工作,但前提是它可以调用 API。
`pi-mcp-server` 在沙盒外作为普通用户进程运行,并将 pi 会话暴露为 MCP 工具。Claude Code 作为 MCP 客户端通过本地 HTTP 连接, 可以启动 pi 代理,发送提示,检查成本,并在不离开沙盒的情况下处理完毕。
## 快速入门
```sh
npm install -g @a-fig/pi-mcp-server
export OPENROUTER_API_KEY=sk-or-... # 或在 shell 配置中设置
pi-mcp-server & # 监听 http://127.0.0.1:3700/mcp
claude mcp add pi-mcp --transport http --url http://127.0.0.1:3700/mcp
```
您还需要 `~/.pi/agent/` 下的正常 pi 设置。服务器将身份验证委托给 pi:如果 `~/.pi/agent/auth.json` 包含有效的 OAuth 令牌,pi 将使用它;否则,它将回退到 `OPENROUTER_API_KEY` 等提供商环境变量。有关首次设置的详细信息,请参阅 pi 的文档。
## Claude 如何使用它
连接后,Claude 有四个工具。
| 工具 | 输入 | 输出 |
| --- | --- | --- |
| `pi_new_session` | `cwd`,`model`,可选 `max_cost_usd`,`thinking_level`,`system_prompt` | `{ sessionId }` |
| `pi_prompt` | `sessionId`,`text` | `{ text, costUsd, cumulativeCostUsd, inputTokens, outputTokens, finishReason, errorMessage? }` |
| `pi_list_sessions` | (无) | `{ sessions: SessionInfo[] }` |
| `pi_close_session` | `sessionId` | `{ closed: true }` |
一个典型的 Claude 工作流程:在您的仓库中用廉价模型和 `max_cost_usd` 为 0.50 创建一个会话;向 pi 发送类似“重写此模块以使用异步迭代器,然后运行测试”的提示;pi 工作,编辑文件,完成时返回;检查累计成本;关闭会话。
## 工具参考
### `pi_new_session`
在 `SESSION_DIR` 下的 `agentDir` 中启动一个新的 pi 代理。返回一个 `sessionId` 以供其他三个工具使用。
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `cwd` | 字符串 | 是 | 绝对路径。Pi 运行在此处启动。 |
| `model` | 字符串 | 是 | `provider:modelId`,例如 `openrouter:anthropic/claude-3.5-haiku`。 |
| `max_cost_usd` | number > 0 | 否 | 每会话美元上限。参见 [成本上限](#成本上限)。 |
| `thinking_level` | 枚举 | 否 | `off`,`minimal`,`low`,`medium`,`high` 或 `xhigh`。 |
| `system_prompt` | 字符串 | 否 | 此会话的自定义系统提示。 |
输出 (`structuredContent`):
```json
{ "sessionId": "0a2c..." }
```
当 `cwd` 不是绝对路径时,或者 `max_cost_usd` <= 0 或 NaN 时,或者 pi 拒绝模型字符串时,或者底层代理启动失败时,`isError: true`。
### `pi_prompt`
向命名的会话发送 `text` 并等待 pi 完成该回合。每 10 秒发出 `notifications/progress` 以便 MCP 客户端不会超时。
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `sessionId` | 字符串 | 是 | 由 `pi_new_session` 返回。 |
| `text` | 字符串(非空) | 是 | 用户消息发送。 |
输出 (`structuredContent`):
```json
{
"text": "...",
"costUsd": 0.000123,
"cumulativeCostUsd": 0.000456,
"inputTokens": 512,
"outputTokens": 88,
"finishReason": "completed",
"errorMessage": "..."
}
```
`finishReason` 是 `completed`、`error` 或 `cost_cap` 之一。当且仅当 `finishReason` 为 `completed` 时,工具返回成功。`errorMessage` 仅在 `finishReason !== 'completed'` 时存在。对于 `cost_cap`,`text` 可能仍包含 pi 终止前流式传输的部分响应。
### `pi_list_sessions`
无输入。返回此服务器进程已知的所有会话,按创建时间排序。
输出 (`structuredContent`):
```json
{
"sessions": [
{
"sessionId": "0a2c...",
"cwd": "/abs/path",
"model": "openrouter:amazon/nova-micro-v1",
"cumulativeCostUsd": 0.000456,
"maxCostUsd": 0.5,
"createdAt": 1747958400000,
"lastActiveAt": 1747958412000
}
]
}
```
当未设置上限时,`maxCostUsd` 为 `null`。
### `pi_close_session`
释放会话并释放其资源。幂等:关闭未知 `sessionId` 成功。
| 参数 | 类型 | 必需 | 描述 |
| --- | --- | --- | --- |
| `sessionId` | 字符串 | 是 | 要关闭的会话。 |
输出 (`structuredContent`):
```json
{ "closed": true }
```
仅当 pi 本身出现意外处理失败时,`isError: true`。
## 配置
| 变量 | 默认值 | 描述 |
| --- | --- | --- |
| `PORT` | `3700` | HTTP 端口。必须是 1-65535。 |
| `SESSION_DIR` | `~/.pi-mcp` | 每会话 pi 代理目录的根目录。 |
| `LOG_LEVEL` | `info` | `debug`、`info`、`warn`、`error` 之一。stderr 中的 JSON 行日志。 |
| `OPENROUTER_API_KEY` | 无 | 传递给 pi 会话。如果 `~/.pi/agent/auth.json` 不包含 OAuth 令牌,则需要。 |
| `ANTHROPIC_API_KEY` | 无 | 相同的备用链。pi 知道的其他提供商环境变量也适用。 |
HTTP 服务器仅绑定 `127.0.0.1` 并无身份验证。它检查每个请求的 `Host` 头部并拒绝与 `421 Misdirected Request` 不匹配的请求,以防止来自攻击者控制的页面的 DNS 重新绑定。如果您需要多机器访问或身份验证,请在前面放置反向代理;此版本的身份验证和远程绑定支持不在此范围内。欢迎 PR。
## 成本上限
`max_cost_usd` 通过两个检查强制执行每个会话:
1. **预检**。在每次提示之前,如果累计支出已经达到或超过上限,则提示直接失败,无需调用 pi。它返回 `finishReason: 'cost_cap'` 和 `costUsd: 0`。
2. **中间检查**。在每次助理 `message_end` 时,pi 报告的累计使用量与上限进行比较。如果达到或超过上限,则中止 pi(尽力而为),并以 `finishReason: 'cost_cap'` 完成该回合。任何已经流式传输的文本都保留在结果中。
上限不是硬保证。pi 可能在检查触发时有一个正在进行的 LLM 调用;该调用被允许完成其当前助理消息,这可能会使支出略微超过上限。后续提示将在同一会话中直接失败。设置上限时留一些余地。
## 模型
模型通过 `provider:modelId` 地址。示例:
- `openrouter:anthropic/claude-3.5-haiku`
- `openrouter:amazon/nova-micro-v1`
- `openrouter:google/gemini-2.0-flash-exp:free`
pi 通过其自己的 `ModelRegistry` 解析标识符。任何 pi 识别的内容,此服务器都可以使用。对于开发和测试,我们依赖 `openrouter:amazon/nova-micro-v1`,因为一次快速运行的成本远低于一毛钱。
## 限制和已知问题
- **服务器重启后无持久性**。会话存在于服务器进程中。pi 在 `~/.pi-mcp/sessions/<uuid>/` 下写入其自己的 JSONL 会话文件,但此服务器目前不会在启动时重新连接它们。重启服务器,会话映射丢失。
- **磁盘使用量增长**。每个会话都会留下其 `agentDir`。当没有活动的服务器使用它时,请使用 `rm -rf ~/.pi-mcp/sessions/`(或 Windows 等价物)定期清理。
- **Windows 信号处理是尽力而为**。大多数 shell 下的 SIGINT 有效;如果关闭卡住,请使用 `taskkill /PID <pid>`。
- **无身份验证**。绑定到 `127.0.0.1`。不要在公开接口上公开端口而不添加身份验证。
- **每个 `sessionId` 会话**。并发会话通过每个会话的 `agentDir` 隔离,因此繁忙的服务器可以并行运行许多会话;单个会话一次只能处理一个提示。
## 开发
```sh
npm install
npm run build # tsc → dist/
npm run dev # tsx watch src/index.ts
npm test # vitest,55 个单元测试,无网络
npm run typecheck # tsc --noEmit
```
实时冒烟测试会访问真实的 OpenRouter,需要设置 `OPENROUTER_API_KEY`。每次运行的成本低于 0.001 美元,适用于 `amazon/nova-micro-v1`。
```sh
npx tsx scripts/smoke-pi.ts # PiSession 针对真实的 OpenRouter
npx tsx scripts/smoke-l4.ts # 完整的 4 工具 MCP 客户端端到端
npx tsx scripts/smoke-l5.ts # 成本上限行为,预检 + 中间检查
```
`npm run test:live` 是一个占位符;今天的实时覆盖率是由上述冒烟脚本提供的。
## 架构
```
Claude Code (MCP 客户端,可流式传输 HTTP)
|
v
pi-mcp-server (Node, 127.0.0.1:3700/mcp)
| - 一个 McpServer 每个 HTTP MCP-Session-Id
| - 一个进程范围的 SessionManager
| - 每个提示 10 秒 notifications/progress 心跳
| - DNS 重新绑定 Host 头检查
|
+--> PiSession A (createAgentSession, 隔离的 agentDir)
+--> PiSession B (createAgentSession, 隔离的 agentDir)
+--> ...
```
pi 通过 `createAgentSession` SDK 在进程内使用,而不是作为子进程。这避免了 JSONL 分帧的脆弱性,为我们提供了类型化的事件,并允许服务器在每个 `message_end` 处拦截使用情况更新,以强制执行成本上限。
## 贡献
问题和 PR 欢迎访问 <https://github.com/a-fig/pi-mcp-server>。测试套件是 `npm test`;请保持绿色并为任何新行为添加单元测试。对于影响实时 pi 的更改,请在打开 PR 之前运行相关的冒烟脚本。
## 许可证
MIT。参见 [LICENSE](./LICENSE)。
## 致谢
- [`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) 用于底层代理 SDK。
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) 用于 MCP 服务器和可流式传输的 HTTP 传输。