bl2-mcp
by lumi-fiona
README.md
# bl2-mcp
Let an AI assistant (Claude Code, or any [MCP](https://modelcontextprotocol.io) client) look
inside a **running Borderlands 2** and drive it: read any object, list a function's real
arguments, run Python in the game, load a character, fast travel, take screenshots. No
keypresses, and the game window never has to be in front.
It has two halves:
| Half | What it is | Where it runs |
|---|---|---|
| **MCP Bridge** (`mcp_bridge.sdkmod`) | A small [willow2 mod manager](https://github.com/bl-sdk/willow2-mod-manager) mod. While enabled, every few frames it checks one folder for a `request.py`, runs it, and writes the output back. | Inside the game |
| **bl2-mcp** (`bl2_mcp.py`) | An MCP server (stdio). It turns tool calls into requests and waits for the answers. | Your machine, started by your MCP client |
## Why
Modding BL2 from outside means guessing: guess how the game works, restart, walk
somewhere, look, guess again. Most of those guesses are facts the running game could tell you:
how many arguments a function really takes, which property holds an item's rarity, what the map
is called at the moment a character spawns. With the bridge, an assistant can look those up in
seconds.
## Security: read this first
**The bridge runs, with no sandbox, any Python that appears in
`<game>/sdk_mods/settings/mcp_bridge/request.py`.** Nothing listens on the network. But any
program or person that can write to that folder can run code inside your game process, with
your user's rights.
- The mod is **off by default**. Enable it in the Mods menu only while you use it. While it is
off it has no hooks, so it reads and writes nothing.
- The mod manager remembers the on/off state between launches. Switch it off when you are done.
- Don't put the game folder in a synced or shared location (cloud-sync folder, network share)
while the bridge is on.
- The MCP server gives your assistant the same power. Only connect assistants you would trust
with a Python prompt on your machine.
## Install
Requirements: Windows, Borderlands 2 with the
[willow2 mod manager](https://github.com/bl-sdk/willow2-mod-manager/releases) v3.8 or newer,
and Python 3.10+ with the `mcp` package for the server.
### 1. The bridge mod
1. Download `mcp_bridge.sdkmod` from the releases page. You can also build it yourself with
`python tools/build_sdkmod.py`, which writes `dist/mcp_bridge.sdkmod`.
2. Drop it into `<game>/sdk_mods/`.
3. Start the game. Open **Mods**, select **MCP Bridge**, read the warning, and enable it.
Optional setting: **Check every N frames** (default 30). This is how often the bridge looks for
a request. Lower numbers answer faster.
### 2. The MCP server
```
pip install mcp # or: pip install . (from a clone; installs the bl2-mcp command)
```
Claude Code:
```
claude mcp add bl2 -- python /path/to/bl2_mcp.py
# or, if the game is not in a Steam library it can find:
claude mcp add bl2 -e BL2_GAME_DIR="C:/Games/Borderlands 2" -- python /path/to/bl2_mcp.py
```
Any MCP client (JSON config):
```json
{
"mcpServers": {
"bl2": {
"command": "python",
"args": ["C:/path/to/bl2_mcp.py"],
"env": { "BL2_GAME_DIR": "C:/Games/Borderlands 2" }
}
}
}
```
**Finding the game.** It uses the first of these that is set:
1. `--game-dir DIR`
2. the `BL2_GAME_DIR` environment variable
3. Steam: it reads the install path from the registry, then `steamapps/libraryfolders.vdf`, then
the app 49520 manifest in each library
The bridge folder is `<game>/sdk_mods/settings/mcp_bridge`. Override it with `--exchange-dir` or
`BL2_MCP_EXCHANGE` if your mods folder is somewhere else.
## Tools
| Tool | What it does |
|---|---|
| `game_status` | Running or not (pid), crash message boxes, bridge heartbeat, current map, character, playthrough |
| `game_launch(until, restart)` | Starts the game with no input. `until="menu"` also walks the title screen to the main menu. Refuses if the game is already running, unless `restart=True`. |
| `game_load_character(playthrough)` | From the main menu, loads the most recent character and waits until its backpack can be read |
| `game_quit(force)` | Sends the console `exit` command, then force-kills after 30 s |
| `game_eval(code)` | Runs Python in the game and returns stdout/stderr. Globals persist between calls; `unrealsdk` and `get_pc` are predefined. |
| `game_find(class_name, contains)` | Lists loaded objects of a class |
| `game_object(path, class_name, only)` | Shows an object's declared properties and their values |
| `game_function(class_name, function_name)` | Shows a function's parameters, in order |
| `game_backpack` | Lists the backpack: class, rarity, level, name, balance |
| `game_travel(station)` | Fast travels by station name. With no name, lists the stations. |
| `game_log(lines, only)` | Tails `Binaries/Win32/Plugins/unrealsdk.log` |
| `game_screenshot(save_to, max_width)` | Captures the game window with `PrintWindow` without focusing it, and returns the image |
An [example session](docs/example-session.md) walks through a real use.
## Limitations
- **Windows only.** Process checks, screenshots and launching use Win32 APIs.
- **Borderlands 2 only, for now.** The Pre-Sequel and Assault on Dragon Keep use the same SDK
and the same `WillowPlayerController:PlayerTick`, so the bridge may work there. It is
untested, and the server is hard-wired to `Borderlands2.exe` and Steam app 49520.
- The bridge only answers while `PlayerTick` runs: on the title screen, in menus and in the
world. It is silent during level loads and while a modal dialog is up. Requests wait.
- Requests run on the game thread, so a slow request freezes the game until it finishes.
- One request at a time, and one MCP server per game.
- Launching without focus is a hint (`SW_SHOWNOACTIVATE`). Windows may still bring the new
window to the front.
## For AI agents
These rules were learned by crashing the game or losing time. Read them before your first
`game_eval`.
**Setup and safety**
- **One game process.** Two copies exhaust the 32-bit memory limit ("Ran out of video memory")
and fight over the one request file. `game_launch` refuses a second copy, so start from
`game_status`.
- **Throwaway characters.** Create a new character for experiments and back up the save folder
(`Documents/My Games/Borderlands 2/WillowGame/SaveData/<id>/`) first. Nothing here has undo.
Steam Cloud uploads saves when the game closes, so restore the backup before the next upload
if you need the old state back.
- **Restart after editing mod files.** Python the game has already imported stays imported, so
a test against stale code looks exactly like a failing fix. Use `game_launch(restart=True)`.
**Reading timeouts**
- **A timeout is not a failure.** If the error says the bridge *picked the request up*, your code
probably ran, or is still running. Check `game_status` and the game state before you resend.
If it says *never picked up*, nothing ran and the request was withdrawn. The game is loading,
showing a dialog, or the bridge is off.
- **Before you kill the game, look.** A long silence during a level load is normal. The game
saves on travel, and a kill during that save can corrupt it.
**Moving and spawning**
- **Move the player with game functions.** Assigning `pc.Pawn.Location` crashed the game. Use
`game_travel` between maps. Within a map, `WillowPlayerController.TeleportPlayer(...)` is the
engine's own teleport; check its parameters with `game_function` first.
- **Live objects, not archetypes.** `find_all("WillowAIPawn")` also returns archetypes and
`Default__` objects, and calling native functions on archetypes crashed the game. For live
pawns, walk `pc.WorldInfo.PawnList`.
- **AI pawns refuse `Destroy()`** (it returns False). `Suicide()` works.
**Navigating without input**
- **Menus without input.** Injected keypresses don't reach the game (`InputKey` returns False),
so every menu step is a function call:
- Title screen to main menu: `WillowGFxMoviePressStart.BeginStartupProcess()`. This is what
`game_launch(until="menu")` does.
- Continue: `pc.LoadLastSavedSaveGame()`, then `FrontendGFxMovie.LaunchSaveGame(n)`, where `n`
is the playthrough. This skips the difficulty and OP prompts. It is what
`game_load_character` does.
- New game: `FrontendGFxMovie.LaunchNewGame()`. This does exactly what the New Game button
does and creates a fresh level-1 save. On class select,
`CharacterSelectionReduxGFxMovie.HandleCharacterClicked(0, i)` picks class `i`, and
`HandleCustomizationMenuEntryClick(0, 3)` confirms. Save a new character explicitly with
`pc.SaveGame()`.
- **Wait for a readable backpack.** The map name is reported before the map's contents exist.
"Loaded" means `len(pc.GetPawnInventoryManager().Backpack)` works.
- **Fields before functions** on freshly spawned items. `RarityLevel` and
`DefinitionData.GameStage` are right, while `GetRarityLevel()` and `GetGameStage()` returned 0.
**Calling game functions**
- **Look before you call.** Use `game_function` to get the real parameter list. Guessing
argument counts costs a round trip per guess.
- **The game hands back its own objects.** `AddBackpackInventory` stores a new object with the
same `UniqueId`, not the one you passed in. Re-read the backpack to get the real object.
- **`SetTimer` from the bridge does nothing** (`IsTimerActive` stays False). Hooks do fire on
functions the bridge calls.
- **`from __future__` still works.** The server's marker is a comment line, so a request may
start with `from __future__ import annotations`.
## How it differs from existing tools
As far as a search in September 2026 found, there was no public MCP server for Borderlands 2 or
pyunrealsdk. The nearest tools:
- **pyunrealsdk's `pyexec` console command** runs a Python file in the game, but someone has to
type it into the console.
- **The mod manager's debugpy and ipykernel hooks** (see
[pyunrealsdk](https://github.com/bl-sdk/pyunrealsdk)) are for a human with a debugger or
Jupyter. They open a local TCP port. They are powerful, but they are not a tool an assistant
can call, and they don't launch, load, screenshot or diagnose a crashed game.
bl2-mcp adds a file-based bridge with no network listener, and an off switch in the Mods menu.
On top of that it has task-shaped tools that know BL2's quirks: menus without input, readiness
checks, crash-box detection, and screenshots without focus.
## Credits
Built on [pyunrealsdk / unrealsdk](https://github.com/bl-sdk/pyunrealsdk) and the
[willow2 mod manager](https://github.com/bl-sdk/willow2-mod-manager) by apple1417 and the
[bl-sdk](https://github.com/bl-sdk) contributors. Without them none of this could reach the
game. It uses the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).
## Development
```
python -m unittest discover -s tests -v # offline: no game needed
python tools/build_sdkmod.py # -> dist/mcp_bridge.sdkmod
```
The tests start the real server over stdio, and run the real bridge protocol
(`mod/mcp_bridge/exchange.py`) in a stand-in process that plays the game's side.
License: [MIT](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues