romdevtools
v0.149.1
Published
Tool server giving coding agents full control of homebrew ROM development AND reverse-engineering/romhacking across 18 retro platforms (NES, SNES, GB, Genesis, Atari, C64, PC Engine, MSX, GameTank, sync32, PlayStation, N64, Dreamcast, ...) via WASM toolch
Maintainers
Readme
romdev
The entry point for romdev - vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank, sync32 - and the 3D consoles N64, PlayStation, and Dreamcast), plus the PICO-8 fantasy console (via FAKE-08), with one command - drive it yourself or let a coding assistant do it.
npx romdevtoolsWhat you get:
- Build - bundled per-platform toolchains (cc65, SDCC, RGBDS, asar, vasm, SGDK, PVSnesLib, libtonc, ...) as WASM. Write source, compile, get a real ROM.
- Run + see + drive - load the ROM into an emulated console (libretro cores as WASM), step frames, screenshot, script controller input. The 2D consoles render in software; the 3D consoles (N64 via glide64, PlayStation via Beetle PSX HW, Dreamcast via Flycast) render on the real GPU through
native-gles- headless OpenGL/EGL, no browser. PlayStation ships OpenBIOS (MIT) embedded, so there's no proprietary firmware to supply. Dreamcast is EXPERIMENTAL, but no longer just a curiosity - the SH-4 runs on a WASM recompiler, so commercial discs boot, render, respond to input, and drive through their menus into gameplay. Across the five discs used for testing: all five boot and take controller input, and two reach live in-game scenes (3D world rendering with a working HUD); the rest land on title menus or save-file prompts. Throughput is ~70-140fps headless, against ~16fps when the SH-4 was interpreted through core 0.2.0. AICA sound is still interpreted and measures under ~12% of frame time. What hasn't been characterised is sustained play - a full level, long-run stability, framerate under the heaviest scenes - so treat it as young rather than finished. - Inspect + romhack - read CPU/video/save RAM, watch memory, write-breakpoints, the Cheat-Engine value-search loop, a bundled cheat database, mapper-aware disassembly, and a byte-exact rebuildable-project disassembler (
disasm({target:'project'})splits a ROM into byte-exact region.asm;build({output:'reassemble'})rebuilds it into a byte-identical ROM in one call on all 15 classic platforms - edit a region and rebuild, the "cmp before commit" gate for any structural hack). - Reverse-engineering analysis engine (all 18 platforms - incl. the 3D consoles' MIPS R3000/R4300 + SH-4) - control-flow graphs, deep cross-references, auto-detected functions (ranked real-code-first), a one-shot structural map, and a Ghidra decompiler (C-like pseudocode, with hardware registers named and 6502 SLEIGH clutter folded to readable C; N64/PS1/Dreamcast images are analyzed at their true load address, and an N64 splat project gives segment-exact mapping with provenance):
disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})andsymbols({op:'analyze'}). And the piece no static tool has: live computed-jumptable recovery -breakpoint({on:'jumptable'})runs the emulator to resolve theJMP (table,X)/ RTS-trick dispatchers (state machines, script/battle VMs) that static analysis collapses to "could not recover." Understand how a routine works before you touch it - no $3,000 IDA license, no install. - Convert assets - PNG → platform tiles/tilemaps, quantize-to-palette, audio importers (BRR for SNES, XGM2 PCM for Genesis).
- Active Bezels - an executable companion to a specific ROM (a
.abpackage) that runs once per emulated frame, reads the core's live memory, and renders the complete final scene: a map, a HUD, reconstructed world graphics, around or over the game.loadMedia({useActiveBezel:true})picks up the same-basename sidecar (Game.nes→Game.ab) and makes the composite the presented and captured picture. romdev is where these get verified, not just played: a package can load, tick without trapping, emit valid draw commands, and still be completely wrong about the game, soframe({op:'screenshot', source:'both'})returns the composite and the raw core framebuffer for the SAME frame - plus the three geometries (core framebuffer / intended display aspect / bezel scene) that are easy to conflate. Compare them against the guest's own memory reads to check the package's interpretation, not just that it drew something. - Matching decompilation (
decomp, N64 proven) - the function-level generate → compile → compare → refine loop against a registered splat project's OWN compiler and build (IDO via ido-static-recomp or MIPS GCC; the per-file compile command is captured from the project'smake).comparecompiles a candidate inside its real translation unit and returns ONE aggregate verdict - strict instruction + relocation equality vs the extracted asm, ROM-linked words vs the base ROM, the function's own jump tables/literals, and every other function in the TU unchanged; a check that could not run isunknown/error, neverexact, and the verifier version is part of the result cache key. Around it:import/resolve(the splat segment map is the only VA→ROM resolver; overlay VAs are ambiguous until named),plan/batch(call graph from the built objects' relocations),generate(m2c with the TU's preprocessed context;types propose:trueturns asm access widths into struct proposals fed back asextraContext/declarations),search/job(bounded decomp-permuter jobs with reports),integrate/verify(patch → full build → ROM sha1, auto-revert),progress(code bytes per object; asm, C, library, handwritten asm and retained inline asm kept apart), and runtime ops on a live session -trace(a real PC break at the function: a0-a3/f12/f14/stack at entry, v0/v1/f0 at return),coverage(instruction-exact from the core's PC bitmap, attributed to functions and basic blocks),overlays/symbolize,state,smoke. State lives in~/.romdev/decomp/<project>/; nothing touches the checkout exceptintegrate apply:true. Backends pinned under~/.romdev/tools/(m2c, decomp-permuter). Doc:platform({op:'doc', platform:'n64', name:'decomp'}). - Native game runtimes (beyond emulation) - the same run/see/drive loop also hosts two native game formats: wasmcart (
.wasc- WASM games from any language; 2026) and jsgame (.jsgame- JavaScript canvas/WebGL games; 2024).loadMedia({platform:'wasmcart'|'jsgame'})→frame({op:'step'|'screenshot'})→input, over the same tools - pluswasm({op:'conformance'})(the 'won't load, why?' verdict - required exports / manifest-vs-instance / declared caps / debug + determinism consistency),wasm({op:'info'|'exports'|'read'|'write'})(live WCInfo + heap + module exports), named debug state (wasm({op:'debugState'})and read/write BY NAME - the cart's opt-inwc_debug_statetable:player_xinstead of a raw offset), frame-stamped events (wasm({op:'events'})drainswc_debug_markannotations + capturedwc_log- a run navigable by the moments the cart marked), deterministic replay (loadMedia({deterministicSeed})seedswc_set_seed+ fixes the virtual clock, soregression({op:'capture'/'check'})frame-hash goldens are airtight on carts that declareWC_FLAG_DETERMINISTIC; named-state checkpoints cover the carts that can't),audioDebug({op:'record'})(capture the cart's audio to WAV - ears to go with the eyes), headless GL (a cart whose wasm imports from theglmodule renders on an offscreen WebGL2 context - screenshots,frame({op:'verify'}), and regression goldens see the real GPU draws, no window;status.glreports rendered vs stubbed), andpack({target:'wasc'|'jsgame'})to zip a source dir into the distributable archive (the "build" step; romdev doesn't compile the WASM - bring your own). A capability descriptor marks the emulator-only tools (memory regions / cpuState / disasm) not-applicable for these kinds. Depends on thewasmcart+rungamepackages (jsgame runs in avmrealm - the server self-re-execs with--experimental-vm-modules). - PICO-8 (fantasy console) - the FAKE-08 player (MIT, no BIOS) runs PICO-8
.p8(Lua source) and.p8.png(cart-in-a-label-PNG) carts at 128×128 with sound.loadMedia({platform:'pico8'})→frame/inputwork like any core;build({platform:'pico8', source: lua})PACKAGES a runnable.p8from Lua (+ optional gfx/sfx/map sections) - it's a cart assembler, not a CPU compiler (the Lua IS the code). PICO-8 is a Lua VM, so instead of machine-code disasm/decompile you read the cart's Lua directly withdisasm({target:'source'});memory({region:'system_ram'})exposes the full 64KB PICO-8 address space (sprite sheet, map, sfx, general RAM, screen buffer). Its capability descriptor is afantasytier - cpuState/decompile/tile-inspectors report not-applicable (there's no CPU or tile hardware to inspect). Ships theromdev-core-fake08core package.
Point any coding agent at it three ways:
- Plain HTTP -
POST http://127.0.0.1:7331/tool/{name}; browse/try every tool at/documentation. - Agent Skill -
GET /skills/romdev/SKILL.md(the Agent Skills standard; save it to your skills dir asskills/romdev/SKILL.md; ~100 tokens until invoked). - MCP - it's also a Model Context Protocol server at
/mcpfor clients that want it. Both protocol eras are served on that one endpoint: legacy clients (theinitializehandshake +Mcp-Session-Id) work unchanged, and clients speaking the stateless 2026-07-28 revision - no handshake, no session id, per-request_meta- are served natively, withserver/discoveradvertising what the server supports. Since that revision has no protocol session to hold your identity, the server takes it from (in order) a_meta["dev.romdev/sessionHandle"]handle, the optionalsessionargument every tool accepts, or - when a call names nothing - your keep-alive connection, in which case each result ends with asession: <id>line you can pass back to pin it. Over plain HTTP the same job is done byx-romdev-session.
This package contains all the JavaScript - the tool surface, the WASM emulator host, the per-platform example games, runtime/library source, and debug helpers - but no emulator or compiler WASM itself. Those ship in the romdev-* binary packages it depends on; each platform's core/toolchain WASM is resolved (import.meta.resolve) and instantiated only the first time you build or run that platform, so memory stays proportional to what you actually use.
For the full project - what romdev is, the supported-platform matrix, how the pieces fit together, and how to develop on it - see the repository README.
What's in this package
binromdevtools→ the tool server (src/mcp/server.js). Serves the HTTP tool routes,/documentation,/skills/romdev/SKILL.md, and an MCP endpoint onhttp://127.0.0.1:7331by default (PORT/HOSTto override).romdev-mcpis kept as an alias of the same command.romdevtools-cli→ a smoke/utility CLI, incl.romdevtools-cli play <rom>(SDL window, hot-plug controllers, live fps in the title bar).
src/- the server, MCP tools, WASM host, core/toolchain resolvers, per-platform memory interpretation, and bundled library/runtime source (cc65 libs, PVSnesLib, SGDK, libtonc/libgba, hUGEDriver, ...) that forked projects link against.examples/- per-platform example games (complete, working, forkable) and minimal references.
Dependencies
romdevtools depends on the binary/data packages it needs (exact-pinned), so a single install gets a matched, tested set:
- 2D cores:
romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank,s32core,fake08} - 3D / GPU cores (rendered through
native-gles):romdev-core-{parallel-n64,beetle-psx-hw,flycast} - Platforms (core + dedicated toolchain bundled):
romdev-platform-{snes,gba,atari2600} - Toolchains:
romdev-toolchain-{cc65,sdcc,m68k-gcc,vasm,rgbds,mips-gcc,sh-gcc}(mips-gcc = N64/PS1 C; sh-gcc = Dreamcast C) - GPU deps (OPTIONAL - only the 3D cores need them; the 2D cores never touch GL):
native-gles+webgl-node. A headless user without a GPU stack can still run all the 2D platforms. - Analysis:
romdev-analysis(Rizin → WASM: control-flow graphs, cross-references, function detection) andromdev-analysis-decompiler(Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all 18 CPUs, incl. MIPS R3000/R4300 + SH-4). Powerdisasm({target:'cfg'|'xrefs'|'functions'|'decompile'})andsymbols({op:'analyze'}). Lazy-loaded on first use. - Data:
romdev_game_codes- the bundled game-code / cheat database (a free labeled RAM/code map for thousands of known ROMs), split out so it can grow independently. Lazy-loaded one platform at a time. - Native runtimes:
wasmcart(runs.wascWASM game carts) andrungame(runs.jsgameJavaScript games headlessly). Alsoromdev-audio-resampler(WASM+SIMD S16-stereo resampler, used by the live-window audio sink). - Active Bezels:
active-bezel- the.abpackage format, its reference runtime, and the compositor, shared with retroemu so the composite an agent inspects is the one a player sees. Imported lazily, only when a package is actually attached.
@kmamal/sdl is used only by playtest() / romdevtools-cli play (the live window). It ships its native binary via its own install script, which npm skips when romdev is a transitive dep (e.g. under npx) - so romdev's postinstall fetches it, and playtest() also self-heals at runtime if the binary is still missing (downloading the prebuilt before the first window open). Either way, if the binary can't be fetched (offline/locked-down network), the headless server is unaffected - only the live window degrades, and the error tells you the one command to fix it.
Connect
npx romdevtools
# then, e.g. for Claude Code (MCP):
claude mcp add --transport http romdev http://127.0.0.1:7331/mcpIt's a standard streamable-HTTP MCP server at http://127.0.0.1:7331/mcp. For opencode, Codex CLI, and other clients, see Connect in the repository README. An optional human observer (live tool-call view) is at /livestream.
Agents: the server delivers AGENTS.md as connection-time instructions - the workflow guide for the full tool surface. Or just connect your agent and call catalog({op:'categories'}) to explore the tools live, and catalog({op:'status'}) for the running version + session snapshot.
Prefer not to use MCP? Use HTTP or a Skill
Most agents support MCP, but you don't have to use it. Run the server
(npx romdevtools) and skip wiring it into your agent's MCP
config - no claude mcp add, no mcp.json entry, no MCP client at all. The
same 32 tools are reachable over plain HTTP / as an Agent Skill against the
running server:
- Plain HTTP:
POST http://127.0.0.1:7331/tool/{name}with the args as a JSON body; the response is JSON. Browse/try every tool at/documentation(Swagger UI, served locally - no CDN), or get the machine spec at/openapi.json. The agent picks its own session id and sends it as thex-romdev-sessionheader on every call - it's required (no header →401; the server won't silently run you in a throwaway session). Make it unique and task-descriptive (e.g.nes-platformer-build), since it's also the label shown in the/livestreamobserver. The emulator host is per-session, so the same id keeps your ROM across calls, and several agents can share one server by each using a different id. Running many sessions at once? Also send a stablex-romdev-agentheader - one value for you, across all your sessions. It is optional and cooperative, and it buys two things: at the session/host caps eviction targets the largest holder's oldest session, so your parallel burst evicts its own idle sessions instead of another agent's, and the first response of each new session you open carriesagentSessionReminder- your other live sessions, what each holds, how idle they are - which is exactly the list you need to sweep up afterwards. When you finish with a session,host({op:'shutdown'})releases all of it (emulator, session record, livestream entry) immediately rather than waiting out the idle timer. A call that fails returns a non-2xx (4xx) with the reason in the body - never a 200 that hides an error. romdev runs locally and tool path args (path,outputPath, ...) are local filesystem paths, not uploads - pass absolute paths on the same machine. - Agent Skill:
GET /skills/romdev/SKILL.mdis a portable Agent SkillsSKILL.md(works in Claude Code, opencode, OpenClaw, Hermes, ...). Drop it in your agent's skills dir; it costs ~100 tokens until invoked (vs always-on MCP tool defs), then teaches the workflows + thePOST /tool/{name}calls.
Both are generated from the same tool registry as the MCP surface, so they never
drift. You still run the server - npx romdevtools (it hosts the
emulators/toolchains in-process and serves these routes on :7331). What the
HTTP/skill path removes is the MCP client/protocol and its always-on context
cost - not the server. There's no separate install beyond romdev itself, and
never a host gcc or emulator.
License
romdev's code is MIT, and the games you build are yours - including to sell. Full details + third-party component inventory: LICENSE and NOTICE.
