内容
# baton-proxy
子进程包装的MCP代理。包装一个stdio MCP服务器,在握手过程中注入注释工具,并将摩擦事件发送到一个或多个接收器(stderr、JSONL文件或Baton控制台)。
对底层MCP服务器零修改。代理*是*克劳德视角下的MCP服务器;真正的服务器是其子进程。
```
┌──────────┐ ┌───────────────┐ ┌────────────────────┐
│ 克劳德 │ ◀──▶ │ baton-proxy │ ◀──▶ │ 您的MCP服务器 │
└──────────┘ └───────┬───────┘ └────────────────────┘
│
│ 异步扇出 — 选择任意子集
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ stderr: │ │ file:// │ │ Baton控制台 │
│ JSONL流 │ │ JSONL文件 │ │ (HTTPS POST) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## 快速开始
```bash
pipx install baton-proxy # 或:pip install baton-proxy
```
`pipx` 将CLI安装到其自己的隔离虚拟环境中,并将 `baton-proxy` 放在您的PATH上 — 因此克劳德的配置可以直接调用它,而无需环境激活。普通的 `pip install` 也可以,如果您已经管理自己的Python环境。
替换克劳德配置中的MCP服务器条目:
```jsonc
// 之前
{ "command": "npx", "args": ["@vendor/mcp-server"] }
// 之后 — 零配置:事件发送到stderr + /tmp/baton-proxy.jsonl
{ "command": "baton-proxy", "args": ["--", "npx", "@vendor/mcp-server"] }
```
这就是整个安装过程。重启克劳德,驱动包装的服务器,然后:
- 询问克劳德 **"显示此会话的摩擦报告"** — 代理注入了一个 `baton_session_report` 工具,该工具直接在对话中返回一个可共享的markdown报告,或
- `cat /tmp/baton-proxy.jsonl` 查看原始摩擦事件。
无需环境变量,无需后端,无需凭证。报告是Baton仪器化供应商在其控制台中看到的票证形状的预览。
要将事件发送到控制台(或另外),请添加四个环境变量:
```jsonc
{
"command": "baton-proxy",
"args": ["--", "npx", "@vendor/mcp-server"],
"env": {
"BATON_EVENT_SINK": "https://console.example.com",
"BATON_TENANT_ID": "your-tenant",
"BATON_API_KEY": "...",
"BATON_CONSENT_TOKEN": "..."
}
}
```
代理在上游服务器的工具列表中添加了两个工具:
- `baton_annotate` — 克劳德在遇到摩擦时调用它(无需提示);发出注释事件。
- `baton_session_report` — 克劳德在客户要求报告时调用它;返回会话摩擦的供应商可共享的markdown摘要。**仅在本地接收器安装中注入** — 使用 `http(s)://` 接收器(生产模式)的供应商获得干净的工具列表;供应商的控制台在那里渲染票证。
并且代理为每个真实工具调用发出一个摩擦事件。
## 尝试使用一个命令:`scan`
在您运行的服务器上预览代理可能遇到的摩擦 — 无需永久安装,无需更改克劳德配置:
```bash
uvx baton-proxy scan --config github
```
`scan` 以一个**已经在克劳德中配置**(按名称)的服务器为目标,重用该条目的保存凭证。它写入一个短暂的配置,通过包装的服务器驱动一个无头代理(`claude -p`,计费到您自己的身份验证),并渲染 `./baton-report.md`。所有操作都在本地运行 — 没有任何东西离开您的机器,您也没有输入任何秘密。报告被标记为 **预飞行/推断**:它预览了可能的摩擦,这不是真实用户数据(这就是上面的永久包装捕获的内容)。
为什么使用 `--config` (而不是原始服务器命令)?摩擦报告仅在您实际运行的服务器上提供其洞察力 — 其真实工具,其真实身份验证,您的真实工作流程。因此,`scan` 解析一个配置条目,而不是扫描陌生人的服务器。它从 `~/.claude.json` 或 `./.mcp.json` 读取 `--config <name>`;指向特定文件使用 `--config-file`:
```bash
uvx baton-proxy scan --config github --config-file ./.mcp.json
```
详细信息:
- 解析条目的凭证(其 `env`,包括 `${VAR}` 引用)不加修改地流向包装的服务器。
- 已经是 `baton-proxy` 包装的条目会自动解包装,其 `BATON_*` 变量会被删除,以便扫描会话保持本地,而不是发送到您真正的控制台。
- 远程/ OAuth(`http`/`sse`)条目尚不支持 — `scan` 包装stdio服务器。
- `--timeout` 绑定运行(默认300秒;超时时渲染部分报告)。 `--out` 设置报告路径。
## 发出内容
每个真实工具调用,三个事件类型匹配 [Baton线协议](https://github.com/good-timing/baton/blob/main/docs/SPEC.md) (`tool_calls_start` / `tool_calls_end` / `tool_calls_error`):
| 事件 | 载荷 |
|---|---|
| `tool_calls_start` | `{tool_name, params}` |
| `tool_calls_end` | `{tool_name, result, duration_ms}` |
| `tool_calls_error` | `{tool_name, error_type, error_body, duration_ms}` |
每个事件携带一个会话ID(每个代理进程一个),单调序列号和上游MCP请求的 `_meta` 块(用于周期关联)。
注入的 `baton_annotate` 工具本身由代理处理;上游服务器从未看到它。
## 配置
所有旋钮都是环境变量。每个与发射相关的变量都有一个默认值;零配置安装(无环境变量)将事件写入stderr + `/tmp/baton-proxy.jsonl`。
| 变量 | 默认值 | 目的 |
|---|---|---|
| `BATON_EVENT_SINK` | `stderr:,file:///tmp/baton-proxy.jsonl` | 事件发送到哪里。URL方案选择接收器:`https://console.example.com` 向 `{url}/v0/events` 发送POST,`file:///tmp/events.jsonl` 追加每个事件的JSON行,`stderr:` 将JSONL写入stderr。逗号分隔的值扇出到所有这些。 |
| `BATON_TENANT_ID` | `local` | 租户标识符。为占位符;当发送到控制台时替换。 |
| `BATON_CONSENT_TOKEN` | `local` | 每个进程的同意令牌。**占位符;您必须在指向 `http(s)://` 接收器之前替换它** — 代理拒绝以此组合启动,以免发生具有占位符标记的事件的意外远程泄漏。 |
| `BATON_API_KEY` | _(未设置)_ | 承载令牌。仅当接收器方案为 `http(s)://` 时需要;`file://` 和 `stderr:` 接收器忽略它。 |
| `BATON_VENDOR_ID` | _(未设置)_ | 为操作员标记安装(对多个供应商客户有用的grep JSONL)。**不前缀注入的工具名称** — 在v1中,它保持 `baton_annotate`。需要白色标签工具名称的供应商将在请求时获得一个开关。 |
| `BATON_PROXY_LOG_FILE`| _(未设置)_ | 代理日志记录到(默认:仅stderr)的文件路径。 |
### 三个梯级
选择您需要的梯级;环境变量的差异是整个区别。
| 梯级 | 接收器 | 环境变量添加 |
|---|---|---|
| **1. 默认(安装即用)** | stderr + `/tmp/baton-proxy.jsonl` | _(无)_ |
| **2. 自定义本地捕获** | 您想要的任何地方 | `BATON_EVENT_SINK=file:///path/to/your.jsonl` |
| **3. 发送到控制台** | 主机 | `BATON_EVENT_SINK=https://console.example.com` + `BATON_API_KEY=...` + `BATON_TENANT_ID=your-tenant` + `BATON_CONSENT_TOKEN=real-token` |
### 在本地查看
安装(`{ "command": "baton-proxy", "args": ["--", "npx", "@vendor/mcp-server"] }` 在您的克劳德配置中)并重启克劳德,驱动几个工具调用并尝试:
**对话式** — 询问克劳德:
> 显示此会话的摩擦报告。
克劳德调用注入的 `baton_session_report` 工具;代理返回一个markdown报告(每个工具的分解,错误调用的输入/错误详细信息,模型发出的任何注释),克劳德直接在对话中中继。
**原始** — 检查JSONL流:
```sh
cat /tmp/baton-proxy.jsonl | jq -c '{type: .event_type, payload}'
```
查看 `examples/live-claude-invocation/` 以获取指导性的演练,该演练还涵盖了注入的 `baton_annotate` 工具的诱发行为。
### 接收器错误配置失败
当以下情况时,代理拒绝启动:
- 配置了 `http(s)://` 接收器,但未设置 `BATON_API_KEY`
- 配置了 `http(s)://` 接收器,但 `BATON_CONSENT_TOKEN` 仍为占位符 `"local"`
- 接收器URL具有不支持的方案
这些作为代理启动错误发出,以便错误配置的安装永远不会无声地丢弃或无声地错误标记事件。
## 信任属性
- **开源,Apache 2.0.** 可审计的端到端。
- **失败开放.** 控制台宕机、网络问题或仪表化错误永远不会破坏MCP管道。由 `tests/test_emitter.py::test_stop_is_clean_when_console_dead` 和 `tests/test_injection.py` 测试。
- **仅出站.** 代理永远不会接受入站连接。事件发送到配置的接收器(`https://` 接收器的HTTP POST,`file://` 接收器的本地文件写入);这是唯一的出口表面。
- **无依赖.** 纯标准库。无pydantic,无httpx,无第三方运行时需求。
- **热路径上的发射.** 事件发射被排队到后台线程;代理I/O泵不会等待POST。端到端超时的测量待定。
**信任模型.** `baton-proxy` 和包装的MCP服务器在同一信任域(同一用户,供应商自己的MCP服务器)中运行。代理从上游子进程环境中过滤 `BATON_*` — 上游不需要Baton凭证,并且调试日志记录、崩溃报告环境转储、未来插件等意外泄漏路径不应该看到它们。这不是跨进程信任边界;不要使用 `baton-proxy` 来仪器化您不信任的MCP服务器 — 这不是代理设计的威胁模型。
## 工作原理
两个单向泵:
- **客户端 → 服务器**:将标准输入行转发到子进程。拦截 `tools/call` 以获取 `baton_annotate` (代理合成响应)。对于其他所有 `tools/call`,排队一个 `tool_calls_start` 事件并记录请求ID。
- **服务器 → 客户端**:将子进程的标准输出转发到客户端。修改 `initialize` 响应以追加注释工具指令;修改 `tools/list` 响应以追加 `baton_annotate` 工具。根据ID关联响应以发出 `tool_calls_end` / `tool_calls_error`。
第三个后台线程排空内存队列,并将事件一个一个地发送到配置的接收器(`https://` 的HTTP POST,`file://` 的JSONL追加)。失败的交付被记录并丢弃 — 代理永远不会在热路径上重试。
## 开发
```bash
git clone https://github.com/good-timing/baton-proxy
cd baton-proxy
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
```
## 相关
- **[baton-sdk](https://github.com/good-timing/baton)** — 进程内替代。供应商控制其MCP服务器,添加 `install_baton(mcp, ...)` 而不是子进程包装。相同的线协议,相同的接收器;与一行供应商代码紧密集成。
- **[Baton线协议](https://github.com/good-timing/baton/blob/main/docs/SPEC.md)** — 事件信封,信号分类和HTTPS合同,`baton-proxy` 和 `baton-sdk` 都针对其发出。
## 路线图
- `params` 和 `result` 有效载荷的PII擦除(当前原样传递)。
- 静态链接的单二进制分发(PyInstaller,然后可能是分发形状设置为Go重写)。
- 主机HTTP MCP服务器的Helm图表。
- 主机评估模式(每个请求的同意令牌)。## License
Apache 2.0. See [LICENSE](LICENSE).