@ema.co/machina 0.1.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.
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { pathToFileURL } from "node:url";
11
+ import { realpathSync } from "node:fs";
12
+
13
+ // src/mcp/capabilities.ts
14
+ var CAPABILITIES = [
15
+ {
16
+ name: "config",
17
+ status: "wired",
18
+ description: "Inspect eMachina's readiness (the needs/state check). Run this FIRST, and whenever a downstream tool reports a gap. Returns which capabilities are wired vs pending, plus required next steps.",
19
+ methods: [{ name: "status", description: "return the readiness snapshot" }]
20
+ },
21
+ {
22
+ name: "yield",
23
+ status: "wired",
24
+ description: "Manage harvest YIELD artifacts \u2014 the deterministic substrate. The agent RUNS the harvest procedure (intake/gather/synthesis = thinking); this tool validates/lists/reads/scaffolds the yields it produces. `validate` is a blocking gate over the canonical contract.",
25
+ methods: [
26
+ { name: "validate", description: "run the canonical validator (blocking gate)" },
27
+ { name: "list", description: "list yield slugs under .harvest/" },
28
+ { name: "get", description: "read a yield by slug" },
29
+ { name: "scaffold", description: "create a new yield from the canonical skeleton" }
30
+ ],
31
+ extraProperties: {
32
+ slug: { type: "string", description: "Yield slug (directory under .harvest/) \u2014 for get/scaffold/validate" },
33
+ path: { type: "string", description: "Explicit path to a yield .md \u2014 overrides slug for validate" }
34
+ }
35
+ },
36
+ {
37
+ name: "curate",
38
+ status: "wired",
39
+ description: "Memorialize / keep-memory-healthy over the corpus (publish, supersede, dedup, advise), powered by the owned atom kernel's submitChanges \u2014 content-addressed dedup + kernel-enforced validation (wire-conformant with canonical KS via golden vectors). Local corpus is the staging tier; org-wide COMPOSES with the ema toolkit's knowledge() by swapping the StorageBackend.",
40
+ methods: [
41
+ { name: "publish", description: "memorialize a yield (by slug) or explicit content as an atom" },
42
+ { name: "supersede", description: "publish an atom that supersedes a prior one (needs `supersedes`)" },
43
+ { name: "advise", description: "pre-flight the lib's validation on a yield without writing" },
44
+ { name: "status", description: "report curate readiness" }
45
+ ],
46
+ extraProperties: {
47
+ slug: { type: "string", description: "Yield slug to memorialize (for publish/supersede/advise)" },
48
+ name: { type: "string", description: "Atom title (if not using slug)" },
49
+ description: { type: "string", description: "Atom description (if not using slug)" },
50
+ body: { type: "string", description: "Atom body/content (if not using slug)" },
51
+ audience: { type: "string", description: "Audience token: public | internal[:slug] | tenant:slug | customer:slug" },
52
+ supersedes: { type: "string", description: "Atom id this one supersedes (for method=supersede)" }
53
+ }
54
+ },
55
+ {
56
+ name: "retrieve",
57
+ status: "wired",
58
+ description: "Search / rank the LOCAL corpus (curated atoms + .harvest yields) via the owned atom kernel's search \u2014 works offline, with or without DE. Org-wide search COMPOSES with the ema toolkit's knowledge(), not a rebuilt DE client (the scar tissue harvest set out to avoid).",
59
+ methods: [{ name: "search", description: "query the corpus (relevance-ranked, audience-filtered)" }],
60
+ extraProperties: {
61
+ query: { type: "string", description: "Search query (for method=search)" },
62
+ limit: { type: "number", description: "Max results (default 10)" },
63
+ audience: { type: "string", description: "Caller audience tokens to filter by (default public+internal)" }
64
+ }
65
+ }
66
+ ];
67
+ var WIRED = CAPABILITIES.filter((c) => c.status === "wired").map((c) => c.name);
68
+ var PENDING = CAPABILITIES.filter((c) => c.status === "pending").map((c) => c.name);
69
+
70
+ // src/mcp/tools.ts
71
+ function generateTools() {
72
+ return CAPABILITIES.map((c) => ({
73
+ name: c.name,
74
+ description: c.description,
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ method: {
79
+ type: "string",
80
+ enum: c.methods.map((m) => m.name),
81
+ description: c.methods.map((m) => `${m.name}: ${m.description}`).join(" | ")
82
+ },
83
+ ...c.extraProperties ?? {}
84
+ },
85
+ required: ["method"]
86
+ }
87
+ }));
88
+ }
89
+
90
+ // src/mcp/state.ts
91
+ import { execFileSync } from "node:child_process";
92
+ import { existsSync } from "node:fs";
93
+ import { dirname, join } from "node:path";
94
+ var VALIDATOR_REL = "skills/harvest/evals/scripts/validate_yield.py";
95
+ function findRepoRoot(start) {
96
+ let dir = start;
97
+ for (let i = 0; i < 20; i++) {
98
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, ".harvest"))) return dir;
99
+ const parent = dirname(dir);
100
+ if (parent === dir) break;
101
+ dir = parent;
102
+ }
103
+ return start;
104
+ }
105
+ function checkYieldValidator(repoRoot) {
106
+ const validatorPath = join(repoRoot, VALIDATOR_REL);
107
+ if (!existsSync(validatorPath)) {
108
+ return { ready: false, detail: `validator not found at ${VALIDATOR_REL}` };
109
+ }
110
+ try {
111
+ execFileSync("python3", ["--version"], { stdio: "ignore" });
112
+ return { ready: true, detail: `${VALIDATOR_REL} reachable via python3` };
113
+ } catch {
114
+ return { ready: false, detail: "python3 not on PATH (needed to run the yield validator)" };
115
+ }
116
+ }
117
+ function checkKnowledgeSubstrate() {
118
+ return {
119
+ ready: true,
120
+ detail: "local atom kernel owned in-process (curate/retrieve wired, offline-capable); org-wide composes with the ema MCP knowledge() at runtime"
121
+ };
122
+ }
123
+ function getState(repoRoot = findRepoRoot(process.cwd())) {
124
+ const harvestReady = existsSync(join(repoRoot, ".harvest"));
125
+ const harvestDir = {
126
+ ready: harvestReady,
127
+ detail: harvestReady ? ".harvest/ present" : ".harvest/ not found (run from the eMachina repo)"
128
+ };
129
+ const yieldValidator = checkYieldValidator(repoRoot);
130
+ const knowledgeSubstrate = checkKnowledgeSubstrate();
131
+ const requiredNextSteps = [];
132
+ if (!harvestDir.ready) {
133
+ requiredNextSteps.push("No .harvest/ found \u2014 run the server from the eMachina repo root.");
134
+ }
135
+ if (!yieldValidator.ready) {
136
+ requiredNextSteps.push(`yield(method="validate") needs python3 + ${VALIDATOR_REL}: ${yieldValidator.detail}.`);
137
+ }
138
+ return { repoRoot, harvestDir, yieldValidator, knowledgeSubstrate, requiredNextSteps };
139
+ }
140
+
141
+ // src/mcp/middleware.ts
142
+ function enrichWithState(result, _ctx) {
143
+ try {
144
+ const state = getState();
145
+ if (state.requiredNextSteps.length > 0) {
146
+ result._required_next_steps ??= state.requiredNextSteps;
147
+ }
148
+ } catch {
149
+ }
150
+ return result;
151
+ }
152
+
153
+ // src/mcp/responses.ts
154
+ function success(data = {}, hints = {}) {
155
+ return { status: "success", ...data, ...hints };
156
+ }
157
+ function error(message, hints = {}) {
158
+ return { status: "error", error: message, ...hints };
159
+ }
160
+ function clarificationNeeded(message, options, hints = {}) {
161
+ return { status: "clarification_needed", message, options, ...hints };
162
+ }
163
+ function analysis(summary, issues, next_steps, hints = {}) {
164
+ return { status: "analysis", summary, issues, next_steps, ...hints };
165
+ }
166
+ function list(key, items, hints = {}) {
167
+ return { status: "list", count: items.length, [key]: items, ...hints };
168
+ }
169
+ function singleItem(key, item, hints = {}) {
170
+ return { status: "single_item", [key]: item, ...hints };
171
+ }
172
+
173
+ // src/mcp/handlers/config/formatters.ts
174
+ function formatStatus(state) {
175
+ return success(
176
+ {
177
+ repoRoot: state.repoRoot,
178
+ // Per-capability runtime readiness (distinct from the surface definition in the registry).
179
+ readiness: {
180
+ config: { ready: true, detail: "always available" },
181
+ yield: state.yieldValidator,
182
+ curate: { ready: state.knowledgeSubstrate.ready, detail: state.knowledgeSubstrate.detail },
183
+ retrieve: { ready: state.knowledgeSubstrate.ready, detail: state.knowledgeSubstrate.detail }
184
+ },
185
+ harvest_dir: state.harvestDir,
186
+ wired: WIRED,
187
+ pending: PENDING
188
+ },
189
+ {
190
+ _next_step: state.requiredNextSteps.length ? 'Address _required_next_steps, or call a wired tool \u2014 e.g. yield(method="list").' : 'All checked capabilities ready \u2014 yield(method="list") to see the corpus.',
191
+ ...state.requiredNextSteps.length ? { _required_next_steps: state.requiredNextSteps } : {}
192
+ }
193
+ );
194
+ }
195
+
196
+ // src/mcp/handlers/config/index.ts
197
+ async function dispatchConfig(args, ctx) {
198
+ const method = args.method;
199
+ switch (method) {
200
+ case "status":
201
+ return formatStatus(getState(ctx.repoRoot));
202
+ case void 0:
203
+ return clarificationNeeded("Which operation?", ["status"], {
204
+ _tip: 'config(method="status") returns the readiness snapshot.'
205
+ });
206
+ default:
207
+ return error(`Unknown method: ${method}`, { _tip: "config supports: status." });
208
+ }
209
+ }
210
+
211
+ // src/mcp/handlers/config/adapter.ts
212
+ async function handleConfig(args) {
213
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
214
+ return dispatchConfig(args, ctx);
215
+ }
216
+
217
+ // src/mcp/handlers/yield/index.ts
218
+ import { execFileSync as execFileSync2 } from "node:child_process";
219
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
220
+ import { join as join2 } from "node:path";
221
+
222
+ // src/mcp/handlers/yield/formatters.ts
223
+ function formatValidate(ok, path, detail) {
224
+ if (ok) {
225
+ return success({ valid: true, path, validator_output: detail }, { _tip: "Yield conforms to the contract." });
226
+ }
227
+ return analysis(
228
+ `Yield failed validation: ${path}`,
229
+ [{ severity: "critical", message: detail }],
230
+ ["Fix the reported issues", 'Re-run yield(method="validate", slug="\u2026")'],
231
+ {
232
+ _warning: "The yield does NOT conform to the contract \u2014 treat it as not accepted.",
233
+ _fix: detail
234
+ }
235
+ );
236
+ }
237
+
238
+ // src/mcp/handlers/yield/index.ts
239
+ var HARVEST = ".harvest";
240
+ var ASSET_REL = "skills/harvest/assets/harvest-yield.example.md";
241
+ function yieldPath(repoRoot, slug) {
242
+ return join2(repoRoot, HARVEST, slug, "harvest-yield.md");
243
+ }
244
+ async function dispatchYield(args, ctx) {
245
+ const method = args.method;
246
+ const { repoRoot } = ctx;
247
+ const slug = args.slug;
248
+ switch (method) {
249
+ case "validate": {
250
+ const path = args.path ?? (slug ? yieldPath(repoRoot, slug) : void 0);
251
+ if (!path) return error("validate needs a slug or path", { _tip: 'yield(method="validate", slug="my-slug").' });
252
+ if (!existsSync2(path)) return error(`Yield not found: ${path}`, { _tip: 'Check the slug, or yield(method="list").' });
253
+ try {
254
+ const out = execFileSync2("python3", [join2(repoRoot, VALIDATOR_REL), path], { encoding: "utf8" });
255
+ return formatValidate(true, path, out.trim());
256
+ } catch (e) {
257
+ const err = e;
258
+ const detail = ((err.stdout ?? "") + (err.stderr ?? "")).trim() || err.message || String(e);
259
+ return formatValidate(false, path, detail);
260
+ }
261
+ }
262
+ case "list": {
263
+ const root = join2(repoRoot, HARVEST);
264
+ if (!existsSync2(root)) return error(`.harvest/ not found under ${repoRoot}`, { _tip: "Run from the eMachina repo root." });
265
+ const slugs = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(yieldPath(repoRoot, d.name))).map((d) => d.name);
266
+ return list("slugs", slugs, { _next_step: 'yield(method="get", slug="\u2026") to read one.' });
267
+ }
268
+ case "get": {
269
+ if (!slug) return error("get needs a slug", { _tip: 'yield(method="get", slug="my-slug").' });
270
+ const path = yieldPath(repoRoot, slug);
271
+ if (!existsSync2(path)) return error(`Yield not found for slug: ${slug}`, { _tip: 'yield(method="list") to see available slugs.' });
272
+ return singleItem("yield", { slug, path, content: readFileSync(path, "utf8") });
273
+ }
274
+ case "scaffold": {
275
+ if (!slug) return error("scaffold needs a slug", { _tip: 'yield(method="scaffold", slug="my-new-slug").' });
276
+ const path = yieldPath(repoRoot, slug);
277
+ if (existsSync2(path)) return error(`Yield already exists for slug: ${slug}`, { _tip: "Pick a new slug, or get the existing one." });
278
+ let content;
279
+ try {
280
+ content = scaffoldFromAsset(repoRoot, slug);
281
+ } catch (e) {
282
+ return error(e instanceof Error ? e.message : String(e), {
283
+ _tip: "Run from the eMachina repo root so the canonical skeleton is reachable."
284
+ });
285
+ }
286
+ mkdirSync(join2(repoRoot, HARVEST, slug), { recursive: true });
287
+ writeFileSync(path, content);
288
+ return success(
289
+ { slug, path },
290
+ {
291
+ _next_step: `Fill the placeholders (Ask/Why/Goals/Findings/Sources), then yield(method="validate", slug="${slug}").`,
292
+ _warning: "Scaffold copies the canonical skeleton \u2014 the agent authors the content (the MCP never invents findings)."
293
+ }
294
+ );
295
+ }
296
+ case void 0:
297
+ return clarificationNeeded("Which operation?", ["validate", "list", "get", "scaffold"], {
298
+ _tip: 'e.g. yield(method="list").'
299
+ });
300
+ default:
301
+ return error(`Unknown method: ${method}`, { _tip: "yield supports: validate, list, get, scaffold." });
302
+ }
303
+ }
304
+ function scaffoldFromAsset(repoRoot, slug) {
305
+ const assetPath = join2(repoRoot, ASSET_REL);
306
+ if (!existsSync2(assetPath)) {
307
+ throw new Error(`Canonical yield skeleton not found at ${ASSET_REL} \u2014 cannot scaffold without it (no hardcoded fallback, to avoid contract drift).`);
308
+ }
309
+ return readFileSync(assetPath, "utf8").replace(/^name:.*$/m, `name: ${slug}`).replace(/^[ \t]*updated:.*\r?\n/m, "");
310
+ }
311
+
312
+ // src/mcp/handlers/yield/adapter.ts
313
+ async function handleYield(args) {
314
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
315
+ return dispatchYield(args, ctx);
316
+ }
317
+
318
+ // src/mcp/atom/hash.ts
319
+ import { createHash } from "node:crypto";
320
+ function sha256Hex(input) {
321
+ return createHash("sha256").update(input).digest("hex");
322
+ }
323
+ function canonicalJson(value) {
324
+ return JSON.stringify(canonicalize(value));
325
+ }
326
+ function canonicalize(value) {
327
+ if (value !== null && typeof value === "object" && typeof value.toJSON === "function") {
328
+ return canonicalize(value.toJSON());
329
+ }
330
+ if (Array.isArray(value)) return value.map(canonicalize);
331
+ if (value !== null && typeof value === "object") {
332
+ const source = value;
333
+ const out = {};
334
+ for (const key of Object.keys(source).sort()) {
335
+ const v = source[key];
336
+ if (v !== void 0) out[key] = canonicalize(v);
337
+ }
338
+ return out;
339
+ }
340
+ return value;
341
+ }
342
+
343
+ // src/mcp/atom/id.ts
344
+ var AUDIENCE_RE = /^(public|internal(:[a-z0-9-]+)?|tenant:[a-z0-9-]+|customer:[a-z0-9-]+)$/;
345
+ function isValidAudience(token) {
346
+ return typeof token === "string" && AUDIENCE_RE.test(token);
347
+ }
348
+ function contentAddressId(c) {
349
+ const hash = sha256Hex(canonicalJson({ name: c.name, description: c.description, body: c.body }));
350
+ return `atom:${hash.slice(0, 32)}`;
351
+ }
352
+
353
+ // src/mcp/atom/validate.ts
354
+ var LIMITS = {
355
+ MAX_NAME: 512,
356
+ MAX_DESCRIPTION: 4096,
357
+ MAX_BODY: 1e6
358
+ // 1M chars — generous for free-form knowledge, catches runaway/garbage
359
+ };
360
+ var ATOM_ID_RE = /^atom:[a-f0-9]{16,64}$/;
361
+ function isValidAtomId(v) {
362
+ return typeof v === "string" && ATOM_ID_RE.test(v);
363
+ }
364
+ var ValidationError = class extends Error {
365
+ errors;
366
+ constructor(errors) {
367
+ super(`validation failed: ${errors.join("; ")}`);
368
+ this.name = "ValidationError";
369
+ this.errors = errors;
370
+ }
371
+ };
372
+ function reqString(v, field, max, errors) {
373
+ if (typeof v !== "string" || v.trim() === "") {
374
+ errors.push(`${field} is required and must be a non-empty string`);
375
+ } else if (v.length > max) {
376
+ errors.push(`${field} exceeds max length ${max} (got ${v.length})`);
377
+ }
378
+ }
379
+ function validateAtomInput(input) {
380
+ const errors = [];
381
+ if (input === null || typeof input !== "object") return ["atom input must be an object"];
382
+ reqString(input.name, "name", LIMITS.MAX_NAME, errors);
383
+ reqString(input.description, "description", LIMITS.MAX_DESCRIPTION, errors);
384
+ reqString(input.body, "body", LIMITS.MAX_BODY, errors);
385
+ if (input.audience !== void 0 && !isValidAudience(input.audience)) {
386
+ errors.push(`invalid audience token "${input.audience}" (expect public|internal[:slug]|tenant:slug|customer:slug)`);
387
+ }
388
+ if (input.supersedes !== void 0 && !isValidAtomId(input.supersedes)) {
389
+ errors.push(`supersedes must be a valid atom id (atom:<hex>), got "${String(input.supersedes)}"`);
390
+ }
391
+ if (input.produced_by !== void 0 && typeof input.produced_by !== "string") {
392
+ errors.push("produced_by must be a string");
393
+ }
394
+ return errors;
395
+ }
396
+ function validateChange(change) {
397
+ if (change === null || typeof change !== "object") return ["change must be an object"];
398
+ switch (change.op) {
399
+ case "publish":
400
+ if (change.atom === void 0) return ["publish requires `atom`"];
401
+ return validateAtomInput(change.atom);
402
+ case "retract":
403
+ return isValidAtomId(change.id) ? [] : [`retract requires a valid atom id, got "${String(change.id)}"`];
404
+ case "set_audience": {
405
+ const errors = [];
406
+ if (!isValidAtomId(change.id)) errors.push(`set_audience requires a valid atom id, got "${String(change.id)}"`);
407
+ if (change.audience === void 0 || !isValidAudience(change.audience)) {
408
+ errors.push(`set_audience requires a valid audience token, got "${change.audience}"`);
409
+ }
410
+ return errors;
411
+ }
412
+ default:
413
+ return [`unknown op "${String(change.op)}" (expect publish|retract|set_audience)`];
414
+ }
415
+ }
416
+ function validate(change) {
417
+ const errors = validateChange(change);
418
+ return { valid: errors.length === 0, errors };
419
+ }
420
+ function assertValidChange(change) {
421
+ const errors = validateChange(change);
422
+ if (errors.length > 0) throw new ValidationError(errors);
423
+ }
424
+
425
+ // src/mcp/atom/core.ts
426
+ function buildAtom(input) {
427
+ const errors = validateAtomInput(input);
428
+ if (errors.length > 0) throw new ValidationError(errors);
429
+ const audience = input.audience ?? "internal";
430
+ const id = contentAddressId({ name: input.name, description: input.description, body: input.body });
431
+ return {
432
+ id,
433
+ audience,
434
+ name: input.name,
435
+ description: input.description,
436
+ body: input.body,
437
+ status: "active",
438
+ produced_by: input.produced_by ?? "unknown",
439
+ produced_at: (/* @__PURE__ */ new Date()).toISOString(),
440
+ ...input.supersedes !== void 0 ? { supersedes: input.supersedes } : {},
441
+ ...input.derived_from !== void 0 ? { derived_from: input.derived_from } : {}
442
+ };
443
+ }
444
+ async function project(retrieval, fn) {
445
+ if (!retrieval) return void 0;
446
+ try {
447
+ await fn(retrieval);
448
+ return void 0;
449
+ } catch (err) {
450
+ return `retrieval projection (${retrieval.name}) failed: ${err.message}`;
451
+ }
452
+ }
453
+ async function submitChanges(change, backends) {
454
+ assertValidChange(change);
455
+ const { storage, retrieval } = backends;
456
+ switch (change.op) {
457
+ case "publish": {
458
+ if (!change.atom) throw new Error("submitChanges(publish): `atom` is required");
459
+ const atom = buildAtom(change.atom);
460
+ if (await storage.has(atom.id)) {
461
+ return { status: "deduplicated", id: atom.id, atom: await storage.get(atom.id) ?? atom };
462
+ }
463
+ await storage.put(atom);
464
+ const warning = await project(retrieval, (r) => r.index(atom));
465
+ return { status: "published", id: atom.id, atom, ...warning ? { warning } : {} };
466
+ }
467
+ case "retract": {
468
+ if (!change.id) throw new Error("submitChanges(retract): `id` is required");
469
+ const existing = await storage.get(change.id);
470
+ if (!existing) throw new Error(`submitChanges(retract): atom "${change.id}" not found`);
471
+ const tombstoned = { ...existing, status: "retracted" };
472
+ await storage.put(tombstoned);
473
+ await project(retrieval, (r) => r.remove(change.id));
474
+ return { status: "retracted", id: change.id, atom: tombstoned };
475
+ }
476
+ case "set_audience": {
477
+ const id = change.id;
478
+ const audience = change.audience;
479
+ const existing = await storage.get(id);
480
+ if (!existing) throw new Error(`submitChanges(set_audience): atom "${id}" not found`);
481
+ const updated = { ...existing, audience };
482
+ await storage.put(updated);
483
+ await project(retrieval, (r) => r.index(updated));
484
+ return { status: "audience_set", id, atom: updated };
485
+ }
486
+ default: {
487
+ throw new Error(`submitChanges: unknown op "${change.op}"`);
488
+ }
489
+ }
490
+ }
491
+ async function search(query, opts, backends) {
492
+ if (!backends.retrieval) throw new Error("search: no retrieval backend configured");
493
+ return backends.retrieval.search(query, opts);
494
+ }
495
+
496
+ // src/mcp/atom/backends.ts
497
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "node:fs";
498
+ import { join as join3 } from "node:path";
499
+ function idToFile(id) {
500
+ return `${id.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
501
+ }
502
+ var LocalStorageBackend = class {
503
+ constructor(dir) {
504
+ this.dir = dir;
505
+ mkdirSync2(dir, { recursive: true });
506
+ }
507
+ name = "local-fs";
508
+ async put(atom) {
509
+ writeFileSync2(join3(this.dir, idToFile(atom.id)), JSON.stringify(atom, null, 2));
510
+ }
511
+ async get(id) {
512
+ const path = join3(this.dir, idToFile(id));
513
+ if (!existsSync3(path)) return null;
514
+ return JSON.parse(readFileSync2(path, "utf8"));
515
+ }
516
+ async has(id) {
517
+ return existsSync3(join3(this.dir, idToFile(id)));
518
+ }
519
+ async list() {
520
+ if (!existsSync3(this.dir)) return [];
521
+ return readdirSync2(this.dir).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(readFileSync2(join3(this.dir, f), "utf8")));
522
+ }
523
+ async delete(id) {
524
+ const path = join3(this.dir, idToFile(id));
525
+ if (existsSync3(path)) rmSync(path);
526
+ }
527
+ };
528
+ function audienceVisible(atomAudience, caller) {
529
+ return atomAudience === "public" || Array.isArray(caller) && caller.includes(atomAudience);
530
+ }
531
+ function tokenize(s) {
532
+ return s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
533
+ }
534
+ var StoreScanRetrieval = class {
535
+ name = "store-scan";
536
+ atoms = /* @__PURE__ */ new Map();
537
+ /** Optionally hydrate the projection from the primary store. */
538
+ async rebuildFrom(storage) {
539
+ this.atoms.clear();
540
+ for (const a of await storage.list()) this.atoms.set(a.id, a);
541
+ }
542
+ async index(atom) {
543
+ this.atoms.set(atom.id, atom);
544
+ }
545
+ async remove(id) {
546
+ this.atoms.delete(id);
547
+ }
548
+ async search(query, opts) {
549
+ const caller = opts.audience ?? ["public"];
550
+ const qTokens = tokenize(query);
551
+ const hits = [];
552
+ for (const atom of this.atoms.values()) {
553
+ if (atom.status === "retracted") continue;
554
+ if (!audienceVisible(atom.audience, caller)) continue;
555
+ const hay = tokenize(`${atom.name} ${atom.description} ${atom.body}`);
556
+ const haySet = new Set(hay);
557
+ const matched = qTokens.filter((t) => haySet.has(t)).length;
558
+ if (matched === 0) continue;
559
+ hits.push({
560
+ id: atom.id,
561
+ name: atom.name,
562
+ description: atom.description,
563
+ audience: atom.audience,
564
+ score: qTokens.length ? matched / qTokens.length : 0
565
+ });
566
+ }
567
+ hits.sort((a, b) => b.score - a.score);
568
+ return hits.slice(0, opts.limit ?? 10);
569
+ }
570
+ };
571
+
572
+ // src/mcp/handlers/corpus.ts
573
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
574
+ import { join as join4 } from "node:path";
575
+ var HARVEST2 = ".harvest";
576
+ var CORPUS_REL = ".knowledge/atoms";
577
+ function corpusStore(repoRoot) {
578
+ return new LocalStorageBackend(join4(repoRoot, CORPUS_REL));
579
+ }
580
+ function readYield(repoRoot, slug) {
581
+ const path = join4(repoRoot, HARVEST2, slug, "harvest-yield.md");
582
+ return existsSync4(path) ? readFileSync3(path, "utf8") : null;
583
+ }
584
+ function deriveDescription(content, slug) {
585
+ const body = content.replace(/^---[\s\S]*?---/m, "");
586
+ for (const line of body.split("\n")) {
587
+ const t = line.trim();
588
+ if (t && !t.startsWith("#") && !t.startsWith(">") && !t.startsWith("`")) return t.slice(0, 200);
589
+ }
590
+ return slug;
591
+ }
592
+
593
+ // src/mcp/handlers/curate/index.ts
594
+ function atomInputFromArgs(repoRoot, args) {
595
+ const audience = typeof args.audience === "string" ? args.audience : void 0;
596
+ const supersedes = typeof args.supersedes === "string" ? args.supersedes : void 0;
597
+ const slug = typeof args.slug === "string" ? args.slug : void 0;
598
+ if (slug) {
599
+ const content = readYield(repoRoot, slug);
600
+ if (content === null) return { err: `Yield not found for slug: ${slug}` };
601
+ return {
602
+ input: {
603
+ name: slug,
604
+ description: deriveDescription(content, slug),
605
+ body: content,
606
+ produced_by: "curate",
607
+ ...audience ? { audience } : {},
608
+ ...supersedes ? { supersedes } : {}
609
+ }
610
+ };
611
+ }
612
+ const name = args.name;
613
+ const description = args.description;
614
+ const body = args.body;
615
+ if (!name || !description || !body) {
616
+ return { err: "Provide either a `slug` (of a .harvest yield) or explicit name + description + body." };
617
+ }
618
+ return {
619
+ input: {
620
+ name,
621
+ description,
622
+ body,
623
+ produced_by: "curate",
624
+ ...audience ? { audience } : {},
625
+ ...supersedes ? { supersedes } : {}
626
+ }
627
+ };
628
+ }
629
+ async function dispatchCurate(args, ctx) {
630
+ const method = args.method;
631
+ const storage = corpusStore(ctx.repoRoot);
632
+ switch (method) {
633
+ case "advise": {
634
+ const { input, err } = atomInputFromArgs(ctx.repoRoot, args);
635
+ if (err || !input) return error(err ?? "invalid input", { _tip: 'curate(method="advise", slug="\u2026").' });
636
+ const v = validate({ op: "publish", atom: input });
637
+ if (v.valid) {
638
+ return success({ advice: "valid", name: input.name }, {
639
+ _next_step: `curate(method="publish", slug="${args.slug ?? input.name}") to memorialize it.`
640
+ });
641
+ }
642
+ return {
643
+ status: "analysis",
644
+ summary: "Yield fails atom-kernel validation \u2014 fix before publishing.",
645
+ issues: v.errors,
646
+ next_steps: ["Fix the listed issues, then re-run curate(advise)."],
647
+ _warning: "Validation is enforced by the atom kernel; publish would be rejected."
648
+ };
649
+ }
650
+ case "publish":
651
+ case "supersede": {
652
+ const { input, err } = atomInputFromArgs(ctx.repoRoot, args);
653
+ if (err || !input) {
654
+ return error(err ?? "invalid input", {
655
+ _tip: 'curate(method="publish", slug="\u2026"), or pass name/description/body. supersede also needs `supersedes`.'
656
+ });
657
+ }
658
+ if (method === "supersede" && !input.supersedes) {
659
+ return error("supersede requires `supersedes` (the atom id being superseded).", {
660
+ _tip: 'curate(method="supersede", slug="\u2026", supersedes="atom:\u2026").'
661
+ });
662
+ }
663
+ try {
664
+ const res = await submitChanges({ op: "publish", atom: input }, { storage });
665
+ return success(
666
+ { id: res.id, curate_status: res.status, name: input.name, audience: res.atom?.audience },
667
+ {
668
+ _next_step: 'retrieve(method="search", query="\u2026") \u2014 the atom is now in the local corpus.',
669
+ _tip: res.status === "deduplicated" ? "Identical content already memorialized \u2014 content-addressed dedup (no-op)." : "Memorialized to the local corpus (swap StorageBackend for GCS org-wide)."
670
+ }
671
+ );
672
+ } catch (e) {
673
+ if (e instanceof ValidationError) {
674
+ return {
675
+ status: "analysis",
676
+ summary: "Validation failed (enforced by the atom kernel).",
677
+ issues: e.errors,
678
+ next_steps: ["Fix the issues, then re-run curate."]
679
+ };
680
+ }
681
+ return error(e instanceof Error ? e.message : String(e));
682
+ }
683
+ }
684
+ case "status":
685
+ return success({ wired: true }, { _tip: "curate \u2192 atom-kernel submitChanges (local corpus StorageBackend)." });
686
+ case void 0:
687
+ return clarificationNeeded("Which operation?", ["publish", "supersede", "advise", "status"], {
688
+ _tip: 'e.g. curate(method="publish", slug="my-slug").'
689
+ });
690
+ default:
691
+ return error(`Unknown method: ${method}`, { _tip: "curate supports: publish, supersede, advise, status." });
692
+ }
693
+ }
694
+
695
+ // src/mcp/handlers/curate/adapter.ts
696
+ async function handleCurate(args) {
697
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
698
+ return dispatchCurate(args, ctx);
699
+ }
700
+
701
+ // src/mcp/handlers/retrieve/index.ts
702
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "node:fs";
703
+ import { join as join5 } from "node:path";
704
+ async function loadCorpus(repoRoot) {
705
+ const retrieval = new StoreScanRetrieval();
706
+ const ids = /* @__PURE__ */ new Set();
707
+ for (const atom of await corpusStore(repoRoot).list()) {
708
+ await retrieval.index(atom);
709
+ ids.add(atom.id);
710
+ }
711
+ const root = join5(repoRoot, HARVEST2);
712
+ if (existsSync5(root)) {
713
+ for (const d of readdirSync3(root, { withFileTypes: true })) {
714
+ if (!d.isDirectory() || d.name.startsWith(".")) continue;
715
+ const content = readYield(repoRoot, d.name);
716
+ if (content === null) continue;
717
+ const atom = buildAtom({
718
+ name: d.name,
719
+ description: deriveDescription(content, d.name),
720
+ body: content,
721
+ audience: "internal",
722
+ produced_by: "harvest"
723
+ });
724
+ await retrieval.index(atom);
725
+ ids.add(atom.id);
726
+ }
727
+ }
728
+ return { retrieval, count: ids.size };
729
+ }
730
+ async function dispatchRetrieve(args, ctx) {
731
+ const method = args.method ?? "search";
732
+ if (method !== "search") {
733
+ return error(`Unknown method: ${method}`, { _tip: "retrieve supports: search." });
734
+ }
735
+ const query = args.query;
736
+ if (!query || query.trim() === "") {
737
+ return clarificationNeeded("What should I search the corpus for?", [], {
738
+ _tip: 'retrieve(method="search", query="\u2026").'
739
+ });
740
+ }
741
+ const limit = typeof args.limit === "number" ? args.limit : 10;
742
+ const audience = Array.isArray(args.audience) ? args.audience : ["public", "internal"];
743
+ const { retrieval, count } = await loadCorpus(ctx.repoRoot);
744
+ const hits = await search(query, { audience, limit }, { retrieval });
745
+ const results = hits.map((h) => ({ slug: h.name, score: Number(h.score.toFixed(3)), description: h.description }));
746
+ const top = results[0];
747
+ return {
748
+ ...list("results", results, {
749
+ _next_step: top ? `yield(method="get", slug="${top.slug}") to read the top hit.` : "No local hits \u2014 broaden the query, or compose with the ema toolkit's knowledge() for org-wide search.",
750
+ _tip: `Searched ${count} local atoms (curated + .harvest yields; audience: ${audience.join(", ")}). Org-wide composes with ema-toolkit knowledge() (DE).`
751
+ }),
752
+ composes_with: ["ema-toolkit:knowledge()"]
753
+ };
754
+ }
755
+
756
+ // src/mcp/handlers/retrieve/adapter.ts
757
+ async function handleRetrieve(args) {
758
+ const ctx = { repoRoot: findRepoRoot(process.cwd()) };
759
+ return dispatchRetrieve(args, ctx);
760
+ }
761
+
762
+ // src/mcp/server.ts
763
+ var toolHandlers = {
764
+ config: (args) => handleConfig(args),
765
+ yield: (args) => handleYield(args),
766
+ curate: (args) => handleCurate(args),
767
+ retrieve: (args) => handleRetrieve(args)
768
+ };
769
+ function assertSurfaceParity() {
770
+ for (const c of CAPABILITIES) {
771
+ if (!toolHandlers[c.name]) throw new Error(`Capability "${c.name}" has no handler in the dispatch map`);
772
+ }
773
+ for (const name of Object.keys(toolHandlers)) {
774
+ if (!CAPABILITIES.some((c) => c.name === name)) throw new Error(`Handler "${name}" has no capability spec`);
775
+ }
776
+ }
777
+ assertSurfaceParity();
778
+ function wrap(result) {
779
+ return {
780
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
781
+ isError: result.status === "error"
782
+ };
783
+ }
784
+ async function startMcpServer() {
785
+ const server = new Server(
786
+ { name: "emachina", version: "0.1.0" },
787
+ { capabilities: { tools: {} } }
788
+ // add `{ listChanged: true }` only if adopting the dynamic surface
789
+ );
790
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: generateTools() }));
791
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
792
+ const { name, arguments: args } = req.params;
793
+ const handler = toolHandlers[name];
794
+ if (!handler) {
795
+ return wrap(
796
+ error(`Unknown tool: ${name}`, { _tip: `Available tools: ${Object.keys(toolHandlers).join(", ")}.` })
797
+ );
798
+ }
799
+ try {
800
+ let result = await handler(args ?? {});
801
+ try {
802
+ result = enrichWithState(result, { tool: name });
803
+ } catch {
804
+ }
805
+ return wrap(result);
806
+ } catch (err) {
807
+ const message = err instanceof Error ? err.message : String(err);
808
+ return wrap(error(message, { _tip: "Unexpected handler error \u2014 likely a bug; check server logs." }));
809
+ }
810
+ });
811
+ await server.connect(new StdioServerTransport());
812
+ }
813
+ var argv1 = process.argv[1];
814
+ var isMain = !!argv1 && import.meta.url === pathToFileURL(realpathSync(argv1)).href;
815
+ if (isMain) {
816
+ startMcpServer().catch((e) => {
817
+ console.error(e);
818
+ process.exit(1);
819
+ });
820
+ }
821
+ export {
822
+ assertSurfaceParity,
823
+ startMcpServer
824
+ };