Skip to content

Releases

v0.1.4 — auto-reconnect actually works after mid-session disconnects

Choose a tag to compare

@dmang-dev dmang-dev released this 16 May 22:18

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)

Choose a tag to compare

@dmang-dev dmang-dev released this 16 May 20:37

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_screenshot now defaults to gpu.buffer.renderColor (reads the active GPU render target via GPU_GetCurrentFramebuffer(RENDER)) instead of gpu.buffer.screenshot (which reads PPSSPP's final composited output via the crash-prone GPU_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 source parameter (enum 'render' | 'output', default 'render') lets callers opt into the post-processed gpu.buffer.screenshot path 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

Choose a tag to compare

@dmang-dev dmang-dev released this 16 May 20:27

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 from call() into a shared ensureConnected() helper, then routed fireAndForget() through it too. ppsspp_pause, ppsspp_resume, and ppsspp_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-readyPromise safety as before: concurrent callers share a single underlying connect attempt.

Changed

  • fireAndForget() is now async (returns Promise<void> instead of void). All in-tree callers in src/tools.ts and scripts/smoke.cjs updated to await it.

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

Choose a tag to compare

@dmang-dev dmang-dev released this 16 May 19:57

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's memory.read_u8 / _u16 / _u32 responses use the field name value, not uintValue (which I'd assumed from the singular-register cpu.getReg/setReg responses, which DO use uintValue). Memory r/w round-trip now validated end-to-end: write 0xDEADBEEF to 0x08800000 + read back = match.

  • cpu.getAllRegs response 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.stepping and cpu.resume were 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 ticketed call() was waiting 10s for a reply that never comes. Added PpssppClient.fireAndForget(event, params) and PpssppClient.waitForState(predicate, opts) which polls cpu.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_screenshot now auto-pauses + auto-resumes — PPSSPP's gpu.buffer.screenshot requires CPU or GPU to be stepping (otherwise "Neither CPU or GPU is stepping" error). The tool now transparently cpu.steppings, captures, and cpu.resumes. If the caller had already paused, it leaves the state alone on exit. Also requests type: "base64" explicitly (PPSSPP's default is "uri" which returns a data: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 with PPSSPP_PORT=<port> node scripts/smoke.cjs.

Known limitations

  • gpu.buffer.screenshot is 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 ⚠️ backend/game-dependent

Upgrade

npm install -g mcp-ppsspp@0.1.1

The bridge protocol is the same — no PPSSPP-side changes required.

v0.1.0 — initial public release

Choose a tag to compare

@dmang-dev dmang-dev released this 16 May 07:44

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_screenshot returns 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

  1. Install PPSSPP (1.7+) from ppsspp.org/download
  2. Launch PPSSPP, load any PSP ISO/EBOOT
  3. Settings → Tools → Developer Tools → Allow remote debugger
  4. PPSSPP will show the active host:port — note the port number
  5. npm install -g mcp-ppsspp
  6. Register with your MCP client, setting PPSSPP_PORT env 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.stepInto steps one MIPS instruction, not one rendered frame). For frame stepping, set a breakpoint at the vblank handler and resume.