dspre
by webadeva
README.md
<div align="center">
# dspre-mcp
**Edit Gen IV Pokémon DS ROMs by talking to an agent.**
Point it at a `.nds`, say _"give Whitney a Garchomp holding a Focus Sash"_, and the bytes change.
[](https://www.npmjs.com/package/@webadeva/dspre-mcp)
[](https://github.com/webadeva/dspre-mcp/actions/workflows/ci.yml)
[](https://nodejs.org)
[](LICENSE)
Diamond · Pearl · Platinum · HeartGold · SoulSilver
</div>
---
```
open_rom { romPath: "/roms/heartgold.nds" }
lookup_names { category: "trainer", query: "whitney" }
→ [{ id: 30, name: "Whitney" }, { id: 714, name: "Whitney" }]
get_trainer { trainer: 30 } → party, slot numbers, current items
set_trainer { trainer: 30, party: [{ slot: 2, species: "Garchomp", level: 40,
heldItem: "Focus Sash",
moves: ["Earthquake", "Dragon Claw",
"Crunch", "Swords Dance"] }] }
set_pokemon { species: "Pikachu", baseStats: { attack: 90, speed: 110 } }
save_rom { outPath: "/roms/heartgold-hack.nds" }
```
Names or IDs, either works. Writes are staged, previewable with `dryRun`, and undoable. Nothing
touches a `.nds` until `save_rom`, and `save_rom` never overwrites the ROM you opened.
## Install
**Requirements:** Node ≥ 22.18 · a legally obtained `.nds` of a supported game · the `dsrom`
binary, which the server can install for you.
<table>
<tr><th align="left">Claude Code</th></tr>
<tr><td>
```sh
claude mcp add dspre -- npx -y @webadeva/dspre-mcp
```
Add `--scope user` for every project, or `--scope project` to write it into the repo's `.mcp.json`.
</td></tr>
<tr><th align="left">Claude Desktop · Cursor · Windsurf</th></tr>
<tr><td>
```json
{ "mcpServers": { "dspre": { "command": "npx", "args": ["-y", "@webadeva/dspre-mcp"] } } }
```
Claude Desktop: `claude_desktop_config.json` (Settings → Developer → Edit Config). Cursor:
`~/.cursor/mcp.json`. Windsurf: `~/.codeium/windsurf/mcp_config.json`.
</td></tr>
<tr><th align="left">VS Code</th></tr>
<tr><td>
```json
{ "servers": { "dspre": { "command": "npx", "args": ["-y", "@webadeva/dspre-mcp"] } } }
```
`.vscode/mcp.json` — note VS Code nests under `servers`, not `mcpServers`.
</td></tr>
<tr><th align="left">WSL, or from a clone</th></tr>
<tr><td>
WSL works in three placements, including running the server in Linux against a project folder on
the Windows drive that DSPRE's GUI also opens. See [`docs/WSL.md`](docs/WSL.md).
From a clone: `pnpm install && pnpm build`, then open the folder in Claude Code — the committed
`.mcp.json` registers and builds the server for you.
</td></tr>
</table>
### Or just paste this prompt into your AI agent
Any agent that can edit files and run a shell — Claude Code, Codex, Cursor, Windsurf, Copilot,
Gemini CLI, Cline, Continue — can register the server itself. Hand it this:
```text
Set up the dspre-mcp MCP server for me. It lets you edit Gen IV Pokémon DS ROMs
(Diamond/Pearl, Platinum, HeartGold/SoulSilver) conversationally.
Repo: https://github.com/webadeva/dspre-mcp
Package: @webadeva/dspre-mcp on npm
Launch: stdio MCP server, started with `npx -y @webadeva/dspre-mcp`
Needs: Node >= 22.18
Please:
1. Check `node --version` is at least 22.18 and tell me if it is not.
2. Register the server, under the name "dspre", with whichever MCP client you are
running inside. Prefer that client's own CLI if it has one (for example
`claude mcp add`); otherwise edit its MCP config file directly. Do not guess
the path or the schema — find the config file that already exists on this
machine and match the shape it already uses. Most clients nest servers under
"mcpServers"; VS Code uses "servers" instead.
3. Show me the exact change you made, and tell me whether I need to restart or
reload the client before it will connect.
4. Once the tools are visible, call `install_dsrom`. The server shells out to an
external `dsrom` binary for ROM extract and repack, and that tool downloads
the official hash-verified release into a per-user cache. If I tell you DSPRE
is already installed, call `configure {dspreDir: "<its folder>"}` instead.
5. Verify by calling `get_docs` with no arguments — it is the one tool that works
with no ROM open — and then read the `dspre://guide` resource.
Then stop. Do not open, edit or rebuild any ROM until I ask, and remember that I
have to supply my own legally obtained .nds file.
```
### First run
`open_rom` needs the external `dsrom` binary ([`ds-rom`](https://github.com/AetiasHax/ds-rom), the
same CLI DSPRE shells out to). Pick one:
- **Call `install_dsrom`** — downloads the official, hash-verified v0.8.0 release for your platform
into a per-user cache. Idempotent. Outside MCP: `npx @webadeva/dspre-mcp install-dsrom`.
- **Already have DSPRE?** `configure {dspreDir: "C:\\pokehg\\tools\\DSPRE"}` — it takes the install
root, the `Tools` folder or an explicit binary path, validates it and remembers it.
Only `open_rom` and `save_rom` need the binary; every other tool works on an already-extracted
project folder. `open_rom` extracts into `<rom>_DSPRE_contents` beside the ROM unless you pass
`projectDir` — worth doing if you'd rather not have a 120 MB folder appear there.
## Why this exists
[DSPRE](https://github.com/DS-Pokemon-Rom-Editor/DSPRE) is the reference tool for Gen IV ROM
hacking, and it is a Windows WinForms app with no API, no CLI, nothing to wrap. So this project
**reimplements DSPRE's binary-format logic in TypeScript** and exposes it over MCP. DSPRE's C#
source is the format spec; a real retail ROM is the oracle.
## Read this before you rely on it
> [!IMPORTANT]
>
> **HeartGold (English) is the only validated game.** Diamond/Pearl and Platinum are ported from
> the C# and typecheck, but no DP/Pt code path has ever run against a real ROM. Treat them as
> unproven — `open_rom` says so when it applies.
- **3D and geometry are not implemented** — map models, building placement, terrain. Map _events_
(NPCs, signs, warps, triggers) are editable; the geometry they stand on is not. Those files
round-trip untouched, so keep using DSPRE for them.
- **A rebuilt ROM boots.** melonDS, full speed, reaches Professor Oak's intro with `set_text` edits
rendered in the game's own font. Record: [`docs/BOOT-TEST.md`](docs/BOOT-TEST.md).
- **Rebuild differs from retail in exactly 4 bytes** — the two header CRC16 fields, which cannot be
recomputed without the copyrighted ARM7 BIOS key table. DSPRE ships the same values. The e2e
suite pins those four offsets, so a real regression still fails.
- **`set_script`: prefer `edits` over whole-text.** The line-level path is byte-exact (443/443); the
whole-listing path is byte-exact for only 154/497, because vanilla scripts share container bytes
between functions and carry dead code the text form cannot express.
- **DP/Pt egg moves** go through overlay 5 and are covered by synthetic round-trips only.
## What you can edit
34 tools. Every species / move / item / ability / type / trainer argument takes a **name or an ID**,
and every one in a reply comes back as `{id, name}`.
#### Project
| Tool | Purpose |
| ----------------------------- | ----------------------------------------------------------------------------------------- |
| `open_rom` / `save_rom` | Extract a `.nds` into a DSPRE-compatible project folder; rebuild it into a playable one. |
| `get_rom_info` | Details of the open project, or the NDS header of any ROM without extracting it. |
| `list_data_types` | Every kind of packed data this game exposes, where it lives, whether it is present. |
| `lookup_names` | Name ↔ ID lookup across species, moves, items, abilities, types, classes, trainers, maps. |
| `install_dsrom` / `configure` | Get the `dsrom` binary, or point the server at one you already have. |
#### Pokémon and battle data
| Tool | Purpose |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_pokemon` / `set_pokemon` | Base stats, types, abilities, EV yield, held items, gender ratio, egg data, growth rate, learnset, evolutions, egg moves, TM/HM compatibility as the moves they teach. |
| `get_move` / `set_move` | Type, damage class, power, accuracy, PP, priority, effect id and chance, target, contest data, flag bitfield as named booleans. |
| `get_item` / `set_item` | Price, hold/Pluck/Fling effects, Natural Gift, pockets, field and battle use, party-use parameters — de-aliased through the ARM9 item table. |
| `get_trainer` / `set_trainer` | Class, AI flags, battle type, held items, battle messages, and the full party: species, form, level, item, moves, gender/ability overrides, ball seal. |
#### Text and scripts
| Tool | Purpose |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `get_text` / `set_text` | Read and replace messages in a text archive, by index, paginated. |
| `search_text` | Find messages by substring — one archive, or all 829 at once (~0.2 s). |
| `get_script` / `set_script` | Disassemble a script into DSPRE-style text and write it back, as line-level `edits` or a whole listing. Level scripts are read and written as trigger lists. |
#### World
| Tool | Purpose |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `get_encounters` / `set_encounters` | Wild encounter tables, every slot named and numbered so individual slots can be patched. |
| `get_events` / `set_events` | One map's NPCs, signs, warps and script triggers. Patch a record, append one, delete one. |
| `get_world_data` / `set_world_data` | `mapHeader`, `matrix` (including resizing), `headbutt`, `trade`, `flyDestination`, `safariZone`. |
| `get_rom_table` / `set_rom_table` | `machineMoves`, `starters`, `spawn`, `hiddenItems`, `pickup`, `rockSmashItems` — these live in ARM9/overlays. |
#### ROM patches and safety
| Tool | Purpose |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `list_rom_patches` / `apply_rom_patch` | ARM9 expansion, matrix expansion, BDHCAM, building rotation, trainer-name expansion — and unlike DSPRE, undoable here. |
| `list_changes` | The journal of what this server has written to the project, newest first. |
| `undo_changes` | Roll back the last N changes. Refuses if a file was edited outside the server since. |
| `revert_changes` | Undo everything (`confirm: true`), returning the folder to how the server first found it. |
| `get_docs` | Serves the agent documentation as a tool. The one tool that works with no ROM open. |
## How it's verified
Every format module is validated by round-tripping **every entry of every archive** in a real
retail HeartGold ROM and asserting the bytes come back identical. Measured, not estimated.
| Area | Result |
| ------------------------------------------ | -------------------------------------------------------------------------------------- |
| Text archives | **829/829** byte-exact |
| Messages | **49,984/49,984** string-identical to DSPRE's own decode |
| Scripts (parse → serialize) | **497/497** byte-exact |
| Script line-level edits | **443/443** same-length edits keep the file's recorded layout |
| Level scripts | **468/468** byte-exact, including through `set_script`'s JSON trigger list |
| Disassembly listings | **965/965** textually identical to DSPRE's `.script` output |
| Personal data | **508/508** |
| Learnsets | **508/508** |
| Evolutions | **508/508** |
| Moves | **471/471** |
| Items | **514/514** |
| Item → data-record table | **537/537** entries; rewriting it reproduces `arm9.bin` byte-for-byte |
| Trainers | **738/738** (properties + party archives) |
| Trainer battle messages | **1,717/1,717** records and **735/735** offsets |
| Map headers | **540/540** |
| Headbutt encounter files | **540/540** |
| Rock Smash item files | **540/540** |
| Event files (NPCs, signs, warps, triggers) | **491/491** byte-exact — 1,034 spawnables, 2,667 overworlds, 1,317 warps, 195 triggers |
| Matrices | **288/288** (also matching DSPRE's unpacked output), plus 5/5 synthetic resize cases |
| Wild encounter files | **142/142** |
| Safari Zone areas | **12/12** |
| In-game trades | **13/13** |
| Fly destinations | **30/30** — rewriting the table reproduces all 1,120,352 bytes of `arm9.bin` |
| ROM-global tables (`get_rom_table`) | all six reproduce `arm9.bin`, `ov001`, `ov012` and `ov036` byte-for-byte |
| Overlay BLZ decompression | **127/127** compressed retail overlays, byte-for-byte vs ds-rom |
| NARC | byte-exact round-trip; members match DSPRE's own unpacked output |
| Name tables | ~3,000 names round-trip name → ID → name across all 8 categories |
**331 pass / 2 skip / 0 fail** (252 core + 79 MCP end-to-end), on Node 22 and 24 in CI on every
push. The MCP suite drives a real `Client` over an in-memory transport, so it exercises the same
path a live agent does — including a full 124 MB `save_rom` rebuild and the ROM-toolbox patches
applied for real.
Where DSPRE itself is lossy, we round-trip instead of reproducing the bug — DSPRE's headbutt writer
expands 480 of 540 vanilla files from 4 bytes to 76, zeroes DP/Pt's fourth water encounter table,
appends a stray padding word to 240 learnsets, and writes 36 bytes for a 34-byte item record. A
no-op edit here changes nothing.
## The safety layer
The intended operator is an LLM — the actor most likely to issue a confident, wrong, bulk edit —
and the project folder is the only copy. So every write goes through one transaction layer.
| | |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Preview** | `dryRun: true` on every `set_*` and `apply_rom_patch` reports the files, entries and new values the call would touch, and writes nothing. |
| **All-or-nothing** | `set_pokemon` alone writes up to four archives. Every file lands in a temp directory first, and only when all of them are down does each get renamed onto its target. |
| **Undo survives restart** | `<project>/.dspre-mcp/` holds a journal and pre-image blobs of the changed _span_, not the file — editing a map header journals 24 bytes, not the 1.1 MB `arm9.bin`. |
| **Refuses to guess** | Undo refuses rather than silently discarding work if something edited the folder behind the server's back. |
| **Your ROM is safe** | `save_rom` never overwrites the ROM you opened; the default output is `<rom>.edited.nds`, and an `outPath` resolving to the source is refused. |
| **Serialised** | Every handler, reads included, queues behind one process-global mutex, so a read never observes a half-open transaction. |
Ambiguity and typos **fail loudly**. HeartGold really has two trainer entries called Whitney and 24
called Silver, so resolving that throws and lists the colliding IDs rather than editing the wrong
battle and looking like it worked. "Garchmop" answers "Did you mean GARCHOMP (445)?".
## For AI agents
> **Reading this repository as an agent?** The complete, unsummarised version of this document —
> every install placement, every tool, every measured pass rate, every documented refusal — is
> **[`README.agents.md`](README.agents.md)**. Go there.
Once the server is connected it documents itself, and an agent needs nothing from this repository:
- **`instructions`**, sent on connect: the workflow, the staging model, the safety net, names-vs-IDs.
- **`dspre://guide`** — mental model, pagination, what a write replies, what is validated versus
merely ported, and the ~20 gotchas that actually bite.
- **`dspre://recipes`** — worked multi-step tasks with the exact calls and the shape of each reply.
- **`dspre://reference/<topic>`** — the field-by-field reference each tool description points at:
`pokemon`, `moves`, `items`, `trainers`, `text`, `scripts`, `encounters`, `events`, `world-data`,
`rom-tables`, `patches`, `history`, `families`, `setup`.
- **`dspre://project`** — a live summary of the open project, or `no ROM open`.
- **Five prompts**: `edit-trainer`, `edit-dialogue`, `rebalance-species`, `add-npc`,
`safe-edit-session`.
- **`get_docs {topic?}`** serves the same text as a tool, for clients that do not surface resources.
Every enum table, per-kind field list and slot-rate table in those documents is **rendered from the
code at request time**, not transcribed, so a document cannot drift from the handler it describes.
Tool descriptions are deliberately short, because `tools/list` is sent before an agent has done
anything and every character is paid for up front.
## Under the hood
```
packages/dspre-core Pure data layer, zero MCP dependency. NDS ROM parse/extract/build,
NARC pack/unpack, one module per DSPRE editor (data/ for the flat
tables, world/ for map data, text.ts + script/ for text and scripts,
overlay/ for ARM9, BLZ and ROM-toolbox patches), plus names.ts and
project-data.ts.
apps/dspre-mcp Thin MCP server over @modelcontextprotocol/sdk. Schema, validation,
a call into core, a formatted reply. No format logic lives here.
packages/tsconfig Shared strict tsconfig both of the above extend.
```
Parse and serialize are pure functions over `Buffer` (`parseX(buf)` / `serializeX(x)`), with file
I/O at the edges — which is what makes the round-trip tests above trivial to write and impossible
to fake.
It was ported one module per `ROMFiles` class under a standing rule: **never invent a byte layout.**
Where the C# is ambiguous the field is left out and the ambiguity recorded rather than guessed — a
plausible-but-wrong offset produces a file that looks fine and corrupts a save. Field _meaning_ is
cross-checked against the corresponding WinForms editor, since the `ROMFiles` class gives offsets
and the editor gives semantics and valid ranges.
Large tables are generated, not transcribed: the text charmap from DSPRE's `charmap.json`, the
per-family script command tables from DSPRE's script databases, each by a committed script. The
text codec does not exist in the C# at all (DSPRE shells out to `chatot.exe`), so it was rederived
from the charmap and validated by the 829/829 re-encode plus agreement with DSPRE's decoded output
on all 49,984 messages.
Some things are **deliberately refused** rather than guessed at: growing the fly table (no length
field), DP/Pt map header flag bit order (the reference contradicts itself), ground-item _names_ in
event files, editing the hidden-item table's length, removing a matrix section, and the side effects
of changing a starter. A BLZ _compressor_ was written, measured at 68/127 byte-exact, and deleted
rather than shipped.
One practical note for anyone working on this: search the C# reference with **`grep -a`**. Four
files under `DS_Map/` are not valid UTF-8, and plain `grep -r` skips them _silently_. That cost
real work — matrix resizing sat in the known-gaps list for a whole phase as "not in the public
source", when `ResizeMatrix` was at `GameMatrix.cs:122` the whole time.
Further reading: [`docs/STATE.md`](docs/STATE.md) is the current-state summary,
[`docs/PROGRESS.md`](docs/PROGRESS.md) the full journal of decisions and reversals,
[`docs/ASSESSMENT.md`](docs/ASSESSMENT.md) a deliberately unkind review of this codebase,
[`docs/PORTING.md`](docs/PORTING.md) the conventions, [`docs/WSL.md`](docs/WSL.md) the WSL matrix.
## Development
```sh
pnpm build # turbo build — tsc across the workspace
pnpm lint # turbo lint — eslint, type-checked
pnpm test # turbo test — node:test, no framework
pnpm dev # core in tsc --watch, server under node --watch
pnpm format # prettier
```
Tests that need a real ROM read from a gitignored `.fixtures/` directory and `t.skip()` cleanly when
it is absent, so the suite passes on a machine without one. With fixtures present a full run costs
~30 s against a native `dsrom` and ~2.5 min against DSPRE's bundled `dsrom.exe` — almost all of it
the two tests that build a real 134 MB ROM.
Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md). Releasing (maintainer): push a `v*` tag; the
full procedure is in [`README.agents.md`](README.agents.md#releasing).
## Legal
Licensed **AGPL-3.0-or-later** — see [`LICENSE`](LICENSE). That is not a preference: this project
ports DSPRE's binary-format logic, and DSPRE is AGPL-3.0, so the copyleft carries. If you run a
modified version of this server over a network, the AGPL requires you to offer its source to its
users.
This project **ships no game data.** You must supply your own legally obtained `.nds` of a game you
own; nothing here contains or distributes copyrighted Nintendo material.
Credits: [DSPRE](https://github.com/DS-Pokemon-Rom-Editor/DSPRE) (AGPL-3.0), whose C# source is the
format spec and whose `charmap.json` and script databases generated the tables here; and
[ds-rom](https://github.com/AetiasHax/ds-rom) (MIT, © Aetias 2025), invoked as an external binary
and not vendored. Full text in [`NOTICE`](NOTICE).
Pokémon, Nintendo, Game Freak and Creatures Inc. are trademarks of their respective owners. This
project is not affiliated with, endorsed by, or sponsored by any of them.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues