balatro-codex-mcp
# balatro-codex-mcp
`balatro-codex-mcp` 让 Codex 桌面端通过本地 STDIO MCP 工具控制 Steam 版
《Balatro / 小丑牌》。Python Server 只暴露经过约束的正常游戏动作,不向 Codex
提供任意 JSON-RPC、调试、作弊、存档读写、截图或返回菜单能力。
项目基于:
- [BalatroBot v1.5.2](https://github.com/coder/balatrobot/releases/tag/v1.5.2),
commit `9052d76f14723293f6c6b2cecaa791a5c4ae68f3`
- 核对过的 BalatroBot main commit
[`e7c6db8a9ad88318f6e4128eefd6e61aafc94885`](https://github.com/coder/balatrobot/commit/e7c6db8a9ad88318f6e4128eefd6e61aafc94885)
- [官方 MCP Python SDK v2.1.1](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.1)
- [Codex MCP 当前文档](https://developers.openai.com/codex/mcp)
### 2026-09-06 升级验证
- MCP SDK 和 `mcp-types` 已升级并锁定为 2.1.1,保留官方 `MCPServer` 与 STDIO 入口。
- macOS 已验证 Steamodded [26.829.0](https://github.com/Steamodded/smods/releases/tag/26.829.0)
与 BalatroBot v1.5.2、Lovely 0.9.0 的启动及只读连接兼容性。
- 验证涵盖 `health`、`rpc.discover`、`gamestate` 和 MCP `tools/list`;升级验收未执行
游戏动作,完整对局及无尽模式仍需实际游玩验证。
- 启动脚本将 `localhost`、`127.0.0.1`、`::1` 加入启动进程的 `NO_PROXY` / `no_proxy`,
保留已有绕过规则,避免 macOS 系统代理向健康检查返回 HTML 而触发 `JSONDecodeError`。
此设置不修改系统代理或 BalatroBot 上游源码。
- 升级 Steamodded 前应将旧目录备份到 `Mods` 以外的位置,避免同时加载两份 Mod。
本机升级备份位于 `~/Library/Application Support/balatro-codex-mcp/backups/`。
- BalatroBot 保留稳定版。无尽模式弹窗修复 [PR #200](https://github.com/coder/balatrobot/pull/200)
目前只在 `dev`;该分支还要求专用配置档并自动全解锁,本项目不默认切换。
## 架构
```text
Codex 桌面端
↓ STDIO MCP(唯一游戏控制入口)
本项目的 Python MCP Server
↓ HTTP JSON-RPC 2.0(默认 http://127.0.0.1:12346)
BalatroBot
↓ 游戏内 Mod API
Steam 版 Balatro
```
MCP Server 和 BalatroBot 使用独立生命周期。Codex 启动 MCP Server 时,Server
会尽力读取 `rpc.discover`,但绝不会启动、关闭、重置游戏或加载存档;即使
BalatroBot 离线,MCP 初始化和 `tools/list` 仍会成功,`balatro_health` 会返回
可执行的修复命令。
### 为什么 Codex 桌面端使用 MCP
MCP 把参数类型、工具说明、只读/写入提示和 server-level instructions 一起交给
Codex。模型调用 `balatro_play_cards(cards=[...], expected_state_token="...")`,
而不是通过 Shell 拼接 BalatroBot JSON。这样可以集中执行:
- 0-based 索引和完整排列校验
- `state_token` 乐观并发保护
- 写锁内二次状态校验
- 写操作禁止自动重试
- 工具白名单和危险接口隔离
- stdout 仅承载 MCP 协议,诊断日志只写 stderr
## Balatro、Lovely、Steamodded 与 BalatroBot
- **Balatro** 是 Steam 游戏本体。
- **Lovely Injector** 把 Mod 加载能力注入 Balatro 的 LÖVE 运行时。
- **Steamodded** 是 Balatro 的 Mod loader/API。
- **BalatroBot** 是运行在游戏内的 Mod,并在本机提供 HTTP JSON-RPC 2.0。
- **本项目** 不重写或修改 BalatroBot,只把它的正常游戏 API 收窄为 Codex MCP 工具。
安装 Mod 时遵循
[BalatroBot 安装文档](https://coder.github.io/balatrobot/installation/)、
[Lovely 文档](https://github.com/ethangreen-dev/lovely-injector)和
[Steamodded 文档](https://github.com/Steamodded/smods/wiki)。本项目脚本不会复制、
删除或修改游戏、Mod 和存档文件。
## 前置条件
- macOS 与 Steam 版 Balatro 1.0.1+
- Lovely 0.8.0+
- Steamodded 1.0.0-beta-1221a+
- BalatroBot Mod v1.5.2
- Python 3.13+(项目 `.python-version` 固定 3.13 系列)
- [uv](https://docs.astral.sh/uv/)
- Codex 桌面端;建议同时有 Codex CLI
BalatroBot 当前常见路径:
```text
游戏:
~/Library/Application Support/Steam/steamapps/common/Balatro/Balatro.app
Lovely:
~/Library/Application Support/Steam/steamapps/common/Balatro/liblovely.dylib
Mods:
~/Library/Application Support/Balatro/Mods
BalatroBot:
~/Library/Application Support/Balatro/Mods/balatrobot
```
## Quick Start
在项目根目录:
```bash
uv sync
./scripts/doctor_macos.sh
./scripts/start_balatro_macos.sh --fast
```
启动脚本在前台运行 BalatroBot v1.5.2,默认显示游戏窗口。不要关闭这个终端;
`Ctrl+C` 会直接转发给 BalatroBot。`--fast` 可省略,也可以追加任何
`balatrobot serve` 参数,例如 `--no-shaders`。脚本不会后台运行、不会杀死占用端口
的进程,也不会重置游戏或存档。
打开另一个终端:
```bash
./scripts/verify_connection.sh
./scripts/configure_codex_mcp.sh
```
然后:
1. 重启 Codex 桌面端。
2. 打开本项目。
3. 在对话框输入 `/mcp`。
4. 确认 `balatro` 已连接。
5. 输入下文“推荐游戏提示词”。
如果 `doctor_macos.sh` 报告 Mod 或游戏缺失,先按上游文档补齐;该脚本是只读检查,
不会自动安装或修改系统。
## 安装 Python 项目
```bash
uv sync
```
该命令创建项目私有环境并生成/使用 `uv.lock`。完成后,真正交给 Codex 的入口是:
```text
<仓库绝对路径>/.venv/bin/balatro-codex-mcp
```
可以直接验证入口存在:
```bash
test -x .venv/bin/balatro-codex-mcp
```
不用 `uv run` 启动 Codex MCP 的原因是:macOS GUI 进程未必继承 Homebrew 或用户
Shell 的 `PATH`。`.venv/bin` 中的绝对控制台入口已经包含正确的 Python 环境,
Codex 无需找到 `uv`、`python3` 或 Homebrew。
## macOS 检查与启动 BalatroBot
只读检查:
```bash
./scripts/doctor_macos.sh
```
它检查 macOS、uv、Python 3.13+、Steam Balatro、Lovely、Steamodded、BalatroBot
Mod、12346 端口、BalatroBot health、Codex CLI、项目 `.venv` 和绝对 MCP 入口。
前台启动:
```bash
./scripts/start_balatro_macos.sh
```
快速模式:
```bash
./scripts/start_balatro_macos.sh --fast
```
额外参数原样传给上游:
```bash
./scripts/start_balatro_macos.sh --fast --no-shaders --fps-cap 60
```
脚本使用上游推荐的 `uvx balatrobot serve` 路径并明确固定 v1.5.2。在 macOS,
BalatroBot CLI 直接执行:
```text
~/Library/Application Support/Steam/steamapps/common/Balatro/
Balatro.app/Contents/MacOS/love
```
同时通过 `DYLD_INSERT_LIBRARIES` 加载 `liblovely.dylib`。这是 BalatroBot 当前
macOS launcher 的实现;不是鼠标、OCR、截图或 UI 自动化。
### 只读验证 health
BalatroBot 启动后,在第二个终端运行:
```bash
./scripts/verify_connection.sh
```
该脚本严格只调用:
- `health`
- `rpc.discover`
- `gamestate`
它不会调用 `start`、`play`、`discard`、`buy`、`sell`、`reroll`、`pack`、
`next_round` 或任何其他写操作。
## 配置 Codex 桌面端 MCP
自动配置:
```bash
./scripts/configure_codex_mcp.sh
```
脚本先确认 `.venv` 和绝对入口存在,然后执行等价于:
```bash
codex mcp add balatro \
--env BALATROBOT_URL=http://127.0.0.1:12346 \
-- /ABSOLUTE/PATH/TO/balatro-codex-mcp/.venv/bin/balatro-codex-mcp
```
如果 `balatro` 已存在,脚本会停止并要求先检查/移除旧配置,不会静默覆盖。成功后
会运行:
```bash
codex mcp list
```
也可以在 Codex 桌面端手动添加:
```text
Settings → MCP servers → Add server → STDIO
Name: balatro
Command: <仓库绝对路径>/.venv/bin/balatro-codex-mcp
Environment: BALATROBOT_URL=http://127.0.0.1:12346
```
保存后必须重启 Codex 桌面端。重启后在对话框输入:
```text
/mcp
```
确认 `balatro` 状态为 connected。可参考
[`.codex/config.toml.example`](.codex/config.toml.example);示例只含占位路径,
不包含开发机用户名。
### 环境变量
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `BALATROBOT_URL` | `http://127.0.0.1:12346` | BalatroBot JSON-RPC 根地址 |
| `BALATROBOT_TIMEOUT` | `15` | 单次 HTTP 超时秒数 |
| `BALATROBOT_ALLOW_NON_LOOPBACK` | `0` | 仅显式设为 `1` 才允许非回环地址 |
| `BALATROBOT_READ_RETRIES` | `1` | 只读连接级重试次数,范围 0–3 |
默认只接受 `localhost`、`127.0.0.1` 和 `::1`。URL 不允许凭据、额外路径、query
或 fragment。非回环模式会扩大信任边界,不建议用于普通本地游戏。
## MCP 工具
所有工具都有 `balatro_` 前缀。
### 只读工具
| 工具 | 用途 |
| --- | --- |
| `balatro_health()` | MCP/BalatroBot 状态、health、版本、当前阶段、discover 结果、缺失能力和 macOS 修复命令 |
| `balatro_capabilities(refresh=false)` | 安全上游方法的精简 OpenRPC 参数与 required states;可强制刷新缓存 |
| `balatro_get_state(view="decision")` | 读取紧凑 decision 或保留全部未知字段的 full state |
| `balatro_available_actions()` | 当前允许/不允许动作、原因、参数、有效索引范围与 token |
### 写工具
每个写工具都强制要求 `expected_state_token`,不提供可选默认值。
| 工具 | 上游方法 | 主要约束 |
| --- | --- | --- |
| `balatro_start_run(deck, stake, expected_state_token, seed=None)` | `start` | 仅 `MENU` |
| `balatro_select_blind(expected_state_token)` | `select` | 仅 `BLIND_SELECT` |
| `balatro_skip_blind(expected_state_token)` | `skip` | Boss Blind 禁止跳过 |
| `balatro_play_cards(cards, expected_state_token)` | `play` | 1–5 张、唯一、有效手牌索引 |
| `balatro_discard_cards(cards, expected_state_token)` | `discard` | 检查剩余弃牌和高亮上限 |
| `balatro_cash_out(expected_state_token)` | `cash_out` | 仅 `ROUND_EVAL` |
| `balatro_buy_shop_item(kind, index, expected_state_token)` | `buy` | `kind` 转为 `card`/`voucher`/`pack` 参数 |
| `balatro_reroll_shop(expected_state_token)` | `reroll` | 仅 `SHOP`,检查明显资金不足 |
| `balatro_sell_item(kind, index, expected_state_token)` | `sell` | `joker`/`consumable`,拒绝 Eternal |
| `balatro_use_consumable(consumable_index, target_cards, expected_state_token)` | `use` | 不猜测目标数量 |
| `balatro_choose_pack_item(item_index, target_cards, expected_state_token)` | `pack` | 转为 `{card, targets}` |
| `balatro_skip_pack(expected_state_token)` | `pack` | 转为 `{skip: true}` |
| `balatro_rearrange(area, order, expected_state_token)` | `rearrange` | `order` 必须是完整排列 |
| `balatro_next_round(expected_state_token)` | `next_round` | 仅 `SHOP` |
所有写操作统一执行:
1. 读取最新 `gamestate`。
2. 计算并比较 token。
3. 校验阶段、参数、资源和索引。
4. 获取唯一写锁。
5. 在锁内再次读取状态并校验 token。
6. 向 BalatroBot 发送恰好一次写 RPC。
7. 再次读取状态;如果该只读请求失败,使用写响应中的 gamestate 作为明确降级。
8. 返回上游结果、旧/新 token、完整新 decision state、warnings,并写动作日志。
## decision state、state_token 与 0-based 索引
`balatro_get_state(view="decision")` 保留当前阶段、seed、deck、stake、ante、round、
Blind 目标分数/奖励(上游提供时)/特殊效果、分数、钱、`bankrupt_at`(上游提供时)、
剩余手数/弃牌数、胜负、手牌、Joker、消耗牌、voucher、商店、补充包、reroll
费用、槽位上限、可用动作、warnings 和 `state_token`。
每张卡或物品都保留原始 BalatroBot 字段,并额外提供:
- 当前 `index`
- `balatrobot_id`、`key`、`label`/`name`
- `description`
- `rank`、`suit`
- `enhancement`、`edition`、`seal`、`debuff`
- `buy_cost`、`sell_value`
所有索引均为 **0-based**。任何状态改变后,手牌、商店、Joker、消耗牌和 Pack 的
旧索引立即失效。
`state_token` 使用 action-relevant 状态的规范化 JSON、稳定 key 排序和 SHA-256
生成。同一状态重复读取会得到相同 token;阶段、主要资源、手牌顺序、商店、
Joker、消耗牌或 Pack 改变都会换 token。
如果传入旧 token,工具返回 `STALE_STATE`、实际 token 和最新 decision state,
且不会执行上游写操作。正确处理方式是重新决策,不是盲目重试原动作。
## 开始或继续一局
推荐把下面整段交给 Codex:
```text
请用 balatro MCP 玩小丑牌。先调用 balatro_health,再读取
balatro_get_state(view="decision")。如果已有一局,继续当前局,不要返回菜单,
不要开始新局覆盖它;如果状态是 MENU,则用 RED deck、WHITE stake 开始一局。
每次动作前都重新读取最新 decision state,只使用 available_actions 中允许的动作,
所有索引按 0-based,并把最新 state_token 作为 expected_state_token。每个写操作后
检查 new_state。不要盲目重试失败工具,不要跳过 Boss Blind;商店操作前检查钱和
槽位。进入 GAME_OVER 后停止操作,汇总 ante、round、胜负和关键 Joker。
```
如果只想继续、不允许开始新局,把其中 `MENU` 分支改为“若为 MENU,只报告当前没有
进行中的局,不要调用 balatro_start_run”。
## 常见错误
| 错误/现象 | 含义与处理 |
| --- | --- |
| `CONNECTION_FAILED` | 在独立终端运行 `./scripts/doctor_macos.sh`,再运行 `./scripts/start_balatro_macos.sh --fast` |
| 启动时 `JSONDecodeError` | 若本机 HTTP 请求被系统代理返回 HTML,使用已加入本机地址 `NO_PROXY` 的启动脚本。其他非 JSON 响应仍须按日志排查,不能靠重复游戏动作修复 |
| `UPSTREAM_TIMEOUT` | 确认游戏未卡死;动作结果未知时先读状态,绝不能重复写操作 |
| `STALE_STATE` | 使用错误附带的 `latest_state` 重新决策 |
| `INVALID_GAME_STATE` | 当前界面不允许该动作;读 `balatro_available_actions` |
| `INDEX_OUT_OF_RANGE` | 状态已变化或索引错误;重新读取 decision state |
| `DUPLICATE_INDEX` | 卡牌索引或排列有重复 |
| `INCOMPLETE_PERMUTATION` | `rearrange.order` 必须包含区域的全部索引且只出现一次 |
| `INSUFFICIENT_MONEY` / `NO_FREE_SLOT` | 先卖出、选择其他物品或离开商店 |
| `BOSS_BLIND_CANNOT_BE_SKIPPED` | 必须选择并打 Boss Blind |
| 12346 被占用 | 启动脚本会报出监听进程但不会杀进程;确认是否已有 BalatroBot |
| `balatro` 配置已存在 | `codex mcp get balatro` 检查;确认后用移除脚本,再重新添加 |
| `/mcp` 看不到新配置 | 完整重启 Codex 桌面端,并确认配置使用 `.venv/bin` 的绝对入口 |
## 更新 BalatroBot 后验证接口
1. 阅读新 release 的 `pyproject.toml`、`src/lua/utils/openrpc.json`、
`src/lua/utils/gamestate.lua` 和 `src/lua/endpoints/*.lua`。
2. 更新 `scripts/start_balatro_macos.sh` 中的固定版本。
3. 启动新版本后运行只读验证:
```bash
./scripts/verify_connection.sh
```
4. 在 Codex 中调用:
```text
balatro_capabilities(refresh=true)
```
5. 运行完整项目验收:
```bash
uv sync
uv run ruff check .
uv run ruff format --check .
uv run ty check
uv run pytest
```
如果上游方法参数或状态发生变化,更新兼容表、action guard、工具说明、README 和
`tools/list` 测试后再允许游戏写操作。
## 当前上游差异与已知限制
实施时核对了 v1.5.2 和 main,而不是依赖旧教程。发现:
1. BalatroBot package/release 为 v1.5.2,但 v1.5.2 内嵌 OpenRPC
`info.version` 仍为 `1.5.1`;main 的相同文件写为 `1.5.2`。health 中的版本因此
明确标注来源为 `rpc.discover.info.version`。
2. 当前 `rpc.discover` 没有发布 endpoint 的 `requires_state`。项目以
v1.5.2 `src/lua/endpoints/*.lua` 的实际约束补齐,并在 capabilities 中标注来源。
3. OpenRPC 的 `State` enum 没列出 `SMODS_BOOSTER_OPENED`,但实际 `pack.lua`
明确要求该状态;项目按运行中的实际 endpoint 名称处理。
同时,v1.5.1/v1.5.2 的 `rpc.discover` 漏列了实际已注册的 `pack` endpoint;
项目仅对这两个已核实版本使用 endpoint 兼容表,并在 capabilities 的
`compatible_openrpc_omissions` 中明确报告,不把它误报为缺失能力。
4. v1.5.2 gamestate 没有显式暴露 `bankrupt_at` 或 Blind 奖励。decision state
在上游缺失时返回 `null`,不伪造数据;购买的最终合法性由 BalatroBot 决定。
5. 消耗牌目标数量不是当前 OpenRPC 参数 schema 的一部分。项目只校验类型、唯一性
和索引范围,具体数量遵从描述和 BalatroBot 错误。
6. `view="full"` 保留上游未知字段;decision 是面向决策的非破坏性字段重组,但不
承诺包含与动作无关的未来顶层字段。
7. 这是控制接口,不负责安装 Mod,不使用 OCR、截图识别或鼠标坐标,也不在开发
验收中自动开始真实游戏。
## 日志
- 动作日志:`logs/actions.jsonl`
- 轮转:单文件约 2 MB,保留 3 个备份
- BalatroBot 自身日志:上游默认 `logs/<timestamp>/<port>.log`
- MCP 普通日志:stderr
- MCP stdout:只含协议数据
动作日志只记录动作名、上游方法、参数形状/索引摘要、结果与 token;不会写环境变量、
完整状态或 seed 值。
## 移除 Codex MCP 配置
```bash
./scripts/remove_codex_mcp.sh
```
脚本会先显示即将执行的 `codex mcp remove balatro`,并且只移除名为 `balatro` 的
Codex MCP 配置。它不会删除项目、虚拟环境、BalatroBot Mod 或游戏存档。
## 安全边界
本项目绝不注册或转发以下 BalatroBot 能力:
- `add`
- `set`
- `load`
- `save`
- `screenshot`
- `menu`
同样不存在 `execute_raw`、`call_method`、任意 JSON-RPC、`debug`、`eval`、改钱、
改分、生成 Joker、跳 Ante、强制胜利或加载 checkpoint 工具。这些名称不会出现在
`tools/list`。
其他边界:
- MCP 是 Codex 唯一的游戏控制入口。
- 写 RPC 自动重试次数永远为 0。
- 所有写操作共用一个 `asyncio.Lock`。
- 只读连接失败可以有限重试。
- HTTP 客户端是单个可复用的 `httpx.AsyncClient`。
- 进程退出时关闭 HTTP client 和动作日志,不遗留阻止退出的后台任务。
- 不修改 BalatroBot 上游源码。
- 不修改 Balatro 存档。
- 返回主菜单只能由用户直接在游戏界面操作。
- `GAME_OVER` 后 instructions 要求停止动作并总结。
## 开发与测试
默认测试全部使用 `httpx.MockTransport`,不需要 Balatro 或 BalatroBot:
```bash
uv sync
uv run ruff check .
uv run ruff format --check .
uv run ty check
uv run pytest
```
测试覆盖连接拒绝、超时、HTTP/JSON/JSON-RPC 错误、ID 不匹配、discover 缓存、
unknown fields、decision view、稳定 token、stale 拒绝、索引与排列、参数转换、
写操作零重试、并发写串行化、危险工具缺失,以及 BalatroBot 离线时的真实 STDIO
MCP 初始化和 `tools/list`。
TDQS
Scored across 18 tools
Most tools map to distinct game actions (play/discard, buy/reroll, etc.), so there is little confusion. The only slight overlap is among the informational tools (get_state, available_actions, capabilities), which all provide state/action data but serve different purposes.
All tools share the 'balatro_' prefix and use snake_case. Most follow a verb_noun pattern, but a few (health, capabilities, available_actions, next_round) are noun-like, creating minor inconsistency. Overall, the naming is still predictable and readable.
With 18 tools, the set is slightly above the ideal 3-15 range, but each tool corresponds to a specific part of the Balatro game cycle. The count feels justified for the game's complexity, though a few informational tools could potentially be consolidated.
The tool set covers the entire game loop: starting a run, selecting/skipping blinds, playing/discarding cards, using consumables, choosing/skipping packs, shop interactions, rearranging, cashing out, and moving to the next round. Informational tools (state, health, capabilities, available actions) fill any remaining needs, leaving no significant dead ends.