Quickstart Guide

Five minutes for the human, then the agent does the rest.

For Humans (5 Minutes)

  1. Sign in with Google, GitHub, or email.
  2. Generate an API key on the dashboard. Copy it — it is shown exactly once.
  3. Wait for beta activation (~24 h). The same key starts working; no regeneration needed.
  4. export VIRTMCU_API_KEY="vtmcu_..."
  5. Configure your agent. Add the VirtMCU MCP server to your agent (e.g. Claude Code, Cursor, or LangGraph) using the session URL and your API key.

For Agents (Full Lifecycle)

# 0. Everything needs your token:
H='Authorization: Bearer '"$VIRTMCU_API_KEY"

# 1. Pick a world (multi-node topologies: UART pairs, CAN pairs, cross-vendor...)
curl -s https://api.virtmcu.com/v1/worlds -H "$H"

# 2. Create a session (demo firmware, or bring your own ELFs as base64)
curl -s -X POST https://api.virtmcu.com/v1/sessions \
  -H "$H" -H "Content-Type: application/json" \
  -d '{"world":"st_usart_pair","use_demo_firmware":true}'
# -> {"session_id":"...","mcp_url":"https://api.virtmcu.com/v1/sessions/<id>/mcp",...}

# 3. Connect your MCP client (Streamable HTTP) to mcp_url, then:
#    initialize -> tools/list -> tools/call
#    e.g. start_node(0), start_node(1),
#         run_until {"console":{"node_id":"0","contains":"READY"}},
#         read_registers(0), read_link(...), read_events()

# 4. Tear down when done
curl -s -X DELETE https://api.virtmcu.com/v1/sessions/<id> -H "$H"

Machine-readable onboarding: /llms.txt and /agents.json. No token? Ask your human (see the note on /pricing).

MCP Tool Reference

The live source of truth is tools/list on an open session.

TOOLDESCRIPTION
list_mcu_typesWhat MCUs/peripherals can I instantiate?
list_link_typesWhat inter-node links can I wire?
list_svdsWhich vendor register maps are vendored?
list_nodesSee the running nodes
get_topologyGet the wiring graph
upload_firmwareFlash a node
start_nodeRun the node
run_untilWait for a breakpoint hit
read_consoleDrain console output
get_timeRead the simulation clock
read_registersDump CPU registers
read_memoryInspect guest memory
read_faultDecode a Cortex-M fault
set_breakpointArm a software breakpoint
resumeResume after a breakpoint
write_consoleSend console input
read_linkObserve a GPIO pin frame
inject_frameDrive a GPIO input pin
read_eventsRead the causal event log
reset_nodeReset a node to its reset vector
request_mcuRequest an MCU we don't have
request_linkRequest a link protocol
request_svdRequest a vendored SVD
report_issueReport a fidelity gap
suggest_featureSuggest a capability
ask_supportAsk an in-band support question

MCP Few-Shot Examples

Real JSON-RPC payloads your agent will use, dynamically synced from the VirtMCU engine contract.

Scenario: handshake

initializeOpen the MCP session

Every MCP session starts with an initialize handshake. The server replies with its protocolVersion and serverInfo.

Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "agent",
      "version": "1"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": {
      "name": "virtmcu",
      "version": "1"
    },
    "capabilities": {
      "tools": {}
    }
  }
}
tools/listDiscover the tools

List every callable tool with its JSON input schema. An agent should call this once and cache it.

Request
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}
Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "list_nodes",
        "description": "…"
      }
    ]
  }
}

Scenario: discover

list_mcu_typesWhat MCUs/peripherals can I instantiate?

Lists the supported MCU cores and SVD-derived peripheral device types. The peripheral rows are derived from the engine's PeriphType registry, so newly-shipped blocks (e.g. the Ethernet/GMAC MACs) appear here automatically.

Request
{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "list_mcu_types",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Supported MCU/peripheral types:\n- cortex-m0/m3/m4/m7/m33/m55: ARM Cortex-M cores (arm-generic-fdt, M-profile)\n- cortex-a15: ARM Cortex-A15 (A-profile)\n- riscv32: RISC-V RV32 core (riscv32-softmmu)\n- st-usart: STM32 G4 USART — SVD-derived UART (STM32G474)\n- st-fdcan: STM32 FDCAN / Bosch M_CAN — SVD-derived CAN-FD (STM32G474)\n- st-eth: STM32H743 Ethernet — Synopsys DWC EQOS MAC, SVD-derived (STM32H743)\n- nxp-enet: NXP i.MX RT1062 Ethernet — Freescale FEC MAC, SVD-derived (MIMXRT1062)\n- nxp-s32k3-gmac: NXP S32K344 GMAC — Synopsys DWC EQOS MAC, SVD-derived (SDV/SOAFEE MCU)\n- s32k1xx-flexcan: NXP FlexCAN mailbox — SVD-derived CAN-FD (S32K144 CAN0)\n- ieee802154 / nrf52840-radio / arm-cc310 / gpio / nrf52-uarte / flexray …\n"
      }
    ]
  }
}
list_link_typesWhat inter-node links can I wire?

Lists the supported link protocols for multi-node worlds.

Request
{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "list_link_types",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Supported link types:\n- uart: inter-node UART serial link …\n- can: CAN / CAN-FD …\n- ieee802154: IEEE 802.15.4 radio …\n"
      }
    ]
  }
}
list_svdsWhich vendor register maps are vendored?

Lists the vendored CMSIS-SVD register maps. The list is generated at build time from assets/svd/vendor/**, so a newly-vendored SVD appears automatically.

Request
{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "list_svds",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 12,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Vendored SVD register maps:\n- MIMXRT1062_ENET: MIMXRT1062 ENET CMSIS-SVD (NXP, ENET peripheral extracted)\n- S32K144: S32K144 CMSIS-SVD (NXP / Freescale Semiconductor)\n- STM32G474: STM32G474 CMSIS-SVD (STMicroelectronics)\n- STM32H743: STM32H743 CMSIS-SVD (STMicroelectronics, via modm-io redistribution)\n- nrf52840: nRF52840 CMSIS-SVD (Nordic Semiconductor)\n"
      }
    ]
  }
}

Scenario: boot and run

list_nodesSee the running nodes

List the active simulated nodes and their links. Use this first to learn node_ids and link_ids.

Request
{
  "jsonrpc": "2.0",
  "id": 20,
  "method": "tools/call",
  "params": {
    "name": "list_nodes",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 20,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "node 0: cortex-m4 (links: sim/uartlink/0)\nnode 1: cortex-m4 (links: sim/uartlink/0)"
      }
    ]
  }
}
get_topologyGet the wiring graph

The full nodes+links graph (who is wired to whom), as structured data.

Request
{
  "jsonrpc": "2.0",
  "id": 21,
  "method": "tools/call",
  "params": {
    "name": "get_topology",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 21,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"nodes\":[…],\"links\":[…]}"
      }
    ]
  }
}
upload_firmwareFlash a node

Upload an ELF for a node (hex-encoded bytes, or a local path on the CLI). The engine validates the ELF magic and persists it as the launch artifact. Pairs with start_node.

Request
{
  "jsonrpc": "2.0",
  "id": 22,
  "method": "tools/call",
  "params": {
    "name": "upload_firmware",
    "arguments": {
      "node_id": 0,
      "firmware_hex": "7f454c46010101000000000000000000"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 22,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "uploaded firmware for node 0 (16 bytes); call start_node to run it"
      }
    ]
  }
}
start_nodeRun the node

Begin/resume execution (QMP cont) and release the clock-barrier hold — the unfreeze primitive for a node launched frozen.

Request
{
  "jsonrpc": "2.0",
  "id": 23,
  "method": "tools/call",
  "params": {
    "name": "start_node",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 23,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "node 0 started"
      }
    ]
  }
}
run_untilWait for a console marker

Advance virtual time until a condition holds. Conditions: events_contains, vtime_at_least, console {node_id, contains}, or link {link_id, contains}. This is the deterministic 'wait for output' primitive.

Request
{
  "jsonrpc": "2.0",
  "id": 24,
  "method": "tools/call",
  "params": {
    "name": "run_until",
    "arguments": {
      "console": {
        "node_id": 0,
        "contains": "Running LPUART example"
      }
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 24,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "condition met at vtime 1200000 ns"
      }
    ]
  }
}
read_consoleDrain console output

Read and drain a node's buffered guest->host console output since the last read.

Request
{
  "jsonrpc": "2.0",
  "id": 25,
  "method": "tools/call",
  "params": {
    "name": "read_console",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 25,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Running LPUART example\r\nInput character to echo...\r\n>"
      }
    ]
  }
}
get_timeRead the simulation clock

The current virtual time in nanoseconds (latest observed vtime). Virtual time is deterministic and independent of wall-clock.

Request
{
  "jsonrpc": "2.0",
  "id": 26,
  "method": "tools/call",
  "params": {
    "name": "get_time",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 26,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "vtime_ns: 1200000"
      }
    ]
  }
}

Scenario: inspect state

read_registersDump CPU registers

Read a node's CPU registers via QMP (a deterministic snapshot at the current vtime).

Request
{
  "jsonrpc": "2.0",
  "id": 30,
  "method": "tools/call",
  "params": {
    "name": "read_registers",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 30,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "R00=00000000 R01=4006b000 … PC=00000410 …"
      }
    ]
  }
}
read_memoryInspect guest memory

Read a guest physical memory range (hex address + length, capped at 1 MiB/call). Useful to read fault-status registers (CFSR @ 0xE000ED28) or an SRAM capture buffer.

Request
{
  "jsonrpc": "2.0",
  "id": 31,
  "method": "tools/call",
  "params": {
    "name": "read_memory",
    "arguments": {
      "node_id": 0,
      "address": "0xE000ED28",
      "length": 4
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 31,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "0xe000ed28: 00 00 00 00"
      }
    ]
  }
}
read_faultDecode a Cortex-M fault

After a crash, decode WHY the firmware faulted (the SCB fault-status registers) AND WHERE (the faulting PC, recovered from the exception stack frame) — the agent-usable HardFault inspection (no breakpoint/gdb). Pair with a fault-handler marker + run_until, then read_fault + read_registers.

Request
{
  "jsonrpc": "2.0",
  "id": 32,
  "method": "tools/call",
  "params": {
    "name": "read_fault",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 32,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Fault detected (CFSR=0x00000082 HFSR=0x40000000): DACCVIOL: data access violation (MPU); FORCED: escalated to HardFault (a configurable fault fired). Faulting address = 0x20004000\nFaulting PC = 0x08000420 (recovered from the MSP exception frame at SP=0x2000ffe0); stacked LR=0x08000311, xPSR=0x61000000."
      }
    ]
  }
}

Scenario: breakpoint

set_breakpointArm a software breakpoint

Arm a deterministic breakpoint at a PC (use the symbol's address from your ELF; addr=0 disarms). The node halts AT the next quantum boundary — virtual time stays stable and the PDES barrier never deadlocks (unlike gdb/QMP stop). Arm it before the firmware reaches the address; pair with run_until {breakpoint}, then read_fault / read_registers, then resume.

Request
{
  "jsonrpc": "2.0",
  "id": 51,
  "method": "tools/call",
  "params": {
    "name": "set_breakpoint",
    "arguments": {
      "node_id": 0,
      "addr": 134218272
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 51,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "node 0 breakpoint armed at 0x8000420"
      }
    ]
  }
}
run_untilWait for a breakpoint hit

Advance virtual time until a node halts at a software breakpoint (see set_breakpoint). Optional {node_id} filter. The node pauses at the quantum boundary with vtime stable; inspect with read_registers / read_fault, then resume.

Request
{
  "jsonrpc": "2.0",
  "id": 52,
  "method": "tools/call",
  "params": {
    "name": "run_until",
    "arguments": {
      "breakpoint": {
        "node_id": 0
      }
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 52,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "condition met"
      }
    ]
  }
}
resumeResume after a breakpoint

Release a breakpoint pause so the node resumes executing under virtual time — the counterpart to set_breakpoint + run_until {breakpoint}.

Request
{
  "jsonrpc": "2.0",
  "id": 53,
  "method": "tools/call",
  "params": {
    "name": "resume",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 53,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "node 0 resumed"
      }
    ]
  }
}

Scenario: console io

write_consoleSend console input

Write host->guest bytes to a node's console (the passthrough boundary; no vtime, routed immediately).

Request
{
  "jsonrpc": "2.0",
  "id": 40,
  "method": "tools/call",
  "params": {
    "name": "write_console",
    "arguments": {
      "node_id": 0,
      "data": "S"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 40,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "wrote 1 byte to node 0 console"
      }
    ]
  }
}

Scenario: wire io

read_linkTap inter-node frames

Frames seen crossing inter-node links (the wiretap tap): src node, vtime, seq, raw bytes (hex). Optional filters: link_id, since_vtime.

Request
{
  "jsonrpc": "2.0",
  "id": 41,
  "method": "tools/call",
  "params": {
    "name": "read_link",
    "arguments": {
      "link_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 41,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"src_node_id\":0,\"delivery_vtime_ns\":1200000,\"sequence_number\":1,\"payload\":\"52756e6e696e67\"}]"
      }
    ]
  }
}
inject_frameInject a frame on a link

Inject a frame onto an inter-node PDES link, scheduled at a virtual time (pass vtime_ns verbatim for reproducible scenarios; omit/0 for as-soon-as-possible). A past vtime is rejected, never crashing the sim. Use write_console for console input.

Request
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "inject_frame",
    "arguments": {
      "link_id": 0,
      "data": "deadbeef",
      "vtime_ns": 2000000
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "injected 4 bytes on link 0 at vtime 2000000 ns"
      }
    ]
  }
}
read_eventsRead the causal event log

The global vtime-ordered event log (link routes + lifecycle). Optional filters: kind, since_vtime, node (src), link_id.

Request
{
  "jsonrpc": "2.0",
  "id": 43,
  "method": "tools/call",
  "params": {
    "name": "read_events",
    "arguments": {
      "kind": "pdes_route",
      "since_vtime": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 43,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"kind\":\"pdes_route\",\"vtime_ns\":1200000,\"summary\":\"node 0 -> link 0 (7 bytes)\"}]"
      }
    ]
  }
}

Scenario: lifecycle

reset_nodeReset a node to its reset vector

Relaunch a node's firmware from the reset vector (virtual time rewinds to 0); the determinism-safe restart — the node re-joins as a fresh boot, reusing the last-uploaded firmware.

Request
{
  "jsonrpc": "2.0",
  "id": 50,
  "method": "tools/call",
  "params": {
    "name": "reset_node",
    "arguments": {
      "node_id": 0
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 50,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "node 0 reset; rejoined at vtime 0"
      }
    ]
  }
}

Scenario: feedback

request_mcuRequest an MCU we don't have

Agents are our customers — requesting a part is a prioritization signal. Recorded to the operator's feedback sink.

Request
{
  "jsonrpc": "2.0",
  "id": 60,
  "method": "tools/call",
  "params": {
    "name": "request_mcu",
    "arguments": {
      "vendor": "Nordic",
      "part": "nRF52840",
      "why": "BLE + Thread firmware bring-up"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 60,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recorded request_mcu — thank you; agent requests prioritize our roadmap. (…)"
      }
    ]
  }
}
request_linkRequest a link protocol

Request an inter-node protocol you need.

Request
{
  "jsonrpc": "2.0",
  "id": 61,
  "method": "tools/call",
  "params": {
    "name": "request_link",
    "arguments": {
      "protocol": "SPI",
      "why": "sensor bus bring-up"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 61,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recorded request_link — thank you …"
      }
    ]
  }
}
request_svdRequest a vendored SVD

Request a CMSIS-SVD register map to be vendored.

Request
{
  "jsonrpc": "2.0",
  "id": 62,
  "method": "tools/call",
  "params": {
    "name": "request_svd",
    "arguments": {
      "vendor": "Nordic",
      "part": "nRF52840"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 62,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recorded request_svd — thank you …"
      }
    ]
  }
}
report_issueReport a fidelity gap

Report a bug or fidelity gap, optionally with a deterministic repro (world + firmware + seed).

Request
{
  "jsonrpc": "2.0",
  "id": 63,
  "method": "tools/call",
  "params": {
    "name": "report_issue",
    "arguments": {
      "summary": "LPUART RDRF clears one byte early",
      "repro": "world=s32k144_lpuart seed=1"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 63,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recorded report_issue — thank you …"
      }
    ]
  }
}
suggest_featureSuggest a capability

Suggest a capability you wish existed.

Request
{
  "jsonrpc": "2.0",
  "id": 64,
  "method": "tools/call",
  "params": {
    "name": "suggest_feature",
    "arguments": {
      "summary": "set_breakpoint(symbol) + HardFault auto-trap"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 64,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Recorded suggest_feature — thank you …"
      }
    ]
  }
}
ask_supportAsk an in-band support question

Ask a support question; returns guidance + doc pointers (keyword-routed).

Request
{
  "jsonrpc": "2.0",
  "id": 65,
  "method": "tools/call",
  "params": {
    "name": "ask_support",
    "arguments": {
      "question": "How do I read console output from a node?"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 65,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Console: use write_console to send input and read_console to drain output …"
      }
    ]
  }
}

Scenario: gpio

inject_frameDrive a GPIO input pin

On the gpio_demo world the firmware's input port (IDR) is wired to link 1. Drive it by injecting a u32 little-endian pin word: to set IDR=0x00001234, send the 4 little-endian bytes 34120000 (NOT 00001234). The demo firmware spin-reads IDR and prints 'N0:idr=00001234' to the console once a nonzero word arrives (read it with read_console). Pass vtime_ns for a reproducible scenario; omit it (or 0) for as-soon-as-possible.

Request
{
  "jsonrpc": "2.0",
  "id": 51,
  "method": "tools/call",
  "params": {
    "name": "inject_frame",
    "arguments": {
      "link_id": 1,
      "data": "34120000",
      "vtime_ns": 5000000
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 51,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "injected at vtime 5000000 (link 1, seq 0, 4 bytes)"
      }
    ]
  }
}
read_linkObserve a GPIO pin frame

Tap link 1 with read_link to see the pin-state frames crossing the GPIO link on the gpio_demo world: the bytes you drove with inject_frame (your input edge) and the firmware's own ODR writes (its output edge) — each a u32 little-endian pin word. The observe stream buffers from the moment you first tap, so call read_link before advancing the clock to catch the firmware's boot-time ODR=0x0000ABCD edge (payload cdab0000).

Request
{
  "jsonrpc": "2.0",
  "id": 52,
  "method": "tools/call",
  "params": {
    "name": "read_link",
    "arguments": {
      "link_id": 1
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 52,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[vtime 5000000 link 1 src node 4294967294 seq 0] 4 bytes: 34120000"
      }
    ]
  }
}

Determinism

Same world + same ELFs + same injection timeline ⇒ bit-identical run. Every injected frame traverses the simulation's quantum barrier with an explicit virtual timestamp and canonical tie-breaking, so "flaky" firmware bugs stop being flaky here: a race condition that reproduces once reproduces forever, until you fix it — and the fix is provable by rerunning the identical timeline.