Skip to content

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 schema-valid inputs fail command validation, 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 canonical CANarchy JSON tool envelope shall exceed the configured output cap (CANARCHY_MCP_MAX_RESPONSE_BYTES, default 512000 bytes, minimum 1024; smaller positive settings shall be rejected before transport startup). The cap shall measure the final serialized JSON text in UTF-8, including warnings and metadata; MCP transport wrapping and SDK-generated schema-validation or protocol errors are excluded. 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 for paths surviving in the final output (removed descendants shall not appear), plus a hint pointing at the CLI for the full result; data that cannot be reduced by list truncation shall be replaced by an actionable stub that preserves ok and a bounded command identifier; oversized diagnostics shall preserve a bounded original error code or use RESPONSE_TRUNCATED when unavailable.
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.
REQ-MCP-24 Unwanted behaviour If an MCP tool call supplies the stdin sentinel - for any parameter that the CLI resolves to a readable input path, the server shall refuse the call with error code STDIN_MCP_EXCLUDED before building the CLI argv, so no reader and no asyncio.to_thread worker is started against the JSON-RPC transport stream.
REQ-MCP-25 Event-driven When the server refuses a stdin sentinel, the response shall carry the canonical envelope (ok: false with the STDIN_MCP_EXCLUDED error object) and set the MCP isError flag, so the failure is visible both structurally and at the protocol level.
REQ-MCP-26 Ubiquitous Every tool parameter covered by REQ-MCP-24 shall document the restriction in its input-schema description; the CLI stdin pipelines (capture-info --file -, stats --file -, filter --file -, and the --stdin JSONL variants) shall remain unchanged.
REQ-MCP-27 Ubiquitous The appended restriction note shall describe only the unavailability of the - value and shall make no claim about the form a parameter's value must otherwise take, since the covered parameters include dataset refs, remote URLs, manifest file ids and session names as well as filesystem paths.
REQ-MCP-28 Event-driven When a canonical tool envelope reports ok=false, the system shall return an explicit MCP result with isError=true, retaining its JSON errors; successful envelopes shall set isError=false.
REQ-MCP-29 Unwanted behaviour If SDK input-schema validation rejects a request, the system shall retain validation and return SDK error text with isError=true before command execution.

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
session verify session_verify
session annotate session_annotate
session attach session_attach
session bundle session_bundle
session import session_import
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 (reference-only; see REQ-UDS-ACT-17)
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/verify/annotate/attach/bundle/import), 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.

Stdin Sentinel Exclusion

Each covered parameter's schema description gains this sentence at import time:

The - stdin sentinel is not accepted here: it is a CLI-only pipeline feature, and over MCP stdin carries the JSON-RPC transport. Supply an explicit value instead.

The wording is deliberately silent on what the value must otherwise be. An earlier version opened with "Must be a real filesystem path", which contradicted the parameters that accept a dataset ref, a remote URL, a manifest file id or a session name — datasets_replay_plan.source read "Dataset ref (e.g. catalog:candid) or remote candump URL. Must be a real filesystem path" and would have steered an agent away from the supported value (issue #544). One sentence appended to every covered parameter can only say what is true of all of them.

- is a documented CLI sentinel meaning "read the capture from stdin" (canarchy stats --file -, canarchy capture-info --file -, canarchy filter --file -, plus the --stdin JSONL variants). Those pipelines are unchanged and remain fully supported on the CLI.

Over MCP the same sentinel is fatal. The server owns sys.stdin as its JSON-RPC transport, so a reader opened for - consumes protocol bytes instead of capture text: the call never returns, and every later call in the session is starved behind the stolen stream (#516). This is a parameter-level exclusion, analogous to the doip:// target-level exclusion above.

The server therefore refuses - at the tool boundary, before _build_argv and therefore before any CLI reader — and before the asyncio.to_thread worker that would host one — is created. Because no worker is ever started, there is nothing to leak on cancellation or shutdown.

_STDIN_CAPABLE_PARAMS in canarchy/mcp_server.py is the authoritative registry of guarded parameters. It covers every parameter that the CLI resolves to a readable input path, i.e. every value that can reach the shared capture reader (LocalTransport._capture_file_path maps - to None, which iter_candump_file reads from sys.stdin) or the capture-info stdin branch:

Parameter Tools
file replay, sequence_replay, filter, stats, capture_info, decode, j1939_decode, j1939_pgn, j1939_spn, j1939_tp, j1939_tp_compare, j1939_dm1, j1939_faults, j1939_summary, j1939_inventory, j1939_map, j1587_decode, j2497_decode, datasets_convert, datasets_replay_plan, re_signals, re_correlate, re_anomalies, re_counters, re_entropy, re_suggest, fuzz_replay, plot, cannelloni_decode
files (array) j1939_compare, re_corpus, compare
dbc decode, encode, dbc_inspect, dbc_signals, dbc_convert, j1939_decode, j1939_pgn, j1939_spn, j1939_dm1, j1939_faults, session_save, re_anomalies, fuzz_signal, plot
capture session_save, re_match_dbc, re_shortlist_dbc
baseline re_anomalies, compare
reference / reference_dbc re_correlate / re_suggest
source export, datasets_replay_plan, datasets_replay_files
artifact / artifacts (array) session_attach / session_save
bundle session_import
corpus fuzz_payload (seed-capture file)

Directory-valued parameters (fuzz_guided.corpus, fuzz_guided.findings_dir, session_verify.root) and output paths (out, output, destination) are deliberately not guarded: neither can resolve to stdin. A contract test (test_every_path_parameter_is_stdin_guarded_or_documented) enforces that a future path parameter is either registered or explicitly exempted.

Each guarded parameter's input-schema description carries the restriction, so an agent reading list_tools learns the rule without having to trigger it.

A refusal returns the canonical envelope and sets the MCP isError flag:

{
  "ok": false,
  "command": "capture_info",
  "data": {},
  "warnings": [],
  "errors": [
    {
      "code": "STDIN_MCP_EXCLUDED",
      "message": "`file` was the stdin sentinel '-', which this tool cannot read: the MCP server owns stdin as its JSON-RPC transport stream.",
      "hint": "Pass a real file path for `file`, for example `/path/to/capture.candump`. Reading from `-` is a CLI-only pipeline feature (`canarchy stats --file -`); over MCP, write the upstream stream to a file first and call the tool with that path."
    }
  ]
}

This refusal sets isError=true, as do all canonical envelopes with ok=false, including relayed CLI failures. The structured error remains available in the JSON text.

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.

MCP tool results set isError=true whenever the canonical JSON envelope has ok=false, including command failures, excluded targets, missing active-transmit acknowledgement, and isolated unexpected exceptions. Codes, messages, and hints remain in the JSON text. Successful calls set isError=false. SDK input-schema validation runs before the command handler: missing required fields or invalid schema types return isError=true with SDK validation text rather than a CANarchy JSON envelope. Correct the input and retry; ordinary errors leave the session available for later calls.

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]
            ├─ _stdin_sentinel_param(name, args) → refuse `-` (REQ-MCP-24)
            ├─ _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_plan for preflight metadata and the CLI for actual stdout streaming
  • reading tool inputs from stdin (the - sentinel and the --stdin JSONL variants); stdin is the JSON-RPC transport, so those pipelines stay CLI-only (see Stdin Sentinel Exclusion). An inline-data parameter for small captures is a possible future addition, deliberately deferred.