MCP

Every Emu198x binary is an MCP server as well as an emulator. Start one with --mcp and it stops being a window you watch: another program boots the machine, runs frames, presses keys, reads a chip register, and writes out a PNG, without knowing anything about that machine's timing loop.

MCP is the Model Context Protocol — a convention for offering a set of callable tools to whatever is on the other end of a pipe. The client sends JSON-RPC frames on standard input, one per line, and the server answers on standard output. A client can ask what a server offers and then call it, without being taught the server's internals first. That is the whole idea, and everything below is a shell pipeline you can paste and run.

The mental model

What works today

All 30 machines take --mcp. The server speaks three JSON-RPC methods — initialize, tools/list and tools/call — and it accepts the notifications/initialized note a client sends after the handshake. Everything the emulator can do arrives as a tool call.

Each machine registers the same core of 32 tools, then adds whatever its hardware justifies. The Spectrum's server lists 48 tools; the Atari 2600's lists the core 32 and no more.

NeedTools
Advance executionrun_frames, run_ticks, step, run_until_pc, run_until_any_pc, run_until_line, run_until_mem_change
Wait for a statewait_for_boot, wait_for_query_contains, wait_for_query_bool
Inspectquery_paths, query, query_cpu, memory_read, disasm, io_trace
Change memorypoke_byte, poke_word
Media and stateload_media, media_transport, save_snapshot, load_snapshot
Capturesave_screenshot, start_audio_recording, stop_audio_recording, save_audio_capture, start_video_recording, stop_video_recording
Inputinput
Source-level debuggingload_debug_info, debug_symbol
Resetreset

What a machine adds on top follows its hardware. Machines with keyboards add press_key, press_keys and type_string. The NES adds dump_nametable, dump_oam and dump_palette. The C64 adds disk write-back and BASIC program loading; the Spectrum adds Z80 port access, tape autoloading, and watch logs for memory and AY register writes. Ask tools/list and you get the machine's own set, each entry carrying a JSON Schema for its arguments.

Starting a server

From an unpacked release archive:

./emu198x-spectrum --mcp

Or from a source checkout:

cargo run --release -p emu198x-spectrum -- --mcp

Nothing prints. The server is waiting on standard input, and it boots the machine before the first call arrives.

To hand a machine to an agent instead, most MCP clients take a server list in this shape. One entry per machine you want to drive; the name on the left is yours to choose.

{
  "mcpServers": {
    "spectrum": {
      "command": "/path/to/emu198x-spectrum",
      "args": ["--mcp"]
    }
  }
}

Driving a machine

Here is a whole session as a shell pipeline: boot a Spectrum, wait for it to finish, press a key, read the text screen back, and save a screenshot. Each line is one JSON-RPC frame.

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"wait_for_boot","arguments":{"max_frames":300}}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"press_key","arguments":{"key":"P"}}}' \
  '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"run_frames","arguments":{"frames":25}}}' \
  '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"query","arguments":{"path":"screen.text.lines"}}}' \
  '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"save_screenshot","arguments":{"path":"boot.png"}}}' \
| ./emu198x-spectrum --mcp

Six replies come back on standard output. Trimmed to the part that matters, they read:

{"kind":"wait_for_boot","frames":87,"reason":"found copyright banner on row 23","row":23}
{"kind":"press_key","key":"P","hold_frames":3,"reached":25439232}
{"kind":"run_frames","frames":25,"reached":32428032,"stop_reason":"ReachedTarget"}
{"kind":"query","result":{"path":"screen.text.lines","value":[ ... ,"PRINT L    "]}}

Every step is answered with what the machine did, not with a promise that it did something. wait_for_boot reports the frame it stopped on and why it believed the machine had booted. press_key holds the key for three frames and releases it, the way a finger does. Pressing P on a 48K Spectrum at the BASIC prompt gives you the whole PRINT keyword, which is what the screen read back says — the trailing L is the cursor. And boot.png is on disk beside the binary.

That is the shape of every session. Advance the machine, read state back, decide what to do next from what you read. Waiting for a condition beats sleeping for an arbitrary number of frames, which is why wait_for_boot, wait_for_query_contains and run_until_pc exist.

Ask the machine what it exposes

The stable pattern for inspection is two calls. Ask what paths exist, pick one, read it. Pass a prefix so the answer stays short:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_paths","arguments":{"prefix":"cpu."}}}' \
  '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"query","arguments":{"path":"cpu.pc"}}}' \
| ./emu198x-spectrum --mcp

The Spectrum answers that prefix with sixteen paths, cpu.af through cpu.sp, including cpu.iff1, cpu.im and cpu.instructions_retired. Four prefixes are on every machine: cpu., run., session. and capture.. Past those, the namespace is the hardware.

MachinePathsSome of what it publishes
ZX Spectrum65screen., basic., tape., keyboard., boot.
Commodore 64164vic., cia1., cia2., drive8., iec., memory.
NES83ppu., apu., mapper., sprites., cartridge.
MSX142vdp., ay., ppi., bios.
Atari 260039tia., input., cartridge.

Read raw memory with memory_read instead — it takes an address and a length and reads the CPU bus with no side effects, which a query path is the wrong shape for.

What you need that we cannot give you

Two things, and neither ships with the emulator.

The machine's firmware. A server boots its machine before the first call arrives, so it needs the same ROMs a window needs, found the same way. The getting started page covers where each machine looks. A machine that cannot find its firmware fails at startup rather than at the first call, which is the easier failure to read.

A client that speaks stdio JSON-RPC. The server talks over standard input and output, one JSON object per line. Any MCP client can drive it, and so can a shell pipeline — the examples above are exactly that, and they are the shortest way to see whether a machine works before wiring an agent to it.

What does not work yet