Skip to content

MCP tools, resources and transports

[!NOTE] The v1 HTTP demo API this file used to describe is archived at docs/archive/v1/mcp_api-v1-http.md. v1 is superseded (see PLAN.md §0).

@codai/axiom-mcp exposes the transactional write gate as an MCP stdio server by default, or as a Streamable HTTP server with --http (see Transports). Every tool carries annotations and an outputSchema; stdout is JSON-RPC only, logs go to stderr at warn. Roots are an explicit allowlist (--root <dir>, repeatable) — there is no cwd fallback.

The registry of record is packages/mcp/spec/tools.json; scripts/check-tool-parity.mjs fails CI when this table, packages/mcp/README.md and that file disagree.

sequenceDiagram
	participant A as Agent (MCP client)
	participant S as axiom mcp --root R
	participant FS as R/.axiom + tree
	A->>S: axiom_plan_compile { plan, root }
	S->>FS: write manifests/<hex>.json (+ CAS blobs)
	S-->>A: ManifestBundle { manifestDigest }
	A->>S: axiom_check { bundle, profile, root }
	S-->>A: CheckReport { verdict }
	A->>S: axiom_apply_dry_run { bundle, root }
	S-->>A: ApplyResult { mode: dry-run, diff }
	A->>S: axiom_apply { bundle, root, confirmDigest }
	S->>FS: 2PC — staging → journal → rename
	S-->>A: ApplyResult { status: applied, journal }
	opt something is wrong
		A->>S: axiom_rollback { root, manifestDigest }
		S->>FS: reverse-replay journal
		S-->>A: { status: rolled-back }
	end
Tool Risk Purpose Input (summary) Output (summary)
axiom_plan_validate read Validate a Plan against the Zod schema; report ERR_* codes with JSON pointers { plan } { ok, planDigest?, errors[] }
axiom_plan_compile act Compile PlanManifestBundle (JCS manifest, sha256 per artifact, inline/CAS blobs); writes only under <root>/.axiom/ when a root is given. ref sources resolve from the root’s CAS only — the tool has no network switch (ERR_NET_DISABLED; fetch with the CLI compile --allow-net) { plan, store?, root? } ManifestBundle { manifest, manifestDigest, attestation?, envelope?, blobs }
axiom_manifest_verify read Re-verify a bundle: canonical form, digest, every blob hash; with root, also the detached DSSE signatures against <root>/.axiom/trust/keys.json (signing.md) { bundle, root? } { ok, manifestDigest?, canonical, signed, missing[], errors[], signatures?: { trustFile, keyids[], findings[], ok, code? } }signatures present only when the root has a trust store; ok is false when it fails and code is ERR_SIGNATURE_MISSING (no signature at all) or ERR_SIGNATURE_INVALID
axiom_check read Run a Profile of predicates over a bundle against a root; fails closed on provider errors; with a root, verifies manifest.preImage against the tree (report.preImage: verified | drifted | unverified, drift → verdict: error) { bundle, profile?, root? } CheckReport
axiom_check_start read Same evaluation as axiom_check, returned immediately as a task (see Tasks); the bundle and root are validated synchronously, so a malformed bundle or a root outside the allowlist is an isError result, never a task { bundle, profile?, root? } { taskId, tool: "axiom_check", status: "working", pollIntervalMs, ttlMs, elapsedMs }
axiom_task_get read Poll a task; while working only the descriptor, once terminal result (completed) or error (failed | cancelled) is attached { taskId } descriptor + result?: CheckReport + error?: { code, message, details? }; unknown/expired → ERR_TASK_NOT_FOUND
axiom_task_cancel act Abort a working task — every running guard process tree is killed, the task ends cancelled with error.code = ERR_TASK_CANCELLED; idempotent on terminal tasks { taskId } descriptor
axiom_plan_begin act Open a chunked plan session (below) with the Plan header — everything except artifacts; the header is validated by the tool’s input schema { name, intent, profile?, capabilities?, checks?, counter?, metadata? } { sessionId, artifacts: 0, bytes: 0, limits: { maxArtifacts, maxBytes }, ttlMs }
axiom_plan_add act Append a chunk of Plan.artifacts[] (validated with PlanArtifactSchema; each call ≤ 4 MiB, inline content ≤ 256 KiB per artifact) { sessionId, artifacts[] } session descriptor; duplicate path → ERR_INVALID_PLAN (with path), sealed/over-budget → ERR_PLAN_SESSION_STATE, unknown session → ERR_TASK_NOT_FOUND
axiom_plan_seal act Assemble header + artifacts into one Plan and compile it through the same code path as axiom_plan_compile (options identical, manifest stored under <root>/.axiom/manifests when a root is given); the session is consumed even on failure { sessionId, store?, root? } ManifestBundle
axiom_apply_dry_run read Containment + pre-image check + staging + unified diff, no user files touched { bundle, root, profile? } ApplyResult { mode: "dry-run", diff, files[] }
axiom_apply destructive Two-phase commit: stage → journal → rename; requires confirmDigest === manifestDigest; single writer via .axiom/lock { bundle, root, profile?, confirmDigest } ApplyResult
axiom_rollback destructive Reverse-replay the journal of an applied manifest, scoped to its recorded paths { root, manifestDigest } { manifestDigest, status, phase, steps[], root }
axiom_manifest_diff read Structural diff between two manifests (added/removed/changed artifacts) { a, b } (bundle or sha256: ref) { added[], removed[], changed[] }
axiom_axm_parse read Parse .axm v2 DSL text into a Plan; diagnostics carry 1-based {line, column} ranges and ERR_* codes; plan present only when error-free (parser loaded lazily) { source } { plan?, diagnostics[] }
axiom_roots_list read List the allowlisted roots the server may touch {} { roots[] }
axiom_repo_snapshot read Deterministic, content-addressed inventory of a root (snapshot.md): regular files and symlinks as { path, bytes, sha256?, mode, kind } sorted by code point, snapshotDigest = sha256(JCS(body)); honours the root .gitignore, always skips .git/ and .axiom/, never follows symlinks or leaves the root; globs containing ..ERR_CONTAINMENT { root?, include?[], exclude?[], maxFiles?, maxBytes?, respectGitignore?, withContentDigest? } RepoSnapshot { apiVersion, kind, root: { kind: "relative" }, snapshotDigest, body: { files[], truncated, counts: { files, bytes } } } — text summary is { snapshotDigest, counts, truncated, paths[≤20] }

Risk classes are derived from the MCP annotations (readOnlyHint → READ, destructiveHint → SENSITIVE, otherwise ACT). packages/mcp/spec/codai-tools.json re-emits the same registry in the entry shape of codai’s packages/agent-core/spec/tools-v2.json so codai agents can gate these tools under their APPROVAL_MATRIX — see integration/codai.md.

CLI-only verbs (no MCP tool): axiom migrate v1 <manifest.json> lifts an AXIOM 1.0.x manifest into a v2 Plan (migrate.md) — a one-off maintenance step that belongs to the operator, not to an agent’s tool surface. Its code is a lazy chunk (dist/migrate-lazy.js).

Deliberately not tools. axiom gc (CAS garbage collection, cas.md) and network fetching of ref sources (compile --allow-net, plan-format.md) are CLI-only: both are operator decisions (disk reclamation, egress), so an agent cannot trigger them through the server. The full verb list is in cli.md.

MCP SDK v2 carries the io.modelcontextprotocol/tasks wire vocabulary but no runtime: tasks/* are excluded from the typed method surface and the 2026-07-28 tools/call codec rejects a CreateTaskResult. AXIOM therefore models long-running work as ordinary toolsaxiom_check_start → poll axiom_task_get every pollIntervalMs (2 s) until status ∈ {completed, failed, cancelled}, axiom_task_cancel to abort — which works on every client that can call a tool (Copilot, codai agent-core, SDK v1 and v2) and on both wire eras. This is what unblocks brivio’s full guard suite: guard.external.timeoutMs now accepts up to 15 min (was 60 s), and a task outlives the client’s per-call timeout (SDK default 60 s).

Semantics: tasks live in the server process — shared by every connection (stdio) or request (HTTP) that process serves, never written to disk; a restart forgets them. Finished tasks are pollable for ttlMs (10 min) then dropped (ERR_TASK_NOT_FOUND); at most 8 tasks run concurrently (ERR_EBUSY). A task’s result is exactly the CheckReport a synchronous axiom_check would have returned, and it is stored under <root>/.axiom/reports the same way. Cancelling sends the abort signal into runChecks — each running guard is killed (taskkill /T on Windows, SIGKILL elsewhere) and reports a provider ERR_TASK_CANCELLED finding. Stopping the server aborts every working task. A synchronous axiom_check still runs guards of any length; the only limit is the client’s own timeout, which is why long suites should use axiom_check_start.

Every call is capped at 4 MiB of JSON (ERR_BUNDLE_TOO_LARGE). A Plan bigger than that is built server-side: axiom_plan_begin (header) → axiom_plan_add × n (artifact chunks, unique paths across chunks) → axiom_plan_seal (compile). Sessions hold at most 2000 artifacts / 64 MiB, expire after 30 min idle, and at most 16 are open per process. Sealing calls the same compilePlan as axiom_plan_compile with the same options, so the sealed manifestDigest is identical to a one-shot compile of the assembled Plan — a fast-check property in packages/mcp/src/tasks.test.ts proves it for arbitrary plans and arbitrary chunkings. Inline blobs are still capped at 4 MiB per bundle (invariant 2); a large sealed Plan needs store: "cas" and therefore a root.

axiom://journal/<root-id> (recent journal entries), axiom://profile/<name> (built-in check profiles: default, strict, permissive), axiom://emitters (static list of {emitter, version, template, description} rows for the template emitters compiled into this server — currently web@2.0.0; see emitters.md). Also axiom://manifest/<sha>, axiom://report/<sha>, axiom://applied/<sha> (stored bundles, reports and results) and axiom://schema/<Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal|RepoSnapshot>.

Transport Start Notes
stdio (default) axiom mcp --root <dir> [--wire 2026|2025|2026-only] JSON-RPC on stdout, JSON-line logs on stderr.
Streamable HTTP axiom mcp --root <dir> --http <host:port> [--http-token-env NAME] [--wire …] POST/GET/DELETE /mcp, GET /health; --http 0 = random loopback port, URL in the http listening stderr log line (--log-level info).

Since 2.2.0 the server is built on MCP TypeScript SDK v2 (@modelcontextprotocol/server 2.0.0) and speaks two eras from one entry point:

Era Revisions Handshake How AXIOM serves it
modern 2026-07-28 none — every request carries a _meta envelope (io.modelcontextprotocol/protocolVersion, clientInfo); server/discover advertises the server; no Mcp-Session-Id default. stdio: serveStdio pins the connection on its opening exchange. HTTP: createMcpHandler builds one server instance per request; tools/list, resources/*, server/discover results carry ttlMs/cacheScope (SEP-2549) from AXIOM’s static cache hints (tools/list 5 min public, resources/read 24 h public — digests are immutable, resources/list 10 s private).
legacy 2024-10-072025-11-25 initialize request; HTTP sessions via Mcp-Session-Id served from the same factory (--wire 2026, the default, and --wire 2025): the SDK pins a stdio connection to the legacy era when it opens with initialize; over HTTP, isLegacyRequest routes claim-less traffic to the sessionful transport described below. --wire 2026-only refuses these openings with the SDK’s unsupported-protocol-version error.

A client on SDK v2 chooses its era with versionNegotiation ({ mode: 'auto' } probes and lands on modern; the default is the 2025 handshake). Clients still on SDK v1 keep working unchanged — they only ever send initialize. The SDK is reached through one seam, packages/mcp/src/adapter.ts (guard check-sdk-adapter), and lives in the lazy chunks dist/mcp-lazy.js / dist/http-lazy.js, so compile/verify/gate/apply never load it.

HTTP rules (v2-architecture §5.4): bind is loopback (127.0.0.1) unless a host is given; a non-loopback host requires a bearer token from the env var named by --http-token-env (default AXIOM_HTTP_TOKEN) or the server refuses to start (ERR_INTERNAL, exit 2). Clients send Authorization: Bearer <token> (constant-time compare; 401 + WWW-Authenticate otherwise). Legacy (2025-era) traffic: one WebStandardStreamableHTTPServerTransport + one server instance per session (Mcp-Session-Id, UUID); a non-initialize request without the header is 400, an unknown/expired id is 404; idle sessions are evicted after 30 min. Modern (2026-07-28) traffic has no sessions. DNS-rebinding protection (Host allowlist) is on for loopback binds; bodies over 4 MiB are 413. The transport is a lazy chunk (dist/http-lazy.js) built on node:http only — the stdio path and the bundle-size budget are unaffected.

Conformance: packages/conformance starts axiom mcp --http 127.0.0.1:0 and runs @modelcontextprotocol/conformance server --url … --expected-failures baseline.yml in CI (ubuntu). The baseline lists the scenarios AXIOM fails by design (prompts, logging, subscribe, sampling, elicitation, progress, non-text content, test:///test_* fixtures); the run fails on any unexpected failure and on any stale baseline entry. Conformance 0.1.16 scores the 2025 eras only (--spec-version2025-11-25), so it exercises the legacy leg; the 2026-07-28 leg is covered by the SDK-v2 client tests in packages/mcp/src/http.test.ts / cli.test.ts (pinned 2026-07-28, auto, and --wire 2026-only rejection).

Every failure is { code: ErrorCode, message, details? } where code is a member of ERROR_CODES in packages/schema/src/errors.ts. Clients and tests branch on code, never on message. The table: error-codes.md.


See also

  • CLI reference — the same engines from the command line, plus the CLI-only verbs
  • Harnessesmcp.json / config snippets for each client
  • Trust model — roots allowlist, confirmDigest, what an agent cannot trigger
  • Error codes — the closed enum every isError result draws from