ppsspp_breakpoint_remove
Removes a CPU execution breakpoint from PPSSPP's breakpoint table by address. Essential for cleaning up breakpoints during debugging; idempotent for non-existent addresses.
Instructions
PURPOSE: Remove a previously-added CPU execution breakpoint. USAGE: Clean up breakpoints when done debugging. To remove all, query ppsspp_breakpoint_list first. BEHAVIOR: Modifies PPSSPP's breakpoint table. Idempotent for non-existent breakpoints (no error). RETURNS: Single line 'Breakpoint removed at ADDR_HEX'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | PSP execution address of the breakpoint to remove. |
Implementation Reference
- src/tools.ts:367-382 (schema)Tool schema/definition for ppsspp_breakpoint_remove. Describes purpose, behavior, and declares input schema requiring an integer 'address' parameter.
{ name: "ppsspp_breakpoint_remove", description: "PURPOSE: Remove a previously-added CPU execution breakpoint. " + "USAGE: Clean up breakpoints when done debugging. To remove all, query ppsspp_breakpoint_list first. " + "BEHAVIOR: Modifies PPSSPP's breakpoint table. Idempotent for non-existent breakpoints (no error). " + "RETURNS: Single line 'Breakpoint removed at ADDR_HEX'.", inputSchema: { type: "object", required: ["address"], properties: { address: { type: "integer", minimum: 0, description: "PSP execution address of the breakpoint to remove." }, }, additionalProperties: false, }, }, - src/tools.ts:597-600 (handler)Handler for ppsspp_breakpoint_remove — calls PPSSPP's cpu.breakpoint.remove event with the given address, then returns a confirmation string.
case "ppsspp_breakpoint_remove": { await pp.call("cpu.breakpoint.remove", { address: a() }); return ok(`Breakpoint removed at ${addrHex(a())}`); } - src/tools.ts:405-613 (registration)Registration: registerTools sets up the MCP server handlers. The tool is dispatched by name via CallToolRequestSchema switch-case.
export function registerTools(server: Server, pp: PpssppClient): void { server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (req) => { const { name, arguments: args = {} } = req.params; const p = args as Record<string, unknown>; const a = () => p.address as number; switch (name) { case "ppsspp_ping": { const r = await pp.call<{ version?: string; name?: string }>("version"); return ok(`pong (${r.name ?? "PPSSPP"} ${r.version ?? "(unknown version)"})`); } case "ppsspp_get_info": { const status = await pp.call<{ game?: { id?: string; title?: string; version?: string } | null; paused?: boolean; stepping?: boolean }>("game.status"); const lines: string[] = []; if (status.game) { lines.push(`Title: ${status.game.title ?? "(unavailable)"}`); lines.push(`Disc ID: ${status.game.id ?? "(unavailable)"}`); lines.push(`Version: ${status.game.version ?? "(unavailable)"}`); } else { lines.push("No game loaded."); } const state = status.stepping ? "stepping (paused)" : status.paused ? "paused" : "running"; lines.push(`State: ${state}`); return ok(lines.join("\n")); } case "ppsspp_read8": { const r = await pp.call<{ value: number }>("memory.read_u8", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read16": { const r = await pp.call<{ value: number }>("memory.read_u16", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read32": { const r = await pp.call<{ value: number }>("memory.read_u32", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read_range": { const r = await pp.call<{ base64: string }>("memory.read", { address: a(), size: p.size }); const bytes = Buffer.from(r.base64 ?? "", "base64"); const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" "); return ok(`${addrHex(a())} [${bytes.length} bytes]:\n${hex}`); } case "ppsspp_read_string": { const r = await pp.call<{ value: string }>("memory.readString", { address: a(), type: "utf-8" }); return ok(`${addrHex(a())}: ${JSON.stringify(r.value ?? "")}`); } case "ppsspp_write8": { await pp.call("memory.write_u8", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write16": { await pp.call("memory.write_u16", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write32": { await pp.call("memory.write_u32", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write_range": { const bytes = Buffer.from(p.bytes as number[]); const base64 = bytes.toString("base64"); await pp.call("memory.write", { address: a(), base64 }); return ok(`Wrote ${bytes.length} bytes → ${addrHex(a())}`); } case "ppsspp_press_buttons": { await pp.call("input.buttons.send", { buttons: p.buttons }); const pressed = Object.entries(p.buttons as Record<string, boolean>) .filter(([, v]) => v).map(([k]) => k); return ok(`Set buttons: ${pressed.length ? pressed.join("+") : "(all released)"}`); } case "ppsspp_press_button": { await pp.call("input.buttons.press", { button: p.button, duration: p.duration ?? 1 }); return ok(`Pressed ${p.button} for ${p.duration ?? 1} frames (auto-released)`); } case "ppsspp_send_analog": { await pp.call("input.analog.send", { stick: p.stick, x: p.x, y: p.y }); return ok(`Set analog stick ${p.stick} to (${p.x}, ${p.y})`); } case "ppsspp_pause": { // cpu.stepping is fire-and-forget per PPSSPP source ("No immediate // response. Once CPU is stepping, a 'cpu.stepping' event will be // sent."). Send it, then poll cpu.status until stepping=true. await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); return ok("Emulation paused"); } case "ppsspp_resume": { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false); return ok("Emulation resumed"); } case "ppsspp_step": { const r = await pp.call<{ pc?: number }>("cpu.stepInto"); return ok(`Stepped one instruction. PC: ${r.pc !== undefined ? addrHex(r.pc) : "(unknown)"}`); } case "ppsspp_reset": { await pp.call("game.reset"); return ok("Game reset"); } case "ppsspp_screenshot": { // PPSSPP's gpu.buffer.* events all require CORE_STEPPING_CPU (or GPU // stepping) state — they fail with "Neither CPU or GPU is stepping" // otherwise. We transparently pause→capture→resume so callers can // screenshot any time without managing pause state. If the emulator // was already paused, we leave it paused. // // source='render' (default) uses gpu.buffer.renderColor → reads the // active GPU render target. Safer: GPU_GetCurrentFramebuffer hits a // different code path than the crash-prone GPU_GetOutputFramebuffer. // // source='output' uses gpu.buffer.screenshot → reads the final // composited output (what's on screen, post scaling/shaders). Can // CRASH PPSSPP on some games: upstream has an `_assert_(buf != nullptr)` // after GPU_GetOutputFramebuffer that fires when the function returns // true with a null buffer (observed on some homebrew). We can't catch // a process abort from outside, but v0.1.2's auto-reconnect means MCP // recovers when PPSSPP is relaunched. const source = (p.source as string | undefined) ?? "render"; const event = source === "output" ? "gpu.buffer.screenshot" : "gpu.buffer.renderColor"; const statusBefore = await pp.call<{ stepping?: boolean; paused?: boolean }>("cpu.status"); const wasStepping = !!statusBefore.stepping; if (!wasStepping) { await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); } try { // type: "base64" returns the raw base64 payload; the default "uri" // returns a "data:image/png;base64,..." prefix which we'd have to strip. const r = await pp.call<{ base64?: string; uri?: string }>(event, { type: "base64" }); let b64 = r.base64; if (!b64 && r.uri) { // Belt-and-suspenders: if PPSSPP returned a URI anyway, strip the prefix. const m = /^data:image\/png;base64,(.*)$/.exec(r.uri); if (m) b64 = m[1]; } if (!b64) { throw new Error(`PPSSPP did not return screenshot data from ${event} (no game loaded, or framebuffer not readable?)`); } return { content: [ { type: "text" as const, text: `Screenshot captured (source: ${source}, event: ${event}).` }, { type: "image" as const, data: b64, mimeType: "image/png" }, ], }; } finally { if (!wasStepping) { try { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false, { timeoutMs: 2000 }); } catch { /* best-effort */ } } } } case "ppsspp_get_registers": { // PPSSPP's cpu.getAllRegs returns categories with PARALLEL arrays: // { categories: [{ name, registerNames: [...], uintValues: [...], floatValues: [...] }] } // Not an array of {name, value} objects as I first assumed. const r = await pp.call<{ categories?: Array<{ name: string; registerNames?: string[]; uintValues?: number[]; floatValues?: string[]; }>; }>("cpu.getAllRegs"); const lines: string[] = []; for (const cat of r.categories ?? []) { lines.push(`── ${cat.name} ──`); const names = cat.registerNames ?? []; const vals = cat.uintValues ?? []; for (let i = 0; i < Math.max(names.length, vals.length); i++) { const nm = names[i] ?? `r${i}`; const v = vals[i]; lines.push(` ${nm.padEnd(8)} = ${v !== undefined ? addrHex(v) : "(unavailable)"}`); } } return ok(lines.join("\n") || "(no registers returned)"); } case "ppsspp_breakpoint_add": { await pp.call("cpu.breakpoint.add", { address: a() }); return ok(`Breakpoint added at ${addrHex(a())}`); } case "ppsspp_breakpoint_remove": { await pp.call("cpu.breakpoint.remove", { address: a() }); return ok(`Breakpoint removed at ${addrHex(a())}`); } case "ppsspp_breakpoint_list": { const r = await pp.call<{ breakpoints?: Array<{ address: number; enabled?: boolean; condition?: string }> }>("cpu.breakpoint.list"); const bps = r.breakpoints ?? []; if (bps.length === 0) return ok("No breakpoints set."); const lines = bps.map((b) => ` ${addrHex(b.address)} ${b.enabled === false ? "(disabled)" : ""}${b.condition ? ` if ${b.condition}` : ""}`); return ok(`${bps.length} breakpoint${bps.length === 1 ? "" : "s"}:\n${lines.join("\n")}`); } default: throw new Error(`Unknown tool: ${name}`); } }); } - src/tools.ts:401-403 (helper)Helper function addrHex — formats a number as an 8-digit uppercase hex address string (used in the response).
function addrHex(n: number): string { return `0x${n.toString(16).toUpperCase().padStart(8, "0")}`; }