@miadi/stateloom-mcp 0.2.1 → 0.2.3

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.
@@ -8,4 +8,17 @@ export interface SwitchResult {
8
8
  /** True when the new path equals the previous one. */
9
9
  unchanged: boolean;
10
10
  }
11
+ export declare function chronicleRoot(env?: Record<string, string | undefined>): string;
12
+ /**
13
+ * Resolve `miadi-chronicle://<episode>/[diagrams/]<name>[.smdf.json]` to the
14
+ * absolute path `<root>/<episode-dir>/diagrams/<name>.smdf.json`.
15
+ *
16
+ * Episode matching: exact folder name first; else a bare number matches
17
+ * `-episode-NNN-` folders. Episode numbers are NOT unique in the chronicle
18
+ * (011, 016, 043, 078, 083, 098, 120 are doubled) — ambiguity is a HARD
19
+ * ERROR naming every candidate, never a silent first match: a resolver that
20
+ * guesses writes the wrong episode into the record. A missing diagram file
21
+ * stays legitimate — naming it is how an episode's FIRST diagram is created.
22
+ */
23
+ export declare function resolveChronicleUri(uri: string, env?: Record<string, string | undefined>): string;
11
24
  export declare function resolveProjectSwitch(next: string, previous: string): SwitchResult;
@@ -1,11 +1,74 @@
1
1
  /**
2
2
  * Pure logic behind the `set_project_file` MCP tool (path power — the agent
3
3
  * chooses which `.smdf.json` document the loom weaves).
4
+ *
5
+ * Two modalities, by William's orientation (2026-08-07): STANDALONE — a plain
6
+ * path, no episode, just designing — and RELATIONAL — the document lives
7
+ * inside a chronicle episode and is addressed as
8
+ * `miadi-chronicle://<episode>/<diagram>`, resolving over
9
+ * $MIADI_CHRONICLE_ROOT to `<episode-dir>/diagrams/<diagram>.smdf.json`.
10
+ * The relationship is carried by WHERE the file lives (the scalar truth of
11
+ * jgwill/Miadi#593); wheel edges stay derived/ceremonial, never minted here.
4
12
  */
5
- import { existsSync } from "fs";
6
- import { resolve } from "path";
13
+ import { existsSync, readdirSync, statSync } from "fs";
14
+ import { join, resolve } from "path";
15
+ const CHRONICLE_SCHEME = /^miadi-chronicle:\/\//i;
16
+ export function chronicleRoot(env = process.env) {
17
+ return env.MIADI_CHRONICLE_ROOT ?? "/srv/miadi/episodes/miadi-chronicle";
18
+ }
19
+ /**
20
+ * Resolve `miadi-chronicle://<episode>/[diagrams/]<name>[.smdf.json]` to the
21
+ * absolute path `<root>/<episode-dir>/diagrams/<name>.smdf.json`.
22
+ *
23
+ * Episode matching: exact folder name first; else a bare number matches
24
+ * `-episode-NNN-` folders. Episode numbers are NOT unique in the chronicle
25
+ * (011, 016, 043, 078, 083, 098, 120 are doubled) — ambiguity is a HARD
26
+ * ERROR naming every candidate, never a silent first match: a resolver that
27
+ * guesses writes the wrong episode into the record. A missing diagram file
28
+ * stays legitimate — naming it is how an episode's FIRST diagram is created.
29
+ */
30
+ export function resolveChronicleUri(uri, env = process.env) {
31
+ const rest = uri.replace(CHRONICLE_SCHEME, "").replace(/[?#].*$/, "");
32
+ const segments = rest.split("/").filter(Boolean);
33
+ if (segments.length < 2) {
34
+ throw new Error(`chronicle address needs <episode>/<diagram> (got '${uri}') — e.g. miadi-chronicle://103/film-preprod`);
35
+ }
36
+ const episodeRef = segments[0];
37
+ const name = segments[segments.length - 1];
38
+ const root = chronicleRoot(env);
39
+ let dirs;
40
+ try {
41
+ dirs = readdirSync(root).filter((d) => {
42
+ try {
43
+ return statSync(join(root, d)).isDirectory();
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ });
49
+ }
50
+ catch {
51
+ throw new Error(`chronicle root not readable: ${root} (MIADI_CHRONICLE_ROOT)`);
52
+ }
53
+ let matches = dirs.filter((d) => d === episodeRef);
54
+ if (matches.length === 0 && /^\d+$/.test(episodeRef)) {
55
+ const num = String(parseInt(episodeRef, 10));
56
+ matches = dirs.filter((d) => {
57
+ const m = d.match(/-episode-0*(\d+)-/);
58
+ return m !== null && m[1] === num;
59
+ });
60
+ }
61
+ if (matches.length === 0) {
62
+ throw new Error(`no episode matches '${episodeRef}' under ${root}`);
63
+ }
64
+ if (matches.length > 1) {
65
+ throw new Error(`episode '${episodeRef}' is ambiguous — candidates:\n ${matches.sort().join("\n ")}\nName the full folder.`);
66
+ }
67
+ const file = name.endsWith(".json") ? name : `${name}.smdf.json`;
68
+ return join(root, matches[0], "diagrams", file);
69
+ }
7
70
  export function resolveProjectSwitch(next, previous) {
8
- const path = resolve(next);
71
+ const path = CHRONICLE_SCHEME.test(next) ? resolveChronicleUri(next) : resolve(next);
9
72
  if (!path.endsWith(".json")) {
10
73
  throw new Error(`project file must be a .json document (got '${path}') — the loom weaves .smdf.json files`);
11
74
  }
package/dist/server.js CHANGED
@@ -36,6 +36,7 @@ import { z } from "zod";
36
36
  import { execFileSync, spawn } from "child_process";
37
37
  import { writeFileSync, readFileSync, mkdtempSync, rmSync, existsSync, statSync, realpathSync, } from "fs";
38
38
  import { basename, dirname, join, resolve } from "path";
39
+ import { fileURLToPath } from "node:url";
39
40
  import { tmpdir } from "os";
40
41
  import { createBridgeClient } from "@miadi/stateloom-client";
41
42
  import { envAlias } from "@miadi/stateloom-protocol";
@@ -598,10 +599,27 @@ function glyph(direction) {
598
599
  * Every tool closes over module-level state (PROJECT_FILE, the bridge client),
599
600
  * so instances are interchangeable — building one is registration, not state.
600
601
  */
602
+ /**
603
+ * The version a client sees in `serverInfo`, read from the manifest that npm
604
+ * actually shipped rather than typed in beside it. The literal here said 0.2.0
605
+ * while the package on the registry was 0.2.2 — two releases during which every
606
+ * agent that asked this server who it was got a confident wrong answer, and no
607
+ * test could catch it because the two numbers had no relationship to compare.
608
+ * Reading the manifest gives them one.
609
+ */
610
+ const PACKAGE_VERSION = (() => {
611
+ try {
612
+ const manifest = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
613
+ return String(JSON.parse(readFileSync(manifest, "utf8")).version);
614
+ }
615
+ catch {
616
+ return "0.0.0-unknown";
617
+ }
618
+ })();
601
619
  function buildServer() {
602
620
  const server = new McpServer({
603
621
  name: "stateloom-mcp",
604
- version: "0.2.0",
622
+ version: PACKAGE_VERSION,
605
623
  });
606
624
  // Tools
607
625
  server.tool("create_state_machine", "Create a new state machine definition with namespace and name", { namespace: z.string(), name: z.string(), asynchronous: z.boolean().optional() }, async ({ namespace, name, asynchronous }) => {
@@ -787,7 +805,7 @@ function buildServer() {
787
805
  return { content: [{ type: "text", text: "No events defined yet." }] };
788
806
  return { content: [{ type: "text", text: events.join("\n") }] };
789
807
  });
790
- server.tool("set_project_file", "Choose which .smdf.json path is the active document the loom weaves. Disk persistence AND the live bridge room (the hub keys rooms by this path) both re-point to it. A missing file is legitimate — create_state_machine or load_definition writes it next. Path must end in .json; convention is *.smdf.json.", { path: z.string() }, async ({ path }) => {
808
+ server.tool("set_project_file", "Choose the active document the loom weaves — two modalities. STANDALONE: any .smdf.json path. RELATIONAL: a chronicle address `miadi-chronicle://<episode>/<diagram>` resolving over $MIADI_CHRONICLE_ROOT to <episode-dir>/diagrams/<diagram>.smdf.json — an ambiguous episode number is a hard error naming the candidates. Disk persistence AND the live bridge room both re-point. A missing file is legitimate — naming it is how an episode's first diagram is created.", { path: z.string() }, async ({ path }) => {
791
809
  try {
792
810
  const r = resolveProjectSwitch(path, PROJECT_FILE);
793
811
  const denial = denyProjectSwitch(r.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "MCP server for miadi-stateloom (formerly smcraft-mcp) — lets LLM agents design state machines through conversation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,9 +38,9 @@
38
38
  "node": ">=20"
39
39
  },
40
40
  "dependencies": {
41
- "@miadi/stateloom-cli": "^0.1.2",
42
- "@miadi/stateloom-client": "^0.1.1",
43
- "@miadi/stateloom-protocol": "^0.1.3",
41
+ "@miadi/stateloom-cli": "^0.1.3",
42
+ "@miadi/stateloom-client": "^0.1.2",
43
+ "@miadi/stateloom-protocol": "^0.1.5",
44
44
  "@modelcontextprotocol/sdk": "^1.12.1",
45
45
  "zod": "^4.3.6"
46
46
  },