npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-mcp

Add --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 build

Then 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-mcp

and 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, with total and count).
  • Edits are staged in the project folder; only save_rom produces 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, for pokemon, moves, items, trainers, text, scripts, encounters, events, world-data, rom-tables, patches, history, families and 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. 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: true on every set_* and apply_rom_patch previews the files and entries the call would touch, reports the new values, and writes nothing.
  • All-or-nothing. set_pokemon alone 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 MB arm9.bin it lives in. list_changes, undo_changes and revert_changes read it, and undo refuses rather than silently discarding work if something edited the folder behind the server's back.
  • save_rom never 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    # prettier

All 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.0

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