🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@labwired/mcp

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@labwired/mcp - npm Package Compare versions

Comparing version
0.1.1
to
0.6.0
+185
dist/boards.js
/**
* On-disk example-lab catalog for the high-level `labwired_list_boards` /
* `labwired_run` (a.k.a. the deprecated `labwired_run_lab`) tools.
*
* Each entry is an EXAMPLE LAB — a chip + opinionated wiring + optional demo
* firmware — backed by a real `core/examples/<dir>/system.yaml` on disk that the
* local `labwired` CLI consumes via `--system`.
*
* SINGLE SOURCE OF TRUTH: `@labwired/board-config` owns board/chip IDENTITY, and
* chip identity here is DERIVED from and drift-guarded against it — every lab
* names a board-config `chip` (the module throws at import if it names a chip
* board-config doesn't know), and a lab whose id is a canonical board inherits
* that board's `name`/`description`. The old hand-copied per-entry chip_family /
* chip_yaml / name / description are gone.
*
* What stays REPO-LOCAL (board-config does not model on-disk example labs): the
* list of labs, their `core/examples/<dir>/system.yaml` wiring, the on-disk
* `core/configs/chips/<chip>.yaml` descriptor path the CLI reads, the `arch`
* display label, and the demo-firmware path. Paths are resolved relative to a
* repo-root anchor discovered at runtime by `findRepoRoot()` (walks up from the
* MCP server's __dirname looking for `core/configs/chips/`; falls back to env
* `LABWIRED_REPO_ROOT`).
*/
import { readFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
import { BOARDS as BOARD_CONFIG_BOARDS, getBoard as getBoardConfigBoard, boardChipId } from '@labwired/board-config';
// The list itself is repo-local: these are on-disk example labs, which
// board-config deliberately does not model (it owns canonical boards, not labs).
// `stm32f103-blinky` IS a board-config board, so its name/description are omitted
// here and inherited from board-config — proving the single-source link.
const LAB_SPECS = [
{
id: 'stm32f103-blinky',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'demo-blinky',
demo_firmware: 'core/target/thumbv7m-none-eabi/release/demo-blinky',
},
{
id: 'ntc-thermistor-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'ntc-thermistor-lab',
name: 'NTC Thermistor',
description: 'Analog temperature sensor via Steinhart-Hart math.',
},
{
id: 'ssd1306-hello-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'ssd1306-hello-lab',
name: 'SSD1306 OLED',
description: '128×64 monochrome OLED — full SSD1306 mode machine and framebuffer.',
},
{
id: 'bme280-weather-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'bme280-weather-lab',
name: 'BME280 Weather',
description: 'Temperature, humidity, pressure over I²C.',
},
{
id: 'ili9341-tft-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'ili9341-tft-lab',
name: 'ILI9341 TFT Color',
description: '320×240 SPI TFT — model state machine, 16-bit framebuffer.',
},
{
id: 'epaper-tricolor-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'epaper-tricolor-lab',
name: 'E-Paper 2.9" Tri-color',
description: 'SSD1680 tri-color e-paper — two-plane composition, byte-identical to silicon.',
},
{
id: 'mpu6050-sensor-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'mpu6050-sensor-lab',
name: 'MPU6050 IMU',
description: '6-DoF accelerometer + gyroscope over I²C.',
},
{
id: 'adxl345-sensor-lab',
chip: 'stm32f103',
arch: 'ARM Cortex-M3',
example: 'adxl345-sensor-lab',
name: 'ADXL345 Sensor Lab',
description: '3-axis accelerometer over I²C.',
},
];
// Fail-loud drift guard: every lab's chip must be a real @labwired/board-config
// chip, so a chip id can never silently diverge from the single source of truth.
const KNOWN_CHIPS = new Set(BOARD_CONFIG_BOARDS.map((b) => b.chip));
for (const spec of LAB_SPECS) {
if (!KNOWN_CHIPS.has(spec.chip)) {
throw new Error(`boards.ts: lab "${spec.id}" references chip "${spec.chip}", which is not a ` +
`@labwired/board-config chip. Add it to board-config or fix the chip id.`);
}
}
/** Project a repo-local lab spec into a full catalog entry, sourcing identity
* from board-config where the lab id is a canonical board and resolving on-disk
* YAML paths by repo convention. */
function toEntry(spec) {
const canonical = getBoardConfigBoard(spec.id);
const chip = boardChipId(spec.chip);
return {
id: spec.id,
name: spec.name ?? canonical?.name ?? spec.id,
chip_family: chip,
arch: spec.arch,
description: spec.description ?? canonical?.description ?? '',
chip_yaml: `core/configs/chips/${chip}.yaml`,
system_yaml: `core/examples/${spec.example}/system.yaml`,
...(spec.demo_firmware ? { demo_firmware: spec.demo_firmware } : {}),
};
}
/** The example-lab catalog, derived from LAB_SPECS + board-config identity. */
export const BOARDS = LAB_SPECS.map(toEntry);
/** Try to find the repo root by walking up from this file until core/configs is found. */
function findRepoRoot() {
const fromEnv = process.env.LABWIRED_REPO_ROOT;
if (fromEnv && existsSync(join(fromEnv, 'core/configs/chips')))
return resolve(fromEnv);
let cursor = dirname(fileURLToPath(import.meta.url));
for (let i = 0; i < 8; i++) {
if (existsSync(join(cursor, 'core/configs/chips')))
return cursor;
const parent = dirname(cursor);
if (parent === cursor)
break;
cursor = parent;
}
return null;
}
const REPO_ROOT = findRepoRoot();
export function getBoard(id) {
return BOARDS.find((b) => b.id === id);
}
export async function readBoardYamls(board) {
if (!REPO_ROOT) {
throw new Error('Could not locate LabWired repo root. Set LABWIRED_REPO_ROOT env var to the absolute path of your labwired checkout (e.g. /home/you/Projects/labwired).');
}
const chipYaml = await readFile(join(REPO_ROOT, board.chip_yaml), 'utf-8');
const systemYaml = await readFile(join(REPO_ROOT, board.system_yaml), 'utf-8');
return { chipYaml, systemYaml };
}
/** Absolute on-disk path to the board's system YAML in the labwired repo. */
export function boardSystemYamlPath(board) {
if (!REPO_ROOT) {
throw new Error('Could not locate LabWired repo root. Set LABWIRED_REPO_ROOT env var.');
}
return join(REPO_ROOT, board.system_yaml);
}
/** Absolute on-disk path to the board's chip descriptor YAML. */
export function boardChipYamlPath(board) {
if (!REPO_ROOT) {
throw new Error('Could not locate LabWired repo root. Set LABWIRED_REPO_ROOT env var.');
}
return join(REPO_ROOT, board.chip_yaml);
}
export async function readDemoFirmware(board) {
if (!board.demo_firmware || !REPO_ROOT)
return null;
try {
return await readFile(join(REPO_ROOT, board.demo_firmware));
}
catch {
return null;
}
}
export function listBoards(filter) {
if (!filter)
return BOARDS;
const q = filter.toLowerCase();
return BOARDS.filter((b) => b.id.toLowerCase().includes(q) ||
b.name.toLowerCase().includes(q) ||
b.chip_family.toLowerCase().includes(q));
}
// Content-addressed firmware ref currency for the stdio MCP surface.
//
// The Lab document pins firmware by a `sha256:<hex>` content hash
// (`firmware.artifact.ref`) rather than carrying base64 bytes. This module
// resolves such a ref by fetching the raw ELF from the hosted blob endpoint and
// base64-encoding it, so it slots straight into the existing base64 firmware
// path shared by labwired_run / labwired_verify. One surface, ref-first; inline
// base64 keeps working unchanged.
import { isLabWiredArtifactRef, parseArtifactRef } from '@labwired/board-config';
/** Default hosted API base; overridable via LABWIRED_API for local/staging. */
export function defaultApiBase() {
return process.env.LABWIRED_API ?? 'https://api.labwired.com';
}
/**
* Resolve a `sha256:<hex>` firmware ref to base64 ELF bytes by fetching the raw
* (application/octet-stream) blob from `${apiBase}/v1/blobs/sha256/<hex>`.
* Throws on a malformed ref or a non-200 response.
*/
export async function resolveFirmwareRef(ref, apiBase) {
if (!isLabWiredArtifactRef(ref)) {
throw new Error(`invalid firmware_ref: ${JSON.stringify(ref)} (expected "sha256:<64 hex>")`);
}
const { hex } = parseArtifactRef(ref);
const base = (apiBase ?? defaultApiBase()).replace(/\/+$/, '');
const url = `${base}/v1/blobs/sha256/${hex}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`failed to resolve firmware_ref ${ref}: ${res.status} ${res.statusText} (${url})`);
}
const buf = await res.arrayBuffer();
return Buffer.from(buf).toString('base64');
}
/**
* Pull a firmware ref out of a tool's arguments: a top-level `firmware_ref`
* takes precedence, else a lab document's `diagram.firmware.artifact.ref`.
* Returns undefined when neither is present.
*/
export function firmwareRefFromArgs(args) {
if (!args || typeof args !== 'object')
return undefined;
const a = args;
if (typeof a.firmware_ref === 'string' && a.firmware_ref.trim())
return a.firmware_ref.trim();
const diagram = a.diagram;
if (diagram && typeof diagram === 'object') {
const firmware = diagram.firmware;
if (firmware && typeof firmware === 'object') {
const artifact = firmware.artifact;
if (artifact && typeof artifact === 'object') {
const ref = artifact.ref;
if (typeof ref === 'string' && ref.trim())
return ref.trim();
}
}
}
return undefined;
}
export const AGENT_HARDWARE_LOOP_URI = 'labwired://guides/agent-hardware-loop';
export const AGENT_HARDWARE_LOOP_MIME = 'text/markdown';
const AGENT_HARDWARE_LOOP_TEXT = `# LabWired agent hardware loop
Use LabWired as a deterministic virtual hardware lab for firmware work.
1. Choose a board with \`labwired_list_boards\`.
2. Validate custom system YAML with \`labwired_validate_system\`.
3. Build or update the diagram and validate it with \`labwired_validate_diagram\`.
4. Compile firmware locally and pass the ELF as base64.
5. Run with \`labwired_run\` for a preconfigured board, or \`labwired_verify\` with an oracle for a gated proof.
6. Inspect snapshots with \`labwired_inspect_run\`.
7. Use \`labwired_fuzz\` when firmware exposes the fuzz contract.
8. Iterate until serial, GPIO, cycle counts, and stop reasons match the intended behavior.
The local MCP server shells out to the \`labwired\` CLI. Set \`LABWIRED_CLI\` or \`LABWIRED_REPO_ROOT\` when running outside a checkout.
`;
export const RESOURCES = [
{
uri: AGENT_HARDWARE_LOOP_URI,
name: 'labwired-agent-hardware-loop',
mimeType: AGENT_HARDWARE_LOOP_MIME,
description: 'Guide for using LabWired as an agent-driven virtual hardware lab.',
},
];
export function getResource(uri) {
if (uri !== AGENT_HARDWARE_LOOP_URI)
return null;
return {
uri,
mimeType: AGENT_HARDWARE_LOOP_MIME,
text: AGENT_HARDWARE_LOOP_TEXT,
};
}
import { rankTools as rankToolsShared } from '@labwired/board-config';
import { toolAnnotations, toolTitle } from './tool-metadata.js';
export const SEARCH_TOOLS_TOOL_NAME = 'labwired_search_tools';
export const AGENT_HARDWARE_LOOP_GUIDE_URI = 'labwired://guides/agent-hardware-loop';
export const LOCAL_AGENT_WORKFLOW = [
'labwired_list_boards',
'labwired_validate_diagram',
'labwired_compile_diagram',
'labwired_run',
'labwired_inspect_run',
];
export const SEARCH_TOOLS_TOOL = {
name: SEARCH_TOOLS_TOOL_NAME,
description: 'Search the LabWired MCP tool catalog by keyword and return relevant tool definitions. Use this to find board discovery, diagram validation, firmware run/verify, fuzzing, and snapshot tools.',
inputSchema: {
type: 'object',
required: ['query'],
properties: {
query: {
type: 'string',
description: 'Keywords describing the LabWired capability you need, e.g. "diagram validation" or "run firmware".',
},
limit: {
type: 'integer',
minimum: 1,
maximum: 25,
description: 'Maximum number of tool definitions to return. Defaults to 8.',
},
},
},
};
/** BM25 ranking lives in @labwired/board-config so stdio + hosted can't drift.
* Inject stdio's title/annotation lookups. */
export function rankTools(query, tools, limit = 8) {
return rankToolsShared(query, tools, { title: toolTitle, annotations: toolAnnotations }, limit);
}
/**
* In-memory snapshot store for `labwired_inspect_run`. Lets the agent fetch
* detailed state slices (registers, memory, peripheral) without re-shipping
* full state through `labwired_run`'s return.
*
* Lifecycle: store entries when a run completes, evict after TTL or when over
* MAX_ENTRIES (LRU). The MCP server process holds these; they don't survive a
* restart. Agents that want durable snapshots must capture full state in their
* own memory.
*/
const TTL_MS = 10 * 60 * 1000; // 10 minutes
const MAX_ENTRIES = 50;
const store = new Map();
/** ULID-ish opaque id; cryptographic strength not required. */
function newId() {
return ('snap_' +
Math.random().toString(36).slice(2, 10) +
Math.random().toString(36).slice(2, 10));
}
function evictExpired() {
const now = Date.now();
for (const [id, e] of store) {
if (now - e.snapshot.created_at > TTL_MS)
store.delete(id);
}
}
function evictLruIfNeeded() {
if (store.size <= MAX_ENTRIES)
return;
const sorted = [...store.entries()].sort((a, b) => a[1].last_access - b[1].last_access);
const toEvict = sorted.length - MAX_ENTRIES;
for (let i = 0; i < toEvict; i++)
store.delete(sorted[i][0]);
}
export function putSnapshot(snapshot) {
evictExpired();
const id = newId();
store.set(id, { snapshot, last_access: Date.now() });
evictLruIfNeeded();
return id;
}
export function getSnapshot(id) {
evictExpired();
const e = store.get(id);
if (!e)
return null;
e.last_access = Date.now();
return e.snapshot;
}
export function snapshotCount() {
return store.size;
}
const READ_ONLY_TOOLS = new Set([
'labwired_search_tools',
'labwired_catalog',
'labwired_validate_system',
'labwired_list_boards',
'labwired_inspect_run',
'labwired_validate_diagram',
]);
// labwired_define_component is intentionally absent from READ_ONLY_TOOLS:
// it writes a spec file to .labwired/components/.
const TITLES = {
labwired_catalog: 'Catalog',
labwired_validate_system: 'Validate System',
labwired_list_boards: 'List Boards',
labwired_run: 'Run',
labwired_verify: 'Verify',
labwired_fuzz: 'Fuzz Firmware',
labwired_inspect_run: 'Inspect Run',
labwired_validate_diagram: 'Validate Diagram',
labwired_search_tools: 'Search Tools',
labwired_define_component: 'Define Component',
labwired_compile_diagram: 'Compile Diagram',
labwired_ingest_svd: 'Ingest SVD',
};
export function toolTitle(name) {
return TITLES[name] ?? name
.replace(/^labwired_/, '')
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
export function toolAnnotations(name) {
const title = toolTitle(name);
const readOnly = READ_ONLY_TOOLS.has(name);
return {
title,
readOnlyHint: readOnly,
destructiveHint: false,
...(readOnly ? {} : { openWorldHint: true }),
};
}
export function decorateTools(tools) {
return tools.map((tool) => {
const title = toolTitle(tool.name);
return {
...tool,
title,
annotations: toolAnnotations(tool.name),
};
});
}
+289
-28

@@ -34,43 +34,100 @@ import { execFile } from 'node:child_process';

}
export async function runSimulation(opts) {
const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-'));
/**
* Run an Arduino-ESP32 firmware ELF against the hardware declared in the system
* manifest (the panel + its CS/DC pins are attached via `--system`). Boots the
* firmware and reports what the attached device actually did — e.g. an e-paper
* panel's refresh generation and how much ink reached it (the deterministic
* HARDWARE ORACLE). Uses the generic `arduino-esp32` path, which works for ANY
* symbol-bearing Arduino-ESP32 build (the agent's compiled firmware always has
* symbols) — NOT the `agentdeck` preset, whose hardcoded addresses fit only one
* firmware. (Profiles are a transitional thunk-selector; the end state is "load
* the binary for the chip and run real" with no profile at all.)
*/
export async function runDeviceCapture(opts) {
const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-dev-'));
try {
const firmwarePath = join(work, 'firmware.elf');
const systemPath = join(work, 'system.yaml');
const scriptPath = join(work, 'script.yaml');
const outputDir = join(work, 'out');
const outPath = join(work, 'snap.lwrs');
await writeFile(firmwarePath, Buffer.from(opts.firmwareBase64, 'base64'));
await writeFile(systemPath, opts.systemYaml);
await writeFile(scriptPath, opts.scriptYaml);
const steps = Math.min(opts.steps ?? 45_000_000, 50_000_000);
const { stderr, exitCode } = await runCli([
'snapshot', 'capture',
'--firmware', firmwarePath,
'--system', systemPath,
'--profile', opts.profile ?? 'arduino-esp32',
'--steps', String(steps),
'--output', outPath,
'--progress-every', '0',
]);
// "labwired-cli snapshot: panel (ssd1680|uc8151d) state — refresh_generation=N, power_on=B, black-plane non-FF bytes=M/T"
const m = stderr.match(/panel \((ssd1680|uc8151d)\) state[^\n]*refresh_generation=(\d+)[^\n]*black-plane non-FF bytes=(\d+)\/(\d+)/);
const refresh_generation = m ? parseInt(m[2], 10) : 0;
const ink_bytes = m ? parseInt(m[3], 10) : 0;
return {
exit_code: exitCode,
painted: refresh_generation >= 1 && ink_bytes > 0,
panel: m ? m[1] : null,
refresh_generation,
ink_bytes,
total_bytes: m ? parseInt(m[4], 10) : 0,
readout: m ? m[0] : '(no panel readout — firmware may not drive a display)',
stderr_excerpt: stderr.slice(-3000),
};
}
finally {
await rm(work, { recursive: true, force: true }).catch(() => { });
}
}
/**
* Coverage-guided fuzz a firmware ELF in the simulator. The firmware must follow
* the fuzz contract (RAM length+data buffer, a verdict word). Returns the
* distinct crashing inputs found — replayable on real silicon (HIL-confirm).
*/
export async function fuzzFirmware(opts) {
const work = await mkdtemp(join(tmpdir(), 'labwired-fuzz-'));
try {
const firmwarePath = join(work, 'firmware.elf');
const crashesPath = join(work, 'crashes.json');
await writeFile(firmwarePath, opts.firmware);
const args = [
'test',
'fuzz',
'--chip',
opts.chipYamlPath,
'--system',
opts.systemYamlPath,
'--firmware',
firmwarePath,
'--system',
systemPath,
'--script',
scriptPath,
'--output-dir',
outputDir,
'--no-uart-stdout',
'--collect',
String(opts.collect ?? 8),
'--crashes-out',
crashesPath,
'--max-iters',
String(opts.maxIters ?? 200_000),
];
if (opts.maxCycles) {
args.push('--max-cycles', String(opts.maxCycles));
}
const { stderr, exitCode } = await runCli(args);
let resultJson = null;
let uartLog = '';
if (opts.seed !== undefined)
args.push('--seed', String(opts.seed));
for (const h of opts.seedInputsHex ?? [])
args.push('--seed-input', h);
const c = opts.contract ?? {};
if (c.inputLenAddr)
args.push('--input-len-addr', c.inputLenAddr);
if (c.inputDataAddr)
args.push('--input-data-addr', c.inputDataAddr);
if (c.verdictAddr)
args.push('--verdict-addr', c.verdictAddr);
if (c.doneMagic)
args.push('--done-magic', c.doneMagic);
if (c.faultMagic)
args.push('--fault-magic', c.faultMagic);
const { stdout, stderr, exitCode } = await runCli(args);
let crashes = [];
try {
resultJson = JSON.parse(await readFile(join(outputDir, 'result.json'), 'utf-8'));
crashes = JSON.parse(await readFile(crashesPath, 'utf-8'));
}
catch {
/* keep null when missing */
/* no crashes file = no crash found */
}
try {
uartLog = await readFile(join(outputDir, 'uart.log'), 'utf-8');
}
catch {
/* keep empty when missing */
}
return { resultJson, uartLog, stderr, exitCode, outputDir };
return { exitCode, crashed: crashes.length > 0, crashes, stdout, stderr };
}

@@ -81,2 +138,60 @@ finally {

}
/** Builder base URL for HTTP endpoints (/compile, /run-example). */
function builderBaseUrl() {
const raw = process.env.LABWIRED_COMPILE_URL ?? process.env.LABWIRED_BUILDER_URL;
return raw ? raw.replace(/\/$/, '') : undefined;
}
export async function runBuildViaBuilder(opts) {
const base = builderBaseUrl();
if (!base) {
return {
ok: false,
passed: false,
exit_code: 1,
status: '',
stop_reason: '',
assertions: [],
verdict_lines: [],
uart_excerpt: '',
error: 'No builder URL configured. Set LABWIRED_COMPILE_URL (or LABWIRED_BUILDER_URL) ' +
'to the labwired-builder base URL so the build can run server-side.',
};
}
const secret = process.env.LABWIRED_BUILDER_SECRET ?? process.env.BUILDER_SECRET ?? '';
const headers = { 'content-type': 'application/json' };
if (secret)
headers['x-builder-secret'] = secret;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 600_000);
try {
const resp = await fetch(`${base}/run-build`, {
method: 'POST',
headers,
body: JSON.stringify({
firmware_base64: opts.firmwareBase64,
system_yaml: opts.systemYaml,
test_yaml: opts.testYaml,
}),
signal: controller.signal,
});
const data = (await resp.json());
return data;
}
catch (e) {
return {
ok: false,
passed: false,
exit_code: 1,
status: '',
stop_reason: '',
assertions: [],
verdict_lines: [],
uart_excerpt: '',
error: `builder /run-build unreachable: ${e instanceof Error ? e.message : String(e)}`,
};
}
finally {
clearTimeout(timer);
}
}
export async function validateSystem(systemYaml) {

@@ -110,1 +225,147 @@ const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-validate-'));

}
// ─── High-level lab runner ─────────────────────────────────────────────────
// `runLab` is the agent-friendly tool surface. The agent picks a board, hands
// us an ELF, says "run it for N cycles". We synthesize the test script YAML,
// invoke `labwired test`, read result.json + uart.log + the gpio trace, and
// pack a structured response.
const GPIO_EVENT_CAP = 10_000;
const UART_CAP_BYTES = 256 * 1024;
/**
* Render a `stimuli:` YAML block for test-script schema 1.2. serde_yaml 0.9
* spells the externally-tagged trigger with a YAML tag (`!after_cycles { … }`),
* matching how `faults:` write theirs — NOT a single-key map.
*/
export function renderStimuliBlock(stimuli) {
const lines = ['stimuli:'];
for (const s of stimuli) {
const target = s.component
? `{ component: ${JSON.stringify(s.component)}, channel: ${JSON.stringify(s.channel)} }`
: `{ channel: ${JSON.stringify(s.channel)} }`;
lines.push(` - target: ${target}`);
if (s.afterCycles && s.afterCycles > 0) {
lines.push(` trigger: !after_cycles { cycles: ${Math.floor(s.afterCycles)} }`);
}
lines.push(` value: ${s.value}`);
}
return lines.join('\n');
}
export async function runLab(opts) {
if (!opts.systemYamlPath && !opts.systemYaml) {
throw new Error('runLab: provide either systemYamlPath or systemYaml');
}
const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-lab-'));
try {
const firmwarePath = join(work, 'firmware.elf');
const scriptPath = join(work, 'script.yaml');
const outputDir = join(work, 'out');
await writeFile(firmwarePath, opts.firmware);
// Use the on-disk path when available so relative `chip:` / `descriptor:`
// refs in the system YAML resolve correctly. Fall back to writing inline
// content into the tempdir (which breaks relative refs).
let systemPath;
if (opts.systemYamlPath) {
systemPath = opts.systemYamlPath;
}
else {
systemPath = join(work, 'system.yaml');
await writeFile(systemPath, opts.systemYaml);
}
// Synthesize a minimal "run for N cycles" script. We assert nothing
// about stop_reason because only one stop reason can be true per run,
// and multiple expected_stop_reason assertions all AND together (the
// CLI's "normal stop" rule already treats max_cycles / max_steps / and
// no_progress as success at the exit-code level when assertions pass).
// `inputs:` is required by schema 1.0 even though `--firmware` /
// `--system` flags override it.
const maxCycles = opts.maxCycles ?? 10_000_000;
const hasStimuli = (opts.stimuli?.length ?? 0) > 0;
// Stimuli require schema 1.2. `no_progress_steps` would stop the run before
// an `after_cycles` stimulus fires (a driven sensor doesn't change the PC),
// so drop the stuck-detector whenever stimuli are present.
const scriptLines = [
`schema_version: "${hasStimuli ? '1.2' : '1.0'}"`,
'inputs:',
` firmware: ${JSON.stringify(firmwarePath)}`,
` system: ${JSON.stringify(systemPath)}`,
'limits:',
` max_steps: ${maxCycles}`, // required by schema
` max_cycles: ${maxCycles}`,
];
if (!hasStimuli)
scriptLines.push(` no_progress_steps: 1000`);
scriptLines.push('assertions: []');
if (hasStimuli)
scriptLines.push(renderStimuliBlock(opts.stimuli));
const script = scriptLines.join('\n') + '\n';
await writeFile(scriptPath, script);
const args = [
'test',
'--firmware', firmwarePath,
'--system', systemPath,
'--script', scriptPath,
'--output-dir', outputDir,
'--no-uart-stdout',
'--max-cycles', String(maxCycles),
];
const { stderr, exitCode } = await runCli(args);
// result.json + uart.log are always written by `test` mode (when the run starts)
let raw_result = null;
let serial_output = '';
try {
raw_result = JSON.parse(await readFile(join(outputDir, 'result.json'), 'utf-8'));
}
catch { /* keep null */ }
try {
serial_output = await readFile(join(outputDir, 'uart.log'), 'utf-8');
}
catch { /* keep empty */ }
const serial_truncated = serial_output.length > UART_CAP_BYTES;
if (serial_truncated)
serial_output = serial_output.slice(-UART_CAP_BYTES);
// Tease out structured fields from raw_result. Actual CLI schema:
// { stop_reason: "max_cycles"|"max_steps"|"no_progress"|...,
// cycles: number, cpu_state: { pc: number, ... }, ... }
let exit_reason;
let final_cycles;
let final_pc_hex;
if (raw_result && typeof raw_result === 'object') {
const r = raw_result;
if (typeof r.stop_reason === 'string')
exit_reason = r.stop_reason;
else if (typeof r.exit_reason === 'string')
exit_reason = r.exit_reason; // legacy
if (typeof r.cycles === 'number')
final_cycles = r.cycles;
else if (typeof r.total_cycles === 'number')
final_cycles = r.total_cycles; // legacy
const cpu = r.cpu_state;
if (cpu && typeof cpu === 'object' && typeof cpu.pc === 'number') {
const pc = cpu.pc;
final_pc_hex = `0x${pc.toString(16).toUpperCase().padStart(8, '0')}`;
}
else if (typeof r.final_pc === 'number') {
final_pc_hex = `0x${r.final_pc.toString(16).toUpperCase().padStart(8, '0')}`;
}
else if (typeof r.final_pc === 'string') {
final_pc_hex = r.final_pc;
}
}
return {
exit_code: exitCode,
exit_reason,
final_cycles,
final_pc_hex,
serial_output,
serial_truncated,
// GPIO capture is best-effort; the current `test` subcommand may not emit
// a gpio trace. Left as undefined until the CLI grows that flag.
gpio_events: undefined,
raw_result,
stderr,
};
}
finally {
await rm(work, { recursive: true, force: true }).catch(() => { });
}
}
void GPIO_EVENT_CAP; // reserved for when CLI emits gpio traces
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { listChips, runSimulation, validateSystem } from './cli.js';
import { mkdtemp, writeFile, rm, mkdir, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { listChips, validateSystem, runLab, fuzzFirmware, runCli, runDeviceCapture, runBuildViaBuilder } from './cli.js';
import { listBoards, getBoard, readBoardYamls, boardSystemYamlPath, boardChipYamlPath, } from './boards.js';
import { putSnapshot, getSnapshot } from './snapshots.js';
import { compile, composeDiagnostics, describeBoard, evaluateOracle, oracleClauseCount } from '@labwired/board-config';
import { AGENT_HARDWARE_LOOP_GUIDE_URI, LOCAL_AGENT_WORKFLOW, SEARCH_TOOLS_TOOL, SEARCH_TOOLS_TOOL_NAME, rankTools, } from './search-tools.js';
import { decorateTools } from './tool-metadata.js';
import { RESOURCES, getResource } from './resources.js';
import { resolveFirmwareRef, firmwareRefFromArgs } from './firmware-ref.js';
const SERVER_NAME = '@labwired/mcp';
const SERVER_VERSION = '0.1.1';
const SERVER_VERSION = '0.5.0';
const CatalogInput = z.object({

@@ -15,14 +25,26 @@ filter: z

});
const SimulateInput = z.object({
firmware_base64: z
const ValidateInput = z.object({
system_yaml: z.string().describe('Full contents of the System Manifest YAML to validate.'),
});
const ListBoardsInput = z.object({
filter: z.string().optional().describe('Substring filter on id / name / chip family.'),
});
const RunLabInput = z.object({
target: z
.string()
.describe('Base64-encoded ELF firmware image to load into the simulator.'),
system_yaml: z
.optional()
.describe('Board id / target from labwired_list_boards, e.g. "stm32f103-blinky". Canonical name for board_id.'),
board_id: z
.string()
.describe('Full contents of the System Manifest YAML (chip, peripherals, memory map). ' +
'See https://github.com/w1ne/labwired for schema.'),
script_yaml: z
.optional()
.describe('Deprecated alias for `target` (board id from labwired_list_boards).'),
elf_base64: z
.string()
.describe('Test script YAML: max_cycles, breakpoints, assertions on UART/registers/memory. ' +
'Example schema: core/docs/ci_test_runner.md.'),
.optional()
.describe('Base64-encoded ELF to run. Agent compiles locally and uploads. Optional if firmware_ref is given.'),
firmware_ref: z
.string()
.optional()
.describe('Content-addressed firmware ref (`sha256:<hex>` from labwired_compile_firmware). Resolved from ' +
'the hosted blob store — no base64 to carry. Used when elf_base64 is omitted.'),
max_cycles: z

@@ -32,11 +54,163 @@ .number()

.positive()
.max(100_000_000)
.optional()
.describe('Optional override for max cycles (takes precedence over script value).'),
.describe('Cycle budget (default 10M, hard cap 100M).'),
output: z
.enum(['summary', 'serial', 'peripherals', 'full'])
.optional()
.describe('Response-shape selector: summary | serial | peripherals | full. Default full.'),
stimuli: z
.array(z.object({
channel: z
.string()
.describe('SimInput channel key to drive: "x"/"y"/"z" (accel), "distance", "temperature", …'),
value: z.number().describe("Value in the channel's engineering unit (g, mm, °C, …)."),
after_cycles: z
.number()
.int()
.nonnegative()
.optional()
.describe('Fire this many cycles into the run; omit or 0 → at start.'),
component: z
.string()
.optional()
.describe('Advisory device-name hint; resolution is by unique channel key.'),
}))
.optional()
.describe('Declarative input stimuli: drive a sensor mid-run, then keep going — e.g. run to steady ' +
'state, then set the accelerometer X to 2 g and confirm the firmware reacts. Each reaches ' +
'the sim through the generic SimInput path (no per-device wiring). Requires a driven input ' +
'device on the board.'),
}).refine((v) => Boolean(v.target || v.board_id), {
message: 'Provide `target` (or the legacy `board_id`) — a board id from labwired_list_boards.',
});
const ValidateInput = z.object({
system_yaml: z.string().describe('Full contents of the System Manifest YAML to validate.'),
const DescribeBoardInput = z.object({
board: z
.string()
.describe('Any accepted board/chip spelling (canonical board id, playground id, or chip family key).'),
});
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// ─── Oracle / unmodeled-access helpers (mcp-unification §c) ─────────────────
// A minimal "execute and capture UART" test script for the oracle-block verify
// path: the run just boots and records serial so evaluateOracle can prove over
// it. Its inputs.firmware / inputs.system are overridden by the builder.
function synthCaptureTestYaml() {
return [
'schema_version: "1.0"',
'inputs:',
' firmware: firmware.elf',
' system: system.yaml',
'limits:',
' max_steps: 45000000',
' max_cycles: 45000000',
'assertions: []',
'',
].join('\n');
}
/** Best-effort scan for first-class unmodeled-access faults in a stdio CLI run.
* NOTE: the `labwired test` / `snapshot capture` CLI path that stdio uses does
* NOT emit a structured unmodeled-access event — only the builder HTTP `/run`
* path (services/labwired-builder/src/run.ts) does. So this only catches the
* free-text signal if the CLI happens to print it; absence here is a core/CLI
* gap, not proof the firmware touched nothing unmodeled. */
function scanUnmodeledAccess(...texts) {
const hits = [];
const re = /(unmodel+ed|unimplemented|unmapped)\b[^\n]*/gi;
for (const t of texts) {
if (!t)
continue;
for (const m of t.matchAll(re))
hits.push(m[0].trim());
}
return Array.from(new Set(hits)).slice(0, 20);
}
const FuzzInput = z.object({
board_id: z
.string()
.describe('Board id from labwired_list_boards (resolves chip + system YAML).'),
elf_base64: z
.string()
.describe('Base64-encoded fuzz-target ELF. Must follow the fuzz contract: read input ' +
'length+bytes from RAM, write DONE/FAULT to a verdict word. Agent compiles locally.'),
max_iters: z.number().int().positive().max(10_000_000).optional()
.describe('Max fuzzing iterations (default 200k).'),
seed: z.number().int().optional().describe('RNG seed — fuzzing is deterministic for a fixed seed.'),
collect: z.number().int().positive().max(64).optional()
.describe('Collect up to N distinct crashes (default 8).'),
seed_inputs_hex: z.array(z.string()).optional()
.describe('Seed inputs as hex byte strings, e.g. ["5000"]. Optional.'),
contract: z
.object({
input_len_addr: z.string().optional(),
input_data_addr: z.string().optional(),
verdict_addr: z.string().optional(),
done_magic: z.string().optional(),
fault_magic: z.string().optional(),
})
.optional()
.describe('Override the fuzz contract addresses/markers (hex). Defaults match the F103 fuzz target.'),
});
const InspectRunInput = z.object({
snapshot_id: z.string().describe('snapshot_id returned by labwired_run.'),
scope: z
.enum(['summary', 'serial', 'gpio', 'raw'])
.default('summary')
.describe('summary | serial | gpio | raw. Default summary.'),
});
const DefineComponentInput = z.object({
spec_yaml: z.string().describe('IrComponent spec as YAML'),
name: z.string().optional().describe('Override file name (defaults to spec name, kebab-cased)'),
});
const IngestSvdInput = z.object({
svd_content: z.string().describe('Full contents of the CMSIS-SVD XML file to ingest.'),
filter: z
.string()
.optional()
.describe('Comma-separated peripheral names to ingest (default: all peripherals).'),
});
const ValidateDiagramInput = z.object({
diagram: z
.object({
board: z.string(),
parts: z.array(z.object({ id: z.string(), type: z.string() }).passthrough()),
// wires is optional: v2 diagrams may use only connections/nets
wires: z.array(z.object({
from: z.object({ part: z.string(), pin: z.string() }),
to: z.object({ part: z.string(), pin: z.string() }),
})).optional(),
})
.passthrough()
.describe('Diagram JSON: { board, parts: [{id, type, ...}], wires?: [{from, to}] }.'),
});
// ─── Workspace root (mirrors boards.ts resolution logic) ──────────────────
// Checked in order: LABWIRED_REPO_ROOT env, then walk up from __dirname.
import { existsSync } from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
function resolveWorkspaceRoot() {
const fromEnv = process.env.LABWIRED_REPO_ROOT;
if (fromEnv)
return resolve(fromEnv);
let cursor = dirname(fileURLToPath(import.meta.url));
for (let i = 0; i < 8; i++) {
if (existsSync(join(cursor, 'core/configs/chips')))
return cursor;
const parent = dirname(cursor);
if (parent === cursor)
break;
cursor = parent;
}
// Fall back to cwd when repo root cannot be detected
return process.cwd();
}
/** Convert any string to a safe kebab-case file stem. */
function toKebabCase(s) {
return s
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
function localTools() {
return [
SEARCH_TOOLS_TOOL,
{

@@ -58,28 +232,120 @@ name: 'labwired_catalog',

{
name: 'labwired_simulate',
description: 'Run firmware against the LabWired deterministic simulator. ' +
'Identical inputs always produce identical outputs (cycle-accurate). ' +
'Returns result.json (assertions, exit status, cycles consumed) and the captured UART log. ' +
'Use this to verify firmware behaviour, reproduce bugs, or check whether a fix works.',
name: 'labwired_run_device',
description: 'Boot an Arduino-ESP32 firmware ELF on simulated ESP32 hardware and report what the attached ' +
'device actually did. The peripherals (e.g. an SSD1680/UC8151D e-paper panel and its CS/DC ' +
'pins) are attached from the system_yaml manifest. Works for ANY symbol-bearing Arduino-ESP32 ' +
'build (agent-compiled firmware always has symbols). Use this for real firmware driving a ' +
'display — labwired_verify runs the bare test path which does NOT boot Arduino-ESP32 and will ' +
'fault. Returns whether the panel painted, its refresh generation, and how much ink reached it ' +
'(the deterministic hardware oracle, byte-exact to silicon).',
inputSchema: {
type: 'object',
required: ['firmware_base64', 'system_yaml', 'script_yaml'],
required: ['firmware_base64', 'system_yaml'],
properties: {
firmware_base64: {
firmware_base64: { type: 'string', description: 'Base64-encoded Arduino-ESP32 (Xtensa) ELF.' },
system_yaml: { type: 'string', description: 'System Manifest YAML — chip + connected peripherals (e.g. an e-paper panel on spi3 with CS/DC pins).' },
steps: { type: 'integer', description: 'Cycles to run (default 45,000,000; max 50,000,000).' },
},
},
},
{
name: 'labwired_validate_system',
description: 'Validate a System Manifest YAML against the LabWired schema and confirm referenced ' +
'chip descriptors exist. Returns the validator stdout / stderr. ' +
'Use this before simulate() to catch schema errors fast.',
inputSchema: {
type: 'object',
required: ['system_yaml'],
properties: {
system_yaml: {
type: 'string',
description: 'Base64-encoded ELF firmware image to load into the simulator.',
description: 'Full contents of the System Manifest YAML to validate.',
},
system_yaml: {
},
},
},
{
name: 'labwired_list_boards',
description: 'List supported boards (chip + pre-wired peripherals + demo firmware). Higher-level ' +
'than labwired_catalog: each board has a complete simulation config you can run with ' +
'labwired_run. Returns id, name, chip family, arch, and description for each.',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'System Manifest YAML defining chip, peripherals, memory map. ' +
'See github.com/w1ne/labwired for schema.',
description: 'Substring filter on id / name / chip family.',
},
script_yaml: {
},
},
},
{
name: 'labwired_describe_board',
description: 'Describe a board/chip: its canonical id, chip family, runnable flag, toolchains, the full ' +
'pin table, per-bus (I2C/SPI/UART) default pins, and a starter LED pin. Use this FIRST for ' +
'discovery — it kills target-name guessing and tells you which GPIO to wire each bus to ' +
'before building a diagram or firmware. Accepts any spelling (canonical id, playground id, ' +
'or bare chip key); resolution is normalized. Free; no run required.',
inputSchema: {
type: 'object',
required: ['board'],
properties: {
board: {
type: 'string',
description: 'Test script YAML: max_cycles, breakpoints, assertions on UART / registers / memory.',
description: 'Any accepted board/chip spelling, e.g. "esp32c3-supermini" or "esp32c3".',
},
},
},
},
{
name: 'labwired_run',
description: 'Run a firmware ELF against a pre-configured board and OBSERVE what it did (serial, exit ' +
'reason, cycles). THE canonical execution tool. Pick a board with labwired_list_boards, ' +
"compile locally (we don't compile for you), then upload the ELF. Observational: it reports " +
'behaviour but never mints a "verified" verdict — use labwired_verify with an oracle for a ' +
'gated pass. Returns a snapshot_id for labwired_inspect_run. Deterministic. `output` selects ' +
'the response shape; `stimuli` drives a sensor mid-run.',
inputSchema: {
type: 'object',
properties: {
target: {
type: 'string',
description: 'Board id / target from labwired_list_boards (e.g. "stm32f103-blinky").',
},
board_id: {
type: 'string',
description: 'Deprecated alias for `target`.',
},
elf_base64: {
type: 'string',
description: 'Base64-encoded ELF binary. Agent compiles locally. Optional if firmware_ref is given.',
},
firmware_ref: {
type: 'string',
description: 'Content-addressed firmware ref (`sha256:<hex>` from labwired_compile_firmware); resolved ' +
'from the hosted blob store, so there is no base64 to carry. Used when elf_base64 is omitted.',
},
max_cycles: {
type: 'integer',
description: 'Optional override for max cycles.',
description: 'Cycle budget (default 10M, hard cap 100M).',
},
output: {
type: 'string',
enum: ['summary', 'serial', 'peripherals', 'full'],
description: 'Response-shape selector. Default full.',
},
stimuli: {
type: 'array',
description: 'Declarative input stimuli — drive a sensor mid-run, then keep going.',
items: {
type: 'object',
required: ['channel', 'value'],
properties: {
channel: { type: 'string' },
value: { type: 'number' },
after_cycles: { type: 'integer' },
component: { type: 'string' },
},
},
},
},

@@ -89,22 +355,246 @@ },

{
name: 'labwired_validate_system',
description: 'Validate a System Manifest YAML against the LabWired schema and confirm referenced ' +
'chip descriptors exist. Returns the validator stdout / stderr. ' +
'Use this before simulate() to catch schema errors fast.',
name: 'labwired_fuzz',
description: 'Coverage-guided fuzz a firmware ELF in the silicon-validated simulator and return the ' +
'crashing inputs. AFL-style edge coverage drives mutation; a crash is a CPU fault or the ' +
"firmware's FAULT marker. Because the sim is silicon-validated, crashes found here are " +
'replayable on real hardware (HIL-confirm) — silicon-true findings, not emulation false ' +
'positives. The target firmware must follow the fuzz contract (RAM length+data input ' +
'buffer, a verdict word with DONE/FAULT markers). Deterministic for a fixed seed. Returns ' +
'the distinct crashing inputs (as byte arrays) you can replay or minimize.',
inputSchema: {
type: 'object',
required: ['board_id', 'elf_base64'],
properties: {
board_id: {
type: 'string',
description: 'Board id from labwired_list_boards (resolves chip + system YAML).',
},
elf_base64: {
type: 'string',
description: 'Base64-encoded fuzz-target ELF following the fuzz contract. Agent compiles locally.',
},
max_iters: {
type: 'integer',
description: 'Max fuzzing iterations (default 200000).',
},
seed: {
type: 'integer',
description: 'RNG seed — fuzzing is deterministic for a fixed seed.',
},
collect: {
type: 'integer',
description: 'Collect up to N distinct crashes (default 8).',
},
seed_inputs_hex: {
type: 'array',
items: { type: 'string' },
description: 'Seed inputs as hex byte strings, e.g. ["5000"].',
},
contract: {
type: 'object',
description: 'Override the fuzz contract (hex strings). Defaults match the F103 fuzz target: ' +
'input_len_addr 0x20002800, input_data_addr 0x20002804, verdict_addr 0x20003000, ' +
'done_magic 0xC0DEF022, fault_magic 0xDEADFA17.',
properties: {
input_len_addr: { type: 'string' },
input_data_addr: { type: 'string' },
verdict_addr: { type: 'string' },
done_magic: { type: 'string' },
fault_magic: { type: 'string' },
},
},
},
},
},
{
name: 'labwired_inspect_run',
description: 'Retrieve a deeper slice of state from a prior labwired_run snapshot. Use scope=summary ' +
'for cycles + PC + exit reason, scope=serial for full UART transcript, scope=gpio for pin ' +
'transitions (when CLI emits them), scope=raw for the underlying result.json. Snapshots ' +
'expire after 10 min.',
inputSchema: {
type: 'object',
required: ['snapshot_id'],
properties: {
snapshot_id: {
type: 'string',
description: 'snapshot_id from labwired_run output.',
},
scope: {
type: 'string',
enum: ['summary', 'serial', 'gpio', 'raw'],
description: 'Slice to return. Default summary.',
},
},
},
},
{
name: 'labwired_validate_diagram',
description: 'Structurally validate a wired diagram BEFORE attempting to run it. Returns an array of ' +
'machine-readable diagnostics (severity, code, message, location, suggested fix) — much ' +
'friendlier than waiting for the simulator to fail at run time. Common codes: ' +
'PIN_NOT_ON_CHIP, PIN_LACKS_I2C, BOARDIO_NOT_TO_MCU, NO_MCU, COMPONENT_DANGLING. Use this ' +
'when building circuits programmatically; iterate until you get back an empty array.',
inputSchema: {
type: 'object',
required: ['diagram'],
properties: {
diagram: {
type: 'object',
description: 'Diagram JSON: { board: "stm32f103", parts: [{id, type}], wires: [{from:{part,pin}, to:{part,pin}}] }',
},
},
},
},
{
name: 'labwired_define_component',
description: 'Define a new off-chip device from a declarative IR spec (YAML). Validates the spec ' +
'(stable ICOMP_* diagnostic codes with hints), persists it under ' +
'.labwired/components/<name>.yaml, and returns the exact manifest external_devices ' +
'entry (type: ir, spec_path) needed to wire the device into a system manifest. ' +
'Use this to model any custom sensor, driver, or expander before simulation. ' +
'Keywords: define component, ir spec, custom device, sensor model.',
inputSchema: {
type: 'object',
required: ['spec_yaml'],
properties: {
spec_yaml: {
type: 'string',
description: 'IrComponent spec as YAML',
},
name: {
type: 'string',
description: 'Override file name (defaults to spec name, kebab-cased)',
},
},
},
},
{
name: 'labwired_compile_diagram',
description: 'Compile a wired diagram into a runnable LabWired System Manifest YAML. ' +
'Runs electrical rule checks (ERC) first — any error aborts compilation. ' +
'On success, persists the manifest to <repoRoot>/.labwired/boards/<name>.yaml ' +
'and returns the board_path, system_yaml, and any warnings. ' +
'Use this after labwired_validate_diagram to produce the manifest for labwired_verify. ' +
'Keywords: compile diagram, diagram to manifest, build board.',
inputSchema: {
type: 'object',
required: ['diagram'],
properties: {
diagram: {
type: 'object',
description: 'Diagram JSON: { board, parts: [{id, type, ...}], wires: [{from, to}] }',
},
name: {
type: 'string',
description: 'Optional name for the output file (defaults to board name). Kebab-cased.',
},
},
},
},
{
name: 'labwired_verify',
description: 'THE mandatory-oracle gate: run YOUR OWN build server-side and PROVE it worked. You SUPPLY ' +
'the compiled firmware ELF (base64) + the LabWired system manifest (YAML), plus a passed ' +
'behavioral proof — EITHER a raw `test_yaml` (schema "1.0" assertions, the reference power ' +
'path, evaluated by `labwired test` inside the container) OR an `oracle` block with at least ' +
'one clause (serial / display / registers / gpio), which is evaluated by the shared ' +
'evaluateOracle. Deterministic oracle run on the modeled hardware, not a self-reported claim. ' +
'A missing oracle AND missing test_yaml → ORACLE_REQUIRED (refuses to verify). ' +
'On the stdio path only `serial` (UART) and `display` clauses are observable; register/gpio ' +
'clauses need the hosted /run peripherals path and are flagged unevaluable. ' +
'Returns { ok, proven, verification, status, oracle_results[], assertions[], uart_excerpt }.',
inputSchema: {
type: 'object',
required: ['system_yaml'],
properties: {
firmware_base64: {
type: 'string',
description: 'Base64-encoded compiled firmware ELF for your build. Optional if firmware_ref is given.',
},
firmware_ref: {
type: 'string',
description: 'Content-addressed firmware ref (`sha256:<hex>` from labwired_compile_firmware); resolved ' +
'from the hosted blob store, so there is no base64 to carry. Used when firmware_base64 is omitted.',
},
system_yaml: {
type: 'string',
description: 'Full contents of the System Manifest YAML to validate.',
description: 'LabWired system manifest (YAML) describing the modeled hardware your firmware runs on.',
},
test_yaml: {
type: 'string',
description: 'Optional reference power path: LabWired test script (YAML, schema_version "1.0") with limits + assertions. ' +
'Its inputs.firmware / inputs.system are overridden by the supplied firmware + manifest.',
},
oracle: {
type: 'object',
description: 'Behavioral proof (at least one clause). { serial: [{contains|matches}], ' +
'display: {painted, min_ink_bytes, min_refresh_generation, panel}, ' +
'registers: [{peripheral, register, field?, equals, mask?}], gpio: [{pin, state}] }.',
properties: {
serial: { type: 'array', items: { type: 'object' } },
display: { type: 'object' },
registers: { type: 'array', items: { type: 'object' } },
gpio: { type: 'array', items: { type: 'object' } },
},
},
},
},
},
],
{
name: 'labwired_ingest_svd',
description: 'Ingest a CMSIS-SVD file into runnable declarative PeripheralDescriptor YAML — the ' +
'one-step path from a silicon-vendor SVD to a working chip, with no codegen and no ' +
'recompile. Writes descriptors under .labwired/peripherals/<name>.yaml and returns each ' +
'peripheral with its descriptor YAML, base address, and register count, plus a ' +
'paste-ready chip-yaml peripherals block (type: declarative) to drop straight into a ' +
'chip descriptor. Use this to model any MCU peripheral from its vendor SVD. ' +
'Keywords: svd import, declarative peripheral, chip from svd, register map.',
inputSchema: {
type: 'object',
required: ['svd_content'],
properties: {
svd_content: {
type: 'string',
description: 'Full CMSIS-SVD XML contents.',
},
filter: {
type: 'string',
description: 'Comma-separated peripheral names to ingest (default: all).',
},
},
},
},
];
}
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: decorateTools(localTools()),
}));
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: RESOURCES,
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resource = getResource(request.params.uri);
if (!resource)
throw new Error(`Unknown resource: ${request.params.uri}`);
return { contents: [resource] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === SEARCH_TOOLS_TOOL_NAME) {
const input = (args ?? {});
const query = typeof input.query === 'string' ? input.query : '';
const limit = typeof input.limit === 'number' && Number.isFinite(input.limit)
? Math.trunc(input.limit)
: 8;
return {
content: [{ type: 'text', text: JSON.stringify({
query,
guide_uri: AGENT_HARDWARE_LOOP_GUIDE_URI,
workflow: LOCAL_AGENT_WORKFLOW,
tools: rankTools(query, decorateTools(localTools()), limit),
}) }],
};
}
if (name === 'labwired_catalog') {

@@ -117,15 +607,290 @@ const { filter } = CatalogInput.parse(args ?? {});

}
if (name === 'labwired_simulate') {
const input = SimulateInput.parse(args ?? {});
const run = await runSimulation({
firmwareBase64: input.firmware_base64,
systemYaml: input.system_yaml,
scriptYaml: input.script_yaml,
if (name === 'labwired_run_device') {
const a = (args ?? {});
if (!a.firmware_base64 || !a.system_yaml) {
return { content: [{ type: 'text', text: JSON.stringify({ error: 'firmware_base64 and system_yaml are required' }) }], isError: true };
}
const run = await runDeviceCapture({ firmwareBase64: a.firmware_base64, systemYaml: a.system_yaml, steps: a.steps });
return {
content: [{ type: 'text', text: JSON.stringify(run, null, 2) }],
isError: !run.painted,
};
}
if (name === 'labwired_validate_system') {
const { system_yaml } = ValidateInput.parse(args ?? {});
const result = await validateSystem(system_yaml);
return {
content: [
{
type: 'text',
text: JSON.stringify({ exit_code: result.exitCode, stdout: result.stdout, stderr: result.stderr }, null, 2),
},
],
isError: result.exitCode !== 0,
};
}
if (name === 'labwired_list_boards') {
const { filter } = ListBoardsInput.parse(args ?? {});
const boards = listBoards(filter);
return {
content: [{ type: 'text', text: JSON.stringify({ boards }, null, 2) }],
};
}
if (name === 'labwired_describe_board') {
const { board } = DescribeBoardInput.parse(args ?? {});
const descriptor = describeBoard(board);
if (!descriptor) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'UNKNOWN_BOARD',
message: `No board/chip matches "${board}". Call labwired_list_boards for valid ids.`,
}, null, 2),
},
],
isError: true,
};
}
return { content: [{ type: 'text', text: JSON.stringify(descriptor, null, 2) }] };
}
if (name === 'labwired_run') {
const input = RunLabInput.parse(args ?? {});
const boardId = input.target ?? input.board_id;
const output = input.output ?? 'full';
const board = getBoard(boardId);
if (!board) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'INVALID_BOARD',
message: `Unknown target "${boardId}". Call labwired_list_boards to see available ids.`,
}, null, 2),
},
],
isError: true,
};
}
let yamls;
try {
yamls = await readBoardYamls(board);
}
catch (e) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'BOARD_YAMLS_UNAVAILABLE',
message: e instanceof Error ? e.message : String(e),
hint: 'The MCP server needs the labwired repo on disk to resolve board YAMLs. Set LABWIRED_REPO_ROOT if running outside the repo.',
}, null, 2),
},
],
isError: true,
};
}
// Ref-first firmware currency: inline elf_base64 wins; otherwise resolve a
// content-addressed `sha256:<hex>` ref (top-level firmware_ref or a lab
// document's diagram.firmware.artifact.ref) from the hosted blob store.
let elfBase64 = input.elf_base64;
if (!elfBase64) {
const ref = firmwareRefFromArgs(args);
if (!ref) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'FIRMWARE_REQUIRED',
message: 'Provide elf_base64 or firmware_ref (sha256:<hex> from labwired_compile_firmware).',
}, null, 2),
},
],
isError: true,
};
}
try {
elfBase64 = await resolveFirmwareRef(ref);
}
catch (e) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'FIRMWARE_REF_UNRESOLVED', message: e instanceof Error ? e.message : String(e) }, null, 2),
},
],
isError: true,
};
}
}
const firmware = Buffer.from(elfBase64, 'base64');
if (firmware.length < 4 || firmware.subarray(0, 4).toString('hex') !== '7f454c46') {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'INVALID_ELF',
message: 'Decoded firmware is not an ELF file (missing 0x7F454C46 magic).',
}, null, 2),
},
],
isError: true,
};
}
// Use absolute repo path for systemYaml so relative refs
// (chip:, descriptor:) inside it resolve to core/configs/...
const run = await runLab({
systemYamlPath: boardSystemYamlPath(board),
firmware,
maxCycles: input.max_cycles,
stimuli: input.stimuli?.map((s) => ({
channel: s.channel,
value: s.value,
afterCycles: s.after_cycles,
component: s.component,
})),
});
const snapshot_id = putSnapshot({
registers: undefined,
serial_output: run.serial_output,
gpio_events: run.gpio_events,
gpio_truncated: run.gpio_truncated,
final_pc_hex: run.final_pc_hex,
final_cycles: run.final_cycles,
raw_result: run.raw_result,
board_id: boardId,
created_at: Date.now(),
});
// Trim serial in the inline response — full transcript is in snapshot.
const SERIAL_INLINE_CAP = 8000;
const serialInline = run.serial_output.slice(-SERIAL_INLINE_CAP);
// Surface first-class unmodeled-access faults when the run path exposes
// them (fail-loud, never thunk). See scanUnmodeledAccess: the stdio CLI
// path does NOT emit a structured event, so this is a best-effort text
// scan and its absence is a core/CLI gap, not a clean bill of health.
const unmodeled_access = scanUnmodeledAccess(run.stderr, typeof run.raw_result === 'string' ? run.raw_result : JSON.stringify(run.raw_result ?? ''));
const full = {
ok: run.exit_code === 0,
snapshot_id,
target: boardId,
board_id: boardId,
exit_reason: run.exit_reason,
final_cycles: run.final_cycles,
final_pc_hex: run.final_pc_hex,
serial_tail: serialInline,
serial_truncated_in_response: run.serial_output.length > SERIAL_INLINE_CAP || run.serial_truncated,
stderr_excerpt: run.stderr.slice(0, 2000),
...(unmodeled_access.length ? { unmodeled_access } : {}),
};
// `output` selects the response shape. peripherals are not emitted by the
// stdio local run path — inspect_run reads gpio/serial from the snapshot.
let payload;
if (output === 'summary') {
const { serial_tail: _s, ...rest } = full;
payload = rest;
}
else if (output === 'serial') {
payload = { ok: full.ok, snapshot_id, target: boardId, serial_tail: serialInline, serial_truncated_in_response: full.serial_truncated_in_response };
}
else if (output === 'peripherals') {
payload = {
ok: full.ok,
snapshot_id,
target: boardId,
note: 'The stdio local run path does not emit a peripheral inspect block. ' +
'Use labwired_inspect_run (scope=gpio|serial) on the snapshot_id for state, ' +
'or the hosted /run peripherals path for a full inspect.',
...(unmodeled_access.length ? { unmodeled_access } : {}),
};
}
else {
payload = full;
}
return {
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
isError: run.exit_code !== 0,
};
}
if (name === 'labwired_fuzz') {
const input = FuzzInput.parse(args ?? {});
const board = getBoard(input.board_id);
if (!board) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'INVALID_BOARD',
message: `Unknown board_id "${input.board_id}". Call labwired_list_boards to see available ids.`,
}, null, 2),
},
],
isError: true,
};
}
const firmware = Buffer.from(input.elf_base64, 'base64');
if (firmware.length < 4 || firmware.subarray(0, 4).toString('hex') !== '7f454c46') {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'INVALID_ELF',
message: 'Decoded firmware is not an ELF file (missing 0x7F454C46 magic).',
}, null, 2),
},
],
isError: true,
};
}
let run;
try {
run = await fuzzFirmware({
chipYamlPath: boardChipYamlPath(board),
systemYamlPath: boardSystemYamlPath(board),
firmware,
maxIters: input.max_iters,
seed: input.seed,
collect: input.collect,
seedInputsHex: input.seed_inputs_hex,
contract: input.contract && {
inputLenAddr: input.contract.input_len_addr,
inputDataAddr: input.contract.input_data_addr,
verdictAddr: input.contract.verdict_addr,
doneMagic: input.contract.done_magic,
faultMagic: input.contract.fault_magic,
},
});
}
catch (e) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'FUZZ_FAILED',
message: e instanceof Error ? e.message : String(e),
hint: 'The MCP server needs the labwired repo on disk to resolve board YAMLs. Set LABWIRED_REPO_ROOT.',
}, null, 2),
},
],
isError: true,
};
}
const summary = {
exit_code: run.exitCode,
result: run.resultJson,
uart_log_excerpt: run.uartLog.slice(0, 4000),
uart_log_truncated: run.uartLog.length > 4000,
crashed: run.crashed,
crash_count: run.crashes.length,
// Crashes as hex strings for readability + raw byte arrays for replay.
crashes_hex: run.crashes.map((c) => c.map((b) => b.toString(16).padStart(2, '0')).join('')),
crashes: run.crashes,
note: run.crashed
? 'Crashes are silicon-true: replay any input on the F103 via the HIL-confirm harness to confirm.'
: 'No crash found within the iteration budget.',
stdout_excerpt: run.stdout.slice(0, 2000),
stderr_excerpt: run.stderr.slice(0, 2000),

@@ -135,8 +900,10 @@ };

content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
isError: run.exitCode !== 0,
// A found crash is a finding, not a tool error — surface it as content,
// not isError (which signals the tool itself failed).
isError: false,
};
}
if (name === 'labwired_validate_system') {
const { system_yaml } = ValidateInput.parse(args ?? {});
const result = await validateSystem(system_yaml);
if (name === 'labwired_validate_diagram') {
const { diagram } = ValidateDiagramInput.parse(args ?? {});
const result = composeDiagnostics(diagram);
return {

@@ -146,8 +913,391 @@ content: [

type: 'text',
text: JSON.stringify({ exit_code: result.exitCode, stdout: result.stdout, stderr: result.stderr }, null, 2),
text: JSON.stringify(result, null, 2),
},
],
isError: result.exitCode !== 0,
isError: result.error_count > 0,
};
}
if (name === 'labwired_compile_diagram') {
const input = (args ?? {});
const diagram = input.diagram;
const nameOverride = typeof input.name === 'string' ? input.name : undefined;
if (!diagram || typeof diagram !== 'object') {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', detail: 'diagram is required' }, null, 2) }],
isError: true,
};
}
const result = compile(diagram);
if (!result.ok) {
return {
content: [{ type: 'text', text: JSON.stringify({ ok: false, diagnostics: result.diagnostics }, null, 2) }],
isError: true,
};
}
// Persist to .labwired/boards/<name>.yaml
const diagramAny = diagram;
const boardName = typeof diagramAny.board === 'string' ? diagramAny.board : 'board';
const stem = nameOverride ? toKebabCase(nameOverride) : toKebabCase(boardName);
const fileName = `${stem}.yaml`;
const workspaceRoot = resolveWorkspaceRoot();
const boardsDir = join(workspaceRoot, '.labwired', 'boards');
await mkdir(boardsDir, { recursive: true });
const boardPath = join(boardsDir, fileName);
await writeFile(boardPath, result.systemYaml ?? '');
const absBoardPath = resolve(boardPath);
return {
content: [
{
type: 'text',
text: JSON.stringify({
ok: true,
board_path: absBoardPath,
system_yaml: result.systemYaml,
diagnostics: result.diagnostics,
}, null, 2),
},
],
};
}
if (name === 'labwired_define_component') {
const { spec_yaml, name: nameOverride } = DefineComponentInput.parse(args ?? {});
// Write spec to a temp file for CLI validation
const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-comp-'));
let report;
try {
const tmpSpec = join(work, 'spec.yaml');
await writeFile(tmpSpec, spec_yaml);
const { stdout, exitCode } = await runCli(['asset', 'validate-component', tmpSpec, '--json']);
try {
report = JSON.parse(stdout);
}
catch {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'CLI_PARSE_ERROR', stdout, exit_code: exitCode }, null, 2) }],
isError: true,
};
}
}
finally {
await rm(work, { recursive: true, force: true }).catch(() => { });
}
if (!report.ok) {
return {
content: [{ type: 'text', text: JSON.stringify(report, null, 2) }],
isError: true,
};
}
// Compute file name: explicit override, else spec name kebab-cased
const stem = nameOverride
? toKebabCase(nameOverride)
: toKebabCase(report.name ?? 'component');
const fileName = `${stem}.yaml`;
const workspaceRoot = resolveWorkspaceRoot();
const componentDir = join(workspaceRoot, '.labwired', 'components');
await mkdir(componentDir, { recursive: true });
const specPath = join(componentDir, fileName);
await writeFile(specPath, spec_yaml);
const absSpecPath = resolve(specPath);
return {
content: [
{
type: 'text',
text: JSON.stringify({
ok: true,
name: report.name,
spec_path: absSpecPath,
usage: {
manifest_external_device: {
type: 'ir',
connection: '<i2c peripheral id>',
config: { spec_path: absSpecPath },
},
},
}, null, 2),
},
],
};
}
if (name === 'labwired_ingest_svd') {
const { svd_content, filter } = IngestSvdInput.parse(args ?? {});
const workspaceRoot = resolveWorkspaceRoot();
const peripheralDir = join(workspaceRoot, '.labwired', 'peripherals');
await mkdir(peripheralDir, { recursive: true });
const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-svd-'));
let report;
try {
const tmpSvd = join(work, 'in.svd');
await writeFile(tmpSvd, svd_content);
const cliArgs = [
'asset',
'ingest-svd',
'--input',
tmpSvd,
'--output-dir',
peripheralDir,
'--json',
];
if (filter)
cliArgs.push('--filter', filter);
const { stdout, stderr, exitCode } = await runCli(cliArgs);
try {
report = JSON.parse(stdout);
}
catch {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'CLI_PARSE_ERROR', stdout, stderr, exit_code: exitCode }, null, 2),
},
],
isError: true,
};
}
if (exitCode !== 0 || report.peripheral_count === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'INGEST_FAILED', stderr, exit_code: exitCode, ...report }, null, 2),
},
],
isError: true,
};
}
}
finally {
await rm(work, { recursive: true, force: true }).catch(() => { });
}
// Read back each descriptor and build a paste-ready chip-yaml block.
const peripherals = [];
for (const p of report.peripherals) {
const absPath = resolve(p.descriptor_path);
const descriptor_yaml = await readFile(absPath, 'utf8').catch(() => '');
peripherals.push({ ...p, descriptor_path: absPath, descriptor_yaml });
}
const manifest_snippet = ['peripherals:']
.concat(peripherals.map((p) => [
` - id: ${p.name.toLowerCase()}`,
` type: declarative`,
` base_address: ${p.base_address}`,
` config:`,
` path: ${p.descriptor_path}`,
].join('\n')))
.join('\n');
return {
content: [
{
type: 'text',
text: JSON.stringify({
ok: true,
peripheral_count: report.peripheral_count,
peripherals,
manifest_snippet,
}, null, 2),
},
],
};
}
if (name === 'labwired_verify') {
const a = (args ?? {});
if (typeof a.system_yaml !== 'string' || !a.system_yaml.trim()) {
return {
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'missing required: system_yaml' }, null, 2) }],
isError: true,
};
}
// Ref-first firmware currency (same surface as labwired_run): inline
// firmware_base64 wins; otherwise resolve a content-addressed `sha256:<hex>`
// ref (firmware_ref or diagram.firmware.artifact.ref) from the blob store.
let firmwareBase64 = typeof a.firmware_base64 === 'string' ? a.firmware_base64 : '';
if (!firmwareBase64.trim()) {
const ref = firmwareRefFromArgs(args);
if (!ref) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
ok: false,
error: 'FIRMWARE_REQUIRED',
detail: 'Provide firmware_base64 or firmware_ref (sha256:<hex> from labwired_compile_firmware).',
}, null, 2),
},
],
isError: true,
};
}
try {
firmwareBase64 = await resolveFirmwareRef(ref);
}
catch (e) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ ok: false, error: 'FIRMWARE_REF_UNRESOLVED', detail: e instanceof Error ? e.message : String(e) }, null, 2),
},
],
isError: true,
};
}
}
const systemYaml = a.system_yaml;
const hasTestYaml = typeof a.test_yaml === 'string' && a.test_yaml.trim().length > 0;
const oracle = (a.oracle && typeof a.oracle === 'object' ? a.oracle : undefined);
const clauseCount = oracleClauseCount(oracle ?? null);
// Mandatory-oracle gate: verify needs EITHER a test_yaml (reference power
// path) OR an oracle with at least one clause. Empty → ORACLE_REQUIRED.
if (!hasTestYaml && clauseCount === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'ORACLE_REQUIRED',
detail: 'labwired_verify needs an oracle with at least one clause, or a test_yaml.',
}, null, 2),
},
],
isError: true,
};
}
// Assemble a single OracleRunResult from whichever run path(s) the proof
// needs, then let the shared evaluateOracle mint the verdict.
const needBuilder = hasTestYaml ||
(oracle?.serial?.length ?? 0) > 0 ||
(oracle?.registers?.length ?? 0) > 0 ||
(oracle?.gpio?.length ?? 0) > 0;
const needDevice = Boolean(oracle?.display);
const runResult = { status: 'pass', serial: '' };
let builderResult;
let deviceResult;
if (needBuilder) {
// `labwired test` inside the container is the honest reference oracle for
// test_yaml assertions; for an oracle block with no test_yaml we run a
// minimal capture script so evaluateOracle can prove over the UART.
builderResult = await runBuildViaBuilder({
firmwareBase64,
systemYaml,
testYaml: hasTestYaml ? a.test_yaml : synthCaptureTestYaml(),
});
runResult.serial = builderResult.uart_excerpt ?? '';
runResult.status =
builderResult.status ||
(builderResult.ok ? (builderResult.passed ? 'pass' : 'incomplete') : 'error');
}
if (needDevice) {
// run_device's painted/refresh/ink readout maps onto the display oracle.
deviceResult = await runDeviceCapture({ firmwareBase64, systemYaml });
runResult.peripherals = [
{
name: deviceResult.panel ?? 'panel',
kind: 'epaper_panel',
artifacts: [
{
kind: deviceResult.panel ? `epaper_panel_${deviceResult.panel}` : 'epaper_panel',
meta: {
refresh_generation: deviceResult.refresh_generation,
black_ink_bytes: deviceResult.ink_bytes,
ink_bytes: deviceResult.ink_bytes,
},
},
],
},
];
if (!needBuilder)
runResult.status = deviceResult.exit_code === 0 ? 'pass' : 'error';
}
let proven;
let verification = '';
let oracle_results;
let unevaluable_clauses;
if (oracle && clauseCount > 0) {
const verdict = evaluateOracle(oracle, runResult);
proven = verdict.passed;
oracle_results = verdict.clauses.map((c) => ({ kind: c.kind, passed: c.passed, detail: c.detail }));
// register/gpio clauses need the hosted /run peripherals inspect block,
// which the stdio builder run-build result does not carry — flag them.
const needsInspect = (oracle.registers?.length ?? 0) + (oracle.gpio?.length ?? 0);
if (needsInspect > 0) {
unevaluable_clauses =
'register/gpio clauses require the hosted /run peripheral-inspect block; the stdio ' +
'verify path only observes serial (UART) + display. Those clauses were evaluated against ' +
'an empty peripheral set and cannot pass here — use the hosted verify for them.';
}
verification = proven
? `Oracle proven server-side (deterministic): ${oracle_results
.filter((c) => c.passed)
.map((c) => c.detail)
.join('; ')}.`
: '';
}
else {
// test_yaml-only reference path: the builder's assertion verdict rules.
proven = Boolean(builderResult?.ok && builderResult?.passed);
verification = proven ? 'Test assertions passed server-side (deterministic oracle run).' : '';
}
const ok = needBuilder ? Boolean(builderResult?.ok) : true;
const payload = {
ok,
proven,
verification,
status: builderResult?.status ?? runResult.status,
stop_reason: builderResult?.stop_reason,
assertions: builderResult?.assertions,
oracle_results,
...(unevaluable_clauses ? { unevaluable_clauses } : {}),
verdict_lines: builderResult?.verdict_lines,
uart_excerpt: builderResult?.uart_excerpt,
display: deviceResult
? {
painted: deviceResult.painted,
panel: deviceResult.panel,
refresh_generation: deviceResult.refresh_generation,
ink_bytes: deviceResult.ink_bytes,
readout: deviceResult.readout,
}
: undefined,
error: builderResult?.error,
};
return {
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
isError: !ok || !proven,
};
}
if (name === 'labwired_inspect_run') {
const { snapshot_id, scope } = InspectRunInput.parse(args ?? {});
const snap = getSnapshot(snapshot_id);
if (!snap) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'SNAPSHOT_EXPIRED',
message: `Snapshot ${snapshot_id} not found. TTL is 10 minutes; re-run labwired_run.`,
}, null, 2),
},
],
isError: true,
};
}
const payload = { snapshot_id, scope, board_id: snap.board_id };
if (scope === 'summary' || scope === 'raw') {
payload.final_cycles = snap.final_cycles;
payload.final_pc_hex = snap.final_pc_hex;
}
if (scope === 'serial' || scope === 'summary') {
payload.serial_output = snap.serial_output ?? '';
}
if (scope === 'gpio') {
payload.gpio_events = snap.gpio_events ?? [];
payload.gpio_truncated = snap.gpio_truncated ?? false;
}
if (scope === 'raw') {
payload.raw_result = snap.raw_result;
}
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
}
return {

@@ -154,0 +1304,0 @@ content: [{ type: 'text', text: `Unknown tool: ${name}` }],

+4
-2
{
"name": "@labwired/mcp",
"version": "0.1.1",
"version": "0.6.0",
"description": "Model Context Protocol server exposing the LabWired deterministic firmware simulator as agent tools.",

@@ -15,4 +15,5 @@ "license": "MIT",

"scripts": {
"build": "tsc",
"build": "npm --prefix ../board-config run build && tsc",
"dev": "tsx src/index.ts",
"pretest": "npm run build",
"test": "vitest run",

@@ -22,2 +23,3 @@ "prepublishOnly": "npm run build"

"dependencies": {
"@labwired/board-config": "file:../board-config",
"@modelcontextprotocol/sdk": "^1.20.0",

@@ -24,0 +26,0 @@ "zod": "^3.25.0"

+49
-12

@@ -5,12 +5,42 @@ # @labwired/mcp

LabWired runs ARM Cortex-M / RISC-V / Xtensa firmware against a cycle-accurate, deterministic simulator. This MCP server lets your agent (Claude Code, Cursor, Continue, etc.) **verify firmware behaviour without lying to itself** — same inputs always produce the same outputs, so the agent can iterate against a real oracle.
## One-line install (Claude Code)
```bash
claude mcp add labwired -- npx -y @labwired/mcp
```
That's it. Restart Claude Code if the tools don't appear, then ask: *"List LabWired boards and run my ELF on `stm32f103-blinky`."*
Cursor / other MCP clients: see [Other clients](#install-in-cursor) below.
---
LabWired runs ARM Cortex-M / RISC-V / Xtensa firmware against a silicon-validated, deterministic simulator. This MCP server lets your agent (Claude Code, Cursor, Continue, etc.) **verify firmware behaviour without lying to itself** — same inputs always produce the same outputs, so the agent can iterate against a real oracle.
## What it gives your agent
**Core workflow:**
| Tool | What it does |
|---|---|
| `labwired_catalog` | List supported chips/boards (filter by name). |
| `labwired_simulate` | Run firmware against a test script. Returns `result.json` + UART log. Deterministic. |
| `labwired_list_boards` | High-level: list pre-wired boards (chip + peripherals + demo firmware). Start here. |
| `labwired_run_lab` | High-level: run an ELF on a board by id. Synthesizes the test script, returns cycles + serial + a `snapshot_id`. |
| `labwired_fuzz` | Coverage-guided fuzz a firmware ELF and return the crashing inputs. Silicon-validated sim → crashes are replayable on real hardware (HIL-confirm), not emulation false positives. Deterministic for a fixed seed. |
| `labwired_inspect_run` | Fetch detail from a prior `snapshot_id` — `summary`, `serial`, `gpio`, `raw`. 10-min TTL. |
| `labwired_catalog` | Low-level: list raw chip descriptors. |
| `labwired_simulate` | Low-level: run firmware against a custom System Manifest + test script YAML. Full control. |
| `labwired_validate_system` | Validate a System Manifest YAML before simulating. Fast schema check. |
| `labwired_validate_diagram` | Structurally validate a wired diagram (parts + wires) before running. Returns machine-readable diagnostics (`PIN_NOT_ON_CHIP`, `PIN_LACKS_I2C`, `BOARDIO_NOT_TO_MCU`, `NO_MCU`, `COMPONENT_DANGLING`, …) with suggested fixes. |
| `labwired_compile_diagram` | Compile a validated diagram to a runnable board manifest (ERC-gated; net-derived I2C bus binding). Persists the manifest to `.labwired/boards/<name>.yaml` and returns `board_path` + `system_yaml`. Errors abort with diagnostics; warnings are included in the response. |
| `labwired_define_component` | Define a new off-chip device from a declarative IR spec; validated, persisted, ready to wire into a manifest. |
**Live agent → browser bridge** (optional — for showing humans what the agent is doing):
| Tool | What it does |
|---|---|
| `labwired_create_session` | Open a watch session; returns a `watch_url` like `https://app.labwired.com/?watch=<id>`. Subsequent runs mirror to it automatically over WebSocket. |
| `labwired_set_diagram` | Push the current circuit (parts + wires) to the watch session. |
| `labwired_set_source` | Push the current source code to the watch session. |
| `labwired_end_session` | Close the watch session. |
## Prerequisites

@@ -26,10 +56,2 @@

## Install in Claude Code
```bash
claude mcp add labwired -- npx -y @labwired/mcp
```
That's it. Restart Claude Code if the tools don't appear immediately.
## Install in Cursor

@@ -70,2 +92,3 @@

- `LABWIRED_CLI` — override the CLI binary path. Defaults to `labwired` on `PATH`.
- `LABWIRED_REPO_ROOT` — absolute path to your labwired checkout. The high-level `labwired_run_lab` tool resolves board YAMLs from `core/configs/`. Auto-detected when the MCP server is run from inside the repo; required when running globally.

@@ -80,4 +103,18 @@ ## Why deterministic matters for agents

Early MVP (v0.1). Three tools, stdio transport, no auth required for local use (the simulator runs on your machine). Worker-side metering for hosted runs lands in a future version.
v0.7 — sixteen tools (fifteen plus the `labwired_search_tools` meta-tool), stdio transport, no auth required for local use (the simulator runs on your machine).
Shipped:
- **v0.1** — `labwired_catalog` / `labwired_simulate` / `labwired_validate_system` (raw chip + YAML workflow).
- **v0.2** — high-level board-centric workflow: `labwired_list_boards` / `labwired_run_lab` / `labwired_inspect_run`. ELF-in only — agents compile locally.
- **v0.3** — live agent → browser bridge: `labwired_create_session` / `labwired_set_diagram` / `labwired_set_source` / `labwired_end_session`. Runs stream over WebSocket to `https://app.labwired.com/?watch=<id>`.
- **v0.4** — `labwired_validate_diagram` with machine-readable diagnostics (pin/wire/bus errors + suggested fixes).
- **v0.5** — `labwired_fuzz`: coverage-guided firmware fuzzing in the silicon-validated sim; crashes are HIL-replayable, not emulation false positives.
- **v0.6** — `labwired_define_component`: agent-authored declarative IR specs for off-chip I2C devices; `type: ir` manifest integration; PCA9685 and TMP102 reference specs gated by byte-equivalence tests.
- **v0.7** — Wiring kernel: named nets, electrical rule checks (ERC), and ERC-gated diagram compilation on both MCP surfaces. `labwired_validate_diagram` upgraded with full kernel ERC (I2C bus rules, driver conflicts, voltage mismatches, IRQ ordinal validation). New `labwired_compile_diagram` compiles a diagram to a runnable board manifest with net-derived I2C bus binding.
Next:
- **v0.8** — Worker-side metering for hosted runs.
Issues / requests: <https://github.com/w1ne/labwired/issues>.

@@ -84,0 +121,0 @@