Skip to main content
Glama

bizhawk_write32

Write a 32-bit little-endian value to emulator memory for cheats and pokes. Overwrites four bytes at the specified address with no undo; save state first if rollback needed.

Instructions

PURPOSE: Write an unsigned 32-bit little-endian value to emulator memory at the given address. USAGE: Use for 32-bit cheats and pokes (timestamps, large counters, pointers on 32-bit systems). For 8/16-bit values use bizhawk_write8/write16; for big-endian layouts byteswap and use bizhawk_write_range. BEHAVIOR: DESTRUCTIVE: overwrites four bytes starting at address with no undo (snapshot via bizhawk_save_state first if you need rollback). Direct memory write — bypasses MBC/mapper/DMA, see bizhawk_write8 notes. Returns an error if the domain is unknown, address+4 exceeds the domain, value < 0 or > 4294967295, or the core lacks memory.write_u32_le. RETURNS: Single line 'Wrote VAL_DEC (0xVAL_HEX) → ADDR_HEX (DOMAIN)'.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
addressYesByte offset within the chosen memory domain. Per-domain offsets are 0-based and INDEPENDENT of system bus addresses (e.g. SNES WRAM uses 0x09C6, NOT 0x7E09C6). Reads 4 consecutive bytes starting here. Returns an error if address < 0 or address + 4 exceeds the domain's size.
valueYes32-bit value to write. Must be 0-4294967295 (0x00000000-0xFFFFFFFF). LSB lands at `address`, MSB at `address+3`. Values outside this range return an error.
domainNoOptional case-sensitive memory domain name. Omit to use BizHawk's currently selected domain (see bizhawk_get_info → current_memory_domain). Discover available names with bizhawk_list_memory_domains; they vary per system (WRAM on SNES, RAM on NES, RDRAM on N64, 68K RAM on Genesis, MainRAM on PSX, EWRAM/IWRAM on GBA). Returns an error if the name doesn't match any domain on the loaded core.

Implementation Reference

  • Handler for bizhawk_write32: calls bh.call('write32', ...) with address, value, and optional domain, then returns a success message with formatted hex output.
    case "bizhawk_write32": {
      await bh.call("write32", { address: a(), value: p.value, ...dom() });
      return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`);
  • Tool definition and inputSchema for bizhawk_write32: declares name, description, and JSON schema with required params address (integer, min 0) and value (integer, 0-4294967295), plus optional domain string.
    {
      name: "bizhawk_write32",
      description:
        "PURPOSE: Write an unsigned 32-bit little-endian value to emulator memory at the given address. " +
        "USAGE: Use for 32-bit cheats and pokes (timestamps, large counters, pointers on 32-bit systems). For 8/16-bit values use bizhawk_write8/write16; for big-endian layouts byteswap and use bizhawk_write_range. " +
        "BEHAVIOR: DESTRUCTIVE: overwrites four bytes starting at `address` with no undo (snapshot via bizhawk_save_state first if you need rollback). Direct memory write — bypasses MBC/mapper/DMA, see bizhawk_write8 notes. Returns an error if the domain is unknown, address+4 exceeds the domain, value < 0 or > 4294967295, or the core lacks memory.write_u32_le. " +
        "RETURNS: Single line 'Wrote VAL_DEC (0xVAL_HEX) → ADDR_HEX (DOMAIN)'.",
      inputSchema: {
        type: "object",
        required: ["address", "value"],
        properties: {
          address: { type: "integer", minimum: 0, description: ADDRESS_PARAM_DESC(4) },
          value:   { type: "integer", minimum: 0, maximum: 4294967295, description: "32-bit value to write. Must be 0-4294967295 (0x00000000-0xFFFFFFFF). LSB lands at `address`, MSB at `address+3`. Values outside this range return an error." },
          domain:  { type: "string", description: DOMAIN_PARAM_DESC },
        },
        additionalProperties: false,
      },
    },
  • src/tools.ts:486-660 (registration)
    registerTools function registers all tools via ListToolsRequestSchema (returns TOOLS array) and CallToolRequestSchema (routes to the bizhawk_write32 case at line 555).
    export function registerTools(server: Server, bh: BizhawkServer): 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;
        const dom = () => p.domain ? { domain: p.domain } : {};
    
        switch (name) {
          case "bizhawk_ping": {
            const r = await bh.call<string>("ping");
            return ok(r);
          }
    
          case "bizhawk_get_info": {
            const r = await bh.call<{
              rom_name?: string;
              rom_hash?: string;
              framecount?: number;
              memory_domains?: string[];
              current_memory_domain?: string;
              capabilities?: Record<string, boolean>;
            }>("get_info");
            const lines = [
              `ROM:        ${r.rom_name ?? "(unavailable)"}`,
              `ROM hash:   ${r.rom_hash ?? "(unavailable)"}`,
              `Framecount: ${r.framecount ?? "(unavailable)"}`,
            ];
            if (r.memory_domains?.length) {
              lines.push("");
              lines.push(`Memory domains: ${r.memory_domains.join(", ")}`);
              if (r.current_memory_domain) {
                lines.push(`Active domain (used when 'domain' is omitted): ${r.current_memory_domain}`);
              }
            }
            if (r.capabilities) {
              const missing = Object.entries(r.capabilities).filter(([, v]) => !v).map(([k]) => k);
              if (missing.length) {
                lines.push("");
                lines.push(`Missing capabilities on this BizHawk build: ${missing.join(", ")}`);
              }
            }
            return ok(lines.join("\n"));
          }
    
          case "bizhawk_list_memory_domains": {
            const r = await bh.call<string[]>("list_memory_domains");
            return ok("Memory domains:\n  " + r.join("\n  "));
          }
    
          case "bizhawk_read8":  return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read8", { address: a(), ...dom() }))}`);
          case "bizhawk_read16": return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read16", { address: a(), ...dom() }))}`);
          case "bizhawk_read32": return ok(`${addrHex(a())}: ${fmtHex(await bh.call<number>("read32", { address: a(), ...dom() }))}`);
    
          case "bizhawk_read_range": {
            const bytes = await bh.call<number[]>("read_range", { address: a(), length: p.length, ...dom() });
            const hex = bytes.map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" ");
            return ok(`${addrHex(a())} [${bytes.length} bytes${p.domain ? `, ${p.domain}` : ""}]:\n${hex}`);
          }
    
          case "bizhawk_write8": {
            await bh.call("write8", { address: a(), value: p.value, ...dom() });
            return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`);
          }
          case "bizhawk_write16": {
            await bh.call("write16", { address: a(), value: p.value, ...dom() });
            return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`);
          }
          case "bizhawk_write32": {
            await bh.call("write32", { address: a(), value: p.value, ...dom() });
            return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`);
          }
          case "bizhawk_write_range": {
            const r = await bh.call<{ written: number }>("write_range", { address: a(), bytes: p.bytes, ...dom() });
            return ok(`Wrote ${r.written} bytes → ${addrHex(a())}${p.domain ? ` (${p.domain})` : ""}`);
          }
    
          case "bizhawk_press_buttons": {
            await bh.call("press_buttons", { buttons: p.buttons, player: p.player ?? 1 });
            const pressed = Object.entries(p.buttons as Record<string, boolean>)
              .filter(([, v]) => v).map(([k]) => k);
            return ok(`Set joypad ${p.player ?? 1}: ${pressed.length ? pressed.join("+") : "(all released)"}`);
          }
    
          case "bizhawk_play_input_sequence": {
            const params: Record<string, unknown> = { frames: p.frames };
            if (p.screenshot_every       !== undefined) params.screenshot_every       = p.screenshot_every;
            if (p.screenshot_dir         !== undefined) params.screenshot_dir         = p.screenshot_dir;
            if (p.screenshot_prefix      !== undefined) params.screenshot_prefix      = p.screenshot_prefix;
            if (p.observe_memory         !== undefined) params.observe_memory         = p.observe_memory;
            if (p.stop_on_memory_change  !== undefined) params.stop_on_memory_change  = p.stop_on_memory_change;
            const r = await bh.call<{
              played: number;
              final_framecount?: number;
              stopped_early?: boolean;
              stop_reason?: string;
              observations?: {
                frame_offset: number;
                path?: string;
                memory?: Record<string, number>;
              }[];
            }>("play_input_sequence", params);
    
            const obs = r.observations ?? [];
            const lines = [
              `Played ${r.played} frames. Final framecount: ${r.final_framecount ?? "(unavailable)"}.`,
            ];
            if (r.stopped_early) {
              lines.push(`Stopped early — reason: ${r.stop_reason ?? "(unspecified)"}.`);
            }
            lines.push(`Captured ${obs.length} observation${obs.length === 1 ? "" : "s"}.`);
            // Per-observation lines so the agent can correlate inline images with state
            for (let i = 0; i < obs.length; i++) {
              const o = obs[i];
              const memStr = o.memory
                ? ` memory={${Object.entries(o.memory).map(([k, v]) => `${k}=${v}`).join(", ")}}`
                : "";
              const imgStr = o.path ? ` (image ${i + 1})` : "";
              lines.push(`  obs[${i}] frame_offset=${o.frame_offset}${memStr}${imgStr}`);
            }
    
            // Build the multi-content response: text summary + per-observation
            // inline image blocks. We read each PNG from disk (Lua wrote it),
            // base64-encode for MCP transport.
            const content: ({ type: "text"; text: string } | { type: "image"; data: string; mimeType: string })[] = [
              { type: "text", text: lines.join("\n") },
            ];
            const fs = await import("node:fs");
            for (const o of obs) {
              if (!o.path) continue;
              try {
                const bytes = fs.readFileSync(o.path);
                content.push({
                  type: "image",
                  data: bytes.toString("base64"),
                  mimeType: "image/png",
                });
              } catch (err) {
                content.push({
                  type: "text",
                  text: `(failed to read observation at frame ${o.frame_offset} from ${o.path}: ${(err as Error).message})`,
                });
              }
            }
            return { content };
          }
    
          case "bizhawk_pause":         await bh.call("pause");          return ok("Emulation paused");
          case "bizhawk_unpause":       await bh.call("unpause");        return ok("Emulation resumed");
          case "bizhawk_reset":         await bh.call("reset");          return ok("Core reset");
          case "bizhawk_frame_advance": {
            const f = await bh.call<number>("frame_advance", { count: p.count ?? 1 });
            return ok(`Advanced ${p.count ?? 1} frame(s). Framecount: ${f}`);
          }
    
          case "bizhawk_screenshot": {
            const path = await bh.call<string>("screenshot", { path: p.path });
            return ok(`Screenshot saved: ${path}`);
          }
    
          case "bizhawk_save_state": {
            const r = await bh.call<{ path: string }>("save_state", { path: p.path });
            return ok(`Saved state to ${r.path}`);
          }
          case "bizhawk_load_state": {
            const r = await bh.call<{ path: string }>("load_state", { path: p.path });
            return ok(`Loaded state from ${r.path}`);
          }
    
          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      });
    }
  • BizhawkServer class with the call() method that enqueues commands (including 'write32') and sends them as JSON over TCP to the BizHawk Lua bridge.
    export class BizhawkServer {
      private server: net.Server | null = null;
      private client: net.Socket | null = null;
      private buf = "";
      private queue: PendingCmd[] = [];
      /** Commands sent to BizHawk awaiting a RESULT reply. Keyed by command id. */
      private inflight = new Map<number, PendingCmd>();
      /** True between sending a command and receiving its reply. Lua only handles
       *  one command per round-trip, so we wait for a RESULT before sending the
       *  next command. */
      private awaitingResult = false;
      private nextId = 1;
      private readonly host: string;
      private readonly port: number;
      private readonly timeoutMs: number;
    
      constructor(opts: BizhawkOptions = {}) {
        this.host      = opts.host      ?? "127.0.0.1";
        this.port      = opts.port      ?? 8766;
        this.timeoutMs = opts.timeoutMs ?? 10000;
      }
    
      describeTarget(): string {
        return `tcp listening on ${this.host}:${this.port}`;
      }
    
      isConnected(): boolean {
        return this.client !== null && !this.client.destroyed;
      }
    
      /**
       * True once we've received at least one message (READY or RESULT) from the
       * bridge. A bare TCP connection isn't enough — BizHawk's CLI flag opens the
       * socket at process startup, but `bridge.lua` only starts polling after the
       * user loads it via Tools > Lua Console. Tools should wait for this before
       * issuing commands.
       */
      isBridgeReady(): boolean {
        return this.bridgeReady;
      }
      private bridgeReady = false;
    
      /** Start listening. Resolves once the listen socket is bound. */
      async start(): Promise<void> {
        if (this.server) return;
        return new Promise<void>((resolve, reject) => {
          const srv = net.createServer((sock) => this.attachClient(sock));
          srv.once("error", (err) => reject(err));
          srv.listen(this.port, this.host, () => {
            this.server = srv;
            resolve();
          });
        });
      }
    
      stop(): void {
        this.client?.destroy();
        this.client = null;
        this.server?.close();
        this.server = null;
      }
    
      private attachClient(sock: net.Socket): void {
        if (this.client && !this.client.destroyed) {
          // Already have a client. Refuse the new one — only one BizHawk at a time.
          sock.write("ERROR another BizHawk client already connected\n");
          sock.destroy();
          return;
        }
        process.stderr.write("[mcp-bizhawk] BizHawk client connected (waiting for bridge.lua to start polling)\n");
        this.client = sock;
        this.buf = "";
        this.awaitingResult = false;
        this.bridgeReady = false;
    
        // CRITICAL: disable Nagle. Bridge's socketServerResponse has a 5ms timeout;
        // our 5-byte NONE replies get coalesced by Nagle for up to 200ms otherwise,
        // missing every receive window. With Nagle off, writes flush immediately.
        sock.setNoDelay(true);
    
        sock.setEncoding("utf8");
        sock.on("data", (chunk: string) => this.onData(chunk));
        sock.on("close", () => {
          process.stderr.write("[mcp-bizhawk] BizHawk client disconnected\n");
          this.client = null;
          // Leave inflight commands hanging — they'll time out cleanly.
        });
        sock.on("error", (err) => {
          process.stderr.write(`[mcp-bizhawk] socket error: ${err.message}\n`);
        });
      }
    
      private onData(chunk: string): void {
        if (process.env.MCP_BIZHAWK_DEBUG) {
          process.stderr.write(`[trace] RX raw (${chunk.length}B): ${JSON.stringify(chunk)}\n`);
        }
        this.buf += chunk;
        while (true) {
          const nl = this.buf.indexOf("\n");
          if (nl === -1) break;
          let line = this.buf.slice(0, nl).trim();
          this.buf = this.buf.slice(nl + 1);
          if (line.length === 0) continue;
    
          // BizHawk's SocketServer.SendString prepends every outgoing message
          // with "<byte-count> " as a framing header. Strip it.
          const m = line.match(/^(\d+) (.+)$/);
          if (m) line = m[2];
    
          if (process.env.MCP_BIZHAWK_DEBUG) {
            process.stderr.write(`[trace] RX line: ${JSON.stringify(line)}\n`);
          }
          this.handleMessage(line);
        }
      }
    
      private handleMessage(line: string): void {
        if (!this.bridgeReady) {
          this.bridgeReady = true;
          process.stderr.write("[mcp-bizhawk] bridge.lua is polling — bridge ready\n");
        }
        if (line === "READY") {
          // Lua is asking for work; no result to deliver this round.
          this.maybeDispatchNext();
          return;
        }
        if (line.startsWith("RESULT ")) {
          const json = line.slice(7);
          let parsed: { id?: number; result?: unknown; error?: { code: number; message: string } };
          try {
            parsed = JSON.parse(json);
          } catch (err) {
            process.stderr.write(`[mcp-bizhawk] bad RESULT json: ${(err as Error).message}\n`);
            this.awaitingResult = false;
            this.maybeDispatchNext();
            return;
          }
          const id = parsed.id ?? -1;
          const pending = this.inflight.get(id);
          if (pending) {
            this.inflight.delete(id);
            if (parsed.error) {
              pending.reject(new Error(`BizHawk RPC error [${parsed.error.code}]: ${parsed.error.message}`));
            } else {
              pending.resolve(parsed.result);
            }
          }
          this.awaitingResult = false;
          this.maybeDispatchNext();
          return;
        }
        process.stderr.write(`[mcp-bizhawk] unrecognised line from BizHawk: ${line}\n`);
        this.maybeDispatchNext();
      }
    
      /**
       * Write a single message to the bridge. BizHawk's socket server, since
       * 2.6.2, requires INCOMING messages to be length-prefixed the same way it
       * frames its OUTGOING ones: `"{length:D} {message}"`. Without the prefix,
       * BizHawk's parser silently discards the line and `socketServerResponse()`
       * returns empty — which is exactly the failure mode we hit before finding
       * this in `Lua/_docs_luacats/comm.d.lua`.
       */
      private sendFramed(payload: string): void {
        if (!this.client || this.client.destroyed) return;
        // length = byte count of the payload itself (not including the framing
        // prefix or the trailing newline that delimits the line on the wire).
        const byteLen = Buffer.byteLength(payload, "utf8");
        const wire = `${byteLen} ${payload}\n`;
        if (process.env.MCP_BIZHAWK_DEBUG) {
          process.stderr.write(`[trace] TX: ${JSON.stringify(wire)}\n`);
        }
        this.client.write(wire);
      }
    
      /**
       * Called when Lua sends READY. We MUST reply to every send because
       * BizHawk's Lua bridge polls `socketServerResponse()` once per frame and
       * expects something there — and we want to keep the round-trip turning
       * even when there's no work. Reply with the next queued command if
       * available, otherwise NONE.
       */
      private maybeDispatchNext(): void {
        if (!this.client || this.client.destroyed) return;
        if (this.awaitingResult) {
          // a command is in flight; tell bridge to keep waiting
          this.sendFramed("NONE");
          return;
        }
        const next = this.queue.shift();
        if (!next) {
          this.sendFramed("NONE");
          return;
        }
        this.inflight.set(next.id, next);
        this.awaitingResult = true;
        const msg = JSON.stringify({ id: next.id, method: next.method, params: next.params });
        this.sendFramed(msg);
      }
    
      /** Enqueue a command and return a promise that resolves when BizHawk replies. */
      async call<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
        return new Promise<T>((resolve, reject) => {
          const id = this.nextId++;
          const pending: PendingCmd = {
            id,
            method,
            params,
            resolve: (r) => resolve(r as T),
            reject,
          };
    
          const timer = setTimeout(() => {
            // Drop from queue if still waiting; from inflight if already sent.
            this.queue   = this.queue.filter((p) => p.id !== id);
            this.inflight.delete(id);
            if (this.inflight.size === 0) this.awaitingResult = false;
            reject(new Error(
              `BizHawk call "${method}" timed out (${this.timeoutMs}ms) — ` +
              `is the bridge.lua script still polling?`,
            ));
          }, this.timeoutMs);
    
          // Wrap so the timer always clears
          const origResolve = pending.resolve, origReject = pending.reject;
          pending.resolve = (r) => { clearTimeout(timer); origResolve(r); };
          pending.reject  = (e) => { clearTimeout(timer); origReject(e); };
    
          this.queue.push(pending);
        });
      }
    }
Behavior5/5

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

With no annotations, the description fully discloses destructive behavior (overwrites four bytes, no undo), error conditions, and that it bypasses MBC/mapper/DMA. Provides actionable guidance (save state first).

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

Conciseness5/5

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

Well-structured with clear headings (PURPOSE, USAGE, BEHAVIOR, RETURNS). Every sentence is informative and front-loaded with purpose. No wasted words.

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

Completeness5/5

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

Comprehensive coverage of purpose, usage, behavior, error conditions, and return format. Schema handles parameter details, leaving description to address agent decision-making needs fully.

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

Parameters4/5

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

Schema coverage is 100% with detailed descriptions. The tool description adds practical usage examples (timestamps, large counters) and reinforces error conditions, but does not add significant new semantic detail beyond what schema already provides.

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 writes a 32-bit little-endian value to emulator memory. It distinguishes from siblings like bizhawk_write8 and bizhawk_write16 by specifying bit width and endianness.

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

Usage Guidelines5/5

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

Explicitly recommends use for 32-bit cheats and pokes, and advises using alternative tools for 8/16-bit values or big-endian layouts. Includes when to avoid (e.g., need for rollback) and suggests snapshotting.

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-bizhawk'

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