Repository navigation
Releases
v0.1.4 — auto-reconnect actually works after mid-session disconnects
Closes the auto-reconnect gap that v0.1.2 and v0.1.3 missed.
The bug
v0.1.2 introduced ensureConnected() so that tool calls would auto-reconnect if PPSSPP wasn't reachable. But it only worked for the initial connection. If PPSSPP was up when MCP started and then closed mid-session (taskkill, crash, user-quit), every subsequent tool call would throw:
MCP error -32603: Cannot read properties of null (reading 'send')
That's the very pre-v0.1.2 error format that v0.1.2 was supposed to make impossible.
Root cause
The WebSocket close handler cleared ws and ready to false, but left the memoized readyPromise as a resolved promise pointing at a dead socket:
ws.on("close", (code, reason) => {
this.ready = false;
this.ws = null;
// ❌ this.readyPromise still points at a resolved promise for the dead socket
});Next ensureConnected(): isConnected() returns false (ws is null), await start() returns the cached resolved promise immediately, and call() proceeds to this.ws!.send(...) → null deref.
Fix
One-line addition in the close handler: this.readyPromise = null;. Now start() actually attempts a fresh connect on the next call.
Added
npm run verify:reconnect(scripts/verify-reconnect.cjs) — regression test that exercises the bug specifically.npm run verify:screenshot(scripts/verify-screenshot.cjs) — companion test for v0.1.3's screenshot source param.
Install: npm install -g mcp-ppsspp@0.1.4 or npx -y mcp-ppsspp
Full Changelog: https://github.com/dmang-dev/mcp-ppsspp/blob/v0.1.4/CHANGELOG.md
v0.1.3 — safer screenshot default (no more PPSSPP crashes)
Real PPSSPP crash observed when calling gpu.buffer.screenshot on a homebrew game. Root cause is upstream — _assert_(buf != nullptr) in PPSSPP's GPUBufferSubscriber.cpp fires when GPU_GetOutputFramebuffer returns true with a null buffer. The process aborts; uncatchable from outside.
Changed
ppsspp_screenshotnow defaults togpu.buffer.renderColor(reads the active GPU render target viaGPU_GetCurrentFramebuffer(RENDER)) instead ofgpu.buffer.screenshot(which reads PPSSPP's final composited output via the crash-proneGPU_GetOutputFramebuffer). The render target is the actual rendered scene at PSP-native 480x272, before PPSSPP's post-processing. For agent-vision use cases this is identical content.- New
sourceparameter (enum'render' | 'output', default'render') lets callers opt into the post-processedgpu.buffer.screenshotpath when needed. The tool description carries a clear warning that'output'can crash PPSSPP on certain games.
Known limitations
source: 'output'can still crash PPSSPP — upstream bug. v0.1.2's auto-reconnect ensures MCP recovers cleanly when PPSSPP is relaunched. Will be tracked upstream at hrydgard/ppsspp.
Install: npm install -g mcp-ppsspp@0.1.3 or npx -y mcp-ppsspp
Full Changelog: https://github.com/dmang-dev/mcp-ppsspp/blob/v0.1.3/CHANGELOG.md
v0.1.2 — auto-reconnect for fire-and-forget commands
Auto-reconnect now covers fire-and-forget commands too. Previously call() would lazy-reconnect if PPSSPP had been closed and reopened, but fireAndForget() (used by pause/resume/screenshot) still threw the old 'PPSSPP not connected — did you start() the client?' error, leaving those tools broken across PPSSPP restarts until the MCP client itself restarted.
Fixed
fireAndForget()now auto-reconnects — extracted the lazy connection guard fromcall()into a sharedensureConnected()helper, then routedfireAndForget()through it too.ppsspp_pause,ppsspp_resume, andppsspp_screenshot(which internally uses fire-and-forget for the pause/capture/resume bracket) now survive a PPSSPP close-and-relaunch without needing the MCP server itself to restart. Same memoized-readyPromisesafety as before: concurrent callers share a single underlying connect attempt.
Changed
fireAndForget()is nowasync(returnsPromise<void>instead ofvoid). All in-tree callers insrc/tools.tsandscripts/smoke.cjsupdated toawaitit.
Install: npm install -g mcp-ppsspp@0.1.2 or npx -y mcp-ppsspp
Full Changelog: https://github.com/dmang-dev/mcp-ppsspp/blob/v0.1.2/CHANGELOG.md
v0.1.1 — three live-test bug fixes + auto-pause screenshot
Three real bugs surfaced by the first end-to-end live test of v0.1.0 against PPSSPP v1.20.3 + a PSP homebrew. Protocol mapping was right in the abstract but wrong in three details — all caught and fixed.
Fixed
-
Memory reads were returning
undefined— PPSSPP'smemory.read_u8/_u16/_u32responses use the field namevalue, notuintValue(which I'd assumed from the singular-registercpu.getReg/setRegresponses, which DO useuintValue). Memory r/w round-trip now validated end-to-end: write0xDEADBEEFto0x08800000+ read back = match. -
cpu.getAllRegsresponse shape was wrong — I'd assumed{categories: [{name, registers: [{name, uintValue}]}]}. Actual PPSSPP shape is{categories: [{name, registerNames: [...], uintValues: [...], floatValues: [...]}]}— parallel arrays, not an array of objects. Rewrote the handler with bounds-aware parallel indexing. -
cpu.steppingandcpu.resumewere hanging — PPSSPP source documents these as "No immediate response. Once CPU is stepping, a 'cpu.stepping' event will be sent." (async broadcast with no ticket). My ticketedcall()was waiting 10s for a reply that never comes. AddedPpssppClient.fireAndForget(event, params)andPpssppClient.waitForState(predicate, opts)which pollscpu.status(which IS synchronous) until the state change is detected.ppsspp_pause/ppsspp_resume/ the screenshot internal pause/resume now use this pattern.
Changed
ppsspp_screenshotnow auto-pauses + auto-resumes — PPSSPP'sgpu.buffer.screenshotrequires CPU or GPU to be stepping (otherwise "Neither CPU or GPU is stepping" error). The tool now transparentlycpu.steppings, captures, andcpu.resumes. If the caller had already paused, it leaves the state alone on exit. Also requeststype: "base64"explicitly (PPSSPP's default is"uri"which returns adata:image/png;base64,...prefix); belt-and-suspenders URI-prefix stripping kept as a fallback.
Added
scripts/smoke.cjs— live test harness that connects to PPSSPP, exercises every major event path (ping, game.status, memory r/w, pause/resume, registers, screenshot, breakpoints), and reports per-event status. Used to surface all three bugs above. Run withPPSSPP_PORT=<port> node scripts/smoke.cjs.
Known limitations
gpu.buffer.screenshotis backend-dependent — observed "Could not download output" on a homebrew where the GPU readback path can't fetch the framebuffer. This is a PPSSPP-side limitation (the GPU backend's framebuffer readback support varies), not a bug in our code. Likely works on commercial PSP games with normal rendering.
Validated live
Smoke-tested end-to-end against PPSSPP v1.20.3 (Windows x64) running BTC Miner PSP:
| Event | Result |
|---|---|
WebSocket handshake (debugger.ppsspp.org subprotocol) |
✅ |
version (ping) |
✅ returns PPSSPP v1.20.3 |
game.status |
✅ returns full game metadata |
memory.read_u8/u16/u32 |
✅ returns value field |
memory.read (range, base64) |
✅ |
memory.write_u32 + read-back |
✅ |
cpu.stepping (fire-and-forget + poll) |
✅ |
cpu.resume (fire-and-forget + poll) |
✅ |
cpu.getAllRegs (parallel arrays) |
✅ |
cpu.breakpoint.list |
✅ |
gpu.buffer.screenshot |
Upgrade
npm install -g mcp-ppsspp@0.1.1The bridge protocol is the same — no PPSSPP-side changes required.
v0.1.0 — initial public release
First public release. MCP server bridging PPSSPP — the PlayStation Portable emulator — to any MCP-compatible client via PPSSPP's built-in WebSocket debugger interface.
No Lua plugin needed. Unlike the mcp-bizhawk / mcp-mgba bridges, PPSSPP exposes a native WebSocket debugger that this server just speaks to directly. The simplest of the family to install and use.
What's in 0.1.0
- 20 MCP tools covering memory r/w (u8/u16/u32/range/string), input (buttons + analog), emulator control (pause/resume/step/reset/screenshot), CPU debugger (get_registers), CPU execution breakpoints (add/remove/list).
- Inline PNG screenshots —
ppsspp_screenshotreturns the framebuffer base64-encoded as an MCP image content block, viewable directly in the client. - TDQS-templated tool descriptions — every tool documents purpose, when-to-use vs siblings, side effects + errors, exact return shape, with PSP-specific context (memory map, MIPS-LE, button names) inline.
- Cross-platform install paths —
npm install -g mcp-ppsspp,npx -y mcp-ppsspp, or clone-and-build. - GitHub Actions CI on Node 18/20/22 across Linux/macOS/Windows.
- Dockerfile + glama.json for Glama MCP registry.
Setup
- Install PPSSPP (1.7+) from ppsspp.org/download
- Launch PPSSPP, load any PSP ISO/EBOOT
- Settings → Tools → Developer Tools → Allow remote debugger
- PPSSPP will show the active host:port — note the port number
npm install -g mcp-ppsspp- Register with your MCP client, setting
PPSSPP_PORTenv var to the port from step 4
See README.md for full setup, full tool reference, PSP memory map, and troubleshooting.
Why this matters
PSP was the major retro platform NONE of the existing dmang-dev mcp-* servers covered. With this release, the family covers:
| Platform(s) | Server |
|---|---|
| GBA | mcp-mgba |
| PCSX2 / PINE protocol emulators | mcp-pine |
| Any libretro core via RetroArch | mcp-retroarch |
| NES/SNES/GB/GBC/GBA/Genesis/N64/PSX/Saturn/Atari/... | mcp-bizhawk |
| PSP | mcp-ppsspp |
Known limitations
- No savestate API exposed by PPSSPP's WebSocket — savestate save/load isn't in the debugger interface. Use PPSSPP's keybinds (F1-F8 for slots) via the UI for now.
- Frame-advance is instruction-level only (
cpu.stepIntosteps one MIPS instruction, not one rendered frame). For frame stepping, set a breakpoint at the vblank handler and resume.