cc-codex-a2a-shim
by wuhanhao456
README.md
# cc-codex-a2a-shim
把 **Claude Code** 和 **Codex CLI**(本机二进制)包装成 **MCP 服务器**(HTTP 传输),让任何 MCP 客户端(Hermes Agent 等)直接把 cc/codex 当工具调。
## 它解决什么
- **会话续接**:同一 `contextId` 的多次调用映射到同一个 cc/codex 会话(cc 用 `-r <session_id>`,codex 用 `exec resume <session_id>`),多轮对话不丢上下文;换 contextId = 开新会话
- **提问抑制**:每次调用注入 system 指令,让 cc/codex 按合理假设直接执行并列出假设,不中途等待澄清
- **超时兜底**:子进程超时强杀,返回 `state="timeout"`(结构化 error,不挂死)
- **Bearer 鉴权**:每个 agent(claude/codex)独立 token,请求头 `Authorization: Bearer <token>` 校验;token 绑 agent,越权调用返回 `state=failed, rawError=forbidden_agent`
- **Agent Card**:每个 agent 暴露 `GET /.well-known/agent-card.json`(A2A 风格元数据,方便人读和服务发现)
## 暴露的 MCP 工具
| 工具 | 说明 |
|------|------|
| `send_message` | 发一条消息给 cc 或 codex(`agent`: claude/codex,`text`,`context_id` 可选缺省新建) |
| `list_sessions` | 列出 contextId → 底层 session 的映射表 |
| `reset_session` | 删除某个 contextId 的映射(下条消息开新会话) |
| `get_agent_card` | 读取某个 agent 的能力卡 |
## 部署(Windows,EPYC 实机为例)
完整版见 [`docs/DEPLOY_EPIC.md`](docs/DEPLOY_EPIC.md)(防火墙、开机自启、排障表、复验命令)。最小路径:
```powershell
# 1. 装依赖(仓库根目录)
pip install -e .
# 2. 固定 token(必须!不设 = 每次重启随机生成,Hermes 立刻 401)
python -c "import secrets;print('claude:',secrets.token_urlsafe(24));print('codex :',secrets.token_urlsafe(24))"
setx SHIM_CLAUDE_TOKEN "<生成的第 1 个>"
setx SHIM_CODEX_TOKEN "<生成的第 2 个>"
# 3. 其余配置(setx 只对新开进程生效)
setx SHIM_WORKDIR "C:\Temp\shim-workspace" # cc/codex 的工作目录,写绝对路径
setx SHIM_STATE_FILE "C:\Temp\shim-sessions.json" # contextId 映射落盘,写绝对路径
setx SHIM_TIMEOUT_S "600" # 单次调用子进程超时(秒)
setx SHIM_CLAUDE_BIN "C:\Users\Administrator\AppData\Roaming\Claude\claude-code\2.1.299\f0584be0fbaf\claude.exe"
setx SHIM_CODEX_BIN "C:\Users\Administrator\AppData\Local\OpenAI\Codex\bin\896a0166b774037a\codex.exe"
# codex 的 --skip-git-repo-check 由适配器自动追加,无需配置
# 4. 启动(默认 0.0.0.0:9910,一个进程同时服务 claude/codex)
python -m cc_codex_a2a_shim.server
```
Hermes 侧接入(`~/.hermes/config.yaml`)—— 按 agent 拆两条,token 与 agent 一一对应:
```yaml
mcp_servers:
cc_shim_claude:
url: "http://192.168.1.196:9910/mcp"
headers:
Authorization: "Bearer ${SHIM_CLAUDE_TOKEN}"
timeout: 600 # 与 SHIM_TIMEOUT_S 对齐;cc/codex 跑任务可能很慢
cc_shim_codex:
url: "http://192.168.1.196:9910/mcp"
headers:
Authorization: "Bearer ${SHIM_CODEX_TOKEN}"
timeout: 600
```
- URL 就是 `/mcp`(streamable-http 端点)。`/mcp/mcp` 是旧版路径叠一层的绕法,现在 404。
- 两个 agent 共用同一端点,鉴权头决定身份:claude 的 token 调 `agent=codex` 会返回 `state=failed, rawError=forbidden_agent`(目标行为,见 DEPLOY 3.5「授权模型」;旧版本上越权调用会被直接执行,部署后请用 `/whoami` + 一次跨 agent 调用确认)。
- `timeout` 建议 600 秒,与服务端 `SHIM_TIMEOUT_S` 对齐,可略大(660)以便先收到服务端的结构化超时。
重开会话即可看到 `mcp__cc_shim_claude_send_message` / `mcp__cc_shim_codex_send_message` 等工具。
## 契约说明(客户端要按这个读)
- `send_message` 返回 `{state, contextId, sessionId, output, rawError, durationS}`。
- `state` 有三个值:`completed`、`failed`,以及 **`timeout`**。`timeout` 是 **shim 扩展状态**,A2A 的 `TaskState` 里没有这一档,也不是把失败折了一层——超时就是原样对外返回 `state="timeout"`(`rawError="timeout"`),客户端必须单独分支处理。
- `rawError` 平时为 `null`。`state=completed` 时唯一可能非空的是 `rawError=store_error: ...`:本轮 CLI 已经跑完并拿到结果,但 `contextId → session_id` 映射没写进 `SHIM_STATE_FILE`(磁盘满 / 目录只读)。此时 `output` 照常可用、进程内续接仍然有效,**重启后该 contextId 会退化成新会话**。`state=failed` + `store_error` 则说明本轮根本没跑(`create_new` 删不掉旧映射,无法保证"新会话")。
- Agent Card 里的 `protocolVersion`(当前 `"mcp/1.0"`)是 **shim 自标值**,不是 A2A 标准协商结果、也不是 MCP 协议版本;真正的协议版本以 `POST /mcp` 的 `initialize` 握手响应为准。这张卡只用于人读与服务发现。
## 架构
```
MCP client (Hermes) ──HTTP──> FastAPI :9910
├─ /mcp (MCP streamable http)
├─ /.well-known/agent-card.json?agent=claude|codex (免鉴权)
├─ /healthz (免鉴权)
├─ /whoami → {"agent": "claude"|"codex"} (token 身份自检)
├─ BearerAuth 中间件(每 agent 独立 token)
└─ adapters/
├─ claude_adapter.py → claude.exe -p/--append-system-prompt/-r
└─ codex_adapter.py → codex.exe exec/exec resume
· SessionStore(contextId→session_id, 落盘 sessions.json)
· 超时 kill + 输出解析 + error 包装
```
## 开发
```bash
pip install -e ".[dev]"
pytest tests/ -q
```
适配器/存储测试打桩子进程(不需要真实 cc/codex 二进制);HTTP 与 MCP 层测试用
`TestClient` 在进程内跑完整 ASGI 栈(含 lifespan),不监听端口、不发真实子进程。
## License
MITThis server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues