Five minutes for the human, then the agent does the rest.
export VIRTMCU_API_KEY="vtmcu_..."claude mcp add --transport http virtmcu <mcp_url> --header "Authorization: Bearer $VIRTMCU_API_KEY". LangGraph / Google ADK agents call the REST + MCP endpoints directly.# 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).
The live source of truth is tools/list on an open session.
| TOOL | DESCRIPTION |
|---|---|
| list_mcu_types | What MCUs/peripherals can I instantiate? |
| list_link_types | What inter-node links can I wire? |
| list_svds | Which vendor register maps are vendored? |
| list_nodes | See the running nodes |
| get_topology | Get the wiring graph |
| upload_firmware | Flash a node |
| start_node | Run the node |
| run_until | Wait for a console marker |
| read_console | Drain console output |
| get_time | Read the simulation clock |
| read_registers | Dump CPU registers |
| read_memory | Inspect guest memory |
| read_fault | Decode a Cortex-M fault |
| write_console | Send console input |
| read_link | Tap inter-node frames |
| inject_frame | Inject a frame on a link |
| read_events | Read the causal event log |
| reset_node | Reset a node to its reset vector |
| request_mcu | Request an MCU we don't have |
| request_link | Request a link protocol |
| request_svd | Request a vendored SVD |
| report_issue | Report a fidelity gap |
| suggest_feature | Suggest a capability |
| ask_support | Ask an in-band support question |
Real JSON-RPC payloads your agent will use, dynamically synced from the VirtMCU engine contract.
Every MCP session starts with an initialize handshake. The server replies with its protocolVersion and serverInfo.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "agent",
"version": "1"
}
}
}{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "virtmcu",
"version": "1"
},
"capabilities": {
"tools": {}
}
}
}List every callable tool with its JSON input schema. An agent should call this once and cache it.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_nodes",
"description": "…"
}
]
}
}Lists the supported MCU cores and SVD-derived peripheral device types.
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "list_mcu_types",
"arguments": {}
}
}{
"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"
}
]
}
}Lists the supported link protocols for multi-node worlds.
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "list_link_types",
"arguments": {}
}
}{
"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"
}
]
}
}Lists the CMSIS-SVD register maps the engine derives peripherals from.
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "list_svds",
"arguments": {}
}
}{
"jsonrpc": "2.0",
"id": 12,
"result": {
"content": [
{
"type": "text",
"text": "Vendored SVD register maps:\n- STM32G474: ST STM32G4 …\n- S32K144: NXP S32K144 …\n"
}
]
}
}List the active simulated nodes and their links. Use this first to learn node_ids and link_ids.
{
"jsonrpc": "2.0",
"id": 20,
"method": "tools/call",
"params": {
"name": "list_nodes",
"arguments": {}
}
}{
"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)"
}
]
}
}The full nodes+links graph (who is wired to whom), as structured data.
{
"jsonrpc": "2.0",
"id": 21,
"method": "tools/call",
"params": {
"name": "get_topology",
"arguments": {}
}
}{
"jsonrpc": "2.0",
"id": 21,
"result": {
"content": [
{
"type": "text",
"text": "{\"nodes\":[…],\"links\":[…]}"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 22,
"method": "tools/call",
"params": {
"name": "upload_firmware",
"arguments": {
"node_id": 0,
"firmware_hex": "7f454c46010101000000000000000000"
}
}
}{
"jsonrpc": "2.0",
"id": 22,
"result": {
"content": [
{
"type": "text",
"text": "uploaded firmware for node 0 (16 bytes); call start_node to run it"
}
]
}
}Begin/resume execution (QMP cont) and release the clock-barrier hold — the unfreeze primitive for a node launched frozen.
{
"jsonrpc": "2.0",
"id": 23,
"method": "tools/call",
"params": {
"name": "start_node",
"arguments": {
"node_id": 0
}
}
}{
"jsonrpc": "2.0",
"id": 23,
"result": {
"content": [
{
"type": "text",
"text": "node 0 started"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 24,
"method": "tools/call",
"params": {
"name": "run_until",
"arguments": {
"console": {
"node_id": 0,
"contains": "Running LPUART example"
}
}
}
}{
"jsonrpc": "2.0",
"id": 24,
"result": {
"content": [
{
"type": "text",
"text": "condition met at vtime 1200000 ns"
}
]
}
}Read and drain a node's buffered guest->host console output since the last read.
{
"jsonrpc": "2.0",
"id": 25,
"method": "tools/call",
"params": {
"name": "read_console",
"arguments": {
"node_id": 0
}
}
}{
"jsonrpc": "2.0",
"id": 25,
"result": {
"content": [
{
"type": "text",
"text": "Running LPUART example\r\nInput character to echo...\r\n>"
}
]
}
}The current virtual time in nanoseconds (latest observed vtime). Virtual time is deterministic and independent of wall-clock.
{
"jsonrpc": "2.0",
"id": 26,
"method": "tools/call",
"params": {
"name": "get_time",
"arguments": {}
}
}{
"jsonrpc": "2.0",
"id": 26,
"result": {
"content": [
{
"type": "text",
"text": "vtime_ns: 1200000"
}
]
}
}Read a node's CPU registers via QMP (a deterministic snapshot at the current vtime).
{
"jsonrpc": "2.0",
"id": 30,
"method": "tools/call",
"params": {
"name": "read_registers",
"arguments": {
"node_id": 0
}
}
}{
"jsonrpc": "2.0",
"id": 30,
"result": {
"content": [
{
"type": "text",
"text": "R00=00000000 R01=4006b000 … PC=00000410 …"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 31,
"method": "tools/call",
"params": {
"name": "read_memory",
"arguments": {
"node_id": 0,
"address": "0xE000ED28",
"length": 4
}
}
}{
"jsonrpc": "2.0",
"id": 31,
"result": {
"content": [
{
"type": "text",
"text": "0xe000ed28: 00 00 00 00"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 32,
"method": "tools/call",
"params": {
"name": "read_fault",
"arguments": {
"node_id": 0
}
}
}{
"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"
}
]
}
}Write host->guest bytes to a node's console (the passthrough boundary; no vtime, routed immediately).
{
"jsonrpc": "2.0",
"id": 40,
"method": "tools/call",
"params": {
"name": "write_console",
"arguments": {
"node_id": 0,
"data": "S"
}
}
}{
"jsonrpc": "2.0",
"id": 40,
"result": {
"content": [
{
"type": "text",
"text": "wrote 1 byte to node 0 console"
}
]
}
}Frames seen crossing inter-node links (the wiretap tap): src node, vtime, seq, raw bytes (hex). Optional filters: link_id, since_vtime.
{
"jsonrpc": "2.0",
"id": 41,
"method": "tools/call",
"params": {
"name": "read_link",
"arguments": {
"link_id": 0
}
}
}{
"jsonrpc": "2.0",
"id": 41,
"result": {
"content": [
{
"type": "text",
"text": "[{\"src_node_id\":0,\"delivery_vtime_ns\":1200000,\"sequence_number\":1,\"payload\":\"52756e6e696e67\"}]"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "inject_frame",
"arguments": {
"link_id": 0,
"data": "deadbeef",
"vtime_ns": 2000000
}
}
}{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{
"type": "text",
"text": "injected 4 bytes on link 0 at vtime 2000000 ns"
}
]
}
}The global vtime-ordered event log (link routes + lifecycle). Optional filters: kind, since_vtime, node (src), link_id.
{
"jsonrpc": "2.0",
"id": 43,
"method": "tools/call",
"params": {
"name": "read_events",
"arguments": {
"kind": "pdes_route",
"since_vtime": 0
}
}
}{
"jsonrpc": "2.0",
"id": 43,
"result": {
"content": [
{
"type": "text",
"text": "[{\"kind\":\"pdes_route\",\"vtime_ns\":1200000,\"summary\":\"node 0 -> link 0 (7 bytes)\"}]"
}
]
}
}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.
{
"jsonrpc": "2.0",
"id": 50,
"method": "tools/call",
"params": {
"name": "reset_node",
"arguments": {
"node_id": 0
}
}
}{
"jsonrpc": "2.0",
"id": 50,
"result": {
"content": [
{
"type": "text",
"text": "node 0 reset; rejoined at vtime 0"
}
]
}
}Agents are our customers — requesting a part is a prioritization signal. Recorded to the operator's feedback sink.
{
"jsonrpc": "2.0",
"id": 60,
"method": "tools/call",
"params": {
"name": "request_mcu",
"arguments": {
"vendor": "Nordic",
"part": "nRF52840",
"why": "BLE + Thread firmware bring-up"
}
}
}{
"jsonrpc": "2.0",
"id": 60,
"result": {
"content": [
{
"type": "text",
"text": "Recorded request_mcu — thank you; agent requests prioritize our roadmap. (…)"
}
]
}
}Request an inter-node protocol you need.
{
"jsonrpc": "2.0",
"id": 61,
"method": "tools/call",
"params": {
"name": "request_link",
"arguments": {
"protocol": "SPI",
"why": "sensor bus bring-up"
}
}
}{
"jsonrpc": "2.0",
"id": 61,
"result": {
"content": [
{
"type": "text",
"text": "Recorded request_link — thank you …"
}
]
}
}Request a CMSIS-SVD register map to be vendored.
{
"jsonrpc": "2.0",
"id": 62,
"method": "tools/call",
"params": {
"name": "request_svd",
"arguments": {
"vendor": "Nordic",
"part": "nRF52840"
}
}
}{
"jsonrpc": "2.0",
"id": 62,
"result": {
"content": [
{
"type": "text",
"text": "Recorded request_svd — thank you …"
}
]
}
}Report a bug or fidelity gap, optionally with a deterministic repro (world + firmware + seed).
{
"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"
}
}
}{
"jsonrpc": "2.0",
"id": 63,
"result": {
"content": [
{
"type": "text",
"text": "Recorded report_issue — thank you …"
}
]
}
}Suggest a capability you wish existed.
{
"jsonrpc": "2.0",
"id": 64,
"method": "tools/call",
"params": {
"name": "suggest_feature",
"arguments": {
"summary": "set_breakpoint(symbol) + HardFault auto-trap"
}
}
}{
"jsonrpc": "2.0",
"id": 64,
"result": {
"content": [
{
"type": "text",
"text": "Recorded suggest_feature — thank you …"
}
]
}
}Ask a support question; returns guidance + doc pointers (keyword-routed).
{
"jsonrpc": "2.0",
"id": 65,
"method": "tools/call",
"params": {
"name": "ask_support",
"arguments": {
"question": "How do I read console output from a node?"
}
}
}{
"jsonrpc": "2.0",
"id": 65,
"result": {
"content": [
{
"type": "text",
"text": "Console: use write_console to send input and read_console to drain output …"
}
]
}
}run_until {"console":{"contains":"..."}}.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.