mgba_press_buttons
Hold specified emulator buttons for a given number of frames, then release before the next queued press. Creates distinct edge events for separate presses.
Instructions
Queue a button-press: hold the given buttons for frames frames, then release for release_frames frames before the next queued press starts. Each call appends to the queue rather than overwriting, so consecutive calls produce distinct edge events that ROMs see as separate presses (rather than one continuous hold). Returns immediately; the press fires asynchronously on the emulator's frame callback. Valid button names: A, B, Select, Start, Right, Left, Up, Down, R, L.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| buttons | Yes | List of button names to hold simultaneously for this press | |
| frames | No | Frames to hold the buttons (at 60 fps; default 1) | |
| release_frames | No | Frames to release keys after the hold, before the next queued press fires (default 1). Increase if a ROM debounces input. |
Implementation Reference
- src/tools.ts:258-411 (registration)The registerTools function sets up the ListToolsRequestSchema handler (which returns the TOOLS array) and the CallToolRequestSchema handler (which dispatches tool names to logic via a switch statement). This is where 'mgba_press_buttons' is registered as a callable tool.
export function registerTools(server: Server, mgba: MgbaClient): 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>; switch (name) { case "mgba_ping": { const r = await mgba.call<string>("ping"); return ok(r); } case "mgba_get_info": { const r = await mgba.call<{ title?: string; code?: string; frame?: number; platform?: number | string; capabilities?: Record<string, boolean>; }>("get_info"); const lines = [ `Title: ${r.title ?? "(unavailable)"}`, `Code: ${r.code ?? "(unavailable)"}`, `Platform: ${r.platform ?? "(unavailable)"}`, `Frame: ${r.frame ?? "(unavailable)"}`, ]; if (r.capabilities) { const present = Object.entries(r.capabilities).filter(([, v]) => v).map(([k]) => k); const missing = Object.entries(r.capabilities).filter(([, v]) => !v).map(([k]) => k); lines.push(""); lines.push(`Capabilities present: ${present.length ? present.join(", ") : "(none)"}`); if (missing.length) lines.push(`Missing on this build: ${missing.join(", ")}`); } return ok(lines.join("\n")); } case "mgba_read8": { const v = await mgba.call<number>("read8", { address: p.address }); return ok(`0x${(p.address as number).toString(16).toUpperCase()}: ${formatHex(v)}`); } case "mgba_read16": { const v = await mgba.call<number>("read16", { address: p.address }); return ok(`0x${(p.address as number).toString(16).toUpperCase()}: ${formatHex(v)}`); } case "mgba_read32": { const v = await mgba.call<number>("read32", { address: p.address }); return ok(`0x${(p.address as number).toString(16).toUpperCase()}: ${formatHex(v)}`); } case "mgba_write8": { await mgba.call("write8", { address: p.address, value: p.value }); return ok(`Wrote ${formatHex(p.value)} → 0x${(p.address as number).toString(16).toUpperCase()}`); } case "mgba_write16": { await mgba.call("write16", { address: p.address, value: p.value }); return ok(`Wrote ${formatHex(p.value)} → 0x${(p.address as number).toString(16).toUpperCase()}`); } case "mgba_write32": { await mgba.call("write32", { address: p.address, value: p.value }); return ok(`Wrote ${formatHex(p.value)} → 0x${(p.address as number).toString(16).toUpperCase()}`); } case "mgba_read_range": { const bytes = await mgba.call<number[]>("read_range", { address: p.address, length: p.length, }); const hex = bytes .map((b) => b.toString(16).padStart(2, "0").toUpperCase()) .join(" "); const addr = (p.address as number).toString(16).toUpperCase(); return ok(`0x${addr} [${bytes.length} bytes]:\n${hex}`); } case "mgba_write_range": { const r = await mgba.call<{ written: number }>("write_range", { address: p.address, bytes: p.bytes, }); const addr = (p.address as number).toString(16).toUpperCase(); return ok(`Wrote ${r.written} bytes → 0x${addr}`); } case "mgba_press_buttons": { const r = await mgba.call<{ queued: boolean; queue_size: number }>("press_buttons", { buttons: p.buttons, frames: p.frames ?? 1, release_frames: p.release_frames ?? 1, }); const keys = (p.buttons as string[]).join("+"); return ok( `Queued press: ${keys} ` + `(hold ${p.frames ?? 1}f, release ${p.release_frames ?? 1}f). ` + `Queue size: ${r.queue_size}`, ); } case "mgba_advance_frames": { const frame = await mgba.call<number>("advance_frames", { count: p.count ?? 1 }); return ok(`Advanced ${p.count ?? 1} frame(s). Current frame: ${frame}`); } case "mgba_pause": { await mgba.call("pause"); return ok("Emulation paused"); } case "mgba_unpause": { await mgba.call("unpause"); return ok("Emulation resumed"); } case "mgba_reset": { await mgba.call("reset"); return ok("ROM reset"); } case "mgba_screenshot": { const path = await mgba.call<string>("screenshot", p.path ? { path: p.path } : {}); return ok(`Screenshot saved: ${path}`); } case "mgba_save_state": { if (p.slot === undefined && p.path === undefined) { throw new Error("provide either `slot` (0-9) or `path`"); } const r = await mgba.call<{ slot?: number; path?: string }>("save_state", { ...(p.slot !== undefined ? { slot: p.slot } : {}), ...(p.path !== undefined ? { path: p.path } : {}), }); return ok(r.path ? `Saved state to ${r.path}` : `Saved state to slot ${r.slot}`); } case "mgba_load_state": { if (p.slot === undefined && p.path === undefined) { throw new Error("provide either `slot` (0-9) or `path`"); } const r = await mgba.call<{ slot?: number; path?: string }>("load_state", { ...(p.slot !== undefined ? { slot: p.slot } : {}), ...(p.path !== undefined ? { path: p.path } : {}), }); return ok(r.path ? `Loaded state from ${r.path}` : `Loaded state from slot ${r.slot}`); } default: throw new Error(`Unknown tool: ${name}`); } }); } - src/tools.ts:156-182 (schema)The input schema definition for 'mgba_press_buttons' tool: declares 'buttons' (required array of valid key names), 'frames' (integer, default 1), and 'release_frames' (integer, default 1). Also includes VALID_KEYS = ["A", "B", "Select", "Start", "Right", "Left", "Up", "Down", "R", "L"].
{ name: "mgba_press_buttons", description: `Queue a button-press: hold the given buttons for \`frames\` frames, then release for \`release_frames\` frames before the next queued press starts. Each call appends to the queue rather than overwriting, so consecutive calls produce distinct edge events that ROMs see as separate presses (rather than one continuous hold). Returns immediately; the press fires asynchronously on the emulator's frame callback. Valid button names: ${VALID_KEYS.join(", ")}.`, inputSchema: { type: "object", required: ["buttons"], properties: { buttons: { type: "array", items: { type: "string", enum: VALID_KEYS }, description: "List of button names to hold simultaneously for this press", }, frames: { type: "integer", minimum: 1, default: 1, description: "Frames to hold the buttons (at 60 fps; default 1)", }, release_frames: { type: "integer", minimum: 1, default: 1, description: "Frames to release keys after the hold, before the next queued press fires (default 1). Increase if a ROM debounces input.", }, }, }, }, - src/tools.ts:346-358 (handler)The handler case for 'mgba_press_buttons' in the CallToolRequestSchema switch. It calls mgba.call('press_buttons', ...) with the buttons, frames, and release_frames parameters, then returns a formatted result string with the queue size.
case "mgba_press_buttons": { const r = await mgba.call<{ queued: boolean; queue_size: number }>("press_buttons", { buttons: p.buttons, frames: p.frames ?? 1, release_frames: p.release_frames ?? 1, }); const keys = (p.buttons as string[]).join("+"); return ok( `Queued press: ${keys} ` + `(hold ${p.frames ?? 1}f, release ${p.release_frames ?? 1}f). ` + `Queue size: ${r.queue_size}`, ); } - src/mgba.ts:89-132 (helper)The MgbaClient.call() method is the RPC helper that sends the 'press_buttons' request over TCP to the mGBA Lua bridge. It handles (re)connection, serializes JSON, and resolves/rejects based on the response.
async call<T = unknown>( method: string, params?: Record<string, unknown>, ): Promise<T> { // Lazy (re)connect — bridge.lua reloads kill the socket, and the user // shouldn't have to restart the MCP host every time they edit the script. if (!this.socket || this.socket.destroyed) { try { await this.connect(); } catch (err) { throw new Error( `Cannot reach mGBA bridge at ${this.host}:${this.port}. ` + `Make sure mGBA is running with bridge.lua loaded (Tools > Scripting). ` + `Underlying error: ${(err as Error).message}`, ); } } return new Promise<T>((resolve, reject) => { const sock = this.socket; if (!sock) { reject(new Error("socket vanished after connect")); return; } const id = this.nextId++; this.pending.set(id, (resp) => { if (resp.error) { reject(new Error(`mGBA RPC error [${resp.error.code}]: ${resp.error.message}`)); } else { resolve(resp.result as T); } }); const msg = JSON.stringify({ id, method, params: params ?? {} }) + "\n"; sock.write(msg, (err) => { if (err) { this.pending.delete(id); reject(err); } }); }); } }