ppsspp-dfx-mcp
English | 中文
A MCP (Model Context Protocol) server that turns PPSSPP into an AI-debuggable target. It wraps the PSP emulator's WebSocket debugger into a tool surface for LLM agents: session lifecycle, memory read/write, disassembly, breakpoints, CPU control, input automation, screenshots, replay recording, and diagnostic scripts — with structured contracts, a defensive error taxonomy, and task-level evaluations built in.
Documentation: docs/SCOPE.md (scope & protocol-surface boundaries) · CHANGELOG.md (changelog)
Project status
The project is in alpha and iterating quickly — the tool surface and configuration format may change incompatibly. Read SECURITY.md before running.
Features
- 41 static tools, all with structured
inputSchema/outputSchema— no unconstrained return values; every parameter is typed and documented. - Dynamic script tools: project-specific diagnostic scripts are exposed as
ppsspp_script_<name>tools viascripts.manifest.yaml, with input types driven by each script's Pydantic model;ppsspp_run_scriptinvokes non-exposed scripts andppsspp_list_scriptsinspects the manifest — see Configuration. - Session model: multiple concurrent PPSSPP sessions, readiness probing
(
wait_ready), and wedge self-healing (resilientstart). - Agent ergonomics: composite tools (
ppsspp_frame_snapshot,ppsspp_trace_memory_access,ppsspp_batch_step),session_idauto resolution, defensive error codes ([CODE] messageformat; CPU-freeze vs disconnect disambiguation), with recovery hints embedded in error text. - Background automation: batch jobs run in detached server tasks,
immune to MCP client tool-call timeouts; supports status polling,
cancellation, and registry inventory (
ppsspp_batch_list). - Built-in evaluations (
evals/): 21 scenario cards + deterministic gates- a blind-test runner over recorded fixtures + summary reports — the tool surface is tested the way agents actually use it.
- Honest protocol surface: capabilities are only declared when a working
implementation exists behind them; deliberately
falseswitches carry design rationale.
Running
Requirements: Python 3.14+ (with a dedicated venv — why: see
Run from source); a PPSSPP build with the WebSocket
debugger enabled (the server launches it and connects to
ws://<host>:<port>/debugger); any MCP client (ZCode, Claude Desktop,
MCP Inspector, ...).
Install from PyPI
Use a dedicated venv — this server's MCP SDK v2 cannot coexist with the 1.x
mcp package many other MCP servers pin:
# Windows:
py -3.14 -m venv .venv
# POSIX:
python3.14 -m venv .venv
.venv\Scripts\python -m pip install ppsspp-dfx-mcp # Windows
.venv/bin/python -m pip install ppsspp-dfx-mcp # POSIX
Register the server with your MCP client (the entry point ships with the package; no in-repo scripts needed):
{
"mcpServers": {
"ppsspp-dfx": {
"command": "C:/absolute/path/to/.venv/Scripts/ppsspp-dfx-mcp.exe",
"cwd": "C:/absolute/path/to/your-project"
}
}
}
command points at the entry executable inside the install venv —
.venv/bin/ppsspp-dfx-mcp on POSIX. cwd is the directory where the server
discovers its .ppsspp-dfx/ configuration (see Configuration).
Smoke-test manually:
.venv/Scripts/ppsspp-dfx-mcp.exe # Windows
.venv/bin/ppsspp-dfx-mcp # POSIX
Then simply hand tasks to your agent: "boot the emulator with this ISO and tell me the current PC" — the server handles session startup, readiness probing, and state reads. Tool descriptions follow the PURPOSE / USAGE / BEHAVIOR / RETURNS convention with recovery guidance embedded in error paths; agents are self-sufficient without examples.
Do not insert wrapper scripts between the client and the server: on Windows
os.execvisCreateProcess+ parent wait (not POSIX exec-replacement), so an extra layer makes the innermost server read stdin EOF immediately and exit silently — the symptom is just-32000: Connection closed.
Run from source
The repository checkout ships a bootstrap script and a ready-to-use .mcp.json:
git clone https://github.com/AstralVoidZ/ppsspp-dfx-mcp.git
cd ppsspp-dfx-mcp
# Run in this directory — creates .venv/ppsspp-dfx-mcp and installs
# (editable, with dev extras):
python scripts/check_env.py --bootstrap
# Verify interpreter / SDK version / package imports:
python scripts/check_env.py --check
Register the server with your MCP client — point the client at the in-repo
.mcp.json, or inline it with the same structure:
{
"mcpServers": {
"ppsspp-dfx": {
"command": ".venv/ppsspp-dfx-mcp/Scripts/python.exe",
"args": ["-m", "ppsspp_dfx_mcp"],
"cwd": "${CLAUDE_PROJECT_DIR}"
}
}
}
cwd must be the directory holding both .venv/ and .ppsspp-dfx/
(the repo root in a standalone checkout). On POSIX use
.venv/ppsspp-dfx-mcp/bin/python instead of Scripts/python.exe.
Manual start for verification:
.venv/ppsspp-dfx-mcp/Scripts/python -m ppsspp_dfx_mcp # Windows
.venv/ppsspp-dfx-mcp/bin/python -m ppsspp_dfx_mcp # POSIX
Configuration
Environment variables (all optional):
| Variable | Default | Purpose |
|---|---|---|
PPSSPP_DFX_LOG_LEVEL |
INFO |
Log level |
PPSSPP_DFX_LOG_FORMAT |
text |
Log format (text or json) |
PPSSPP_DFX_RATE_LIMIT |
60 |
Per-tool rate limit (calls/min, 0 disables) |
PPSSPP_DFX_WS_HOST |
127.0.0.1 |
PPSSPP WebSocket host |
PPSSPP_DFX_WS_PORT |
12345 |
PPSSPP WebSocket port |
PPSSPP_DFX_EXE_PATH |
(from yaml) | PPSSPP executable path |
PPSSPP_DFX_SESSIONS_PATH |
~/.ppsspp-dfx/sessions.json |
Session state path |
Project-level YAML configuration lives in .ppsspp-dfx/config/ (relative to
the working directory):
project.yaml—ppsspp_exepath and project metadataaddresses.yaml— named address constants (also feed thecompletionscapability of the memory wizards)scripts.manifest.yaml— diagnostic script manifest. Each entry carries a machine-readablestatus(migrated= runnable,skeleton= body returnsnot_implemented). Scripts flaggedexposed: trueregister asppsspp_script_<name>tools at startup — skeletons excluded; preflight rejects them.ppsspp_reload_scriptsre-syncs the dynamic tool registry with the manifest (no restart) and reports the reconciliation.
Standalone quick start
Ready-to-copy templates for the three config files are in
examples/ — start there, don't hand-write YAML from scratch:
mkdir -p .ppsspp-dfx/config
cp examples/project.yaml examples/addresses.yaml \
examples/scripts.manifest.yaml .ppsspp-dfx/config/
# Then edit .ppsspp-dfx/config/project.yaml: point ppsspp_exe at your
# WS-debugger-enabled PPSSPP build, and replace the PLACEHOLDER addresses in
# addresses.yaml with values you reverse-engineered for your own game.
Two things to know before the first session:
- Without
scripts.manifest.yamlthe server still starts, but allppsspp_script_*tools silently disappear — keep the template even if thescripts:list is empty (check_env.py --checkwarns about exactly this). - With empty config and no placeholder values, everything server-side works;
only session startup needs a real
ppsspp_exe(orPPSSPP_DFX_EXE_PATH), and address constants only matter once you provide your game's values.
Protocol surface
Declared at initialize handshake — and only capabilities with a working
implementation behind them (the SDK derives each capability from whether a
request handler exists, so everything listed here is real):
| Capability | Declared | Notes |
|---|---|---|
tools |
✅ | 41 static tools + dynamic ppsspp_script_<name> |
resources |
✅ | ppsspp://game-state, ppsspp://registers (snapshots) |
prompts |
✅ | memory-breakpoint-wizard, memory-trace-wizard |
completions |
✅ | the wizards' address argument, candidates from addresses.yaml |
logging |
❌ | protocol revision 2026-07-28 removed logging/setLevel |
tasks |
❌ | SDK 2.2.0 type definitions only, no server-side implementation |
tools.list_changed and resources.subscribe are deliberately false.
SDK 2.2.0's MCPServer exposes no handshake-time entry to set
notification_options; declaring them would promise notifications the server
cannot emit. Current substitutes:
ppsspp_reload_scriptsreports what changed (exposed_added/exposed_removed) — agents respond without a notification channel.- The server
instructionsstring tells fresh agents what the tool surface contains.
If the SDK later exposes the entry point, flip the switch and add the
send_*_list_changed calls — the L2 contract tests
(tests/unit/l2_mcp_contract/test_capabilities_contract.py) assert the
current false state and will fail, which is the design signal that the
decision needs re-visiting, not a regression.
Return shapes
Image tools (ppsspp_screenshot, ppsspp_dump_texture,
ppsspp_dump_clut) return a split CallToolResult:
content— oneImageContentblock carrying the pixels.structuredContent— metadata only (file_path/size_bytes/format, plus per-tool fields likemode,width,height,empty). The base64 copy of the image is deliberately not in this channel — it would bloat the schema and duplicate whatcontentalready carries.
Every tool declares a structured outputSchema — no tool returns an
unconstrained object or an array with empty items. The one registered
exception is ppsspp_run_script's input parameter: its shape is decided by
the invoked script, so it is described but not constrained.
Error handling
When the emulated CPU wedges (infinite loop / HLE blocking / GPU pipeline
stall), the server returns CPU_FREEZE_SUSPECTED instead of a blanket
WS_DISCONNECTED — distinguishing "PPSSPP alive but CPU frozen" from
"process dead / WebSocket gone".
Recommended handling for CPU_FREEZE_SUSPECTED:
- Do not restart the session — PPSSPP is still running.
- Take a screenshot with
ppsspp_screenshotto aid diagnosis. - Try
step(action='resume')(may not help a true infinite loop). - Inspect threads with
hle.thread.list(may expose HLE blocking). - Check the instruction stream at the current PC with
ppsspp_disassemble.
Related codes: WS_DISCONNECTED (PID dead — real disconnect), WS_TIMEOUT
(ticketed RPC timeout, conservative default), CPU_STATE_ERROR (current CPU
state unsuitable for the operation). Error text always starts with [CODE]
for programmatic classification; recovery advice is embedded wherever a next
step exists.
Troubleshooting quick reference
| Symptom | Cause / fix |
|---|---|
-32000: Connection closed (no other info) |
A wrapper script sits between the MCP client and the server: on Windows os.execv is really CreateProcess + parent wait (not POSIX exec-replacement), so the inner server's stdin hits EOF immediately and exits silently. Remove the middle layer and use the venv interpreter as command (see Run from source) |
check_env reports "standalone venv missing" |
.venv/ is gitignored, so a fresh clone never has it. Run python scripts/check_env.py --bootstrap |
mcp SDK version unsatisfied / import-time crash |
The system Python's mcp package is often pinned to 1.x by other MCP servers — irreconcilable with SDK v2. Don't install globally — use check_env.py --bootstrap, or install into a dedicated venv per Install from PyPI |
All ppsspp_script_* tools vanish (server starts fine) |
.ppsspp-dfx/config/scripts.manifest.yaml missing — absence only warns, dynamic tools silently empty. Copy the three templates from examples/ (check_env.py --check tells you) |
[PPSSPP_NOT_FOUND] |
PPSSPP executable not configured. Set PPSSPP_DFX_EXE_PATH, or ppsspp_exe in .ppsspp-dfx/config/project.yaml (env > yaml precedence) |
Can't find .ppsspp-dfx/config |
The config dir resolves from CWD (no upward search). Start from a directory containing .ppsspp-dfx/, or set PPSSPP_DFX_CONFIG_DIR |
WebSocket connect fails / WS_DISCONNECTED |
PPSSPP not running, wrong port, or the WebSocket debugger isn't enabled. Verify with check_env.py --check and ppsspp_session(action='get') |
Tool call hangs / times out (WS_TIMEOUT) |
PPSSPP's main loop dispatches WebSocket requests: when the UI is frozen, a modal dialog is up, or emulation is paused, requests are not serviced. Screenshot first to check the UI state |
[BOOT_TIMEOUT] during boot |
Wedged-boot suspicion. start(resilient=true) quarantines the GPU-backend blacklist (renames FailedGraphicsBackends.txt, never deletes) and self-heals (≤2 retries) |
read_u32 returns IR_ENCODING_DETECTED |
You read JIT-IR code, not MIPS instructions. Use ppsspp_disassemble |
Known limitations
Honest statement of the protocol surface's boundaries — each item is also annotated in the corresponding tool description; summarized here:
- No save-state API: PPSSPP's WebSocket debugger exposes no
savestate.*events; save/load cannot be provided. Use PPSSPP's UI hotkeys (F1–F8 slots). - Frame stepping is instruction-granular only:
stepusescpu.stepInto. Whole-frame alternative: breakpoint the vblank handler, thenresume. - Analog stick is persistent shared state:
send_analogwrites stick until the next write; no auto-reset. - VRAM direct-read screenshots are unreliable: direct VRAM reads are not
synchronized with GPU rendering; colors may be wrong. Default channel is
render.source='output'can crash on some titles — use only as the render-channel empty-frame fallback. - Replay clock anchoring: replay timelines use the recording session's absolute game clock from boot; injection only works after a fresh boot via the boot-aligned sequence (documented in the tool response).
- Protected address ranges need explicit
force=true: kernel memory and the top.prx code section reject writes/assembly by default — a mis-write guard, not a limitation bug. - Session state is single-writer:
~/.ppsspp-dfx/sessions.jsonshares session registration across processes; concurrent MCP server instances pointed at the same path last-write-wins.
Community & support
- Report bugs and feature requests via GitHub Issues.
- For configuration and session problems, start with the troubleshooting quick reference and known limitations.
Contributing
See CONTRIBUTING.md.
Development
Start from docs/SCOPE.md (scope & protocol-surface boundaries) and evals/README.md (blind-test evaluation system: scenario cards, deterministic gates, runner, reports).
# Full test suite (unit + contract + integration; ~1500 tests):
.venv/ppsspp-dfx-mcp/Scripts/python -m pytest tests -q
# Regenerate the tool-surface baseline after signature/description changes
# (commit together with the change):
.venv/ppsspp-dfx-mcp/Scripts/python scripts/dump_tool_surface.py
Acknowledgements
- PPSSPP — the debugged target itself. The
WebSocket debugging protocol contract (
debugger.ppsspp.orgsubprotocol, event semantics, HLE introspection fields) was mapped item-by-item against its source (docs/SCOPE.md). - mcp-ppsspp, mcp-bizhawk, mcp-mgba — comparable emulator-MCP bridges; this server's coverage positioning was benchmarked against them (see the comparison section in docs/SCOPE.md).
- Runtime dependencies (MCP Python SDK, pydantic, PyYAML, websockets) are declared in pyproject.toml.
Citation
@misc{ppssppdfxmcp2026,
title={ppsspp-dfx-mcp: a PPSSPP debug MCP server for PSP game localization},
author={AstralVoidZ and contributors},
year={2026},
publisher={GitHub},
howpublished={\url{https://github.com/AstralVoidZ/ppsspp-dfx-mcp}},
}
License
Release files for ppsspp-dfx-mcp 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ppsspp_dfx_mcp-0.1.4.tar.gz | 856.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ppsspp_dfx_mcp-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.2 MB
Release files / ppsspp_dfx_mcp-0.1.4.tar.gz
| Download URL | ppsspp_dfx_mcp-0.1.4.tar.gz |
|---|---|
| Size | 856.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9b12a91ebfc7f958533a71db321afe3889b64728053a77988e0131063768e6f4
|
|
BLAKE2b-256 checksum How to use checksums |
4a2ada933082f03663c83d11c60fec990eccafeb1830185a8cdf948ef91ee8f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / ppsspp_dfx_mcp-0.1.4-py3-none-any.whl
| Download URL | ppsspp_dfx_mcp-0.1.4-py3-none-any.whl |
|---|---|
| Size | 317.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6005aa29264d113d76425703e1ccf00f10b25294aae120498bfa87ee835b35d0
|
|
BLAKE2b-256 checksum How to use checksums |
821617af83e4014e625247ff08247e2364c04a0561afc6659783fc5faa435951
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log