Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Control protocol (CCP)

The Copperline Control Protocol (CCP) is a versioned JSON-RPC 2.0 interface over TCP for programmatic control of the emulator. Scripts, developer tools, CI runners, and automated agents use it to inspect state, set breakpoints, step execution, inject input, change media, and capture the display.

Starting the control server

# Headless mode (server owns execution; paused at power-on):
./target/release/copperline --config copperline.example.toml --noaudio \
    --control :0 --control-info /tmp/ccp.json

# Windowed mode (attaches control server to interactive session):
./target/release/copperline --config copperline.example.toml --control-gui :7710

The listen address is HOST:PORT, :PORT, or PORT; the port-only forms bind 127.0.0.1, and port 0 picks a free port. Both modes serve one client at a time. The windowed server answers a second connection with one JSON error line and closes it; the headless server accepts the next connection only after the current client disconnects, keeping the machine paused in between. shutdown ends the process. The headless server runs unpaced, keeps a PC history, and arms reverse execution at start-up (the windowed server does both when a client connects), with the budget and snapshot interval taken from COPPERLINE_DBG_RR_BUDGET_MB and COPPERLINE_DBG_RR_INTERVAL (see Reverse debugging).

The headless server owns the machine, so --control cannot be combined with --gdb/--gdb-gui, --benchmark-until, the capture flags (--screenshot-after, --expect-screenshot, --save-state-after, --dump-frames, --gif-after), scheduled input, scheduled floppy or CD inserts, or --exit-on-return; use the matching protocol methods instead. --record-input PATH is accepted and records the input clients inject.

--control-gui can share the window with --gdb-gui (see GDB): a machine stop answers whichever resumes are pending on both clients, a stop the control client did not request arrives as event.stopped, and reverse_step, reverse_frame, reverse_continue, last_writer, input.mouse_to, and state.load are refused while the GDB client’s continue is outstanding (pause first). Either client’s pause ends the other’s run with reason pause.

On start-up the server prints its endpoint and token to stderr as one line (copperline-control: listen=ADDR token=TOKEN proto=1). The token is a fresh random 128-bit value unless --control-token TOKEN pins one; a pinned token is visible in the process list, so prefer --control-info FILE, which writes the same details to a JSON file readable only by its owner:

{"listen": "127.0.0.1:52114", "token": "1f0c...", "proto": 1}

With --run PROG, either mode arms a one-shot loadseg stop for the program before the machine runs, the same break-at-entry --gdb has: the first continue (or, windowed, the boot already under way) stops with reason loadseg the moment the guest OS loads the program, before its first instruction, and segments.list then reports its hunks. A windowed session that reaches the stop before any client has attached parks there and tells the first client with event.stopped. The stop fires once; a client that wants every load arms its own loadseg break.

Client usage (copperline-ctl)

copperline-ctl is a command-line client for the protocol. It connects with --info FILE (the --control-info file) or --connect ADDR --token TOKEN, authenticates, and then sends one request or runs a REPL:

# Query status
copperline-ctl --info /tmp/ccp.json status

# Add a PC breakpoint
copperline-ctl --info /tmp/ccp.json break.add '{"kind": "pc", "addr": "0xFC0100"}'

# Resume execution (blocks until a breakpoint or stop event occurs)
copperline-ctl --info /tmp/ccp.json continue

# Interactive REPL session
copperline-ctl --info /tmp/ccp.json --repl

# Describe a save state (no session needed) and write out its thumbnail
copperline-ctl state-info /tmp/at120.clstate --thumbnail /tmp/at120.png

Replies print to stdout as one JSON object per line, and notifications (event.*) print as they arrive. A one-shot request exits with a nonzero status when the reply is a JSON-RPC error. The REPL reads one METHOD [JSON-PARAMS] per line and skips blank lines and # comments.

The other subcommands work without a session: state-info (see State snapshot files), diverge (below), profile, profile-report and size-report (see Per-frame profiling), and exe2adf (see Direct executable launching). copperline-ctl --help prints the usage of every mode.

A/B divergence finder

copperline-ctl diverge launches two headless sessions (two builds, or one build under two configs) and drives them in lockstep over this protocol, comparing the frame digest, the CPU registers and a server-side RAM digest at frame boundaries, then narrowing the first mismatch to the instruction and, for memory, to the byte. See A/B divergence finder.

Debug adapter

copperline-ctl --dap serves the Debug Adapter Protocol over the same bridge: an IDE debugs a program in the emulator with source-level breakpoints, variables and reverse stepping, while the control protocol underneath stays available from the Debug Console (!status, !beam.get).

MCP server

copperline-ctl --mcp exposes the control protocol over standard I/O as a Model Context Protocol server, so AI coding agents in clients such as Claude Code or Cursor can drive and inspect the emulator through tool calls. Each protocol method is a tool, and a few extra tools manage the session and its event queue.

# Unattached: the agent launches or attaches a session with session tools.
copperline-ctl --mcp

# Attached at startup to a running control server:
copperline-ctl --mcp --info /tmp/ccp.json
copperline-ctl --mcp --connect 127.0.0.1:7710 --token HEX

Claude Code registers it with one command:

claude mcp add copperline -- copperline-ctl --mcp

or via .mcp.json in a project:

{
  "mcpServers": {
    "copperline": {
      "command": "copperline-ctl",
      "args": ["--mcp"],
      "env": {"COPPERLINE_BIN": "/path/to/copperline"}
    }
  }
}

initialize returns an instructions summary of the workflow, and tools/list provides descriptions, JSON schemas, and parameter conventions for all tools.

Tool names

MCP tool names allow only [a-zA-Z0-9_-], so a method’s tool name replaces its dots with underscores (warp.get becomes warp_get, media.floppy.insert becomes media_floppy_insert, and capture.screenshot becomes capture_screenshot). Methods without dots (such as status or run_until) keep their names. Tool arguments are the method’s parameters (addresses accept hex strings or integers). A protocol error comes back as a tool result with isError: true carrying the error code and message. The bridge performs the handshake (hello, auth) itself; those methods are not tools.

Session tools

The bridge manages one active session at a time:

Blocking and wait_ms

Execution methods (continue, run_until, step, step_over, step_out, step_copper, step_frame) accept an optional wait_ms parameter. If the emulated machine does not halt within this time limit (in host milliseconds), the bridge automatically pauses execution and returns the stop event with bridge.paused_after_ms set. Without wait_ms, calls block until a breakpoint or stop condition is reached.

Events

The bridge queues the server’s notifications (see Streaming observability) as they arrive, up to 1,024; when the queue is full the oldest is dropped. events_next {"timeout_ms"} returns one event, waiting up to timeout_ms (default 1000, at most 600000), and events_drain returns everything queued. Both report the queue depth and the number of events dropped.

Screenshots

capture_screenshot returns the PNG image as an MCP image content block (image/png) alongside the text result. Without path, the bridge uses a temporary file and deletes it after reading. With path, the image is kept there (a relative path resolves against copperline-ctl’s working directory).

Protocol subset

MCP 2025-06-18 over stdio (newline-delimited JSON-RPC 2.0): supports initialize, notifications/initialized, ping, tools/list, and tools/call. initialize also accepts the 2025-03-26 and 2024-11-05 revisions and echoes the client’s choice. Malformed requests get the standard JSON-RPC error codes (-32700, -32600, -32601, -32602). Standard output carries protocol messages only; diagnostics go to stderr. The server handles one request at a time and exits at end of file on stdin.

Protocol overview

Errors use the JSON-RPC error object. Besides the standard codes (-32700 parse error, -32600 invalid request, -32601 unknown method, -32602 invalid params, -32603 internal error), the server returns:

CodeMeaning
-32000Authentication failed
-32001Not authenticated yet
-32002A resume is already pending
-32003Invalid state (for example, pause before repositioning the machine)
-32004Unsupported in this mode
-32005Host I/O error
-32006Reverse execution ran out of recorded history
-32007Not found

Example stop event payload

{
  "reason": "breakpoint",
  "detail": "Breakpoint at $FC0100",
  "pc": 16515328,
  "frame": 122,
  "vpos": 44,
  "hpos": 101,
  "cck": 8712345,
  "seconds": 2.456,
  "retired_instructions": 1745210
}

reason is one of breakpoint, watchpoint, reg_watch, mmio, beam_trap, copper_break, catch, task_catch, loadseg (a trap fired), step (a step verb finished), target (a run_until target was reached), pause, budget (a bounded run gave up), or double_fault (the CPU halted). reverse_step, reverse_frame and reverse_continue return a stop event with reason reverse. detail is a human-readable description.

A stop on an mmio break (reason mmio) adds access, the CPU access that tripped it: addr, size in bytes, value, access (read or write), the pc of the instruction that made it, and its own position (frame, cck, seconds, vpos, hpos). The stop coordinate itself is the instruction boundary after the access. Exception processing (the stack frame, the vector fetch) runs no instruction of its own: its accesses carry the last instruction retired before the exception, and a watch they trip stops before the handler’s first instruction.

{
  "reason": "mmio",
  "detail": "MMIO write $B8001D.B = $48 (pc $E593B0, f521 v259 h206, cck 37076570)",
  "access": {
    "addr": 12058653, "size": 1, "value": 72, "access": "write", "pc": 15045552,
    "position": {"frame": 521, "cck": 37076570, "seconds": 10.4532, "vpos": 259, "hpos": 206}
  },
  "pc": 15045556, "frame": 521, "vpos": 259, "hpos": 208, "cck": 37076572,
  "seconds": 10.4532, "retired_instructions": 5710489
}

Streaming observability

An authenticated client can subscribe to asynchronous event notifications:

events.subscribe {"events":["frame","serial","interrupt","media","debug","bus"],"frame_interval":50,"frame_digest":true}
events.subscribe {"events":["mmio","cd"],"mmio":[{"addr":"0xB80000","len":64,"access":"write"}]}
events.list
events.unsubscribe {"events":["serial"]}

Subscriptions belong to the connection and add up: each events.subscribe adds its families to the active set, and events.unsubscribe without events drops them all. events.list reports the active families, the settings, the mmio ranges, and the queue limits. The server samples events at its deterministic command and frame boundaries. Every notification carries dropped_notifications, the count the connection’s bounded outbound queue has lost.

Event types

Command reference summary

Session management

Execution control

The resume verbs below block until the machine stops and reply with the stop event. Each accepts collect, an array of {"method", "params"} read-only inspection requests (status, regs.get, mem.read, mem.digest, disasm, custom.read, beam.get, capture.digest, and the other queries) evaluated at the stop and returned in the stop event’s collect array, each as {"ok": RESULT} or {"err": {"code", "message"}}, so one round trip both runs and inspects. A second resume while one is pending is refused.

Speed

Reverse execution

These verbs need reverse execution armed (tt_armed in status), which both servers do by default. A reverse step that runs past the retained history fails with code -32006.

State inspection and modification

Windowed UI

Diagnostics and profiling

Instruction profiles retain at most 262,144 pending samples. profile.status and profile.stop report samples_buffer_limit and cumulative samples_dropped; a nonzero count marks an incomplete instruction capture. See profiling limits for overflow behavior and debugger stepping.

Breakpoints and traps

Input injection

Injected input goes through the same path as live and scripted input, so it is journaled for reverse execution and --record-input. at_seconds takes an absolute emulated time; an absent or past time applies the input now.

Media management

State snapshot files

The same card is printed without a session by copperline-ctl state-info STATE.clstate [--thumbnail FILE.png], as pretty-printed JSON with the same fields.

Framebuffer capture

Streaming events