Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
buttonsYesList of button names to hold simultaneously for this press
framesNoFrames to hold the buttons (at 60 fps; default 1)
release_framesNoFrames 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}`);
        }
      });
    }
  • 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.",
          },
        },
      },
    },
  • 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}`,
      );
    }
  • 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);
            }
          });
        });
      }
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description covers key behaviors: queuing, asynchronous execution, frame timing, and how ROMs interpret the presses. However, it doesn't mention queue size limits or cancellation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loads the main action. It is slightly verbose but each sentence adds value. Could be more concise by removing the redundant button list.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, the description explains the asynchronous return and provides sufficient context for using the tool. It lacks details on potential errors or queue limits, but overall is adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so parameters are already documented. The description adds context about queuing and timing but doesn't provide additional meaning beyond the schema, e.g., the valid button names are already in the enum.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool queues button presses with specific hold and release frames. It explains the queuing behavior and differentiates from siblings (no other button tool exists).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains that consecutive calls produce distinct edge presses, guiding when to use. It does not explicitly mention when not to use or provide alternatives, but the sibling list implies no other button tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dmang-dev/mcp-mgba'

If you have feedback or need assistance with the MCP directory API, please join our Discord server