@webadeva/dspre-mcp
v0.1.0
Published
MCP server for editing Gen IV Pokémon DS ROMs (Diamond/Pearl, Platinum, HeartGold/SoulSilver) — 34 tools over a TypeScript port of DSPRE's binary formats.
Maintainers
Readme
dspre-mcp
An MCP server for editing Nintendo DS Pokémon ROMs (Diamond, Pearl, Platinum, HeartGold,
SoulSilver) conversationally. Point an agent at a .nds, say "give Whitney's Miltank a Choice
Band", and the bytes change.
Install
Register the server with your MCP client, then point it at a .nds.
Requirements: Node >= 22.18 · the dsrom binary (auto-installable — see First run) · a legally
obtained .nds ROM of a supported game · pnpm, for the from-a-clone route only (corepack
enable is enough; the repo pins its version).
Claude Code
claude mcp add dspre -- npx -y @webadeva/dspre-mcpAdd --scope user to make it available in every project, or --scope project to write it into the
repo's .mcp.json. If you already have DSPRE installed, pass its tools folder up front:
claude mcp add dspre -e DSPRE_TOOLS_DIR=/path/to/DSPRE/Tools -- npx -y @webadeva/dspre-mcp.
Claude Desktop / Cursor / Windsurf
{ "mcpServers": { "dspre": { "command": "npx", "args": ["-y", "dspre-mcp"] } } }Claude Desktop: claude_desktop_config.json (Settings → Developer → Edit Config). Cursor:
~/.cursor/mcp.json, or .cursor/mcp.json for one project. Windsurf:
~/.codeium/windsurf/mcp_config.json.
VS Code
.vscode/mcp.json — VS Code nests under servers, not mcpServers:
{ "servers": { "dspre": { "command": "npx", "args": ["-y", "dspre-mcp"] } } }From a clone of this repo
git clone https://github.com/webadeva/dspre-mcp
cd dspre-mcp
pnpm install
pnpm buildThen claude mcp add dspre -- node "$PWD/apps/dspre-mcp/dist/index.js" — or just open the folder in
Claude Code, since the committed .mcp.json registers the server for you and builds it on first
launch if dist/ is missing or stale. (.mcp.json gives the bootstrap script as a relative path, so
it assumes the client runs the server with the repo root as its working directory, which is what
Claude Code does.)
To drive the server yourself without an MCP client, pnpm --filter @webadeva/dspre-mcp start runs
dist/index.js on stdio, speaking JSON-RPC.
WSL
Three placements. Details, including the rules for sharing a project folder with DSPRE, are in
docs/WSL.md.
1. DSPRE on Windows, this server in WSL, one shared project folder — the headline case. DSPRE
is installed on Windows, the ROM and its _DSPRE_contents folder live on the Windows drive, and
the server runs inside WSL against that same folder, so you can alternate between DSPRE's GUI and
this server on one project. Register it the ordinary way inside WSL:
claude mcp add dspre -- npx -y @webadeva/dspre-mcpand pass Windows paths — C:\pokehg\heartgold.nds works, as does /mnt/c/pokehg/heartgold.nds.
Co-editing a project folder with DSPRE is supported; the rules are in docs/WSL.md.
For this placement, point the server at DSPRE's own dsrom.exe rather than installing a Linux
one: configure {dspreDir: "C:\\pokehg\\tools\\DSPRE"}. Counter-intuitively the Windows binary is
about 20× faster than a native Linux build when the project folder is on the Windows drive
(measured: 1.0 s vs 20 s to extract), because the slow part is crossing the filesystem boundary,
not the binary. Match the binary to where the files are — the full matrix is in
docs/WSL.md.
2. Server in WSL, Claude Desktop on Windows. In claude_desktop_config.json:
{
"mcpServers": {
"dspre": { "command": "wsl.exe", "args": ["-e", "bash", "-lc", "npx -y @webadeva/dspre-mcp"] }
}
}This form is verified: the server's stdout stays a clean JSON-RPC stream (Ubuntu's login banner goes
to stderr, so it does not corrupt it). The one thing that does break it is PATH: bash -lc is
a non-interactive login shell, so it never reads ~/.bashrc/~/.zshrc, and an nvm-managed Node is
not on its PATH — npx then fails with node: command not found. If that happens, give the
absolute path to Node instead, or source nvm first; both forms are in docs/WSL.md.
3. Server on Windows Node natively. Use the plain npx -y @webadeva/dspre-mcp JSON above in Claude
Desktop's config. Paths you pass are Windows paths (C:\roms\heartgold.nds).
Have DSPRE installed? Tell the server once: configure {dspreDir: "C:\\pokehg\\tools\\DSPRE"} — or
skip DSPRE entirely with install_dsrom.
First run
open_rom needs the external dsrom binary. If it is not found, the error says exactly what to do:
call the install_dsrom tool, which downloads the official, hash-verified ds-rom v0.8.0
release for your platform into a per-user cache (~/.cache/dspre-mcp/bin, or
%LOCALAPPDATA%\dspre-mcp\bin on Windows) and is idempotent. Outside MCP the same thing is
npx @webadeva/dspre-mcp install-dsrom.
If you already have DSPRE or a dsrom binary, use configure instead — it takes DSPRE's
install root, its Tools folder, or an explicit binary path, validates it, and remembers it. Or set
DSPRE_TOOLS_DIR to a directory containing dsrom or dsrom.exe; DSPRE's own Tools/ folder will
do, and under WSL a Windows path like C:\pokehg\tools\DSPRE\current\Tools is accepted.
Resolution order: an explicit configure argument → DSPRE_TOOLS_DIR (or DSPRE_DIR for the
install root) → the persisted config file → the cache install_dsrom wrote → PATH. Only
open_rom and save_rom need the binary at all; every other tool works on an already-extracted
project folder.
Then point the agent at a .nds and say what you want changed. open_rom extracts into
<rom>_DSPRE_contents beside the ROM by default; pass projectDir to put it somewhere else, which
you want if the ROM sits anywhere you would rather not have a 120 MB folder appear.
Why this exists
DSPRE (DS Pokémon ROM Editor Reloaded) is the
reference tool for Gen IV ROM hacking: a Windows WinForms app (C#, .NET Framework 4.8) that takes a
.nds ROM, reads its header / FAT / FNT, unpacks the NARC archives holding the game data into a
project folder, lets you edit ~20 data types through GUI editors, and repacks everything back into
a .nds.
It is GUI-only — 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
| | |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Validated game | HeartGold (English) only. 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. |
| External binary | ROM extract/repack shells out to dsrom (the ds-rom CLI), exactly as DSPRE does. install_dsrom fetches the official hash-verified release, or configure points at one you already have (DSPRE ships one). Under WSL a Windows .exe is run through interop, which is the faster choice when the project lives on the Windows drive. Everything inside the extracted filesystem is pure TypeScript. |
| Not implemented | 3D and geometry editing (DSPRE's NSBMD/NSBTX territory: map models, building placement, terrain). Map events — NPCs, signs, warps, triggers — are editable; the geometry they stand on is not. Those files round-trip through the project folder untouched, so keep using DSPRE for them. |
| Rebuild is not byte-identical | An extract→rebuild round-trip reproduces the ROM except for 4 bytes: the two header CRC16 fields at 0x06C and 0x15E. The secure-area CRC covers the KEY1-encrypted secure area and cannot be recomputed without the copyrighted ARM7 BIOS key table. DSPRE ships the same values, and ndstool reports the field as invalid for ROMs built by other Gen IV toolchains too. The e2e suite pins it: a project taken back to vanilla and rebuilt through save_rom differs from the retail ROM at exactly 0x6C, 0x6D, 0x15E, 0x15F and nowhere else, so a real regression still fails the test. |
| A rebuilt ROM has been booted | A ROM built by this server boots in melonDS at full speed, reaches Professor Oak's intro, and renders text edited through set_text in the game's own font. Verdict PASS; the full record, with timings and screenshots, is docs/BOOT-TEST.md, re-runnable via scripts/boot-test.sh. Nothing past the opening cutscene was played. |
| set_script caveat | set_script's edits mode (a list of line-level changes) goes through the byte-exact path: of the 497 vanilla HeartGold scripts, 443 contain a nudgeable non-jump command and 443/443 kept their recorded layout, differing only inside the edited command. Its whole-text mode is structurally correct for 496/497 but byte-exact for only 154/497 — vanilla scripts share container bytes between functions and carry dead code the text form cannot express. Prefer edits. |
| Unproven even on HGSS | DP/Pt egg moves (read and written through overlay 5) are covered by synthetic round-trips only — no DP/Pt fixture exists, so the layout against a real overlay 5 is unverified, and set_pokemon says so. |
What actually works
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. These are measured pass rates, not estimates.
| 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 |
Test suite: 309 pass / 1 skip / 0 fail (235 core + 74 MCP end-to-end), run on Node 22 and 24 in
CI on every push. The MCP suite drives a real Client over an in-memory transport against a
throwaway copy of the fixture project, 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 34 tools
Project
| Tool | Purpose |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| open_rom | Extract a .nds into a DSPRE-compatible project folder and make it the open project (~1-2 s when the dsrom binary matches the filesystem the project is on, ~10-20 s when it does not — see docs/WSL.md; reuses an existing folder unless force). |
| save_rom | Rebuild the project folder back into a playable .nds (~8-9 s matched, ~18-50 s mismatched). Never overwrites the ROM that was opened: the default output is <rom>.edited.nds beside it, and an outPath resolving to the source is refused. |
| 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 and browsing across species, moves, items, abilities, types, trainer classes, trainers and locations. |
| install_dsrom | Download the official, hash-verified ds-rom v0.8.0 binary into a per-user cache. Idempotent; reports the path if one is already installed. |
| configure | Tell the server once where DSPRE or a bare dsrom binary lives (install root, Tools folder, or explicit binary path), validate it and persist the choice. Called with no arguments it reports the effective configuration and every location searched. |
Pokémon and battle data
| Tool | Purpose |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| get_pokemon / set_pokemon | One species: base stats, types, abilities, EV yield, held items, gender ratio, egg data, growth rate, learnset, evolutions, egg moves, TM/HM compatibility (rendered as the moves the machines teach). Egg moves come from the eggMoves NARC on HGSS and from overlay 5 on DP/Pt — the latter unverified against a real ROM, and the tool says so. |
| get_move / set_move | One move: type, damage class, power, accuracy, PP, priority, effect id and chance, target, contest data, flag bitfield as named booleans. |
| get_item / set_item | One item: price, hold/Pluck/Fling effects, Natural Gift, pockets, field and battle use, party-use parameters. De-aliased through the ARM9 item table: on vanilla HeartGold 424 of 537 items have a data record whose index is not their item ID (Park Ball is item 500, record 477), and several items share one record — so set_item reports every other item the write also moved. |
| get_trainer / set_trainer | One trainer battle: class, AI flags, battle type (named or raw), held items, the battle messages this trainer says, 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). Paginated, so a common word is pageable rather than merely truncated. |
| get_script / set_script | Disassemble one script archive entry into DSPRE-style script text and write it back — either as a list of line-level edits (the byte-exact path) or as a whole listing. Level scripts have no text form in DSPRE either, so they are read and written as a structured trigger list. |
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 event file: NPCs, signs, warps and script triggers, every record numbered. Patch a record's fields, append one, or delete one; a trainer overworld also reports the trainer it fights as {id, name}. |
| get_world_data / set_world_data | The long tail behind one kind argument: mapHeader, matrix (including resizing it), headbutt, trade, flyDestination, safariZone. |
| get_rom_table / set_rom_table | Six ROM-global tables behind one kind: machineMoves, starters, spawn, hiddenItems, pickup, rockSmashItems. They live in arm9.bin and overlays 1/12/36 rather than in a NARC. |
ROM patches
| Tool | Purpose |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| list_rom_patches | Every ROM-toolbox patch, whether this game supports it, whether it is applied, and which prerequisites are missing. |
| apply_rom_patch | Apply one (ARM9 expansion, matrix expansion, BDHCAM, building rotation, trainer-name expansion). Rewrites ARM9/overlay code — and unlike DSPRE, it is undoable here: the four driven end to end (ARM9 expansion, matrix expansion, BDHCAM, building rotation) each restore every file they touched — arm9.bin, ov001.bin, the synthetic overlay's archive — byte-identically through undo_changes. |
Safety
| Tool | Purpose |
| ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| list_changes | The journal of what this server has written to the project, newest first. |
| undo_changes | Roll back the last N changes, restoring the pre-image bytes. Refuses if a file was edited outside the server since. |
| revert_changes | Undo every recorded change (confirm: true required), returning the folder to how the server first found it. |
Conventions the whole surface follows
- Every species / move / item / ability / type / trainer argument takes a name or an ID; every
one in a reply comes back as
{id, name}. Names are read from the ROM's own text archives, so a hack that renames Garchomp is reflected for free. - Ambiguity and typos fail loudly: HeartGold has 24 trainers called "Silver", so resolving that throws and lists the colliding IDs rather than picking one. "Garchmop" answers "Did you mean GARCHOMP (445)?".
set_*tools are partial — send only what changes; every other byte stays identical.- Anything that could return hundreds of entries paginates (
offset/limit, withtotalandcount). - Edits are staged in the project folder; only
save_romproduces a.nds.
For agents
The server documents itself over MCP, so an agent needs nothing from this repository:
instructions, sent once on connect: the workflow, the staging model, the safety net, names-vs-IDs, and where the rest of the documentation is.dspre://guide— the agent 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, forpokemon,moves,items,trainers,text,scripts,encounters,events,world-data,rom-tables,patches,history,familiesandsetup.dspre://project— a live summary of the open project, orno 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. It is the one tool that works with no ROM open.
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 — one sentence of purpose, what to call first, the rules
that bite, and a dspre:// pointer — because tools/list is sent before an agent has done
anything and every character is paid for up front.
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:
dryRun: trueon everyset_*andapply_rom_patchpreviews the files and entries the call would touch, reports the new values, and writes nothing.- All-or-nothing.
set_pokemonalone writes up to four archives. Each dirty archive is serialized once, every file lands in a temp directory under<project>/.dspre-mcp/tmp/, and only when all of them are down does each get renamed onto its target. A throw mid-handler rolls back with no disk work at all, because nothing was written yet. - Undo survives a restart.
<project>/.dspre-mcp/holds a journal and the pre-image blobs, capped at 200 entries. Pre-images are the changed span, not the file — editing one map header journals 24 bytes, not the 1.1 MBarm9.binit lives in.list_changes,undo_changesandrevert_changesread it, and undo refuses rather than silently discarding work if something edited the folder behind the server's back. save_romnever overwrites the ROM you opened — default output<rom>.edited.nds.- Every handler, reads included, queues behind one process-global mutex, so a read never observes a half-open transaction.
A worked edit
"Give Whitney a Garchomp holding a Focus Sash, and make Pikachu actually usable."
open_rom { romPath: "/roms/heartgold.nds" }
lookup_names { category: "trainer", query: "whitney" }
→ [{ id: 30, name: "Whitney" }, { id: 714, name: "Whitney" }]
get_trainer { trainer: "Whitney" }
✗ "Whitney" is ambiguous: 2 trainer entries share that name —
IDs 30 (Whitney), 714 (Whitney). Pass the numeric ID.
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" }That failure is the point, not a wart: HeartGold really does have two trainer entries called
Whitney (30 and 714) and 24 called Silver, so a tool that quietly picked one would edit the wrong
battle and look like it worked. lookup_names is how you find the ID you meant. get_trainer
before set_trainer because slot numbers and the current party are what the patch addresses;
set_pokemon touches only the two named stats and leaves the other four byte-identical; nothing
reaches a .nds until save_rom. Any of those set_* calls takes dryRun: true first if you
want to see what it would touch, and undo_changes { steps: 1 } puts it back if you don't like it.
Layout
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.
How this was built
Ported from the DSPRE C# source, 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.
Search that reference with grep -a. Four files under DS_Map/ are not valid UTF-8 —
ROMFiles/GameMatrix.cs, Main Window.cs, Main Window.Designer.cs and
Editors/LearnsetLimitWarningForm.cs — and plain grep -r skips them silently, with no "binary
file matches" line. That cost us real work: matrix resizing sat in the known-gaps list for a whole
phase as "not in the public source, so the fill byte would be invented". ResizeMatrix is at
GameMatrix.cs:122, the fill value is GameMatrix.EMPTY at :41, and the feature shipped as soon
as anyone looked. A gap blamed on the reference should be re-checked against the reference before
it is believed.
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.
Deliberate refusals, all documented in docs/STATE.md: growing the fly table (no length field),
DP/Pt map header flag bit order (the reference contradicts itself), ground-item names in event
files (scriptNumber - 7000 indexes a filtered list whose mapping changes once the
standardized-item-numbers patch is applied, so the raw script entry is reported instead), editing
the hidden-item table's length or removing a matrix section (DSPRE's own save path is lossy there),
and the side effects of changing a starter — rival and tag-partner teams, cries, dialogue, the
DP/Pt selection-scene ASM patch. A BLZ compressor was written, measured at 68/127 byte-exact, and
deleted rather than shipped. Where a lookup can only be guessed at, the tool throws naming what it
could not find, rather than falling back to vanilla values the way the C# does.
docs/PROGRESS.md is the full journal — decisions, reversals, and the DSPRE bugs found along the
way. docs/STATE.md is the current-state summary. docs/PORTING.md holds the conventions.
docs/ASSESSMENT.md is a deliberately unkind review of this codebase, and docs/BOOT-TEST.md is
the record of the rebuilt ROM actually running.
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 # prettierAll three run in CI on Node 22 and 24.
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 (112 pass / 93 skip measured on a
fixture-less checkout; the 1 skip with fixtures present is a dynamic-headers test that only
activates on a patched ROM). With the fixtures present a full run costs ~30 s against a native dsrom and ~2.5 min against the bundled
dsrom.exe — almost all of it the two tests that build a real 134 MB ROM.
Releasing
For the maintainer. A release is cut by pushing a tag:
git tag v0.1.0 && git push origin v0.1.0That triggers .github/workflows/release.yml, which installs, builds, lints, tests, publishes to
npm with provenance, and creates a GitHub release. One-time setup: an npm automation token stored
as the NPM_TOKEN repository secret (gh secret set NPM_TOKEN). Manual fallback:
pnpm build && cd apps/dspre-mcp && npm publish --access public.
Legal
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 ROM 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.
Contributing: see CONTRIBUTING.md.
