Skip to main content
Glama

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.

npm CI node licence

Diamond · Pearl · Platinum · HeartGold · SoulSilver


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.

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.

{ "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.

{ "servers": { "dspre": { "command": "npx", "args": ["-y", "@webadeva/dspre-mcp"] } } }

.vscode/mcp.json — note VS Code nests under servers, not mcpServers.

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.

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.

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:

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, 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.

Related MCP server: rpgmaker-mz-mcp

Why this exists

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.

  • 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. 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 is the current-state summary, docs/PROGRESS.md the full journal of decisions and reversals, docs/ASSESSMENT.md a deliberately unkind review of this codebase, docs/PORTING.md the conventions, docs/WSL.md the WSL matrix.

Development

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. Releasing (maintainer): push a v* tag; the full procedure is in README.agents.md.

Licensed AGPL-3.0-or-later — see 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 (AGPL-3.0), whose C# source is the format spec and whose charmap.json and script databases generated the tables here; and ds-rom (MIT, © Aetias 2025), invoked as an external binary and not vendored. Full text in 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.

Related MCP Connectors

Related MCP Servers