Skip to main content
Glama
README.md
[English](README.md) · [Português (BR)](README.pt-BR.md)

# sts2-mcp-bridge

MCP bridge + autopilot for **Slay the Spire 2** (Early Access). The game (via the
`Communication_Mod` mod, BaseLib + Harmony) pushes match state over HTTP to this Python
bridge; the bridge exposes MCP tools and, optionally, plays combat on its own with a
local deterministic policy — **no LLM in the critical path**.

Project write-up: *"I made an agent play Slay the Spire 2 on its own — and what unlocked it was the game saying 'no'"* (dev.to). This repo is the public proof of the part that is mine: the bridge and the policy test harness.

> ⚠️ **What this repo does NOT contain:** the game mod (`Communication_Mod`).
> The mod code belongs to [Manuelbbl/Communication_Mod_STS2](https://github.com/Manuelbbl/Communication_Mod_STS2)
> (no explicit license — all rights reserved), and my patches on top of it
> (selector v3, rejection channel) are versioned as zips with md5, not redistributed here.

## Architecture

```
Slay the Spire 2 (C#/Godot, mod BaseLib+Harmony)
   └─ POST http://127.0.0.1:5000/update_state   ← loopback, fire-and-forget
        ▼
   sts2_bridge.py (this project)
        │  the HTTP response to each push IS the command channel
        │  {"Type":"PlayCard","HandIndex":N,"TargetIndex":M} | {"Type":"EndTurn"} | OK
        ▼
   MCP (10 tools) → agent (operator/observer)
```

- **Loopback-only:** the mod only talks to `127.0.0.1:5000`; the bridge rejects any origin
  that is not localhost. The game never opens a port.
- **Fire-and-forget:** the mod does not wait for the response to keep running, so the HTTP
  response of each push is the only command channel (no queue, no polling).
- **Rejection channel:** when the game refuses a play, the mod POSTs to `/rejection` (a
  dedicated endpoint, outside the state queue) — that is how the agent finds out it was wrong.

## The autopilot (local policy, no LLM)

`_autopilot_decide(gate, trigger)` inside `sts2_bridge.py` decides the play with greedy
score-based rules: damage/cost with a kill bonus, AOE, preventive and desperate block,
scaling powers in long fights, poison when the hand does not kill, target chosen from the
enemy's intent, `EndTurn` when there is no useful play. Zero LLM calls in the loop
(verified by grep).

The game saying "no" becomes state: cards rejected as `NotPlayable` go into an `unplayable`
set that filters the hand in the following decisions — this was the fix for the GRAND_FINALE
softlock (the bot kept retrying a card it could not play and froze the game). No retraining:
the environment's error becomes a constraint in the agent's state.

## How to run

```bash
pip install -r requirements.txt        # fastmcp, uvicorn, httpx
STS2_MCP_PORT=5000 python3 sts2_bridge.py
```

Healthy print: `[sts2-bridge v0.2.7]` + `MCP tools -> http://0.0.0.0:5000/mcp`.

The game mod must be compiled and active (`mods/Communication_Mod`), with the game open.
The bridge does not need to run on the same machine as the game if there is a tunnel/route
to port 5000 of the game PC (but the mod always talks to **its own** loopback).

### Testing the policy without the game (and without fastmcp)

```bash
python3 autopilot_harness.py sts2_bridge.py
```

Loads the bridge up to `import fastmcp`, runs 5 synthetic scenarios and one full turn
simulation. **This is a QA process, not certification** — it proves the policy responds as
expected on synthetic cases, not that it wins runs.

## Honest limitations

- **1 combat validated end-to-end** (2026-09-08: 7 turns, 35 actions, 0 rejections — session
  log). **There is no winrate metric.**
- Multi-class (Silent/Defect/Necrobinder/Regent) is mapped by state mechanics through
  defensive reflection — **not validated end-to-end**.
- Slay the Spire 2 is Early Access: every update can break the mod patches (they target
  methods by name). The bridge survives state changes by reading through defensive
  reflection; the mod, not always.
- Dynamic card costs (e.g. Stomp) and setup powers are only partially modeled in the policy.

## Integrity (md5, verified 2026-09-09)

| Artifact | md5 |
|---|---|
| `sts2_bridge.py` (this repo, v0.2.7) | `b4286ec5522e0edfc31591b644dd6449` |
| `sts2-bridge-v0.2.7.zip` | `5a555e906369d821dd0e6270a1236196` |
| `Communication_Mod_v3selector_v0.3.2.zip` (mod, not redistributed) | `516a4797f21c36f06a2b140bf94661af` |

## License

MIT — see [LICENSE](LICENSE). The code in this repo (bridge, harness) was written from
scratch for this project.