Quickstart

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. Hand it to your agent. For Claude Code, after the agent creates a session: claude mcp add --transport http virtmcu <mcp_url> --header "Authorization: Bearer $VIRTMCU_API_KEY". LangGraph / Google ADK agents call the REST + MCP endpoints directly.

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 console marker
read_consoleDrain console output
get_timeRead the simulation clock
read_registersDump CPU registers
read_memoryInspect guest memory
read_faultDecode a Cortex-M fault
write_consoleSend console input
read_linkTap inter-node frames
inject_frameInject a frame on a link
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.

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 …\n- st-usart: STM32 G4 USART …\n- s32k144-lpuart: NXP S32K144 LPUART …\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 CMSIS-SVD register maps the engine derives peripherals from.

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- STM32G474: ST STM32G4 …\n- S32K144: NXP S32K144 …\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 (and WHERE) from the SCB fault-status registers — 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"
      }
    ]
  }
}

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 …"
      }
    ]
  }
}

Honest Limits

  • No GPIO plane. Injectable today: link frames (UART/CAN/802.15.4 PSDU), console bytes, physics sensor values.
  • No breakpoints or HardFault trap yet. Workaround: have your fault handler print a marker, then run_until {"console":{"contains":"..."}}.
  • Bring prebuilt ELFs. There is no cloud compiler — build with arm-none-eabi-gcc on your side.
  • Max 10 concurrent sessions per account; sessions expire (default TTL 15 minutes).

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.