@codai/axiom-mcp 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,31 @@
1
1
  # @codai/axiom-mcp
2
2
 
3
- MCP server (stdio) and CLI for AXIOM v2 — the transactional write gate for coding agents:
3
+ MCP server (stdio or Streamable HTTP) and CLI for AXIOM v2 — the transactional write gate for coding agents:
4
4
  `Plan` → canonical `ManifestBundle` → set-level checks → hash-gated two-phase `apply` → journal.
5
- `dist/cli.js` (thin entry) + `dist/cli-main.js` (lazy-loaded engines, SDK and zod bundled in); no runtime dependencies.
5
+ `dist/cli.js` (thin entry) + `dist/cli-main.js` (lazy-loaded engines; the MCP SDK v2 and zod are
6
+ bundled into the `mcp-lazy` / `http-lazy` chunks); no runtime dependencies.
6
7
 
7
8
  ## Install & run
8
9
 
9
10
  ```sh
10
11
  npx @codai/axiom-mcp mcp --root /abs/path/to/repo # stdio MCP server
12
+ npx @codai/axiom-mcp mcp --root /abs/path/to/repo --http 127.0.0.1:3411 # Streamable HTTP at /mcp
11
13
  npx @codai/axiom-mcp --help # CLI verbs
12
14
  ```
13
15
 
14
16
  `--root` may repeat. Every tool `root` argument must equal or lie inside one of them; with exactly one
15
17
  root it is the default. There is **no** env-var or `cwd` fallback (`ERR_ROOT_REQUIRED` / `ERR_ROOT_NOT_ALLOWED`).
16
18
 
19
+ ### Protocol revisions — `--wire 2026|2025|2026-only`
20
+
21
+ Built on MCP TypeScript SDK **v2** (`@modelcontextprotocol/server` 2.0.0). The server speaks the
22
+ **2026-07-28** revision (no `initialize`, per-request `_meta` envelope, `server/discover`,
23
+ `ttlMs`/`cacheScope` on list results) **and** the 2025-era revisions (`initialize` handshake,
24
+ HTTP sessions) from the same entry — the SDK pins each stdio connection, or routes each HTTP
25
+ request, to the era the client opened with. `--wire 2026` (default) and `--wire 2025` both serve
26
+ both; `--wire 2026-only` refuses 2025 openings with the unsupported-protocol-version error. Clients
27
+ on SDK v1 need no change. Details: [docs/mcp_api.md](../../docs/mcp_api.md#protocol-revisions---wire-d-19).
28
+
17
29
  ### VS Code — `.vscode/mcp.json`
18
30
 
19
31
  ```json
@@ -28,6 +40,33 @@ root it is the default. There is **no** env-var or `cwd` fallback (`ERR_ROOT_REQ
28
40
  }
29
41
  ```
30
42
 
43
+ ### Streamable HTTP — `--http <host:port>`
44
+
45
+ ```sh
46
+ axiom mcp --root /abs/repo --http 127.0.0.1:3411 # loopback, no token needed
47
+ axiom mcp --root /abs/repo --http 0 # random port; URL logged at info level
48
+ AXIOM_HTTP_TOKEN=$(openssl rand -hex 32) axiom mcp --root /abs/repo --http 0.0.0.0:3411 --log-level info
49
+ ```
50
+
51
+ - Endpoints: `POST /mcp` (2026-07-28 requests are served statelessly; a 2025 `initialize` opens a
52
+ session and returns `Mcp-Session-Id`, which every later 2025 request must send), `GET /mcp`
53
+ (standalone SSE stream, one per 2025 session), `DELETE /mcp` (close the session), `GET /health` →
54
+ `{ ok, name, version }` (unauthenticated). Anything else is `404` JSON.
55
+ - **Loopback by default.** A non-loopback host **refuses to start** unless a bearer token is present in
56
+ the env var named by `--http-token-env <NAME>` (default `AXIOM_HTTP_TOKEN`, ≥ 16 chars). Clients send
57
+ `Authorization: Bearer <token>`; the compare is constant-time. A token is optional on loopback.
58
+ - DNS-rebinding protection is on for loopback binds (`Host` must be `<host>:<port>`, `localhost:<port>`
59
+ or `127.0.0.1:<port>`; otherwise `403`). Request bodies over 4 MiB are `413`.
60
+ - 2025 sessions idle for 30 minutes are evicted; each session (and each 2026 request) has its own
61
+ server instance from one factory (roots, discovered sub-roots and guard settings are shared). The
62
+ transport lives in `dist/http-lazy.js`, loaded only with `--http`, and is plain `node:http` — no
63
+ express/hono at runtime.
64
+ - VS Code: `{ "type": "http", "url": "http://127.0.0.1:3411/mcp" }`; add
65
+ `"headers": { "Authorization": "Bearer ${input:axiom-token}" }` when a token is set.
66
+
67
+ Conformance: `packages/conformance` runs `@modelcontextprotocol/conformance server` against this transport
68
+ in CI with an expected-failures baseline (`packages/conformance/baseline.yml`).
69
+
31
70
  ### Claude Desktop — `claude_desktop_config.json`
32
71
 
33
72
  ```json
@@ -44,14 +83,21 @@ root it is the default. There is **no** env-var or `cwd` fallback (`ERR_ROOT_REQ
44
83
  |---|---|---|---|
45
84
  | `axiom_plan_validate` | READ | `{ plan }` | `{ ok, planDigest?, errors[] }` |
46
85
  | `axiom_plan_compile` | ACT | `{ plan, store?: inline\|cas, root? }` | `ManifestBundle` (writes only under `<root>/.axiom/` — CAS blobs and the stored manifest — when a root is given) |
47
- | `axiom_manifest_verify` | READ | `{ bundle }` | `{ ok, manifestDigest, canonical, signed, missing[], errors[] }` |
48
- | `axiom_check` | READ | `{ bundle, profile?, root? }` | `CheckReport` (`verdict: pass\|fail\|error`) |
86
+ | `axiom_manifest_verify` | READ | `{ bundle, root? }` | `{ ok, manifestDigest, canonical, signed, missing[], errors[], signatures?: { trustFile, keyids[], findings[], ok, code? } }` — `signatures` only when `root` has `.axiom/trust/keys.json`; `code` is `ERR_SIGNATURE_MISSING` \| `ERR_SIGNATURE_INVALID` when not `ok` |
87
+ | `axiom_check` | READ | `{ bundle, profile?, root? }` | `CheckReport` (`verdict: pass\|fail\|error`, `preImage: verified\|drifted\|unverified`) |
88
+ | `axiom_check_start` | READ | `{ bundle, profile?, root? }` | `{ taskId, tool, status: "working", pollIntervalMs, ttlMs, elapsedMs }` — same evaluation as `axiom_check`, returned immediately as a **task** so long `guard.external` suites (up to 15 min per guard) outlive the client's per-call timeout (S-406 / D-24) |
89
+ | `axiom_task_get` | READ | `{ taskId }` | descriptor + `result: CheckReport` once `completed`, or `error: { code, message }` once `failed`/`cancelled`; unknown/expired id → `ERR_TASK_NOT_FOUND` |
90
+ | `axiom_task_cancel` | ACT | `{ taskId }` | descriptor; kills every running guard tree, task ends `cancelled` with `ERR_TASK_CANCELLED` (idempotent on terminal tasks) |
91
+ | `axiom_plan_begin` | ACT | Plan header: `{ name, intent, profile?, capabilities?, checks?, counter?, metadata? }` | `{ sessionId, artifacts: 0, bytes: 0, limits: { maxArtifacts, maxBytes }, ttlMs }` — opens a **chunked plan session** for Plans whose JSON would exceed the 4 MiB call cap |
92
+ | `axiom_plan_add` | ACT | `{ sessionId, artifacts[] }` (each call ≤ 4 MiB) | session descriptor; duplicate path across chunks → `ERR_INVALID_PLAN`, over budget (2000 artifacts / 64 MiB) or sealed → `ERR_PLAN_SESSION_STATE` |
93
+ | `axiom_plan_seal` | ACT | `{ sessionId, store?: inline\|cas, root? }` | `ManifestBundle` — compiled by the same code path as `axiom_plan_compile`, so the digest equals a one-shot compile of the assembled Plan (property-tested); the session is consumed |
49
94
  | `axiom_apply_dry_run` | READ | `{ bundle, root, profile? }` | `ApplyResult{mode:"dry-run", diff}` |
50
95
  | `axiom_apply` | SENSITIVE | `{ bundle, root, profile?, confirmDigest }` | `ApplyResult` |
51
96
  | `axiom_rollback` | SENSITIVE | `{ root, manifestDigest }` | `{ status:"rolled-back", phase, steps }` |
52
97
  | `axiom_manifest_diff` | READ | `{ a: bundle\|"sha256:…", b }` | `{ added[], removed[], changed[] }` |
53
98
  | `axiom_axm_parse` | READ | `{ source }` (`.axm` text) | `{ plan?, diagnostics: [{ severity, code, message, range: { start: {line, column}, end } }] }` |
54
99
  | `axiom_roots_list` | READ | `{}` | `{ roots: [{ path, writable, hasGit }] }` |
100
+ | `axiom_repo_snapshot` | READ | `{ root?, include?[], exclude?[], maxFiles? (20000, cap 50000), maxBytes? (64 MiB), respectGitignore? (true), withContentDigest? (true) }` | `RepoSnapshot { snapshotDigest, body: { files: [{ path, bytes, sha256?, mode, kind }], truncated, counts } }` — sorted, no timestamps/absolute paths; `.git/`, `.axiom/` always skipped; symlinks recorded, never followed (`docs/snapshot.md`) |
55
101
 
56
102
  Every tool carries MCP `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`)
57
103
  and an `outputSchema`; `structuredContent` is the full result, `content[0].text` a small summary (digest,
@@ -62,7 +108,8 @@ codai's `packages/agent-core/spec/tools-v2.json` entry shape (`{ name, risk, des
62
108
  see `docs/integration/codai.md`.
63
109
 
64
110
  Resources: `axiom://manifest/{sha}`, `axiom://report/{sha}`, `axiom://applied/{sha}`,
65
- `axiom://profile/{name}`, `axiom://schema/{Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal}`.
111
+ `axiom://profile/{name}`, `axiom://schema/{Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal|RepoSnapshot}`,
112
+ `axiom://emitters` (template emitters available to `axiom_plan_compile` — `web@2.0.0`, see `docs/emitters.md`).
66
113
 
67
114
  ## Trust model
68
115
 
@@ -72,6 +119,10 @@ Resources: `axiom://manifest/{sha}`, `axiom://report/{sha}`, `axiom://applied/{s
72
119
  Pre-apply checks run against the profile (default `default`, or `<root>/.axiom/profiles/<name>.json`);
73
120
  a non-`pass` verdict aborts with `ERR_CHECKS_FAILED` before any write.
74
121
  - Payloads over 4 MiB are rejected up front (`ERR_BUNDLE_TOO_LARGE`).
122
+ - Tasks and plan sessions live in the server **process** (shared by every connection/request the
123
+ process serves; never on disk). A restart forgets them; finished tasks stay pollable for 10 min, idle
124
+ sessions expire after 30 min; at most 8 tasks run concurrently (`ERR_EBUSY` beyond that). Stopping
125
+ the server aborts every running task and kills its guard trees.
75
126
  - `.axiom/lock` makes apply single-writer per root; the journal makes it crash-safe and reversible.
76
127
  - stdout carries only JSON-RPC. Logs are JSON lines on stderr (`--log-level error|warn|info|debug`, default `warn`).
77
128
  - External guards (`guard.external`) are **off** unless the process is started with `--allow-guards`
@@ -79,45 +130,77 @@ Resources: `axiom://manifest/{sha}`, `axiom://report/{sha}`, `axiom://applied/{s
79
130
  absolute ones must be listed exactly via `--guard-allowlist <abs>` (repeatable). Guards are spawned
80
131
  with an args array (never a shell), a scrubbed environment, a wall-clock timeout, and must print
81
132
  `GuardOutput` JSON — see `docs/checks.md`.
133
+ - **Signed manifests** (`docs/signing.md`): a root can pin Ed25519 public keys in
134
+ `.axiom/trust/keys.json`; a profile with `manifest.requireSigned` then refuses unsigned, tampered or
135
+ untrusted bundles, and with `antiRollback: true` refuses any `counter ≤ .axiom/trust/state.json#lastCounter`.
136
+ `axiom_apply` advances that state only on `status: "applied"`. Private keys never enter the server:
137
+ signing is `axiom sign` with `AXIOM_SIGNING_KEY` or `--key-file`.
82
138
 
83
139
  ## CLI
84
140
 
85
141
  ```
86
142
  axiom mcp [--root <abs>]... [--allow-guards] [--guard-allowlist <abs>]... [--log-level warn]
87
- axiom compile <plan.json> [-o out.json] [--store cas --root .]
88
- axiom verify <bundle.json>
143
+ [--http <host:port>] [--http-token-env AXIOM_HTTP_TOKEN]
144
+ axiom compile <plan.json> [-o out.json] [--store cas --root .] [--allow-net [--net-allow host[,host]]] [--allow-file]
145
+ axiom verify <bundle.json> [--root .] (--root: also verify signatures against .axiom/trust/keys.json)
146
+ axiom verify <bundle.json> --tree <root> [--pre] [--attest out.intoto.json] (tree matches manifest? docs/verify-tree.md)
89
147
  axiom check <bundle.json> --root . [--profile p] [--json] [--allow-guards] [--guard-allowlist <abs>]...
90
148
  axiom apply <bundle.json> --root . [--dry-run] [--profile p] [--confirm <digest>] [--allow-guards] [--guard-allowlist <abs>]...
91
149
  axiom rollback <digest> --root .
150
+ axiom gc --root . [--dry-run] [--older-than 30d] [--keep all-manifests|journal] (CAS garbage collection; CLI only, no MCP tool)
92
151
  axiom diff <a.json> <b.json>
93
- axiom schema <Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal>
94
- axiom gate --stdin [--root <dir>] [--profile <file>] [--strict] [--log-level warn]
152
+ axiom schema <Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal|RepoSnapshot>
153
+ axiom emitters [--json]
154
+ axiom keygen [--out <dir>] [--name <label>] (ed25519; private key → <dir>/axiom-signing-<id>.key 0600, public entry → stdout)
155
+ axiom sign <bundle.json> [--key-file <path>] [-o out.json] [--root-id <id>] (key from --key-file or $AXIOM_SIGNING_KEY; --root-id = root-bound envelope)
156
+ axiom trust add <pub.json> --root . | remove <keyid> --root . | list --root . | root-id [<id>|--clear] --root .
157
+ axiom gate --stdin [--root <dir>] [--profile <file>] [--fail-open] [--no-shell-scan] [--no-root-discovery] [--log-level warn]
158
+ axiom migrate v1 <manifest.json> [-o plan.json] [--profile default] [--cas <root>] [--content <dir>] [--overwrite]
159
+ (v1 manifest → v2 Plan, lazy chunk; exit 1 = migrated with warnings — docs/migrate.md)
160
+ axiom snapshot --root . [-o snap.json] [--include <glob>]... [--exclude <glob>]... [--max-files n] [--max-bytes n] [--no-gitignore] [--no-digest]
161
+ axiom snapshot-diff <a.json> <b.json> (RepoSnapshot → { added, removed, changed })
95
162
  ```
96
163
 
97
164
  Exit codes: `0` ok · `1` verdict fail / apply failed · `2` usage or error. Non-`mcp` verbs print JSON to stdout.
98
165
 
166
+ `ref` sources (`{ type: "ref", uri, digest }`) are offline by default: a digest already in
167
+ `<root>/.axiom/cas` resolves without network, anything else is `ERR_NET_DISABLED`. `--allow-net`
168
+ fetches `https:` only (no redirects, 30 s timeout, 32 MiB cap), optionally restricted to
169
+ `--net-allow` hosts (`*.example.com` wildcards), verifies the pinned digest and stores the blob in
170
+ the CAS — a mismatch stores nothing (`ERR_DIGEST_MISMATCH`). `apply` never fetches. The MCP
171
+ `axiom_plan_compile` tool has no network switch. See [docs/plan-format.md](../../docs/plan-format.md#ref-sources)
172
+ and [docs/cas.md](../../docs/cas.md).
173
+
99
174
  ## Hook mode — `axiom gate --stdin`
100
175
 
101
176
  A PreToolUse hook for Claude Code, Copilot CLI and VS Code agent hooks. It reads **one** harness
102
177
  payload from stdin (both `{tool_name, tool_input, cwd}` and `{toolName, toolArgs, cwd}` casings;
103
178
  `toolArgs` may be a JSON string), extracts the write target(s) of `Write|Edit|MultiEdit|NotebookEdit`,
104
179
  `create_file|replace_string_in_file|insert_edit_into_file|apply_patch|multi_replace_string_in_file|edit_notebook_file`
105
- and generic `write|edit`, and runs **only** the fast predicates: containment + `RelPath` rules
106
- (`..`, `CON`, NTFS ADS `ERR_CONTAINMENT` / `ERR_PATH_*`), `path.deny`, `path.allow`,
107
- `content.noSecrets` and `content.maxBytes` on the new content when the payload carries it.
180
+ and generic `write|edit`, scans **shell** tools (`Bash`, `run_in_terminal`, …) for write
181
+ primitives (`>`, `>>`, `tee`, `rm`, `mv`, `cp`, `sed -i`, `git checkout|reset|clean`, PowerShell
182
+ `Set-Content`/`Remove-Item`, a heuristic, documented in docs/hooks.md), and runs **only** the
183
+ fast predicates: containment + `RelPath` rules (`..`, `CON`, NTFS ADS → `ERR_CONTAINMENT` /
184
+ `ERR_PATH_*`), `path.deny`, `path.allow`, `content.noSecrets` and `content.maxBytes` on the new
185
+ content when the payload carries it. **Fail-closed** (D-18): a non-answer is a deny.
108
186
 
109
187
  | outcome | exit | stdout | stderr |
110
188
  |---|---|---|---|
111
- | allow / unknown tool | `0` | — | — |
112
- | deny | `2` | `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":""}}` | `AXIOM GATE DENY <code>: <reason> (<relpath>)` |
113
- | malformed payload, stdin timeout (2 s), internal error | `0` (**fail open**) | | `AXIOM GATE WARN: …` |
114
- | same, with `--strict` | `2` | deny JSON | `AXIOM GATE DENY ERR_INTERNAL: …` |
115
-
116
- Root = payload `cwd`, else `--root`, else the process cwd (the hook is the one place where cwd is
117
- acceptable: the harness spawns the hook in the project directory and owns that value).
189
+ | allow / non-write, non-shell tool | `0` | — | — |
190
+ | deny | `2` | one object: `hookSpecificOutput{…}` (Claude) + flat `permissionDecision`/`permissionDecisionReason` (Copilot) + `axiom{verdict, code, path, toolClass, standard:"owasp-acs/0.1"}` | `AXIOM GATE DENY <code>: <reason> (<relpath>)` |
191
+ | write tool with no recognised path key | `2` | deny JSON | `AXIOM GATE DENY ERR_UNSUPPORTED_OP: …` |
192
+ | malformed payload, stdin timeout (2 s), internal error | `2` (**fail closed**) | deny JSON | `AXIOM GATE DENY ERR_INTERNAL: (fail-closed; pass --fail-open to allow)` |
193
+ | same, with `--fail-open` | `0` | — | `AXIOM GATE WARN: … — failing open (--fail-open)` |
194
+
195
+ Root = payload `cwd`, walked up to the nearest `.git` / repository `.axiom/` ancestor (never the
196
+ home dir; `--no-root-discovery` disables), else `--root`, else the process cwd (the hook is the
197
+ one place where cwd is acceptable: the harness spawns the hook in the project directory and owns
198
+ that value). Relative targets stay relative to `cwd`.
118
199
  Profile = `--profile <file>` → `<root>/.axiom/gate-profile.json` → `~/.axiom/gate-profile.json` →
119
- built-in `{ deny: [".git/**", ".axiom/**", "**/*.lock", "pnpm-lock.yaml", ".env", ".env.*", "**/node_modules/**"], noSecrets: true }`.
120
- Schema: `{ deny: string[], allow?: string[], noSecrets: boolean, maxBytes?: number }` (strict).
200
+ built-in `{ deny: [".git/**", ".axiom/**", "**/*.lock", "pnpm-lock.yaml", ".env", ".env.*", "**/node_modules/**"], noSecrets: true, pii: false }`.
201
+ Schema: `{ deny: string[], allow?: string[], noSecrets: boolean, pii: boolean, maxBytes?: number }` (strict).
202
+ `pii: true` additionally scans for personal data (`cnp`, `email`, `phoneRo`, `card`) — off by default
203
+ since S-414 (maintainer e-mails in `pyproject.toml` denied real edits; see `docs/checks.md`).
121
204
 
122
205
  `gate` is a separate lazy chunk (`dist/gate-lazy.js`, no MCP SDK): in-process p95 ≈ 5 ms per payload,
123
206
  end-to-end ≈ 150–200 ms including node startup; `check-gate-latency` guards p95 ≤ 250 ms.