Skip to main content
Glama
ppsspp-dfx
by AstralVoidZ

ppsspp-dfx-mcp

English | 中文

PyPI CI Python License: MIT

一个把 PPSSPP 变成 AI 可调试目标的 MCP(Model Context Protocol)服务器。它把 PSP 模拟器的 WebSocket 调试器封装为面向 LLM agent 的工具面: 会话生命周期、内存读写、反汇编、断点、CPU 控制、输入自动化、截图、回放录制与诊断 脚本——并内建结构化契约、防御性错误分类法与任务级评估。

文档:docs/SCOPE.md(范围与协议面边界)· CHANGELOG.md(变更记录)

项目状态

项目处于 alpha 阶段并快速迭代,工具面与配置格式可能出现不兼容变更。 运行前请阅读 SECURITY.md。

Related MCP server: GDB MCP Server

功能特性

  • 37 个静态工具,全部带结构化 inputSchema / outputSchema——没有无约束的 返回值,每个参数都有类型和说明。

  • 动态脚本工具:项目专属的诊断脚本通过 scripts.manifest.yaml 暴露为 ppsspp_script_<name> 工具,输入类型由脚本自带的 Pydantic model 决定; ppsspp_run_script 调用未暴露的脚本,ppsspp_list_scripts 查看清单—— 详见配置。

  • 会话模型:支持多个并发 PPSSPP 会话、就绪探测(wait_ready)与楔死自愈 (resilient 启动)。

  • 面向 agent 的人体工学:组合工具(ppsspp_frame_snapshot、 ppsspp_breakpoint(action="wait"/"trace"/"stats")、ppsspp_batch_step)、 session_id 自动解析、 防御性错误码([CODE] message 格式、CPU 冻结与连接断开的区分),错误文本内嵌 恢复建议。

  • 后台自动化:批量任务跑在独立的服务端任务上,不受 MCP 客户端工具调用超时的 影响;支持状态轮询、取消与注册表盘点(ppsspp_batch_status(batch_id 省略))。

  • 内建评估体系(evals/):49 张场景卡 + 确定性门禁 + 对录制夹具的盲测 runner + 汇总报告——工具面按 agent 实际使用的方式被测试。

  • 诚实的协议面:能力只在其背后存在可用实现时才声明;刻意置 false 的开关 附有设计理由说明。

运行

环境要求:Python 3.13+(配合独立 venv,原因见从源码运行); 带 WebSocket 调试器的 PPSSPP——官方发行版即可,开启方法见 docs/ppsspp-build.md(服务器负责启动它,并连接 ws://<host>:<port>/debugger);一个 MCP 客户端(ZCode、Claude Desktop、 MCP Inspector 等)。

从 PyPI 安装运行

版本状态:PyPI 上是 alpha 阶段的发布快照,可能落后于仓库 main。以你实际 装到的 wheel 版本为准(pip show ppsspp-dfx-mcp),变更记录见 CHANGELOG.md。

使用独立 venv——本服务器的 MCP SDK v2 无法与其他 MCP 服务器锁定的 1.x mcp 包共存:

# 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

把服务器注册到你的 MCP 客户端(入口由安装包提供,无需指向仓库内脚本):

{
  "mcpServers": {
    "ppsspp-dfx": {
      "command": "C:/absolute/path/to/.venv/Scripts/ppsspp-dfx-mcp.exe",
      "cwd": "C:/absolute/path/to/your-project"
    }
  }
}

command 指向安装 venv 内的入口可执行文件,POSIX 上为 .venv/bin/ppsspp-dfx-mcp;cwd 是服务器发现 .ppsspp-dfx/ 配置的目录 (见配置)。

cwd 可以省略(部分 harness 不支持该字段):把项目根同时写进 env.PPSSPP_DFX_PROJECT_ROOT 即可,两者等价。实测从 %TEMP% 启动、不设 cwd、仅给该环境变量,服务器仍能正确定位 project_root / config_dir / output_dir 并完成握手;反之若两者都缺,会退化为告警并丢失 scripts.manifest.yaml(ppsspp_script_* 工具静默消失)。

手动启动验证:

.venv/Scripts/ppsspp-dfx-mcp.exe   # Windows
.venv/bin/ppsspp-dfx-mcp           # POSIX

然后直接给 agent 派任务:"启动模拟器加载这个 ISO,告诉我当前 PC"——服务器 负责会话启动、就绪探测与状态读取。工具描述遵循 PURPOSE / USAGE / BEHAVIOR / RETURNS 约定,错误路径内嵌恢复指引,agent 无需示例即可自助。

不要在客户端与服务器之间插入包装脚本:Windows 上 os.execv 是 CreateProcess + 父进程等待(不是 POSIX 进程替换),多一层会让最内层服务器 立即读到 stdin EOF 并静默退出——表面现象只是 -32000: Connection closed。

从源码运行

仓库检出内自带引导脚本与可直接使用的 .mcp.json:

git clone https://github.com/AstralVoidZ/ppsspp-dfx-mcp.git
cd ppsspp-dfx-mcp

# 在本目录执行——创建 .venv/ppsspp-dfx-mcp 并安装(editable,含 dev 依赖):
python scripts/check_env.py --bootstrap

# 校验解释器 / SDK 版本 / 包导入 / 每份 .mcp.json:
python scripts/check_env.py --check

--bootstrap 只准备主 venv(.venv/ppsspp-dfx-mcp,跑测试与全量回归用)。 服务器启动不再需要它:提交的 .mcp.json 走通用运行器形式,环境由运行器首次运行时自建。

.mcp.json 的两种合法形式(--check 会逐份校验):

配置所在位置

形式

说明

包根(独立检出的仓库根)

["run", "ppsspp-dfx-mcp"]

自定位:运行器按工作目录找到项目

非包根(monorepo 的工作区根)

["run", "--directory", "<包目录>", "ppsspp-dfx-mcp"]

必须钉住包目录,否则会落到工作区自己的项目上

工作目录假设:MCP 客户端通常以 .mcp.json 所在目录为工作目录启动子进程; 相对定位依赖这一点。config.project_root() 的解析规则是 PPSSPP_DFX_PROJECT_ROOT 环境变量 > 工作目录,且不做向上搜索——工作目录一旦落到别处, 输出目录与脚本清单都会错位(表现为动态脚本工具静默消失)。

不要在提交的配置里写平台绑定的解释器路径(…/Scripts/python.exe 或 …/bin/python): 它在另一个操作系统上必然失效。

把服务器注册到你的 MCP 客户端——让客户端读取仓库里的 .mcp.json,或按同样的 结构内联(以包根那份为例):

{
  "mcpServers": {
    "ppsspp-dfx": {
      "command": "uv",
      "args": ["run", "ppsspp-dfx-mcp"]
    }
  }
}

若客户端把 command 解析到别的工作目录(不遵守上述假设),改用 python scripts/check_env.py --print-config 打印的片段——它给出同形的运行器形式 并用 --directory 钉住包目录的绝对路径,可直接粘贴。 另请确保通用运行器在 PATH 上(--check 会检查;缺失时 stdio 传输下客户端只看得见 子进程退出,看不到原因)。

python scripts/check_env.py --print-config

手动启动验证:

.venv/ppsspp-dfx-mcp/Scripts/python -m ppsspp_dfx_mcp   # Windows
.venv/ppsspp-dfx-mcp/bin/python -m ppsspp_dfx_mcp        # POSIX

配置

环境变量(全部可选):

变量

默认值

说明

PPSSPP_DFX_LOG_LEVEL

INFO

日志级别

PPSSPP_DFX_LOG_FORMAT

text

日志格式(text 或 json)

PPSSPP_DFX_RATE_LIMIT

60

单工具限流(次/分钟,0 为关闭)

PPSSPP_DFX_WS_HOST

127.0.0.1

PPSSPP WebSocket 主机

PPSSPP_DFX_WS_PORT

12345

PPSSPP WebSocket 端口

PPSSPP_DFX_EXE_PATH

(来自 yaml)

PPSSPP 可执行文件路径

PPSSPP_DFX_SESSIONS_PATH

~/.ppsspp-dfx/sessions.json

会话状态路径

PPSSPP_DFX_PROJECT_ROOT

cwd

项目根目录,覆盖 cwd 发现(MCP host 从临时目录启动时需设置)。路径不存在时报 CONFIG_INVALID;路径有效但无 .ppsspp-dfx/ 标记目录时告警不阻断

PPSSPP_DFX_CONFIG_DIR

<project_root>/.ppsspp-dfx/config

配置目录路径,覆盖默认发现逻辑

PPSSPP_DFX_IR

未设

设为 1 强制 CPUCore=2 解释器模式。内存断点必需——JIT fastmem 直写下断点不触发;代价是速度,只用于 trace/breakpoint 会话

PPSSPP_DFX_BOOT_HEAL_QUARANTINE

1

1 = 允许 boot 楔死自愈隔离 GPU 后端黑名单文件(仅重命名,从不删除);0 = 该步不执行

PPSSPP_DFX_MEMSTICK_DIR

自动探测

memstick 目录(日志/截图捕获用)

PPSSPP_DFX_WORKSPACE_ROOT

自动探测

scripts/_wire.py 的工作区根(取最近的含 .mcp.json 的祖先目录)。仅引导脚本使用

PPSSPP_DFX_ALLOW_REMOTE_DEBUGGER

未设

设为 1 时接受绑定在非回环地址上的无鉴权调试器(默认 fail-closed 拒绝)。详见 SECURITY.md

PPSSPP_DFX_ALLOW_ABS_SCRIPT

未设

设为 1 允许脚本 manifest 使用绝对路径(默认拒绝,仅相对路径)。详见 SECURITY.md

PPSSPP_DFX_ISO_ROOT

未设

ISO 路径白名单根——设置后 iso_path 仅接受该树内路径。详见 SECURITY.md

PPSSPP_DFX_WS_PORT 只在连接「已在运行的 PPSSPP」时生效:服务器自己启动 PPSSPP 时会随机选空闲端口并在启动后发现它(避开 12345 冲突),此时该变量被忽略。

限流只覆盖协议分发:PPSSPP_DFX_RATE_LIMIT 由中间件在 JSON-RPC tools/call 分发层执行,按 (session_id, tool_name) 分桶——不同会话互不占用额度;无法归属的 请求(参数结构异常、工具名未知/未注册)统一落入 __unknown__ 桶并被限流 (fail-closed)。进程内调用(如测试里的 mcp.call_tool(...)、工具直接调用另一工具) 不经过分发层,因此不受限流约束——它不是进程级全局限流器。

评测专用变量(跑 evals/ 时才需要,日常使用无需设置): PPSSPP_DFX_TEST_MODE、PPSSPP_DFX_FIXTURE_DIR、PPSSPP_DFX_TEST_EXE_PATH、 PPSSPP_DFX_TEST_ISO_PATH、PPSSPP_DFX_TEST_PPSSPP_LOG、PPSSPP_DFX_SKILL_DIR、 PPSSPP_DFX_EVALS_LLM_API_PATH、PPSSPP_DFX_EVAL_GAME_*。

完整可复制的配置模板(含全部变量注释、按用途分组)见源码检出中的 examples/mcp.json.template(PyPI 安装的用户可从 GitHub 仓库 获取)。

项目级 YAML 配置位于 .ppsspp-dfx/config/(相对工作目录):

  • project.yaml — ppsspp_exe 路径与项目元数据

  • addresses.yaml — 命名地址常量(同时为内存向导的 completions 能力提供候选)

  • scripts.manifest.yaml — 诊断脚本清单。每个条目带机器可读的 status (migrated = 可运行,skeleton = 方法体返回 not_implemented)。标记 exposed: true 的脚本在启动时注册为 ppsspp_script_<name> 工具——skeleton 除外,preflight 会拒绝它们。ppsspp_reload_scripts 将动态工具注册表与清单 重新同步(无需重启),并报告声明与注册的对账结果。

独立部署快速开始

三份配置文件(project.yaml / addresses.yaml / scripts.manifest.yaml)有 开箱模板——从这里开始,不要从零手写 YAML。模板的取法取决于安装方式:

从源码检出(模板就在仓库里):

mkdir -p .ppsspp-dfx/config
cp examples/project.yaml examples/addresses.yaml \
   examples/scripts.manifest.yaml .ppsspp-dfx/config/

从 PyPI 安装(wheel 只打包 src/ppsspp_dfx_mcp,不含 examples/, 请在 GitHub 上取同一份模板):

mkdir -p .ppsspp-dfx/config
for f in project.yaml addresses.yaml scripts.manifest.yaml; do
  curl -fsSL "https://raw.githubusercontent.com/AstralVoidZ/ppsspp-dfx-mcp/main/examples/$f" \
    -o ".ppsspp-dfx/config/$f"
done

也可在 examples/ 目录里逐个浏览/下载。取到模板后编辑 .ppsspp-dfx/config/project.yaml:把 ppsspp_exe 指向你的带 WebSocket 调试器的 PPSSPP 构建;把 addresses.yaml 里的 PLACEHOLDER 地址替换为你自己逆向得到的值。

首次会话前需要知道的两件事:

  • 没有 scripts.manifest.yaml 服务器仍能启动,但所有 ppsspp_script_* 工具会 静默消失——即使 scripts: 列表为空也请保留模板(check_env.py --check 报的正是这个警告)。

  • 配置为空且无占位值时,服务器侧一切功能可用;只有会话启动需要真实的 ppsspp_exe(或 PPSSPP_DFX_EXE_PATH),地址常量也只有在你提供自己游戏的 数值后才有意义。

协议面

在 initialize 握手时声明——且只声明实际注册的能力(SDK 从请求处理器 是否存在来推导各项能力,所以这里出现的每一项背后都有可用实现):

能力

声明

说明

tools

✅

37 个静态工具 + 动态 ppsspp_script_<name>

resources

✅

ppsspp://game-state、ppsspp://registers(快照)

prompts

✅

memory-breakpoint-wizard、memory-trace-wizard

completions

✅

两个内存向导的 address 参数,候选来自 addresses.yaml

logging

❌

协议修订 2026-07-28 移除了 logging/setLevel

tasks

❌

仅 SDK 2.2.0 的类型定义,无服务器端实现

tools.list_changed 与 resources.subscribe 刻意置 false。SDK 2.2.0 的 MCPServer 没有暴露握手期设置 notification_options 的入口,声明它们等于承诺 一个服务器发不出的通知。现有替代:

  • ppsspp_reload_scripts 会报告变化内容(exposed_added / exposed_removed),agent 无需通知通道即可响应。

  • 服务器 instructions 字符串告诉新 agent 工具面包含什么。

若未来 SDK 开放了该入口,翻转开关并补上 send_*_list_changed 调用即可——L2 契约测试(tests/unit/l2_mcp_contract/test_capabilities_contract.py)断言当前 的 false 状态并会失败,这是设计信号:该决策需要重新审视,而非回归。

返回形态

图像类工具(ppsspp_screenshot、ppsspp_dump)返回拆成两半的 CallToolResult:

  • content — 一个携带像素的 ImageContent 块。

  • structuredContent — 仅元数据(file_path / size_bytes / format,加上 mode、width、height、empty 等各工具自有字段)。图像的 base64 副本 不在这个通道里——那会膨胀 schema,且重复 content 已承载的内容。

每个工具都声明结构化 outputSchema——没有工具返回无约束对象或 items 为空的 数组。ppsspp_run_script 的 input 参数是唯一注册在案的例外:其形状由被调用的 脚本决定,因此只描述而不约束。

structuredContent 的序列化行为以 mcp SDK 2.2.0(mcp.server.mcpserver)实测 为准。多形态工具(同一工具不同 action 返回不同形状,如 ppsspp_breakpoint / ppsspp_diff_memory / ppsspp_scan / ppsspp_session / ppsspp_batch_step / ppsspp_frame_snapshot 等)的输出契约声明为 partial:完整负载始终经 content 文本通道以 JSON 返回,而 structuredContent 对部分形态的填充行为在不同 SDK 版本上可能不同——机器可读消费方请以文本通道 JSON 为兜底。

错误处理

当被模拟的 CPU 冻结(死循环 / HLE 阻塞 / GPU 管线停滞)时,服务器返回 CPU_FREEZE_SUSPECTED 而不是笼统的 WS_DISCONNECTED——区分"PPSSPP 进程还 活着但 CPU 冻结"与"进程已死 / WebSocket 断开"。

对 CPU_FREEZE_SUSPECTED 的建议处理:

  • 不要重启会话——PPSSPP 还在运行。

  • 用 ppsspp_screenshot 截取当前画面辅助诊断。

  • 尝试 step(action='resume')(对真正的死循环可能无效)。

  • 用 hle.thread.list 查看线程状态(可能暴露 HLE 阻塞)。

  • 在当前 PC 处用 ppsspp_disassemble 检查指令流。

相关错误码:WS_DISCONNECTED(PID 已死,真断开)、WS_TIMEOUT(带票据的 RPC 超时,保守默认)、CPU_STATE_ERROR(当前 CPU 状态不适合该操作)。错误 文本始终以 [CODE] 开头,agent 可编程分类;存在下一步的地方都内嵌了恢复建议。

故障排查速查表

症状

原因 / 修复

-32000: Connection closed(无任何信息)

MCP 客户端与服务器之间有包装脚本:Windows 上 os.execv 实为 CreateProcess + 父进程等待(非 POSIX 替换),内层 server 的 stdin 立即 EOF 静默退出。去掉中间层,直接以 venv 解释器为 command(见从源码运行)

客户端启动 server 报 -32000: Connection closed / command 路径不存在

该配置的相对解释器路径未被 provision(其目录旁没有对应 venv),或客户端把相对 command 解析到了另一个工作目录。运行 python scripts/check_env.py --bootstrap 在各配置目录旁建 venv,或改用 python scripts/check_env.py --print-config 输出的绝对路径片段(见从源码运行)

check_env 报「独立 venv 缺失」

.venv/ 被 gitignore 排除,新 clone 必然没有。运行 python scripts/check_env.py --bootstrap(见从源码运行)

mcp SDK 版本不满足 / 导入期崩溃

系统 Python 的 mcp 包常被其他 MCP server 钉在 1.x,与 SDK v2 不可调和。不要全局安装——用 check_env.py --bootstrap 建独立 venv,或按从 PyPI 安装运行安装到独立 venv

ppsspp_script_* 工具全部消失(服务器正常启动)

.ppsspp-dfx/config/scripts.manifest.yaml 缺失——缺失仅告警不阻断,动态工具静默清空。按独立部署快速开始取三份模板修复(check_env.py --check 会提示;PyPI 安装时模板不在 wheel 内,需从 GitHub 取)

[PPSSPP_NOT_FOUND]

PPSSPP 可执行文件未配置。设 PPSSPP_DFX_EXE_PATH,或 .ppsspp-dfx/config/project.yaml 的 ppsspp_exe(优先级 env > yaml)

找不到 .ppsspp-dfx/config

配置目录按 cwd 发现(无父级上溯)。从含 .ppsspp-dfx/ 的目录启动,或设 PPSSPP_DFX_CONFIG_DIR 指向它

cwd 无 .ppsspp-dfx/ 标记目录(告警)

MCP host 从临时目录启动服务器。设 PPSSPP_DFX_PROJECT_ROOT 显式 pin 项目根目录(告警不阻断,向后兼容)

[CONFIG_INVALID] PPSSPP_DFX_PROJECT_ROOT=... does not exist

环境变量指向的路径不存在。这是显式配置错误——修正路径或取消设置该环境变量以回退到 cwd

WebSocket 连接失败 / WS_DISCONNECTED

PPSSPP 未运行、端口不对,或未启用 WebSocket debugger。check_env.py --check 验证环境,ppsspp_session(action='get') 验证会话

工具调用挂起 / 超时(WS_TIMEOUT)

PPSSPP 主循环负责 dispatch WebSocket 请求:UI 卡死、模态对话框弹出或模拟暂停时请求不会被处理。先截图确认 UI 状态

boot 阶段 [BOOT_TIMEOUT]

启动楔死疑似。start(resilient=true) 会隔离 GPU 后端黑名单(仅重命名 FailedGraphicsBackends.txt,不删除)并自愈重启(≤2 次重试)

已知限制

诚实声明协议面的边界——以下各项均已在对应工具的描述中标注,此处汇总:

  • IR 编码无法在 MCP 侧可靠判别:PPSSPP 的 JIT-IR 代码段用 read_u32 读取不会报错, 但可能得到无意义的值(不是真实 MIPS 指令)——要读代码段请改用 ppsspp_disassemble (搜索指令用 ppsspp_search_disasm)。

  • 条件断点由 MCP 侧求值:该构建的 IR 模式忽略寄存器条件(上游缺陷,已实机建档), 因此 breakpoint 的 condition 不下发 PPSSPP,改由 action='wait' 在命中时用 cpu.evaluate 求值——求值器只在 wait 运行期间生效;假命中自动 resume 并计入 filtered_hits,同一地址 ≥10 次命中且间隔 <1s 触发风暴熔断(自动撤防 + storm_break=true)。CPU 若在布防前已处于暂停态,则无法归因(手动暂停与命中不可 区分):返回 hit=true 并附 note 说明注册的条件未被求值。

  • 无存档 API:PPSSPP 的 WebSocket debugger 不暴露 savestate.* 事件, 服务器无法提供存档保存/加载。用 PPSSPP 的 UI 快捷键(F1-F8 存档槽)。

  • 帧推进只有指令级:step 走 cpu.stepInto。整帧推进的替代:在 vblank 处理器设断点后 resume。

  • analog 摇杆是持久共享态:send_analog 写入后保持到下次写入,无自动复位。

  • VRAM 直读截图不可靠:直读 VRAM 与 GPU 渲染输出不同步,颜色可能失真; 默认走 render 通道。source='output' 在部分游戏上有崩溃风险,仅在 render 通道空帧回退时使用。

  • replay 时钟锚定:replay 时间线使用录制会话 boot 时刻的绝对游戏时钟, 只能在全新 boot 后按 boot 对齐序列注入(工具返回体带对齐序列说明)。

  • 保护地址段写入需显式 force=true:内核内存与 top.prx 代码段默认拒绝 写入/汇编码——这是防误写设计,不是限制性 bug。

  • 会话状态单写者:~/.ppsspp-dfx/sessions.json 跨进程共享会话登记, 并发多个 MCP 服务器实例指向同一路径时后写覆盖。

  • trace 只编排内存断点:ppsspp_breakpoint(action='trace') 布防的是 内存访问断点(默认读访问)。执行断点的一次性等待用 action='set' + action='wait' 组合。

性能参考(本机实测)

参考环境:Windows x64,PPSSPP v1.20.4-605,服务器与 PPSSPP 同机(localhost WS)。 数字随机器与游戏负载浮动,供超时预算估量,非性能承诺:

操作

实测

单次 WS 往返(game.status 级别的轻量调用)

p50 ≈ 0.21 ms,p95 ≈ 0.28 ms(n=60)

全频段 24 MB pattern 扫描(ppsspp_scan background=true,64 KiB 分块)

≈ 40 s(384 次分块读)

断点命中→可观测(热地址 set + wait,resume 后到 wait 确认)

p50 ≈ 11 ms(n=30)

社区与支持

贡献

见 CONTRIBUTING.md。

开发

从 docs/SCOPE.md(范围与协议面边界)与 evals/README.md(盲测评估体系:场景卡、确定性门禁、 runner、报告)入手。

# 前置:pytest 在 dev 依赖组中(默认安装不含)——二选一:
#   uv sync                                # 装入 dependency-groups(含 pytest)
#   pip install -e ".[dev]"                # 或装 dev extra
# 全量测试套件(单元 + 契约 + 集成;1700+ 个测试用例(不含参数化展开)):
.venv/ppsspp-dfx-mcp/Scripts/python -m pytest tests -q

# 工具签名/描述变更后重新生成工具面基线(与变更同笔提交):
.venv/ppsspp-dfx-mcp/Scripts/python scripts/dump_tool_surface.py

CI 门禁边界——勿把「CI 全绿」读作「真机已验证」:CI (.github/workflows/ci.yml)只执行 python -m pytest tests -q,不设置 PPSSPP_DFX_TEST_EXE_PATH / PPSSPP_DFX_TEST_ISO_PATH,因此依赖真实 PPSSPP 与游戏 ISO 的集成用例在 CI 中 一律 skip、不会执行。这些用例属本地真机门控:需在本机显式导出上述两个 环境变量后运行。python scripts/check_skips.py 仅审计「跳过理由是否已登记」, 不改变这一事实。

致谢

  • PPSSPP —— 被调试目标本身。本服务的 WebSocket 调试协议契约(debugger.ppsspp.org 子协议、事件语义与 HLE 内省字段)对照其源码逐项梳理并建档(见 docs/SCOPE.md)。

  • mcp-ppsspp、mcp-bizhawk、 mcp-mgba —— 同类模拟器-MCP 桥接方案;本服务的覆盖定位以它们为对照 (见 docs/SCOPE.md 的「与同类项目的覆盖对比」)。

  • 运行时依赖(MCP Python SDK、 pydantic、PyYAML、websockets)声明于 pyproject.toml。

引用

@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}},
}

许可证

MIT

Available Tools

37 tools
ppsspp_analyze_logA
Read-onlyIdempotent

PURPOSE: Filter a PPSSPP log file for ERROR / WARNING / CRASH lines.

USAGE: log_path optional (defaults to the server-mirrored PPSSPP broadcast log at .ppsspp-dfx/output/ppsspp.log, written while a session runs); filter optional (keyword); session_id optional.

BEHAVIOR: READ-ONLY. Reads and filters a log file. Does not contact PPSSPP.

RETURNS: {log_path, matches: [{line_no, text}...], count, filter, filter_mode, total_matches, truncated}. truncated=true covers both an explicit limit cut and the internal 500-match cap; total_matches is a lower bound (>= the value) whenever truncated=true.

Inputs: limit, filter, log_path, session_id, +1 more. Returns: count, filter, matches, log_path, +3 more

ppsspp_assembleA
Destructive

PURPOSE: Assemble MIPS instruction(s) and write the resulting bytes to memory.

USAGE: session_id + address + code ('

' or ';' separated — PPSSPP assembles one line per call so the tool loops; armips-style ';' comments are NOT supported here).

BEHAVIOR: DESTRUCTIVE. Protected ranges (kernel, top.prx code) need force=true. A partial write is reported with an error directing you to disassemble and inspect.

RETURNS: {address, code, bytes_written, response, text}.

Inputs: code (required), address (required), session_id (required), force. Returns: code, text, address, response, +1 more

ppsspp_batch_cancelA

PURPOSE: Request cancellation of a queued or running background batch job.

USAGE: batch_id from ppsspp_batch_step(background=true); omit batch_id for survey/list mode (e.g. to recover a lost batch_id or audit background activity before touching the session).

BEHAVIOR: STATE-CHANGE. Cancels the detached task; the job's own finally block releases the session lock, so subsequent tool calls are free to use the session immediately. The abort happens at the current step boundary (a press finishes, a mid-wait cuts within ~1s). Cancelling an already-finished job is an error — check ppsspp_batch_status first if unsure.

RETURNS: {batch_id, status, note} — poll ppsspp_batch_status for the terminal state.

Inputs: batch_id (required). Returns: `ppsspp_batch_cancel` 的结构化返回契约。 本工具直接构造 dict 字面量、**没有对应的 Pydantic view**,故手工声明而…

ppsspp_batch_statusA
Read-onlyIdempotent

PURPOSE: Poll the state and progress of a background batch job without touching the session.

USAGE: batch_id from ppsspp_batch_step(background=true); omit batch_id for survey/list mode (e.g. to recover a lost batch_id or audit background activity before touching the session).

BEHAVIOR: Lock-free registry read — never opens the WS transport and never waits for the per-session lock, so it is safe to call while a background batch (or any other tool) owns the session. Executed-step count updates as steps complete; 'completed' carries the full foreground-shaped result. READ-ONLY.

RETURNS: with batch_id → {batch_id, session_id, status: queued|running|completed|failed|cancelled, executed, total, error, result, retention_jobs}; with batch_id omitted (survey) → {jobs: [{batch_id, session_id, status, executed, total, error, result_present}], retention_jobs} in submission order (finished jobs beyond retention are evicted and absent).

Inputs: batch_id. Returns: jobs, error, total, result, +5 more

ppsspp_batch_stepA

PURPOSE: Execute an ordered automation sequence of press / wait / state_probe / screenshot steps in one call, optionally on a detached background task that outlives the client timeout.

USAGE: session_id + steps:[{type: press|wait|state_probe|screenshot, ...}]; on_failure='continue'|'abort' (default continue); background=false|true.

ROUTING: ordered multi-step automation -> here; single CPU-step -> ppsspp_step; background job survey -> ppsspp_batch_status(batch_id omitted). BEHAVIOR: STATE-CHANGE. Foreground (default) holds the session lock for the whole batch; frames are 60fps wall-clock equivalents; sequences estimated >25s are rejected up-front with BATCH_BUDGET_EXCEEDED (the MCP client aborts tool calls at ~30s, killing the remaining steps server-side). Per-step MCP progress is reported when the client requests it. background=true validates and submits instantly, returns {action:'submitted', batch_id,...}, keeps the session lock for the batch duration, and reports progress via ppsspp_batch_status. If any foreground step fails the whole call is isError BATCH_STEP_FAILED — inspect results[] per step. Screenshots are auto-skipped during replay recording.

RETURNS: foreground {total, executed, succeeded, failed, skipped, recording_mode, results[], aborted}; background {action:'submitted', batch_id, session_id, total, estimated_s, hint}.

Inputs: steps (required), session_id (required), background, on_failure. Returns: hint, total, action, failed, +10 more

ppsspp_breakpointA

PURPOSE: Manage breakpoints AND consume their hits — set/remove/update/list CPU execution breakpoints and memory watchpoints, strict-wait for a hit, or one-call arm-hit-capture-resume tracing.

USAGE: session_id optional when exactly one session is active; management actions as below; action='wait' blocks until any breakpoint is hit (set/mem_set first; lock-free; breakpoint stays armed); action='trace' arms a temporary MEMORY breakpoint at address, waits, captures pc/registers/backtrace, always removes it and resumes (defaults to read access; narrow with read/write/size). For EXECUTION breakpoints use action='set' + 'wait'.

ROUTING: persistent breakpoint management -> here; one-shot strict-wait -> action='wait'; armed hit-capture -> action='trace'. BEHAVIOR: MUTATING. trace arms/removes and set/mem_* manage state; Reliable hits need CPUCore=2 (IR Interpreter). mem_remove resolves the watchpoint's real size via mem_list first (address+size matching); mem_update merges existing read/write/change unconditionally (PPSSPP zero-omits omitted bools). CPU set/remove return no data — the tool follows with a list for verification. wait/trace are lock-free during the wait itself (concurrent reads keep working); do NOT submit step/pause/resume during a wait. CONDITION SEMANTICS: PPSSPP's IR mode ignores register conditions, so any condition is enforced MCP-side — the breakpoint is armed unconditionally and each hit's expression is evaluated with cpu.evaluate; a falsy hit is auto-resumed (not surfaced) and counted in filtered_hits.

RETURNS: stats → {mode:"stats", window_s, total_hits, by_pc: [{pc, count, first_seen, last_seen}]} (fixed ~30s sampling window — no shorter-window option, probe_changes?: [{probe, old, new, ts}], note}; management actions → {action, address, enabled, breakpoints[]}; wait → {hit, already_paused, timeout_s, pc, reason, related_address, ticks, condition, condition_filtered, filtered_hits, storm_break}; trace → {hit, already_paused, address, access, timeout_s, hits: [{pc, related_address, reason, ticks, mem_hits?, registers?, backtrace?}], bp_removed, resumed, note}.

Inputs: action (required), log, read, size, +9 more. Returns: pc, hit, hits, mode, +22 more

ppsspp_contextA
Read-onlyIdempotent

PURPOSE: One-call crash-triage pack — identity + disassembly window + optional backtrace for an address.

USAGE: pass a crash PC or call target; identity resolves via addresses.yaml known_functions (IDA offset applied); disasm covers window instructions before/after; include_backtrace=true adds the call stack (pauses the CPU briefly).

BEHAVIOR: READ-ONLY. Unknown addresses return identity=null and the raw window instead of failing; backtrace is skipped (not an error) when the CPU is running — the note field says why. SCOPE: identity/region come from a TOPX-specific address knowledge base (addresses.yaml). For any other ISO they come back empty and note says so explicitly -- an empty identity means 'not in the tables', NOT 'bad address'.

ROUTING: persistent breakpoints around this address -> ppsspp_breakpoint; one armed hit-capture -> ppsspp_breakpoint(action="trace"); recurring sampling -> ppsspp_state_observer.

RETURNS: {address, identity: {name, start, offset} | null, region, disasm: [{address, text}], backtrace: [...], backtrace_note}.

Inputs: address (required), window, session_id, include_backtrace. Returns: disasm, region, address, identity, +2 more

ppsspp_diff_memoryA
Read-only

PURPOSE: Snapshot a memory range and diff it against current memory — the classic "what changed?" variable-locator.

USAGE: diff_memory(action="snapshot", start=..., end=...) → handle; act in game; diff_memory(action="compare", handle=...) → changed-byte list; action="drop"/"list" manage handles.

BEHAVIOR: READ-ONLY. Memory is never written — only the per-server snapshot registry mutates. Large ranges are read across multiple reads; per-snapshot cap 8 MiB; registry cap 4 with FIFO eviction. The registry is process-global: parallel sessions share one cap and FIFO order, so another session's snapshots can evict yours under load. compare requires the handle's exact range.

RETURNS: snapshot → {handle, start, size_bytes, checksum}; compare → {handle, start, size_bytes, changed_count, truncated, changes: [{address, old, new}]}; drop → {handle, dropped}; list → {handles: [...], count, max_snapshots}.

Inputs: action (required), end, size, start, +3 more. Returns: count, start, handle, changes, +7 more

ppsspp_disassembleA
Read-onlyIdempotent

PURPOSE: Disassemble N MIPS instructions at a given address.

USAGE: address required; session_id optional when exactly one session is active; count optional (default 10).

BEHAVIOR: READ-ONLY. Calls memory.disasm via WebSocket. Does not modify memory or CPU state.

RETURNS: {address, count, instructions: [{address, text}...]}.

Inputs: address (required), count, session_id. Returns: count, address, instructions

ppsspp_dumpA
Read-onlyIdempotent

PURPOSE: Dump the currently-bound GPU texture OR CLUT palette as an image plus metadata.

USAGE: kind='texture' → the bound texture (level selects mipmap; PPSSPP captures the currently-bound texture — it does NOT support capture by VRAM address); kind='clut' → the bound palette (level must be 0).

BEHAVIOR: READ-ONLY. An empty capture raises CAPTURE_EMPTY — enter a scene that renders (texture) or uses the palette (clut) and retry.

RETURNS: structuredContent metadata (kind/level/file_path/size_bytes/format); the image itself arrives as an ImageContent block.

Inputs: kind (required), session_id (required), level. Returns: level, format, file_path, size_bytes

ppsspp_evaluateA
Read-onlyIdempotent

PURPOSE: Evaluate a debugger expression (register names, hex literals, simple arithmetic).

USAGE: session_id + expression. No '*addr' dereference syntax — read memory with read_u32 instead.

BEHAVIOR: READ-ONLY. Pauses/resumes the CPU internally.

RETURNS: {expression, value, response, text}.

Inputs: expression (required), session_id (required). Returns: text, value, response, expression

ppsspp_frame_snapshotA

PURPOSE: One-call paused scene snapshot — pause (unless already paused), capture pc + registers + optional named probes, then resume.

USAGE: session_id; probes = optional comma-separated state_observer registry names; want_registers default true. Prefer this over a manual pause + query(registers) + resume sequence.

ROUTING: pause+capture+resume in one call -> here; cheap PC-only check -> ppsspp_query(action='register', name='pc', safe=true); recurring sampled probes -> ppsspp_state_observer. BEHAVIOR: STATE-CHANGE. The session lock is held for the whole call (pause→capture→resume is short). A CPU we paused is resumed before returning; an already-paused CPU stays paused. A failing capture never leaves the game frozen.

RETURNS: {was_stepping, resumed, pc, trust_level, registers, probes} — registers/probes keys are ALWAYS present; they carry null when opted out (want_registers=false / probes omitted) — nullable-key contract, 2026-09-08.

Inputs: session_id (required), probes, want_registers. Returns: pc, probes, resumed, registers, +2 more

ppsspp_gpu_recordA
Read-onlyIdempotent

PURPOSE: Capture the next rendered frame's GE command stream as a binary dump file.

USAGE: session_id. The CPU must be RUNNING — a paused GPU never flips a frame; the MCP pre-probe converts that into a clean CPU_STATE_ERROR.

BEHAVIOR: READ-ONLY. Captures to a binary file under output/gpu_dumps/ (not JSON).

RETURNS: {size_bytes, file_path, raw, text}.

Inputs: session_id (required). Returns: raw, text, file_path, size_bytes

ppsspp_gpu_statsA
Read-onlyIdempotent

PURPOSE: Query GPU counters — fps, vblanks per second, timing info.

USAGE: session_id. The CPU must be RUNNING; paused, the MCP pre-probe returns CPU_STATE_ERROR instead of hanging — which doubles as the cheapest paused-CPU probe.

BEHAVIOR: READ-ONLY.

ON TIMEOUT: the error names WHY it timed out -- expected_stall (the CPU was stepping, so no frame is coming), pairing_broken (a broadcast arrived meanwhile, so ticket pairing failed), or no_producer (nothing was broadcast at all, so the emulator is not producing frames -- check for a modal dialog blocking it). A CPU_FREEZE_SUSPECTED is re-checked against the frame heartbeat first: if no frames are arriving it is reported as WS_TIMEOUT with a no-producer attribution, NOT as a CPU freeze.

RETURNS: {fps, vblanks_per_second, info, timing, raw, text}.

Inputs: session_id (required). Returns: fps, raw, info, text, +2 more

ppsspp_healthA
Read-onlyIdempotent

PURPOSE: Probe MCP server liveness and readiness — plus an optional four-point session health battery.

USAGE: no args for the server-level probe (does NOT contact PPSSPP); pass session_id to also run the session battery (iso_loaded / cpu_running / ws_connected / game_mode_valid — absorbed from the former ppsspp_smoke_test tool).

BEHAVIOR: READ-ONLY. Server counters are read in-memory; the session battery (when requested) contacts PPSSPP over the session transport but never mutates state.

READING session_checks: each entry carries value_status besides passed -- 'ok' (the probe really read), 'stale_address_suspected' (the read succeeded and returned zero on several consecutive readings, so the probe address may have drifted -- a suspicion, not a verdict), 'failed' (the read raised or the data was absent; no value is reported), 'not_configured' (no probe address, so nothing was read). A probe that READ ZERO and one that COULD NOT READ both show passed=false while meaning opposite things: the first is a fact about the game, the second about the tooling. Do not read passed=false alone as a finding about the emulated game.

RETURNS: Dict with status ('ok'/'degraded'), version, python_version, pydantic_version, uptime_s, tool_count, session_count — plus session_checks: [{name, passed, detail, value_status, value?}] and overall_session_status when session_id is provided.

Inputs: session_id. Returns: HealthOutput + the per-session battery keys added when `session_id` is given. T…

ppsspp_hold_buttonsA

PURPOSE: Hold a combination of PSP buttons until a subsequent call changes the state.

USAGE: session_id + buttons required (pipe-separated combination, e.g. 'cross|circle'). Valid names: cross / circle / triangle / square / up / down / left / right / start / select / ltrigger / rtrigger.

BEHAVIOR: STATE-CHANGE. Sets the button-held state in PPSSPP; it persists until the next hold_buttons call. To release, call hold_buttons with buttons='' (every button is then sent as released). Note: send_analog does NOT release buttons -- it drives the analog axes on a separate PPSSPP event.

RETURNS: {buttons}.

Inputs: buttons (required), session_id (required). Returns: buttons

ppsspp_list_addressesA
Read-onlyIdempotent

PURPOSE: List the project's known address constants from addresses.yaml — the single source of truth; never guess hex addresses.

USAGE: optional section filter; an unknown section returns an error listing the valid ones.

CONVERSION: IDA <-> PPSSPP address conversion is plain arithmetic — ppsspp_addr = ida_addr + (top_base.ppsspp - top_base.ida) (defaults 0x08804000 - 0x00000000). ppsspp_convert_address was un-tooled in v0.1.6. BEHAVIOR: READ-ONLY. Int values ≥0x1000 are returned as hex strings that can be pasted straight into address parameters.

RETURNS: {sections, count, section_filter}.

Inputs: section. Returns: `ppsspp_list_addresses` 的结构化返回契约(openspec `tool-schema-contract`)。 本工具直接构造 dict…

ppsspp_list_scriptsA
Read-onlyIdempotent

PURPOSE: List diagnostic scripts declared in .ppsspp-dfx/config/scripts.manifest.yaml.

USAGE: category optional filter (eboot / state / p0ab / ndx / memory / misc / recipe).

BEHAVIOR: READ-ONLY. Reads the in-memory manifest registry (loaded at startup). Does not execute any script.

RETURNS: {scripts: [ScriptEntryView...], count, category}.

Inputs: category. Returns: count, scripts, category

ppsspp_memory_mapB
Read-onlyIdempotent

PURPOSE: Get the PPSSPP memory region map (user / kernel / VRAM ranges).

USAGE: session_id.

BEHAVIOR: READ-ONLY.

RETURNS: {ranges[], mapping, text}.

Inputs: session_id (required). Returns: text, ranges, mapping

ppsspp_press_buttonA

PURPOSE: Simulate a single PSP button press for a duration.

USAGE: session_id + button required; duration optional (default 1 frame). Valid button names: cross / circle / triangle / square / up / down / left / right / start / select / ltrigger / rtrigger.

BEHAVIOR: STATE-CHANGE. Sends input events to PPSSPP. Button state returns to released after the duration elapses.

RETURNS: {button, duration}.

Inputs: button (required), session_id (required), duration. Returns: button, duration

ppsspp_queryA
Read-onlyIdempotent

PURPOSE: Aggregate game-state queries — game_state, registers (all or one), backtrace, threads, modules, and function-list management (funcs/func_scan/func_add/func_remove).

USAGE: action + session_id; 'register' needs name; func_scan/func_remove need address; top_n defaults to 100 (pass 0 for the full list — hle.func.list can reach 700+KB).

ROUTING: one-shot PC read -> query(action='register', name='pc') (safe=true pauses for consistency; safe=false for hot-path polling); pause+capture -> ppsspp_frame_snapshot; recurring named probes -> ppsspp_state_observer; game_state / backtrace / threads / modules / HLE func management also here. BEHAVIOR: READ-ONLY. Lookups only — func_add/func_remove mutate the debugger function list. Verified on a live game: threads / modules / funcs / func_scan respond while the CPU is RUNNING (no pause needed); running-state PC/isCurrent reads are LOW trust unless safe=true (which pauses briefly for a consistent, high-trust read).

RETURNS: {action, data, trust_level} — data shape depends on the action.

Inputs: action (required), session_id (required), name, safe, +4 more. Returns: data, text, action, trust_level

ppsspp_read_memoryA
Read-onlyIdempotent

PURPOSE: Read memory (read_bytes / read_u32 / read_string).

USAGE: action; session_id optional when exactly one session is active; address as '0x' hex string; read_bytes ≤65536 per call (split larger reads); Memory scanning has moved to ppsspp_scan.

BEHAVIOR: READ-ONLY. read_string is ASCII-only (use read_bytes + Shift-JIS decode for game text). Reading code segments: use ppsspp_disassemble — MCP provides no IR-encoding detection (a read_u32 over JIT-IR bytes just returns the raw value).

RETURNS: {action, address, value, size, text, file} — read_bytes has output=value (default; byte list + hex text) / hex (text only, value=null) / file (paths + 64-byte preview; payload saved under .ppsspp-dfx/output/memory_reads/).

Inputs: action (required), size, length, output, +3 more. Returns: file, size, text, value, +2 more

ppsspp_reload_scriptsA
Idempotent

PURPOSE: Manually reload the script manifest YAML and clear the script module cache.

USAGE: No parameters.

BEHAVIOR: MUTATING. Re-reads the manifest file and invalidates cached script modules. Reversible: re-reading an unchanged file produces an equal registry. When the exposed tool set ACTUALLY changes (tools added or removed), the server notifies the client with a tool-list-changed notification so cached tools/list results are invalidated; a no-op reload sends nothing.

RETURNS: {reloaded_count, exposed_count, manifest_path, scripts: [ScriptEntryView...]}.

Inputs: no named parameters. Returns: scripts, exposed_added, exposed_count, manifest_path, +4 more

ppsspp_replayA

PURPOSE: Aggregate PPSSPP replay subsystem — record input sequences, execute them, and save/load .ppr recordings.

USAGE: session_id optional when exactly one session is active; actions: begin/abort/flush/execute/status/time_get/time_set/save/load/wait_complete; execute needs version + base64_input; time_set needs value; save/load take a bare file name (always under output/replays/).

BEHAVIOR: STATE-CHANGE. Recording requires the CPU RUNNING (real input timing); screenshots are rejected while recording. Replay timelines use ABSOLUTE game-clock timestamps anchored at the RECORDING session's boot — a replay only injects correctly when a fresh boot's clock is aligned to them: execute/load ONLY loads the event table and returns t0_s / estimated_end_s + the boot-aligned sequence (reset -> wait_ready -> wait boot+estimated_end_s -> abort); it does NOT play by itself. executing/saving NEVER clear on their own — only abort clears them — so wait_complete times out on any un-aborted replay; completion = the timeline estimate + explicit abort. execute/load auto-abort a live executing/saving state first. restore_rtc defaults to False: setting it rewinds the game-visible wall clock of the RUNNING session and pollutes every in-game timer; when needed, set it before the boot-aligned reset.

RETURNS: {action, executing, saving, version, size, base64, base_rtc, data} — execute/load data carries t0_s, estimated_end_s, event_count and boot_aligned_sequence; fields depend on the action.

Inputs: action (required), value, version, file_path, +6 more. Returns: data, size, action, base64, +5 more

ppsspp_run_scriptA

PURPOSE: Invoke a manifest-registered diagnostic script by name with validated input.

USAGE: name (see ppsspp_list_scripts; skeleton scripts return not_implemented); input dict validated against the script's Pydantic model; session_id required when the script declares requires_ppsspp (missing → SESSION_NOT_FOUND).

BEHAVIOR: STATE-CHANGE. Runs manifest-registered script code. Unknown names → SCRIPT_NOT_FOUND.

RETURNS: {name, output, output_model}.

Inputs: name (required), input, session_id. Returns: name, output, output_model

ppsspp_scanA
Read-only

PURPOSE: Three-mode memory scanner — byte-pattern search, Cheat-Engine-style value scan with narrowing sessions, and charset-aware string harvesting.

USAGE: mode='pattern' + pattern + start_addr/end_addr (migrated from read_memory scan); mode='value' + phase='initial'/value/width → handle, then phase='narrow'/op/value to converge, 'list'/'drop' to manage; mode='strings' + charset + start_addr/end_addr → [{address, text}]. background=true submits a detached job instead (recommended for full-band scans) and returns {action:'submitted', batch_id, ...} — poll ppsspp_batch_status(batch_id=...).

BEHAVIOR: READ-ONLY. Large ranges are read across multiple reads; unreadable regions are skipped per-chunk (one WS round-trip each), but CONSECUTIVE read timeouts (10 s each, >5 in a row) abort the scan — a wedged PPSSPP fails the scan cleanly instead of pinning the session lock. pattern/strings ranges over 2 MiB are AUTO-BACKGROUNDED (returns {action:'submitted', batch_id, ...} even with background=false) — measured: 24 MB @ 4 KiB chunks takes 53-96 s depending on PPSSPP build, always past the ~30s client timeout, while @ 64 KiB chunks it is 3.4-40 s (build-dependent). Value sessions live in a bounded per-server registry (cap 4, FIFO), are bound to the creating session, and the initial-scan cap is 8 MiB foreground / 32 MiB background. Background scans carry a 600 s wall-clock budget; exceeding it fails the job and releases the session. The registry is process-global: parallel sessions share one cap and FIFO order, so another session's scans can evict your handle under load.

ROUTING: what-changed-between-two-points -> ppsspp_diff_memory (snapshots); who-accesses-this-address -> ppsspp_breakpoint(action='trace'); value candidates with known addresses -> read_memory directly.

RETURNS: pattern → {action, address, value: [matches], size}; value initial → {scan_handle, width, candidates, passes}; value narrow → {scan_handle, candidates, passes}; value list → {scan_handle, addresses: [...]}; value drop → {scan_handle, dropped}; strings → {charset, count, strings: [{address, text}]}; background submission (explicit background=true OR pattern/strings range > 2 MiB) → {action: 'submitted', batch_id, session_id, estimated_s}.

Inputs: mode (required), op, phase, value, +13 more. Returns: hint, mode, size, count, +15 more

ppsspp_screenshotA
Read-onlyIdempotent

PURPOSE: Capture the framebuffer as an image (ImageContent) plus metadata.

USAGE: session_id optional when exactly one session is active; source='render' (default; empty frames auto-fall back to VRAM — colors unreliable there) or 'output' (CRASH-RISK, do not use); mutually exclusive with the deprecated mode param.

BEHAVIOR: READ-ONLY. An empty capture returns empty=true instead of an error — advance to a rendered scene and retry.

RETURNS: structuredContent metadata (mode/source/size_bytes/width/height/file_path/format/empty); the image itself arrives as an ImageContent block. The auto-saved PNG/JPG path is in file_path.

Inputs: mode, source, session_id. Returns: mode, empty, width, format, +4 more

ppsspp_search_disasmA
Read-onlyIdempotent

PURPOSE: Loop-search disassembly for a substring, collecting matching instructions with context.

USAGE: session_id + match (a leading '$' is stripped); start address; end=0 wraps the search around the whole region; max_results default 100.

BEHAVIOR: READ-ONLY.

RETURNS: {address, match, end, results[{address, text, name, params}], text}.

Inputs: match (required), address (required), session_id (required), end, +1 more. Returns: end, text, match, address, +1 more

ppsspp_search_memory_infoA
Read-onlyIdempotent

PURPOSE: Search PPSSPP's memory-tracking metadata for allocation/texture tags matching a string.

USAGE: session_id + match (case-insensitive substring, required); optional address/end/type filters.

BEHAVIOR: READ-ONLY. Returns a single extent per matching tag.

RETURNS: {regions[], count, raw, text}.

Inputs: match (required), session_id (required), end, type, +1 more. Returns: raw, text, count, regions

ppsspp_send_analogA

PURPOSE: Send an analog stick position (x, y in [0, 255], 128 = center).

USAGE: session_id + x + y required. 0 = full left / up, 255 = full right / down.

BEHAVIOR: STATE-CHANGE. Sets analog stick position; persists until next send_analog call.

RETURNS: {x, y}.

Inputs: x (required), y (required), session_id (required). Returns: x, y

ppsspp_sessionA

PURPOSE: Start / stop / inspect PPSSPP debug sessions — action=list / start / stop / get / wait_ready; wait_ready blocks until the emulated CPU is up.

USAGE: action='list' takes no other params and returns {sessions, count} (idle sessions >30min are auto-GC'd as a side effect; NOT a per-session health probe — use ppsspp_health(session_id=...) for that); action='start' needs iso_path (pass wait_ready=true to block until the CPU is up in the same call); stop/get/wait_ready need session_id. Call wait_ready AFTER start and BEFORE any memory tool — PPSSPP answers WebSocket before the CPU boots. start(resilient=true) self-heals boot wedges (blacklist quarantine + relaunch with the same session_id, ≤2 retries).

BEHAVIOR: STATE-CHANGE. start spawns a PPSSPP subprocess + WS debugger; stop terminates it (never taskkill the process yourself); wait_ready polls the probe lock-free and fails [BOOT_TIMEOUT] on wedge suspicion; list/get are read-only.

RETURNS: action=start/get → SessionResponse {session_id, iso_path, pid, ws_url, created_at, last_active_at, exec_count, ws_connected, recovered, restored (1 = session record was restored from sessions.json after a server restart, 0 = created in this process), ppsspp_version}; action=wait_ready → {action, ready, elapsed_s, probe_addr, probe_value, note} — elapsed_s is THIS call's wait duration, not the start→ready total; action=list → {sessions: [SessionResponse...], count}.

Inputs: action (required), iso_path, resilient, timeout_s, +3 more. Returns: pid, note, count, ready, +15 more

ppsspp_state_observerA

PURPOSE: Named memory-probe registry plus running-state observation — register probes once, then sample them cheaply every loop.

USAGE: action + session_id for observe; register needs name + address (+size 1/2/4, description); observe takes comma-separated names and samples.

ROUTING: recurring sampled probes across loops -> here; one-shot paused snapshot -> ppsspp_frame_snapshot; single-address access watch -> ppsspp_breakpoint(action="trace"). BEHAVIOR: STATE-CHANGE. register/clear mutate the registry; observe is reliable while RUNNING. The registry is PER-SESSION (keyed by session_id), seeded from addresses.yaml state_probes; clear removes user-registered probes only, so the configured baseline survives. Delete semantics are IDEMPOTENT: clearing an unknown probe name succeeds (ok), unlike ppsspp_breakpoint mem_remove which rejects missing targets.

RETURNS: {registered|probes|observations, count, success_count, failure_count} — shape depends on the action.

Inputs: action (required), session_id (required), name, size, +4 more. Returns: data, count, action, probes, +4 more

ppsspp_stepA

PURPOSE: Aggregate CPU run-state control (pause / resume / reset / run_until / next_hle).

USAGE: action='pause' / 'resume' / 'reset' / 'next_hle' take only session_id (optional when exactly one session is active); 'run_until' requires address.

NOTE (v0.1.6): single-stepping (into/over/out) moved to ppsspp_batch_step's cpu_step step type — this tool no longer accepts those actions.

ROUTING: single run-state operations -> here (run_until for run-to-address); multi-step press/wait/probe sequences and cpu_step -> ppsspp_batch_step. BEHAVIOR: STATE-CHANGE. Advances or changes CPU run state. 'reset' reboots the game (lost in-memory state). 'run_until' sets a temp breakpoint and resumes.

RETURNS: {action, address, pc, ticks, reason, related_address}. pc is stepping-verified (HIGH trust) for 'pause'; for 'resume' pc/ticks are a best-effort LOW-trust cpu.status snapshot of the running CPU (0/0.0 if that read failed); 'reset' reports 0.

Inputs: action (required), address, session_id. Returns: pc, ticks, action, reason, +2 more

ppsspp_wait_framesB
Idempotent

PURPOSE: Wait N frames (wall-clock sleep at 60 FPS by default) to let the emulator advance.

USAGE: session_id + frames required; interval optional (default 1/60 s).

BEHAVIOR: STATE-CHANGE. Sleeps the caller; emulator advances N frames. Session must be alive (validated before sleep).

RETURNS: {frames, elapsed_s}.

Inputs: frames (required), session_id (required), interval. Returns: frames, elapsed_s

ppsspp_watch_valueA
Read-only

PURPOSE: Value-change watch on an address — zero-pause alternative to a read watchpoint.

USAGE: session_id + address + mode + interval. Polls the value every interval_frames and records changes (old/new/frame/time). Pure reads: the CPU is never paused, so hot addresses are safe (storm-free).

BEHAVIOR: READ-ONLY. Polling loop; never mutates state; blocks ~duration_frames/60 seconds (cap 18000 frames).

RETURNS: {address, size, mode, samples, first_value, last_value, changes: [{frame, t_s, old, new}], change_count}.

Inputs: address (required), session_id (required), mode, duration_frames, +1 more. Returns: Structured output contract for ppsspp_watch_value.

ppsspp_write_memoryA
Destructive

PURPOSE: Write u8/u16/u32 or raw bytes to memory.

USAGE: session_id + address ('0x' hex) + data + format ('u8'|'u16'|'u32'|'bytes'; bytes accepts hex or base64).

BEHAVIOR: DESTRUCTIVE. Protected ranges (kernel, top.prx code) need force=true (PROTECTED_ADDRESS).

RETURNS: {address, format, bytes_written, value, text}.

Inputs: address (required), session_id (required), data, force, +2 more. Returns: text, value, format, address, +1 more

ppsspp_write_registerA
Destructive

PURPOSE: Set a CPU register (GPR/FPU/VFPU names, plus pc/hi/lo).

USAGE: session_id + name (MIPS ABI names preferred; numeric aliases like 'r5' normalize to the GPR of that index, i.e. r5 -> a1) + value (hex).

BEHAVIOR: DESTRUCTIVE. Pauses and resumes the CPU automatically (REQUIRED_STEPPING handled internally) — no manual pause needed.

RETURNS: {name, value, response, text}.

Inputs: name (required), value (required), session_id (required). Returns: name, text, value, response

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 37 tools

    Disambiguation4/5

    The tool set has several clusters that a naive agent could confuse (read_memory / query / state_observer / frame_snapshot / watch_value for state reads; step / batch_step for stepping; watch_value / breakpoint(trace) / state_observer for observing), but nearly every description carries an explicit ROUTING block that names the intended tool for each case, which sharply reduces misselection. Boundaries remain slightly blurred by design given the mega-tools (breakpoint, query, scan, replay) that fold many actions into one.

    Naming Consistency5/5

    Every tool uses the identical ppsspp_ snake_case prefix and mostly a verb_noun shape (read_memory, write_register, press_button, hold_buttons, batch_step), with a few noun-named aggregators (context, health, session, replay). The convention is uniform and predictable throughout.

    Tool Count3/5

    37 tools is on the heavy side for a single MCP server and several obvious groupings (batch_step/batch_status/batch_cancel, list_scripts/run_script/reload_scripts) could plausibly be folded together. The domain genuinely spans sessions, memory, breakpoints, GPU, input, replay, and scripting, so the breadth is partly earned, but it sits at the upper edge of comfortable.

    Completeness5/5

    The surface covers the full debugging lifecycle end to end: session start/stop/wait, memory read/write/scan/diff, disassembly and search, breakpoints and value watches, register/PC evaluation, GPU stats/dump/record, screenshots, input, replay, batching, and script management. There are no obvious dead ends for the stated PPSSPP-debugging purpose.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      An MCP server that exposes PPSSPP — the PlayStation Portable emulator — to any MCP-compatible client (Claude Desktop, Claude Code, etc.) via PPSSPP's built-in WebSocket debugger interface. Read and write PSP memory, drive games with button input, capture screenshots, set CPU breakpoints, inspect MIPS Allegrex registers — all through a clean tool interface. No bridge plugin needed; PPSSPP's debugg
      23
      34 npm
      13
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to interact with GDB for debugging via the MCP protocol. Supports setting breakpoints, stepping through code, inspecting memory and registers, and more.
      85
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Enables AI agents to programmatically control Cheat Engine for memory scanning, editing, cheat table management, and reverse engineering tasks.
      42
      MIT