@labwired/mcp
Advanced tools
+1
-1
| /** | ||
| * On-disk example-lab catalog for the high-level `labwired_list_boards` / | ||
| * On-disk example-lab catalog for the high-level `labwired_list` / | ||
| * `labwired_run` (a.k.a. the deprecated `labwired_run_lab`) tools. | ||
@@ -4,0 +4,0 @@ * |
+53
-0
@@ -195,2 +195,55 @@ import { execFile } from 'node:child_process'; | ||
| } | ||
| export async function compileViaBuilder(opts) { | ||
| const base = builderBaseUrl(); | ||
| if (!base) { | ||
| return { | ||
| ok: false, | ||
| diagnostics: [], | ||
| error: 'No builder URL configured. Set LABWIRED_COMPILE_URL (or LABWIRED_BUILDER_URL) ' + | ||
| 'to the labwired-builder base URL so firmware can compile 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}/compile`, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify({ | ||
| source: opts.source, | ||
| board: opts.board, | ||
| language: opts.language, | ||
| files: opts.files, | ||
| lib_deps: opts.lib_deps, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
| const data = (await resp.json()); | ||
| return { | ||
| ok: Boolean(data.ok), | ||
| ...(data.ok && data.elfBase64 ? { elf_base64: data.elfBase64 } : {}), | ||
| runnable: data.runnable ?? false, | ||
| ...(data.ok && data.flashImages?.length | ||
| ? { flash_images: data.flashImages, flash_chip: data.flashChip } | ||
| : {}), | ||
| diagnostics: data.diagnostics ?? [], | ||
| ...(data.error ? { error: data.error } : {}), | ||
| ...(data.ok ? {} : { detail: 'Firmware did not compile. Fix the diagnostics below and retry.' }), | ||
| }; | ||
| } | ||
| catch (e) { | ||
| return { | ||
| ok: false, | ||
| diagnostics: [], | ||
| error: `builder /compile unreachable: ${e instanceof Error ? e.message : String(e)}`, | ||
| }; | ||
| } | ||
| finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| export async function validateSystem(systemYaml) { | ||
@@ -197,0 +250,0 @@ const work = await mkdtemp(join(tmpdir(), 'labwired-mcp-validate-')); |
+117
-495
@@ -9,7 +9,7 @@ #!/usr/bin/env node | ||
| import { join, resolve } from 'node:path'; | ||
| import { listChips, validateSystem, runLab, fuzzFirmware, runCli, runDeviceCapture, runBuildViaBuilder } from './cli.js'; | ||
| import { validateSystem, runLab, fuzzFirmware, runCli, runDeviceCapture, runBuildViaBuilder, compileViaBuilder } 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 { composeDiagnostics, describeBoard, describeComponent, componentPinCount, evaluateOracle, oracleClauseCount, toolsForSurface, CATALOG, } from '@labwired/board-config'; | ||
| import { AGENT_HARDWARE_LOOP_GUIDE_URI, LOCAL_AGENT_WORKFLOW, SEARCH_TOOLS_TOOL_NAME, rankTools, } from './search-tools.js'; | ||
| import { decorateTools } from './tool-metadata.js'; | ||
@@ -19,15 +19,3 @@ import { RESOURCES, getResource } from './resources.js'; | ||
| const SERVER_NAME = '@labwired/mcp'; | ||
| const SERVER_VERSION = '0.5.0'; | ||
| const CatalogInput = z.object({ | ||
| filter: z | ||
| .string() | ||
| .optional() | ||
| .describe('Optional substring filter applied to chip/board names.'), | ||
| }); | ||
| 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 SERVER_VERSION = '0.7.0'; | ||
| const RunLabInput = z.object({ | ||
@@ -37,7 +25,7 @@ target: z | ||
| .optional() | ||
| .describe('Board id / target from labwired_list_boards, e.g. "stm32f103-blinky". Canonical name for board_id.'), | ||
| .describe('Board id / target from labwired_list, e.g. "stm32f103-blinky". Canonical name for board_id.'), | ||
| board_id: z | ||
| .string() | ||
| .optional() | ||
| .describe('Deprecated alias for `target` (board id from labwired_list_boards).'), | ||
| .describe('Deprecated alias for `target` (board id from labwired_list).'), | ||
| elf_base64: z | ||
@@ -50,3 +38,3 @@ .string() | ||
| .optional() | ||
| .describe('Content-addressed firmware ref (`sha256:<hex>` from labwired_compile_firmware). Resolved from ' + | ||
| .describe('Content-addressed firmware ref (`sha256:<hex>` from labwired_compile). Resolved from ' + | ||
| 'the hosted blob store — no base64 to carry. Used when elf_base64 is omitted.'), | ||
@@ -87,9 +75,4 @@ max_cycles: z | ||
| }).refine((v) => Boolean(v.target || v.board_id), { | ||
| message: 'Provide `target` (or the legacy `board_id`) — a board id from labwired_list_boards.', | ||
| message: 'Provide `target` (or the legacy `board_id`) — a board id from labwired_list.', | ||
| }); | ||
| const DescribeBoardInput = z.object({ | ||
| board: z | ||
| .string() | ||
| .describe('Any accepted board/chip spelling (canonical board id, playground id, or chip family key).'), | ||
| }); | ||
| // ─── Oracle / unmodeled-access helpers (mcp-unification §c) ───────────────── | ||
@@ -132,3 +115,3 @@ // A minimal "execute and capture UART" test script for the oracle-block verify | ||
| .string() | ||
| .describe('Board id from labwired_list_boards (resolves chip + system YAML).'), | ||
| .describe('Board id from labwired_list (resolves chip + system YAML).'), | ||
| elf_base64: z | ||
@@ -174,16 +157,2 @@ .string() | ||
| }); | ||
| 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) ────────────────── | ||
@@ -218,359 +187,19 @@ // Checked in order: LABWIRED_REPO_ROOT env, then walk up from __dirname. | ||
| const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } }); | ||
| function localTools() { | ||
| return [ | ||
| SEARCH_TOOLS_TOOL, | ||
| { | ||
| name: 'labwired_catalog', | ||
| description: "List supported chips/boards in the LabWired catalog. Returns name, family, " + | ||
| 'architecture, pass rate, and verification status. Use this to discover what targets ' + | ||
| 'are available before running a simulation. Free; no API key required.', | ||
| inputSchema: { | ||
| type: 'object', | ||
| properties: { | ||
| filter: { | ||
| type: 'string', | ||
| description: 'Optional substring filter applied to chip/board names.', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| 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'], | ||
| properties: { | ||
| 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: 'Full contents of the System Manifest YAML to validate.', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| 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: 'Substring filter on id / name / chip family.', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| 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: '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: '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' }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| 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: '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).', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| ]; | ||
| // The advertised stdio surface is built from the single MCP tool registry | ||
| // (@labwired/board-config), decorated with stdio titles/annotations — so the | ||
| // advertised names/schemas can never drift from dispatch or from the hosted | ||
| // surface. A drift-guard test asserts tools/list == toolsForSurface('stdio'). | ||
| const STDIO_TOOLS = decorateTools(toolsForSurface('stdio')); | ||
| /** Component library listing (mirrors the hosted labwired_list): every catalog | ||
| * part as { type, class, pin_count }, optionally substring-filtered on type. */ | ||
| function listComponents(filter) { | ||
| return Object.keys(CATALOG) | ||
| .filter((type) => !filter || type.includes(filter)) | ||
| .map((type) => { | ||
| const descriptor = describeComponent(type); | ||
| return { type, class: descriptor.class, pin_count: componentPinCount(descriptor) }; | ||
| }); | ||
| } | ||
| server.setRequestHandler(ListToolsRequestSchema, async () => ({ | ||
| tools: decorateTools(localTools()), | ||
| tools: STDIO_TOOLS, | ||
| })); | ||
@@ -600,63 +229,102 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ | ||
| workflow: LOCAL_AGENT_WORKFLOW, | ||
| tools: rankTools(query, decorateTools(localTools()), limit), | ||
| tools: rankTools(query, STDIO_TOOLS, limit), | ||
| }) }], | ||
| }; | ||
| } | ||
| if (name === 'labwired_catalog') { | ||
| const { filter } = CatalogInput.parse(args ?? {}); | ||
| const chips = await listChips(filter); | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify(chips, null, 2) }], | ||
| }; | ||
| // Folds the old labwired_catalog + labwired_list. `kind` narrows the | ||
| // listing (board | mcu | component); omit for everything. | ||
| if (name === 'labwired_list') { | ||
| const input = (args ?? {}); | ||
| const kind = input.kind === 'board' || input.kind === 'mcu' || input.kind === 'component' | ||
| ? input.kind | ||
| : undefined; | ||
| const filter = typeof input.filter === 'string' && input.filter ? input.filter : undefined; | ||
| const out = {}; | ||
| if (!kind || kind === 'board') { | ||
| out.boards = listBoards(filter); | ||
| } | ||
| if (!kind || kind === 'component' || kind === 'mcu') { | ||
| const components = listComponents(filter); | ||
| out.components = kind === 'mcu' ? components.filter((c) => c.class === 'mcu') : components; | ||
| } | ||
| return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] }; | ||
| } | ||
| if (name === 'labwired_run_device') { | ||
| // Renamed from labwired_describe. Accepts `id` (registry schema) or the | ||
| // legacy `board` key. A board/MCU/chip spelling → the rich board descriptor; | ||
| // a peripheral/passive type → the flat component descriptor. | ||
| if (name === 'labwired_describe') { | ||
| 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 id = (typeof a.id === 'string' ? a.id : typeof a.board === 'string' ? a.board : '').trim(); | ||
| if (!id) { | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'id is required.' }, null, 2) }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| const run = await runDeviceCapture({ firmwareBase64: a.firmware_base64, systemYaml: a.system_yaml, steps: a.steps }); | ||
| const descriptor = describeBoard(id); | ||
| if (descriptor) { | ||
| return { content: [{ type: 'text', text: JSON.stringify(descriptor, null, 2) }] }; | ||
| } | ||
| const component = describeComponent(id); | ||
| if (component) { | ||
| return { content: [{ type: 'text', text: JSON.stringify(component, null, 2) }] }; | ||
| } | ||
| 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), | ||
| text: JSON.stringify({ | ||
| error: 'UNKNOWN_BOARD', | ||
| message: `No board/chip/component matches "${id}". Call labwired_list for valid ids.`, | ||
| }, null, 2), | ||
| }, | ||
| ], | ||
| isError: result.exitCode !== 0, | ||
| isError: true, | ||
| }; | ||
| } | ||
| 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) { | ||
| // Folds the old labwired_validate_system + labwired_validate_diagram. A | ||
| // `diagram` runs the ERC/pin diagnostics; a `system_yaml` runs the schema | ||
| // validator. Diagram wins if both are present. | ||
| if (name === 'labwired_validate') { | ||
| const a = (args ?? {}); | ||
| if (a.diagram && typeof a.diagram === 'object' && !Array.isArray(a.diagram)) { | ||
| const result = composeDiagnostics(a.diagram); | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], | ||
| isError: result.error_count > 0, | ||
| }; | ||
| } | ||
| if (typeof a.system_yaml === 'string' && a.system_yaml.trim()) { | ||
| const result = await validateSystem(a.system_yaml); | ||
| 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), | ||
| text: JSON.stringify({ exit_code: result.exitCode, stdout: result.stdout, stderr: result.stderr }, null, 2), | ||
| }, | ||
| ], | ||
| isError: true, | ||
| isError: result.exitCode !== 0, | ||
| }; | ||
| } | ||
| return { content: [{ type: 'text', text: JSON.stringify(descriptor, null, 2) }] }; | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify({ error: 'INVALID_ARGS', message: 'Provide a diagram (JSON) or a system_yaml (string) to validate.' }, null, 2) }], | ||
| isError: true, | ||
| }; | ||
| } | ||
| // Compile C/C++ firmware → ELF on the hosted PlatformIO toolchain (builder | ||
| // /compile). Same standardized tool as the hosted + builder surfaces. | ||
| if (name === 'labwired_compile') { | ||
| const a = (args ?? {}); | ||
| const result = await compileViaBuilder({ | ||
| source: typeof a.source === 'string' ? a.source : undefined, | ||
| board: typeof a.board === 'string' ? a.board : undefined, | ||
| language: typeof a.language === 'string' ? a.language : undefined, | ||
| files: a.files, | ||
| lib_deps: a.lib_deps, | ||
| }); | ||
| return { | ||
| content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], | ||
| isError: !result.ok, | ||
| }; | ||
| } | ||
| if (name === 'labwired_run') { | ||
@@ -674,3 +342,3 @@ const input = RunLabInput.parse(args ?? {}); | ||
| error: 'INVALID_BOARD', | ||
| message: `Unknown target "${boardId}". Call labwired_list_boards to see available ids.`, | ||
| message: `Unknown target "${boardId}". Call labwired_list to see available ids.`, | ||
| }, null, 2), | ||
@@ -714,3 +382,3 @@ }, | ||
| error: 'FIRMWARE_REQUIRED', | ||
| message: 'Provide elf_base64 or firmware_ref (sha256:<hex> from labwired_compile_firmware).', | ||
| message: 'Provide elf_base64 or firmware_ref (sha256:<hex> from labwired_compile).', | ||
| }, null, 2), | ||
@@ -813,3 +481,3 @@ }, | ||
| 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, ' + | ||
| 'Use labwired_inspect (scope=gpio|serial) on the snapshot_id for state, ' + | ||
| 'or the hosted /run peripherals path for a full inspect.', | ||
@@ -837,3 +505,3 @@ ...(unmodeled_access.length ? { unmodeled_access } : {}), | ||
| error: 'INVALID_BOARD', | ||
| message: `Unknown board_id "${input.board_id}". Call labwired_list_boards to see available ids.`, | ||
| message: `Unknown board_id "${input.board_id}". Call labwired_list to see available ids.`, | ||
| }, null, 2), | ||
@@ -913,57 +581,2 @@ }, | ||
| } | ||
| if (name === 'labwired_validate_diagram') { | ||
| const { diagram } = ValidateDiagramInput.parse(args ?? {}); | ||
| const result = composeDiagnostics(diagram); | ||
| return { | ||
| content: [ | ||
| { | ||
| type: 'text', | ||
| text: JSON.stringify(result, null, 2), | ||
| }, | ||
| ], | ||
| 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') { | ||
@@ -1131,3 +744,3 @@ const { spec_yaml, name: nameOverride } = DefineComponentInput.parse(args ?? {}); | ||
| error: 'FIRMWARE_REQUIRED', | ||
| detail: 'Provide firmware_base64 or firmware_ref (sha256:<hex> from labwired_compile_firmware).', | ||
| detail: 'Provide firmware_base64 or firmware_ref (sha256:<hex> from labwired_compile).', | ||
| }, null, 2), | ||
@@ -1277,4 +890,13 @@ }, | ||
| } | ||
| if (name === 'labwired_inspect_run') { | ||
| const { snapshot_id, scope } = InspectRunInput.parse(args ?? {}); | ||
| if (name === 'labwired_inspect') { | ||
| // Accept the registry `output` selector (summary|serial|peripherals|full) | ||
| // and the legacy `scope` (summary|serial|gpio|raw). The stdio snapshot | ||
| // exposes gpio/serial/raw; map peripherals→gpio and full→raw. | ||
| const a = (args ?? {}); | ||
| const rawSel = typeof a.output === 'string' ? a.output : typeof a.scope === 'string' ? a.scope : 'summary'; | ||
| const scope = rawSel === 'serial' ? 'serial' | ||
| : rawSel === 'gpio' || rawSel === 'peripherals' ? 'gpio' | ||
| : rawSel === 'raw' || rawSel === 'full' ? 'raw' | ||
| : 'summary'; | ||
| const { snapshot_id } = InspectRunInput.parse({ snapshot_id: a.snapshot_id, scope }); | ||
| const snap = getSnapshot(snapshot_id); | ||
@@ -1281,0 +903,0 @@ if (!snap) { |
@@ -7,8 +7,8 @@ export const AGENT_HARDWARE_LOOP_URI = 'labwired://guides/agent-hardware-loop'; | ||
| 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. | ||
| 1. Choose a board with \`labwired_list\` (pass \`kind\` to narrow: board | mcu | component). | ||
| 2. Validate a diagram or custom system YAML with \`labwired_validate\`. | ||
| 3. Compile firmware with \`labwired_compile\` (hosted PlatformIO toolchain), or compile locally and pass the ELF as base64. | ||
| 4. Describe any board/chip/component with \`labwired_describe\`. | ||
| 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\`. | ||
| 6. Inspect snapshots with \`labwired_inspect\`. | ||
| 7. Use \`labwired_fuzz\` when firmware exposes the fuzz contract. | ||
@@ -15,0 +15,0 @@ 8. Iterate until serial, GPIO, cycle counts, and stop reasons match the intended behavior. |
| 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 SEARCH_TOOLS_TOOL_NAME = 'labwired_search'; | ||
| 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_list', | ||
| 'labwired_validate', | ||
| 'labwired_compile', | ||
| 'labwired_run', | ||
| 'labwired_inspect_run', | ||
| 'labwired_inspect', | ||
| ]; | ||
@@ -12,0 +12,0 @@ export const SEARCH_TOOLS_TOOL = { |
| /** | ||
| * In-memory snapshot store for `labwired_inspect_run`. Lets the agent fetch | ||
| * In-memory snapshot store for `labwired_inspect`. Lets the agent fetch | ||
| * detailed state slices (registers, memory, peripheral) without re-shipping | ||
@@ -4,0 +4,0 @@ * full state through `labwired_run`'s return. |
+17
-18
| const READ_ONLY_TOOLS = new Set([ | ||
| 'labwired_search_tools', | ||
| 'labwired_catalog', | ||
| 'labwired_validate_system', | ||
| 'labwired_list_boards', | ||
| 'labwired_inspect_run', | ||
| 'labwired_validate_diagram', | ||
| 'labwired_search', | ||
| 'labwired_list', | ||
| 'labwired_describe', | ||
| 'labwired_validate', | ||
| 'labwired_inspect', | ||
| ]); | ||
| // labwired_define_component is intentionally absent from READ_ONLY_TOOLS: | ||
| // it writes a spec file to .labwired/components/. | ||
| // labwired_define_component / labwired_ingest_svd / labwired_compile are | ||
| // intentionally absent from READ_ONLY_TOOLS: they write spec/peripheral files | ||
| // or drive a server-side build. | ||
| const TITLES = { | ||
| labwired_catalog: 'Catalog', | ||
| labwired_validate_system: 'Validate System', | ||
| labwired_list_boards: 'List Boards', | ||
| labwired_run: 'Run', | ||
| labwired_verify: 'Verify', | ||
| labwired_search: 'Search Tools', | ||
| labwired_list: 'List Catalog', | ||
| labwired_describe: 'Describe Component', | ||
| labwired_validate: 'Validate Diagram', | ||
| labwired_compile: 'Compile Firmware', | ||
| labwired_run: 'Run Firmware', | ||
| labwired_inspect: 'Inspect Run', | ||
| labwired_verify: 'Verify Firmware', | ||
| labwired_fuzz: 'Fuzz Firmware', | ||
| labwired_inspect_run: 'Inspect Run', | ||
| labwired_validate_diagram: 'Validate Diagram', | ||
| labwired_search_tools: 'Search Tools', | ||
| labwired_ingest_svd: 'Ingest SVD', | ||
| labwired_define_component: 'Define Component', | ||
| labwired_compile_diagram: 'Compile Diagram', | ||
| labwired_ingest_svd: 'Ingest SVD', | ||
| }; | ||
@@ -25,0 +24,0 @@ export function toolTitle(name) { |
+1
-1
| { | ||
| "name": "@labwired/mcp", | ||
| "version": "0.6.0", | ||
| "version": "0.7.0", | ||
| "description": "Model Context Protocol server exposing the LabWired deterministic firmware simulator as agent tools.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
84882
-16.98%1764
-15.6%18
12.5%