@codai/axiom-mcp 1.0.23 → 2.0.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/dist/index.js ADDED
@@ -0,0 +1,904 @@
1
+ import { createRequire } from "node:module";
2
+ import { ApplyResultSchema, AxiomError, CheckReportSchema, DigestRefSchema, ErrorCodeSchema, JournalPhaseSchema, JournalSchema, ManifestBodySchema, ManifestBundleSchema, PlanSchema, ProfileSchema } from "@codai/axiom-schema";
3
+ import { z } from "zod";
4
+ import { realpath } from "node:fs";
5
+ import { access, constants, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
6
+ import * as path from "node:path";
7
+ import { promisify } from "node:util";
8
+ import { loadProfile, runChecks } from "@codai/axiom-checks";
9
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { appliedPath, apply, rollback } from "@codai/axiom-apply";
11
+ import { compilePlan, diffManifests, verifyBundle } from "@codai/axiom-plan";
12
+ //#region src/jsonschema.ts
13
+ const SCHEMA_KINDS = [
14
+ "Plan",
15
+ "Manifest",
16
+ "ManifestBundle",
17
+ "CheckReport",
18
+ "ApplyResult",
19
+ "Profile",
20
+ "Journal"
21
+ ];
22
+ const BY_KIND = {
23
+ Plan: PlanSchema,
24
+ Manifest: ManifestBodySchema,
25
+ ManifestBundle: ManifestBundleSchema,
26
+ CheckReport: CheckReportSchema,
27
+ ApplyResult: ApplyResultSchema,
28
+ Profile: ProfileSchema,
29
+ Journal: JournalSchema
30
+ };
31
+ function isSchemaKind(v) {
32
+ return typeof v === "string" && SCHEMA_KINDS.includes(v);
33
+ }
34
+ /** Draft 2020-12 JSON Schema, byte-identical to `packages/schema/schemas/<kind>.schema.json`. */
35
+ function jsonSchemaFor(kind) {
36
+ const json = z.toJSONSchema(BY_KIND[kind], {
37
+ target: "draft-2020-12",
38
+ io: "input",
39
+ unrepresentable: "any"
40
+ });
41
+ return {
42
+ $id: `https://axiom.dev/schemas/v2/${kind}.schema.json`,
43
+ title: kind,
44
+ ...json
45
+ };
46
+ }
47
+ /** Compact JSON Schema for tool input/output (spec/tools.json). */
48
+ function toolJsonSchema(schema) {
49
+ return z.toJSONSchema(schema, {
50
+ target: "draft-2020-12",
51
+ io: "input",
52
+ unrepresentable: "any"
53
+ });
54
+ }
55
+ //#endregion
56
+ //#region src/log.ts
57
+ /**
58
+ * stderr-only JSON-lines logger. stdout belongs to the MCP transport; nothing in
59
+ * this package may write there except the transport itself (and CLI verbs).
60
+ */
61
+ const LOG_LEVELS = [
62
+ "error",
63
+ "warn",
64
+ "info",
65
+ "debug"
66
+ ];
67
+ function isLogLevel(v) {
68
+ return typeof v === "string" && LOG_LEVELS.includes(v);
69
+ }
70
+ const RANK = {
71
+ error: 0,
72
+ warn: 1,
73
+ info: 2,
74
+ debug: 3
75
+ };
76
+ function createLogger(opts = {}) {
77
+ const level = opts.level ?? "warn";
78
+ const write = opts.write ?? ((line) => void process.stderr.write(line));
79
+ const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
80
+ const emit = (lvl, msg, fields) => {
81
+ if (RANK[lvl] > RANK[level]) return;
82
+ write(`${JSON.stringify({
83
+ level: lvl,
84
+ ts: now(),
85
+ msg,
86
+ ...fields
87
+ })}\n`);
88
+ };
89
+ return {
90
+ level,
91
+ error: (m, f) => emit("error", m, f),
92
+ warn: (m, f) => emit("warn", m, f),
93
+ info: (m, f) => emit("info", m, f),
94
+ debug: (m, f) => emit("debug", m, f)
95
+ };
96
+ }
97
+ const silentLogger = createLogger({ write: () => {} });
98
+ //#endregion
99
+ //#region src/roots.ts
100
+ const realpathNative = promisify(realpath.native);
101
+ const IS_WIN32 = process.platform === "win32";
102
+ async function realDir(p, code) {
103
+ let real;
104
+ try {
105
+ real = await realpathNative(p);
106
+ } catch (cause) {
107
+ throw new AxiomError(code, `root does not exist: ${p}`, {
108
+ cause,
109
+ details: { root: p }
110
+ });
111
+ }
112
+ if (IS_WIN32 && real.startsWith("\\\\?\\")) real = real.slice(4);
113
+ if (!(await stat(real)).isDirectory()) throw new AxiomError(code, `root is not a directory: ${p}`, { details: { root: p } });
114
+ return real;
115
+ }
116
+ function norm(p) {
117
+ const n = path.normalize(p).replace(/[\\/]+$/, "");
118
+ return IS_WIN32 ? n.toLowerCase() : n;
119
+ }
120
+ /** True iff `child` equals `parent` or lies inside it (case-insensitive on win32). */
121
+ function isSameOrInside(parent, child) {
122
+ const p = norm(parent);
123
+ const c = norm(child);
124
+ if (p === c) return true;
125
+ return c.startsWith(p.endsWith(path.sep) ? p : p + path.sep);
126
+ }
127
+ /** Build the policy from `--root <abs>` arguments. Each must exist and be a directory. */
128
+ async function createRootsPolicy(rootArgs) {
129
+ const roots = /* @__PURE__ */ new Set();
130
+ for (const r of rootArgs) {
131
+ if (!path.isAbsolute(r)) throw new AxiomError("ERR_ROOT_NOT_DIR", `--root must be absolute: ${r}`, { details: { root: r } });
132
+ roots.add(await realDir(r, "ERR_ROOT_NOT_DIR"));
133
+ }
134
+ return Object.freeze({ roots: Object.freeze(roots) });
135
+ }
136
+ /**
137
+ * Resolve a tool's `root` argument against the allowlist. No env fallback, no cwd:
138
+ * none requested + one root → that root; none + many → ERR_ROOT_REQUIRED;
139
+ * requested → realpath, must equal or be inside an allowlisted root → else ERR_ROOT_NOT_ALLOWED.
140
+ */
141
+ async function resolveRoot(policy, requested) {
142
+ if (requested === void 0 || requested === "") {
143
+ if (policy.roots.size === 1) {
144
+ const only = [...policy.roots][0];
145
+ return {
146
+ rootReal: only,
147
+ effectiveRoot: only
148
+ };
149
+ }
150
+ throw new AxiomError("ERR_ROOT_REQUIRED", policy.roots.size === 0 ? "server has no allowlisted roots (start with --root <dir>)" : "root is required when more than one root is allowlisted", { details: { roots: [...policy.roots] } });
151
+ }
152
+ if (!path.isAbsolute(requested)) throw new AxiomError("ERR_ROOT_NOT_ALLOWED", `root must be absolute: ${requested}`, { details: { root: requested } });
153
+ const real = await realDir(requested, "ERR_ROOT_NOT_ALLOWED");
154
+ for (const allowed of policy.roots) if (isSameOrInside(allowed, real)) return {
155
+ rootReal: real,
156
+ effectiveRoot: allowed
157
+ };
158
+ throw new AxiomError("ERR_ROOT_NOT_ALLOWED", `root is outside the allowlist: ${requested}`, { details: {
159
+ root: requested,
160
+ real,
161
+ roots: [...policy.roots]
162
+ } });
163
+ }
164
+ //#endregion
165
+ //#region src/store.ts
166
+ /** `<root>/.axiom/manifests/<hex>.json` and `<root>/.axiom/reports/<hex>.json`. */
167
+ function manifestsDir(root) {
168
+ return path.join(root, ".axiom", "manifests");
169
+ }
170
+ function reportsDir(root) {
171
+ return path.join(root, ".axiom", "reports");
172
+ }
173
+ function hexOf(ref) {
174
+ return ref.slice(7);
175
+ }
176
+ function toDigestRef(shaOrRef) {
177
+ const ref = shaOrRef.startsWith("sha256:") ? shaOrRef : `sha256:${shaOrRef}`;
178
+ const parsed = DigestRefSchema.safeParse(ref);
179
+ if (!parsed.success) throw new AxiomError("ERR_NOT_FOUND", `not a sha256 digest: ${shaOrRef}`);
180
+ return parsed.data;
181
+ }
182
+ async function writeJsonAtomic(file, value) {
183
+ await mkdir(path.dirname(file), { recursive: true });
184
+ const tmp = `${file}.tmp-${process.pid}`;
185
+ await writeFile(tmp, JSON.stringify(value), "utf8");
186
+ await rename(tmp, file);
187
+ }
188
+ async function readJsonOrUndefined(file) {
189
+ try {
190
+ return JSON.parse(await readFile(file, "utf8"));
191
+ } catch (err) {
192
+ if (err.code === "ENOENT") return void 0;
193
+ throw err;
194
+ }
195
+ }
196
+ async function saveManifest(root, bundle) {
197
+ const file = path.join(manifestsDir(root), `${hexOf(bundle.manifestDigest)}.json`);
198
+ await writeJsonAtomic(file, bundle);
199
+ return file;
200
+ }
201
+ async function saveReport(root, report) {
202
+ const file = path.join(reportsDir(root), `${hexOf(report.manifestDigest)}.json`);
203
+ await writeJsonAtomic(file, report);
204
+ return file;
205
+ }
206
+ /** Search every root (allowlisted + seen) for a stored bundle. */
207
+ async function loadManifest(roots, ref) {
208
+ for (const root of roots) {
209
+ const raw = await readJsonOrUndefined(path.join(manifestsDir(root), `${hexOf(ref)}.json`));
210
+ if (raw !== void 0) return ManifestBundleSchema.parse(raw);
211
+ }
212
+ }
213
+ async function loadReport(roots, ref) {
214
+ for (const root of roots) {
215
+ const raw = await readJsonOrUndefined(path.join(reportsDir(root), `${hexOf(ref)}.json`));
216
+ if (raw !== void 0) return CheckReportSchema.parse(raw);
217
+ }
218
+ }
219
+ async function loadApplied(roots, ref) {
220
+ for (const root of roots) {
221
+ const raw = await readJsonOrUndefined(appliedPath(root, ref));
222
+ if (raw !== void 0) return ApplyResultSchema.parse(raw);
223
+ }
224
+ }
225
+ async function listStored(roots, sub) {
226
+ const out = [];
227
+ for (const root of roots) {
228
+ let names;
229
+ try {
230
+ names = await readdir(path.join(root, ".axiom", sub));
231
+ } catch {
232
+ continue;
233
+ }
234
+ for (const n of names) {
235
+ const m = /^([0-9a-f]{64})\.json$/.exec(n);
236
+ if (m?.[1] !== void 0) out.push({
237
+ root,
238
+ sha: m[1]
239
+ });
240
+ }
241
+ }
242
+ return out;
243
+ }
244
+ //#endregion
245
+ //#region src/tools.ts
246
+ /** Hard cap on any single `bundle`/`plan` argument, measured as UTF-8 JSON bytes (§(f) payload size). */
247
+ const BUNDLE_BYTES_MAX = 4194304;
248
+ /** Findings/errors echoed in the text summary. */
249
+ const SUMMARY_LIST_MAX = 20;
250
+ const READ = {
251
+ readOnlyHint: true,
252
+ destructiveHint: false,
253
+ idempotentHint: true,
254
+ openWorldHint: false
255
+ };
256
+ /** Writes only under `<root>/.axiom/` (CAS blobs, stored manifests) — never the working tree. */
257
+ const ACT = {
258
+ readOnlyHint: false,
259
+ destructiveHint: false,
260
+ idempotentHint: true,
261
+ openWorldHint: false
262
+ };
263
+ const WRITE = {
264
+ readOnlyHint: false,
265
+ destructiveHint: true,
266
+ idempotentHint: true,
267
+ openWorldHint: false
268
+ };
269
+ function riskClassOf(a) {
270
+ if (a.readOnlyHint) return "READ";
271
+ return a.destructiveHint ? "SENSITIVE" : "ACT";
272
+ }
273
+ function defineTool(def) {
274
+ return {
275
+ ...def,
276
+ riskClass: riskClassOf(def.annotations)
277
+ };
278
+ }
279
+ const LooseObject = z.record(z.string(), z.unknown());
280
+ const RootArg = z.string().optional().describe("Absolute repository root; must equal or lie inside an allowlisted --root");
281
+ const ProfileArg = z.string().optional().describe("Profile name (builtin default|strict|permissive, or <root>/.axiom/profiles/<name>.json)");
282
+ /** Reject oversized payloads before any deeper parsing. */
283
+ function guardPayloadSize(label, value) {
284
+ const bytes = Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
285
+ if (bytes > 4194304) throw new AxiomError("ERR_BUNDLE_TOO_LARGE", `${label} is ${bytes} bytes; max ${BUNDLE_BYTES_MAX}`, { details: {
286
+ bytes,
287
+ max: BUNDLE_BYTES_MAX
288
+ } });
289
+ }
290
+ function parseBundle(raw) {
291
+ guardPayloadSize("bundle", raw);
292
+ const parsed = ManifestBundleSchema.safeParse(raw);
293
+ if (!parsed.success) throw new AxiomError("ERR_INVALID_MANIFEST", "bundle does not match ManifestBundleSchema", { details: { issues: parsed.error.issues.slice(0, 20).map((i) => ({
294
+ path: i.path.map(String).join("."),
295
+ message: i.message
296
+ })) } });
297
+ return parsed.data;
298
+ }
299
+ async function profileFor(ctx, bundle, name, rootReal) {
300
+ const searchDirs = rootReal === void 0 ? [] : [path.join(rootReal, ".axiom", "profiles")];
301
+ const profileName = name ?? bundle.manifest.profile;
302
+ ctx.log.debug("profile", {
303
+ name: profileName,
304
+ searchDirs
305
+ });
306
+ return loadProfile(profileName, { searchDirs });
307
+ }
308
+ async function checkBundle(ctx, bundle, profileName, rootReal) {
309
+ const opts = {
310
+ bundle,
311
+ profile: await profileFor(ctx, bundle, profileName, rootReal),
312
+ checks: bundle.manifest.checks,
313
+ ...ctx.guards
314
+ };
315
+ if (rootReal !== void 0) {
316
+ opts.root = rootReal;
317
+ opts.casDir = path.join(rootReal, ".axiom", "cas");
318
+ }
319
+ const report = await runChecks(opts);
320
+ if (rootReal !== void 0) {
321
+ await saveReport(rootReal, report);
322
+ ctx.seenRoots.add(rootReal);
323
+ }
324
+ return report;
325
+ }
326
+ /** Optional root: explicit → allowlist check; absent → the single root if there is one, else none. */
327
+ async function optionalRoot(ctx, requested) {
328
+ if (requested !== void 0 && requested !== "") return (await resolveRoot(ctx.policy, requested)).rootReal;
329
+ if (ctx.policy.roots.size === 1) return (await resolveRoot(ctx.policy)).rootReal;
330
+ }
331
+ function countBy(items, key) {
332
+ const out = {};
333
+ for (const it of items) out[key(it)] = (out[key(it)] ?? 0) + 1;
334
+ return out;
335
+ }
336
+ const IssueSchema = z.object({
337
+ path: z.string(),
338
+ message: z.string(),
339
+ code: z.string().optional()
340
+ });
341
+ const PlanValidateOutput = z.object({
342
+ ok: z.boolean(),
343
+ planDigest: DigestRefSchema.optional(),
344
+ errors: z.array(IssueSchema)
345
+ });
346
+ const ManifestVerifyOutput = z.object({
347
+ ok: z.boolean(),
348
+ manifestDigest: DigestRefSchema.optional(),
349
+ canonical: z.boolean(),
350
+ signed: z.boolean(),
351
+ missing: z.array(z.string()),
352
+ errors: z.array(z.object({
353
+ code: ErrorCodeSchema,
354
+ message: z.string(),
355
+ path: z.string().optional()
356
+ }))
357
+ });
358
+ const RollbackOutput = z.object({
359
+ manifestDigest: DigestRefSchema,
360
+ status: z.literal("rolled-back"),
361
+ phase: JournalPhaseSchema,
362
+ steps: z.int().nonnegative(),
363
+ root: z.string()
364
+ });
365
+ const ManifestDiffOutput = z.object({
366
+ added: z.array(z.string()),
367
+ removed: z.array(z.string()),
368
+ changed: z.array(z.object({
369
+ path: z.string(),
370
+ from: z.string().nullable(),
371
+ to: z.string().nullable()
372
+ }))
373
+ });
374
+ const RootsListOutput = z.object({ roots: z.array(z.object({
375
+ path: z.string(),
376
+ writable: z.boolean(),
377
+ hasGit: z.boolean()
378
+ })) });
379
+ const Pos = z.object({
380
+ line: z.int().positive(),
381
+ column: z.int().positive()
382
+ });
383
+ const AxmParseOutput = z.object({
384
+ plan: PlanSchema.optional(),
385
+ diagnostics: z.array(z.object({
386
+ severity: z.enum(["error", "warning"]),
387
+ code: ErrorCodeSchema,
388
+ message: z.string(),
389
+ range: z.object({
390
+ start: Pos,
391
+ end: Pos
392
+ })
393
+ }))
394
+ });
395
+ const BundleOrRef = z.union([DigestRefSchema, LooseObject]).describe("A ManifestBundle object, or `sha256:<hex>` of a bundle stored under <root>/.axiom/manifests");
396
+ const TOOL_DEFS = [
397
+ defineTool({
398
+ name: "axiom_plan_validate",
399
+ title: "Validate a Plan",
400
+ description: "Validate a Plan against PlanSchema and, when all sources are inline, compute its planDigest. Read-only; touches no files.",
401
+ inputSchema: { plan: LooseObject.describe("Plan document (apiVersion axiom.dev/v2, kind Plan)") },
402
+ outputSchema: PlanValidateOutput,
403
+ annotations: READ,
404
+ async handler(_ctx, { plan }) {
405
+ guardPayloadSize("plan", plan);
406
+ const parsed = PlanSchema.safeParse(plan);
407
+ if (!parsed.success) return {
408
+ ok: false,
409
+ errors: parsed.error.issues.map((i) => {
410
+ const code = i.params?.code;
411
+ const out = {
412
+ path: i.path.map(String).join("."),
413
+ message: i.message
414
+ };
415
+ if (typeof code === "string") out.code = code;
416
+ return out;
417
+ })
418
+ };
419
+ try {
420
+ const { bundle } = await compilePlan(parsed.data, { store: "inline" });
421
+ return {
422
+ ok: true,
423
+ planDigest: bundle.manifest.planDigest,
424
+ errors: []
425
+ };
426
+ } catch (err) {
427
+ if (err instanceof AxiomError && err.code === "ERR_BLOB_MISSING") return {
428
+ ok: true,
429
+ errors: []
430
+ };
431
+ throw err;
432
+ }
433
+ },
434
+ summarize: (o) => ({
435
+ ok: o.ok,
436
+ planDigest: o.planDigest,
437
+ errors: o.errors.slice(0, 20)
438
+ })
439
+ }),
440
+ defineTool({
441
+ name: "axiom_plan_compile",
442
+ title: "Compile a Plan into a ManifestBundle",
443
+ description: "Compile a Plan into a content-addressed ManifestBundle (sorted artifacts, sha256 digests, in-toto planDigest). `store: cas` writes blobs under <root>/.axiom/cas instead of inlining them. When a root is given the bundle is stored under <root>/.axiom/manifests/<hex>.json so later tools can reference it by digest.",
444
+ inputSchema: {
445
+ plan: LooseObject.describe("Plan document"),
446
+ store: z.enum(["inline", "cas"]).optional().describe("Blob transport; default inline"),
447
+ root: RootArg
448
+ },
449
+ outputSchema: ManifestBundleSchema,
450
+ annotations: ACT,
451
+ async handler(ctx, { plan, store, root }) {
452
+ guardPayloadSize("plan", plan);
453
+ const rootReal = root !== void 0 || store === "cas" ? (await resolveRoot(ctx.policy, root)).rootReal : void 0;
454
+ const opts = { store: store ?? "inline" };
455
+ if (rootReal !== void 0) opts.root = rootReal;
456
+ const { bundle } = await compilePlan(plan, opts);
457
+ if (rootReal !== void 0) {
458
+ await saveManifest(rootReal, bundle);
459
+ ctx.seenRoots.add(rootReal);
460
+ }
461
+ ctx.log.info("compiled", {
462
+ manifestDigest: bundle.manifestDigest,
463
+ artifacts: bundle.manifest.artifacts.length
464
+ });
465
+ return bundle;
466
+ },
467
+ summarize: (b) => ({
468
+ manifestDigest: b.manifestDigest,
469
+ planDigest: b.manifest.planDigest,
470
+ name: b.manifest.name,
471
+ profile: b.manifest.profile,
472
+ artifacts: b.manifest.artifacts.length,
473
+ blobs: Object.keys(b.blobs).length
474
+ })
475
+ }),
476
+ defineTool({
477
+ name: "axiom_manifest_verify",
478
+ title: "Verify a ManifestBundle",
479
+ description: "Structural and content-address verification: schema, recomputed manifestDigest, every inline blob hashes to its key, attestation subject matches. Never writes.",
480
+ inputSchema: { bundle: LooseObject.describe("ManifestBundle") },
481
+ outputSchema: ManifestVerifyOutput,
482
+ annotations: READ,
483
+ async handler(_ctx, { bundle }) {
484
+ guardPayloadSize("bundle", bundle);
485
+ const r = verifyBundle(bundle);
486
+ const out = {
487
+ ok: r.ok,
488
+ canonical: r.canonical,
489
+ signed: r.signed,
490
+ missing: r.missing,
491
+ errors: r.errors
492
+ };
493
+ if (r.manifestDigest !== void 0) out.manifestDigest = r.manifestDigest;
494
+ return out;
495
+ },
496
+ summarize: (o) => ({
497
+ ok: o.ok,
498
+ manifestDigest: o.manifestDigest,
499
+ canonical: o.canonical,
500
+ missing: o.missing.length,
501
+ errors: o.errors.slice(0, 20)
502
+ })
503
+ }),
504
+ defineTool({
505
+ name: "axiom_check",
506
+ title: "Run policy checks on a bundle",
507
+ description: "Evaluate the profile's predicates (plus the manifest's own checks) against the bundle. Repo facts are read from the root when one is available and the profile allows it. Verdict `error` means a provider could not run — never a silent pass.",
508
+ inputSchema: {
509
+ bundle: LooseObject.describe("ManifestBundle"),
510
+ profile: ProfileArg,
511
+ root: RootArg
512
+ },
513
+ outputSchema: CheckReportSchema,
514
+ annotations: READ,
515
+ async handler(ctx, { bundle, profile, root }) {
516
+ return checkBundle(ctx, parseBundle(bundle), profile, await optionalRoot(ctx, root));
517
+ },
518
+ summarize: summarizeReport
519
+ }),
520
+ defineTool({
521
+ name: "axiom_apply_dry_run",
522
+ title: "Dry-run apply (stage + diff, no writes to the tree)",
523
+ description: "Stage the bundle under <root>/.axiom/staging, run pre-apply checks and produce a unified diff against the current tree. Nothing outside .axiom/ is touched. Echo the returned manifestDigest as `confirmDigest` to axiom_apply.",
524
+ inputSchema: {
525
+ bundle: LooseObject.describe("ManifestBundle"),
526
+ root: RootArg,
527
+ profile: ProfileArg
528
+ },
529
+ outputSchema: ApplyResultSchema,
530
+ annotations: READ,
531
+ async handler(ctx, { bundle, root, profile }) {
532
+ const parsed = parseBundle(bundle);
533
+ const { rootReal } = await resolveRoot(ctx.policy, root);
534
+ return await apply({
535
+ bundle: parsed,
536
+ root: rootReal,
537
+ mode: "dry-run",
538
+ preChecks: () => checkBundle(ctx, parsed, profile, rootReal)
539
+ });
540
+ },
541
+ summarize: summarizeApply
542
+ }),
543
+ defineTool({
544
+ name: "axiom_apply",
545
+ title: "Apply a bundle to the filesystem (two-phase commit)",
546
+ description: "Transactionally write the bundle into the root: pre-image verification, staging, journal, atomic renames, scoped rollback on failure. Requires `confirmDigest === bundle.manifestDigest` (echo the digest you saw in dry-run). Idempotent: re-applying an applied digest is a no-op.",
547
+ inputSchema: {
548
+ bundle: LooseObject.describe("ManifestBundle"),
549
+ root: RootArg,
550
+ profile: ProfileArg,
551
+ confirmDigest: z.string().optional().describe("Must equal bundle.manifestDigest"),
552
+ mode: z.enum(["fs", "pr"]).optional().describe("fs (default) writes files; pr additionally creates a git branch and commits exactly the touched paths (no push, no PR creation)"),
553
+ branch: z.string().optional().describe("pr mode: branch name (default axiom/<name>/<digest12>)"),
554
+ commitMessage: z.string().optional().describe("pr mode: commit message (passed to git on stdin)")
555
+ },
556
+ outputSchema: ApplyResultSchema,
557
+ annotations: WRITE,
558
+ async handler(ctx, { bundle, root, profile, confirmDigest, mode, branch, commitMessage }) {
559
+ const parsed = parseBundle(bundle);
560
+ if (confirmDigest !== parsed.manifestDigest) throw new AxiomError("ERR_CONFIRM_DIGEST_MISMATCH", "confirmDigest must equal bundle.manifestDigest", { details: {
561
+ confirmDigest: confirmDigest ?? null,
562
+ manifestDigest: parsed.manifestDigest
563
+ } });
564
+ const { rootReal } = await resolveRoot(ctx.policy, root);
565
+ const result = await apply({
566
+ bundle: parsed,
567
+ root: rootReal,
568
+ mode: mode ?? "fs",
569
+ confirmDigest,
570
+ ...branch === void 0 ? {} : { branch },
571
+ ...commitMessage === void 0 ? {} : { commitMessage },
572
+ preChecks: () => checkBundle(ctx, parsed, profile, rootReal)
573
+ });
574
+ if (result.status === "applied" || result.status === "noop") {
575
+ await saveManifest(rootReal, parsed);
576
+ ctx.seenRoots.add(rootReal);
577
+ }
578
+ ctx.log.info("apply", {
579
+ manifestDigest: parsed.manifestDigest,
580
+ status: result.status,
581
+ root: rootReal
582
+ });
583
+ return result;
584
+ },
585
+ summarize: summarizeApply
586
+ }),
587
+ defineTool({
588
+ name: "axiom_rollback",
589
+ title: "Roll back an applied manifest",
590
+ description: "Replay the journal of a committed/committing manifest in reverse: restore backups, remove created files, drop the applied marker.",
591
+ inputSchema: {
592
+ root: RootArg,
593
+ manifestDigest: z.string().describe("`sha256:<hex>` (or bare hex) of the manifest to roll back")
594
+ },
595
+ outputSchema: RollbackOutput,
596
+ annotations: WRITE,
597
+ async handler(ctx, { root, manifestDigest }) {
598
+ const ref = toDigestRef(manifestDigest);
599
+ const { rootReal } = await resolveRoot(ctx.policy, root);
600
+ const journal = await rollback(rootReal, ref);
601
+ ctx.log.info("rollback", {
602
+ manifestDigest: ref,
603
+ root: rootReal,
604
+ phase: journal.phase
605
+ });
606
+ return {
607
+ manifestDigest: ref,
608
+ status: "rolled-back",
609
+ phase: journal.phase,
610
+ steps: journal.steps.length,
611
+ root: rootReal
612
+ };
613
+ },
614
+ summarize: (o) => o
615
+ }),
616
+ defineTool({
617
+ name: "axiom_manifest_diff",
618
+ title: "Diff two manifests",
619
+ description: "Compare two manifests by artifact path and digest. Each side is a ManifestBundle or a `sha256:<hex>` reference to a bundle stored under an allowlisted root.",
620
+ inputSchema: {
621
+ a: BundleOrRef,
622
+ b: BundleOrRef
623
+ },
624
+ outputSchema: ManifestDiffOutput,
625
+ annotations: READ,
626
+ async handler(ctx, { a, b }) {
627
+ const [ba, bb] = await Promise.all([resolveBundleOrRef(ctx, a, "a"), resolveBundleOrRef(ctx, b, "b")]);
628
+ return diffManifests(ba.manifest, bb.manifest);
629
+ },
630
+ summarize: (d) => ({
631
+ added: d.added.length,
632
+ removed: d.removed.length,
633
+ changed: d.changed.length,
634
+ sample: {
635
+ added: d.added.slice(0, 20),
636
+ removed: d.removed.slice(0, 20),
637
+ changed: d.changed.slice(0, 20)
638
+ }
639
+ })
640
+ }),
641
+ defineTool({
642
+ name: "axiom_axm_parse",
643
+ title: "Parse .axm source into a Plan",
644
+ description: "Parse .axm v2 text into a Plan with 1-based {line, column} diagnostics; `plan` is present only when error-free. Read-only.",
645
+ inputSchema: { source: z.string().describe(".axm source text") },
646
+ outputSchema: AxmParseOutput,
647
+ annotations: READ,
648
+ async handler(_ctx, { source }) {
649
+ guardPayloadSize("source", source);
650
+ const { parseAxm } = await import("./axm-lazy-WdJHPy6W.js");
651
+ const r = parseAxm(source);
652
+ return r.plan === void 0 ? { diagnostics: r.diagnostics } : r;
653
+ },
654
+ summarize: (o) => ({
655
+ ok: o.plan !== void 0,
656
+ name: o.plan?.name,
657
+ diagnostics: o.diagnostics.slice(0, 20)
658
+ })
659
+ }),
660
+ defineTool({
661
+ name: "axiom_roots_list",
662
+ title: "List allowlisted roots",
663
+ description: "The frozen set of roots this server may read and write, as given by --root at startup.",
664
+ inputSchema: {},
665
+ outputSchema: RootsListOutput,
666
+ annotations: READ,
667
+ async handler(ctx) {
668
+ const roots = [];
669
+ for (const p of ctx.policy.roots) {
670
+ const [writable, hasGit] = await Promise.all([access(p, constants.W_OK).then(() => true, () => false), stat(path.join(p, ".git")).then(() => true, () => false)]);
671
+ roots.push({
672
+ path: p,
673
+ writable,
674
+ hasGit
675
+ });
676
+ }
677
+ return { roots };
678
+ },
679
+ summarize: (o) => o
680
+ })
681
+ ];
682
+ async function resolveBundleOrRef(ctx, v, label) {
683
+ if (typeof v === "string") {
684
+ const ref = toDigestRef(v);
685
+ const found = await loadManifest(/* @__PURE__ */ new Set([...ctx.policy.roots, ...ctx.seenRoots]), ref);
686
+ if (found === void 0) throw new AxiomError("ERR_NOT_FOUND", `${label}: no stored manifest for ${ref}`, { details: { ref } });
687
+ return found;
688
+ }
689
+ return parseBundle(v);
690
+ }
691
+ function summarizeReport(r) {
692
+ return {
693
+ manifestDigest: r.manifestDigest,
694
+ profile: r.profile,
695
+ verdict: r.verdict,
696
+ counts: countBy(r.findings, (f) => f.severity),
697
+ findings: r.findings.slice(0, 20).map((f) => ({
698
+ id: f.id,
699
+ severity: f.severity,
700
+ message: f.message,
701
+ path: f.path
702
+ })),
703
+ providers: r.providers,
704
+ durationMs: r.durationMs
705
+ };
706
+ }
707
+ function summarizeApply(r) {
708
+ return {
709
+ manifestDigest: r.manifestDigest,
710
+ mode: r.mode,
711
+ status: r.status,
712
+ root: r.root,
713
+ files: countBy(r.files, (f) => f.status),
714
+ diffBytes: r.diff === void 0 ? void 0 : Buffer.byteLength(r.diff, "utf8"),
715
+ journal: r.journal,
716
+ error: r.error,
717
+ sample: r.files.slice(0, 20).map((f) => ({
718
+ path: f.path,
719
+ op: f.op,
720
+ status: f.status
721
+ }))
722
+ };
723
+ }
724
+ function toolByName(name) {
725
+ return TOOL_DEFS.find((t) => t.name === name);
726
+ }
727
+ //#endregion
728
+ //#region src/server.ts
729
+ const pkg = createRequire(import.meta.url)("../package.json");
730
+ const SERVER_NAME = "axiom";
731
+ const SERVER_VERSION = pkg.version;
732
+ function toStructuredError(err) {
733
+ if (err instanceof AxiomError) return err.toJSON();
734
+ if (err instanceof z.ZodError) return {
735
+ code: "ERR_INVALID_PLAN",
736
+ message: "input does not match schema",
737
+ details: { issues: err.issues.slice(0, 20).map((i) => ({
738
+ path: i.path.join("."),
739
+ message: i.message
740
+ })) }
741
+ };
742
+ return {
743
+ code: "ERR_INTERNAL",
744
+ message: err instanceof Error ? err.message : String(err)
745
+ };
746
+ }
747
+ function errorResult(err) {
748
+ const structured = toStructuredError(err);
749
+ return {
750
+ isError: true,
751
+ content: [{
752
+ type: "text",
753
+ text: JSON.stringify(structured)
754
+ }],
755
+ structuredContent: structured
756
+ };
757
+ }
758
+ /** Wrap a tool handler: never throws; AxiomError → isError result with the closed code. */
759
+ function wrapHandler(def, ctx) {
760
+ return async (input) => {
761
+ try {
762
+ const output = await def.handler(ctx, input);
763
+ const structured = def.outputSchema.parse(output);
764
+ return {
765
+ content: [{
766
+ type: "text",
767
+ text: JSON.stringify(def.summarize(structured))
768
+ }],
769
+ structuredContent: structured
770
+ };
771
+ } catch (err) {
772
+ ctx.log.warn("tool failed", {
773
+ tool: def.name,
774
+ error: toStructuredError(err)
775
+ });
776
+ return errorResult(err);
777
+ }
778
+ };
779
+ }
780
+ function json(uri, value) {
781
+ return { contents: [{
782
+ uri,
783
+ mimeType: "application/json",
784
+ text: JSON.stringify(value, null, 2)
785
+ }] };
786
+ }
787
+ function notFound(uri) {
788
+ throw new AxiomError("ERR_NOT_FOUND", `resource not found: ${uri}`);
789
+ }
790
+ function createServer(policy, opts = {}) {
791
+ const log = opts.log ?? silentLogger;
792
+ const ctx = {
793
+ policy,
794
+ log,
795
+ seenRoots: /* @__PURE__ */ new Set()
796
+ };
797
+ if (opts.guards !== void 0) ctx.guards = opts.guards;
798
+ const server = new McpServer({
799
+ name: SERVER_NAME,
800
+ version: SERVER_VERSION
801
+ }, { capabilities: {
802
+ tools: {},
803
+ resources: {}
804
+ } });
805
+ for (const def of opts.tools ?? TOOL_DEFS) server.registerTool(def.name, {
806
+ title: def.title,
807
+ description: def.description,
808
+ inputSchema: def.inputSchema,
809
+ outputSchema: def.outputSchema,
810
+ annotations: {
811
+ title: def.title,
812
+ ...def.annotations
813
+ }
814
+ }, wrapHandler(def, ctx));
815
+ const allRoots = () => /* @__PURE__ */ new Set([...policy.roots, ...ctx.seenRoots]);
816
+ const listOf = (sub, scheme) => async () => {
817
+ return { resources: (await listStored(allRoots(), sub)).map((i) => ({
818
+ uri: `axiom://${scheme}/${i.sha}`,
819
+ name: i.sha,
820
+ mimeType: "application/json"
821
+ })) };
822
+ };
823
+ server.registerResource("manifest", new ResourceTemplate("axiom://manifest/{sha}", { list: listOf("manifests", "manifest") }), {
824
+ title: "Stored ManifestBundle",
825
+ mimeType: "application/json"
826
+ }, async (uri, { sha }) => {
827
+ const found = await loadManifest(allRoots(), toDigestRef(String(sha)));
828
+ return found === void 0 ? notFound(uri.href) : json(uri.href, found);
829
+ });
830
+ server.registerResource("report", new ResourceTemplate("axiom://report/{sha}", { list: listOf("reports", "report") }), {
831
+ title: "Last CheckReport for a manifest",
832
+ mimeType: "application/json"
833
+ }, async (uri, { sha }) => {
834
+ const found = await loadReport(allRoots(), toDigestRef(String(sha)));
835
+ return found === void 0 ? notFound(uri.href) : json(uri.href, found);
836
+ });
837
+ server.registerResource("applied", new ResourceTemplate("axiom://applied/{sha}", { list: listOf("applied", "applied") }), {
838
+ title: "ApplyResult of an applied manifest",
839
+ mimeType: "application/json"
840
+ }, async (uri, { sha }) => {
841
+ const found = await loadApplied(allRoots(), toDigestRef(String(sha)));
842
+ return found === void 0 ? notFound(uri.href) : json(uri.href, found);
843
+ });
844
+ server.registerResource("profile", new ResourceTemplate("axiom://profile/{name}", { list: async () => ({ resources: [
845
+ "default",
846
+ "strict",
847
+ "permissive"
848
+ ].map((n) => ({
849
+ uri: `axiom://profile/${n}`,
850
+ name: n,
851
+ mimeType: "application/json"
852
+ })) }) }), {
853
+ title: "Resolved check profile",
854
+ mimeType: "application/json"
855
+ }, async (uri, { name }) => {
856
+ const searchDirs = [...policy.roots].map((r) => `${r}/.axiom/profiles`);
857
+ return json(uri.href, await loadProfile(String(name), { searchDirs }));
858
+ });
859
+ server.registerResource("schema", new ResourceTemplate("axiom://schema/{kind}", {
860
+ list: async () => ({ resources: SCHEMA_KINDS.map((k) => ({
861
+ uri: `axiom://schema/${k}`,
862
+ name: k,
863
+ mimeType: "application/schema+json"
864
+ })) }),
865
+ complete: { kind: (v) => SCHEMA_KINDS.filter((k) => k.toLowerCase().startsWith(v.toLowerCase())) }
866
+ }), {
867
+ title: "JSON Schema (draft 2020-12)",
868
+ mimeType: "application/schema+json"
869
+ }, async (uri, { kind }) => {
870
+ const k = String(kind);
871
+ if (!isSchemaKind(k)) notFound(uri.href);
872
+ return { contents: [{
873
+ uri: uri.href,
874
+ mimeType: "application/schema+json",
875
+ text: JSON.stringify(jsonSchemaFor(k), null, 2)
876
+ }] };
877
+ });
878
+ log.info("server created", {
879
+ name: SERVER_NAME,
880
+ version: SERVER_VERSION,
881
+ roots: [...policy.roots]
882
+ });
883
+ return server;
884
+ }
885
+ //#endregion
886
+ //#region src/spec.ts
887
+ function buildToolsSpec() {
888
+ return TOOL_DEFS.map((t) => ({
889
+ name: t.name,
890
+ description: t.description,
891
+ riskClass: t.riskClass,
892
+ annotations: t.annotations,
893
+ inputSchema: toolJsonSchema(z.object(t.inputSchema)),
894
+ outputSchema: toolJsonSchema(t.outputSchema)
895
+ }));
896
+ }
897
+ /** Stable text form (2-space JSON + trailing newline) used both by the generator and the parity test. */
898
+ function renderToolsSpec() {
899
+ return `${JSON.stringify(buildToolsSpec(), null, 2)}\n`;
900
+ }
901
+ //#endregion
902
+ export { BUNDLE_BYTES_MAX, LOG_LEVELS, SCHEMA_KINDS, SERVER_NAME, SERVER_VERSION, SUMMARY_LIST_MAX, TOOL_DEFS, buildToolsSpec, createLogger, createRootsPolicy, createServer, isLogLevel, isSameOrInside, isSchemaKind, jsonSchemaFor, renderToolsSpec, resolveRoot, riskClassOf, toStructuredError, toolByName };
903
+
904
+ //# sourceMappingURL=index.js.map