Design Spec: MCP Server¶
Document Control¶
| Field | Value |
|---|---|
| Status | Implemented |
| Command surface | canarchy mcp serve |
| Primary area | CLI, agent integration |
| Coverage audit | #323 (matrix in MCP Coverage Decisions below) |
Goal¶
Expose the CANarchy command surface as a native Model Context Protocol (MCP) server so agents can invoke CANarchy tools directly over the MCP wire protocol instead of spawning subprocesses and parsing stdout.
User-Facing Motivation¶
Agents that already call tools via MCP (Claude, OpenCode, etc.) can integrate CANarchy without subprocess overhead or fragile stdout parsing. The MCP server turns CANarchy into a first-class tool-call surface: structured inputs, structured outputs, consistent error codes, and tool discovery built into the protocol.
Requirements¶
| ID | Type | Requirement |
|---|---|---|
REQ-MCP-01 |
Ubiquitous | The system shall provide a canarchy mcp serve subcommand that starts an MCP server over stdio. |
REQ-MCP-02 |
Ubiquitous | Each command selected for the MCP surface shall surface as an MCP tool whose name is the command string with spaces replaced by underscores (e.g. j1939 monitor → j1939_monitor). |
REQ-MCP-03 |
Ubiquitous | Each MCP tool's input schema shall be derived from the argparse parameter definitions of the corresponding CLI command. |
REQ-MCP-04 |
Event-driven | When an MCP tool call is received, the system shall return the canonical command result envelope (ok, command, data, warnings, errors) serialised as JSON text content. |
REQ-MCP-05 |
Event-driven | When an MCP tool call is received with invalid inputs, the system shall return the same structured error codes as the equivalent CLI invocation. |
REQ-MCP-06 |
Event-driven | When a list_tools request is received, the system shall return all registered MCP tools with name, description, and input schema. |
REQ-MCP-07 |
Ubiquitous | The MCP server shall use stdio transport only. |
REQ-MCP-08 |
Unwanted behaviour | If a call_tool request names an unregistered tool, the system shall raise an error indicating the tool is unknown. |
REQ-MCP-09 |
Ubiquitous | The mcp package shall be declared as a project dependency in pyproject.toml. |
REQ-MCP-10 |
Ubiquitous | The server shall not expose shell or tui as MCP tools; those are interactive front-end commands with no RPC equivalent. |
REQ-MCP-11 |
Event-driven | The call_tool handler shall execute execute_command in a thread pool via asyncio.to_thread so that the asyncio event loop is not blocked during file I/O or analysis, preventing MCP keepalive timeouts on large captures. |
REQ-MCP-12 |
Ubiquitous | File-backed J1939 tools (j1939_decode, j1939_pgn, j1939_spn, j1939_tp, j1939_tp_compare, j1939_dm1, j1939_faults, j1939_summary, j1939_inventory, j1939_compare, j1939_map) shall expose optional max_frames (integer) and seconds (number) parameters that bound analysis to the first N frames or first T seconds of the capture, respectively. |
REQ-MCP-13 |
Ubiquitous | Dataset provider workflows selected for MCP shall expose provider list, search, inspect, fetch, cache list, cache refresh, conversion, replay file listing, and safe replay planning tools while excluding streaming dataset frame output. |
REQ-MCP-14 |
Ubiquitous | Skills provider workflows selected for MCP shall expose provider list, search, fetch, cache list, and cache refresh tools while preserving the same CLI result envelope. |
REQ-MCP-15 |
Ubiquitous | Reverse-engineering helpers selected for MCP shall include re signals, re counters, re entropy, re correlate, re match-dbc, re shortlist-dbc, and re suggest (heuristic path only; the external --llm enrichment is CLI-only). |
REQ-MCP-16 |
Ubiquitous | Every implemented CLI command shall be either exposed as an MCP tool or listed in the documented exclusion set (shell, tui, mcp serve, mcp install, completion, datasets stream, datasets download, dbc generate-c); a test shall enforce this invariant so new commands cannot silently drift out of coverage. |
REQ-MCP-20 |
Ubiquitous | No tool response shall exceed the configured output cap (CANARCHY_MCP_MAX_RESPONSE_BYTES, default 512000 bytes). Oversized list-shaped data shall be truncated with data.truncated: true and a data.truncation block recording, per trimmed list, the original total_items and returned_items, plus a hint pointing at the CLI for the full result; data that cannot be reduced by list truncation shall be replaced by a stub that preserves the envelope. |
REQ-MCP-21 |
Unwanted behaviour | If a tool call raises an unexpected exception, the server shall return a canonical envelope with error code TOOL_EXECUTION_ERROR instead of propagating the exception to the stdio transport, so one failing or oversized call never makes the remaining tools unavailable for the session. |
REQ-MCP-22 |
Ubiquitous | A tool's parameter surface shall match the underlying CLI command's flags: every flag _build_argv forwards shall be a real option of the target command (enforced by a contract test over all tools), and the stats tool shall expose the same top/sa/pgn knobs the CLI offers. |
REQ-MCP-23 |
Unwanted behaviour | When a relayed CLI result reports the generic command name cli (a parse-level failure that occurs before a subcommand resolves), the server shall relabel the envelope's command field with the invoked tool name so errors remain programmatically attributable. |
Command Surface¶
canarchy mcp serve
The serve subcommand accepts no positional arguments or output flags. The server runs until the stdio transport closes (client disconnect or EOF).
The current MCP tool surface is a curated non-interactive subset of the CLI. It intentionally excludes interactive commands and streaming workflows that do not fit MCP's buffered tool-response model.
Tool Naming Convention¶
| CLI command | MCP tool name |
|---|---|
capture |
capture |
send |
send |
generate |
generate |
gateway |
gateway |
replay |
replay |
filter |
filter |
stats |
stats |
compare |
compare |
capture-info |
capture_info |
decode |
decode |
encode |
encode |
dbc inspect |
dbc_inspect |
dbc signals |
dbc_signals |
dbc convert |
dbc_convert |
dbc provider list |
dbc_provider_list |
dbc search |
dbc_search |
dbc fetch |
dbc_fetch |
dbc cache list |
dbc_cache_list |
dbc cache prune |
dbc_cache_prune |
dbc cache refresh |
dbc_cache_refresh |
export |
export |
session save |
session_save |
session load |
session_load |
session show |
session_show |
j1939 monitor |
j1939_monitor |
j1939 decode |
j1939_decode |
j1939 pgn |
j1939_pgn |
j1939 spn |
j1939_spn |
j1939 tp sessions |
j1939_tp |
j1939 tp compare |
j1939_tp_compare |
j1939 dm1 |
j1939_dm1 |
j1939 faults |
j1939_faults |
j1939 summary |
j1939_summary |
j1939 inventory |
j1939_inventory |
j1939 compare |
j1939_compare |
j1939 map |
j1939_map |
j1587 decode |
j1587_decode |
j1587 pids |
j1587_pids |
j2497 decode |
j2497_decode |
j2497 mids |
j2497_mids |
uds scan |
uds_scan |
uds trace |
uds_trace |
uds services |
uds_services |
config show |
config_show |
datasets provider list |
datasets_provider_list |
datasets search |
datasets_search |
datasets inspect |
datasets_inspect |
datasets fetch |
datasets_fetch |
datasets cache list |
datasets_cache_list |
datasets cache refresh |
datasets_cache_refresh |
datasets convert |
datasets_convert |
datasets replay --dry-run |
datasets_replay_plan |
datasets replay --list-files |
datasets_replay_files |
skills provider list |
skills_provider_list |
skills search |
skills_search |
skills fetch |
skills_fetch |
skills cache list |
skills_cache_list |
skills cache refresh |
skills_cache_refresh |
re signals |
re_signals |
re correlate |
re_correlate |
re counters |
re_counters |
re entropy |
re_entropy |
re match-dbc |
re_match_dbc |
re shortlist-dbc |
re_shortlist_dbc |
re suggest |
re_suggest (heuristic path only) |
dbc signals |
dbc_signals |
doctor |
doctor |
sequence replay |
sequence_replay |
fuzz payload |
fuzz_payload |
fuzz replay |
fuzz_replay |
fuzz arbitration-id |
fuzz_arbitration_id |
fuzz signal |
fuzz_signal |
fuzz spn |
fuzz_spn |
MCP Coverage Decisions¶
This matrix is the authoritative CLI-to-MCP coverage audit. Every
implemented CLI command (the IMPLEMENTED_COMMANDS set in canarchy.cli)
is accounted for as Exposed, Excluded (with rationale), or
Deferred (the command does not exist yet). The
test_every_cli_command_is_exposed_or_documented guard in
tests/test_mcp.py fails the build if a future command is added without
landing here.
Exposed¶
| CLI surface | Rationale |
|---|---|
Transport reads (capture, filter, stats, capture-info, decode, encode) |
Non-interactive commands with bounded JSON envelopes. |
compare |
File-backed, multi-capture frame-rate/entropy/cycle-time diff against a baseline; same safety profile as stats/re anomalies, no transmit. |
MCP-gated active transmit (send, generate, gateway, replay, sequence replay, xcp scan) |
In _ACTIVE_TRANSMIT_TOOLS: schemas require ack_active=true and default dry_run=true. xcp scan transmits an XCP CONNECT, so its MCP tool is gated and --dry-run plans the frame without sending. |
Fuzzing (fuzz payload, fuzz replay, fuzz arbitration-id, fuzz signal, fuzz spn, fuzz guided) |
In _ACTIVE_TRANSMIT_TOOLS: mandatory ack_active=true, default dry_run=true. fuzz guided is response-feedback guided fuzzing — active transmit, gated the same way. |
DBC + DBC provider (dbc inspect, dbc signals, dbc convert, dbc provider list, dbc search, dbc fetch, dbc cache list/prune/refresh) |
Bounded inspection, conversion, and provider/cache workflows. dbc_inspect.layout=true exposes cantools-rendered bit layouts without ANSI parsing; dbc_convert returns the serialized database (or writes it to out) — file generation is a developer action, so no active-transmit gate applies. |
| Datasets provider/cache/fetch/search/inspect/convert | Metadata and local conversion workflows return bounded JSON envelopes. |
datasets replay --dry-run (datasets_replay_plan) and --list-files (datasets_replay_files) |
Safe planning and manifest inspection do not open or stream remote frame data. |
| Skills provider/cache/search/fetch | Non-interactive provider workflows with canonical JSON envelopes. |
Plugin inspection (plugins list, plugins info) |
Read-only discovery and metadata inspection with bounded JSON envelopes. |
J1939 analysis (j1939 decode/pgn/spn/tp sessions/tp compare/dm1/faults/summary/inventory/compare/map/monitor) |
File-backed analysis commands are safe, bounded, and deterministic; j1939_map returns passive nodes/edges topology data derived only from the capture. |
J1587/J1708 (j1587 decode, j1587 pids) |
File-backed legacy heavy-vehicle decoding and a static PID catalog; safe, bounded, and deterministic. |
J2497/PLC4TRUCKS (j2497 decode, j2497 mids) |
File-backed trailer power-line frame decoding and a static MID catalog; safe, bounded, and deterministic. Live PLC access requires external hardware and is not exposed. |
Reverse-engineering helpers (re signals/counters/entropy/correlate/anomalies/match-dbc/shortlist-dbc, and re suggest heuristic path) |
File-backed analysis commands are safe and deterministic. re_suggest exposes the offline heuristic path only; the external --llm enrichment is a CLI-only operator action behind explicit confirmation. |
Session (session save/load/show), export, config show, doctor, UDS (uds scan/trace/services), XCP (xcp trace/read/commands) |
Bounded, non-interactive envelopes. |
Excluded¶
| CLI surface | Rationale |
|---|---|
shell, tui |
Interactive front ends with no one-shot RPC equivalent. |
web serve |
Long-running HTTP/WebSocket front end, like shell/tui; read-only by design (docs/design/web-serve.md). |
cannelloni send |
Transmits UDP datagrams to an arbitrary host:port — a CLI-only operator action, not a CAN-interface tool. cannelloni decode (passive) is exposed. |
mcp serve |
The server itself; not a tool it would expose. |
mcp install |
Writes a client config file — a user action, like plugins enable/disable, kept off the agent surface. |
plugins enable, plugins disable |
Write user plugin configuration under ~/.canarchy/config.toml; kept CLI-only. |
dbc generate-c |
Generates C source/header files to disk — a developer action, not an agent tool call. |
completion |
Emits a raw shell script, not a JSON envelope. |
datasets stream, non-dry-run datasets replay |
Emit frame records to stdout and need streaming semantics outside MCP's current buffered response model. |
datasets download |
Writes bulk dataset bytes to an arbitrary host path — a CLI-only operator action. datasets fetch (provenance) and datasets replay --dry-run/--list-files (metadata) are exposed. |
uds subservices, uds ecu-reset, uds tester-present, uds security-seed, uds dump-dids, uds read-memory, uds auto |
Active UDS workflows that transmit invasive diagnostic requests (ECU reset, SecurityAccess seed collection, DID/memory extraction, ranged service/subfunction enumeration, and a multi-id reconnaissance chain). More intrusive than the single-broadcast uds scan; kept CLI-only operator actions behind the active-transmit safety gate (docs/design/uds-active-workflows.md). The reference uds services catalog stays exposed; its active-probe mode only activates when a CLI caller supplies an interface. |
xcp info, xcp dump |
Active XCP workflows that connect to a slave and read its capabilities / a bounded memory range. More intrusive than the single-broadcast xcp scan; kept CLI-only operator actions behind the active-transmit gate (docs/design/xcp-workflows.md). The broadcast xcp scan and the passive xcp trace/xcp read/xcp commands stay exposed. |
doip discovery, doip services, doip ecu-reset, doip tester-present, doip security-seed, doip dump-dids |
The dedicated DoIP command group is active network egress to an arbitrary host (UDP vehicle-identification discovery + TCP diagnostic sessions), like the doip:// target exclusion below. Kept CLI-only operator actions behind the active-transmit gate (docs/design/doip-diagnostic-workflows.md). |
fuzz identify |
Stateful, multi-round human-in-the-loop replay/narrowing workflow: each invocation replays a bisected window and the operator records an effect/no-effect observation before re-invoking. Does not map to a single buffered tool call; kept CLI-only behind the active-transmit gate (docs/design/fuzz-identify.md). |
The uds_scan / uds_trace tools are exposed for CAN interfaces, but a
doip:// target is a target-level exclusion: DoIP routes the workflow over
active TCP egress to an arbitrary network host, which (like cannelloni send) is
a CLI-only operator action. The tools refuse a doip:// interface with code
DOIP_MCP_EXCLUDED rather than connecting.
Deferred (not yet implemented)¶
There are no deferred implemented CLI commands in the current MCP matrix.
As of this audit, every implemented command that should have MCP coverage
does; plugin toggles and dbc generate-c are intentionally excluded because
they write user/developer files. There are no missing mirrors, orphan tools,
or ungated active-transmit MCP tools.
Response Envelope¶
Every tool call returns a single TextContent item whose text field is a JSON object with the canonical command result shape:
{
"ok": true,
"command": "<cli-command-string>",
"data": { "events": [...], ... },
"warnings": [],
"errors": []
}
Error responses set "ok": false and populate errors with structured error objects (code, message, optional hint), matching CLI exit-code semantics exactly.
Architecture¶
Agent / MCP client
│ stdio (JSON-RPC 2.0)
▼
canarchy mcp serve
└─ mcp_server.py
├─ list_tools() → returns _TOOLS catalogue
└─ call_tool(name, args) [async]
├─ _build_argv(name, args) → CLI argv list
└─ asyncio.to_thread(execute_command, argv)
└─ execute_command(argv) → CommandResult [thread pool]
│ .to_payload()
▼
TextContent(JSON)
The server delegates directly to execute_command() from cli.py, so all validation, error handling, and output formatting logic is shared with the CLI. No protocol logic is duplicated.
execute_command runs in a thread pool via asyncio.to_thread so the asyncio event loop remains live during file I/O. Without this, processing a large capture file would block the event loop, preventing MCP keepalive messages from being handled and causing client-side timeout errors (-32001/-32000).
Responsibilities And Boundaries¶
In scope:
- stdio MCP transport only
- buffered (non-streaming) tool responses for all commands including live-capture variants (scaffold backend returns a fixed event batch)
- a curated non-interactive CLI subset covering transport, protocol, export, session, and configuration workflows
- dataset provider metadata workflows and dry-run replay planning for dataset refs or direct URLs
Out of scope:
- HTTP/SSE transport
- streaming tool responses / MCP notifications for live capture
- authentication or access control
- plugin or custom tool registration
- exposing every implemented CLI command automatically
- exposing CANarchy skills as MCP tools, resources, prompts, or a separate MCP discovery surface in phase 1
- streaming dataset frame output through MCP; agents should use
datasets_replay_planfor preflight metadata and the CLI for actual stdout streaming