@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.
- package/dist/projectSwitch.d.ts +13 -0
- package/dist/projectSwitch.js +66 -3
- package/dist/server.js +20 -2
- package/package.json +4 -4
package/dist/projectSwitch.d.ts
CHANGED
|
@@ -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;
|
package/dist/projectSwitch.js
CHANGED
|
@@ -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:
|
|
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
|
|
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.
|
|
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.
|
|
42
|
-
"@miadi/stateloom-client": "^0.1.
|
|
43
|
-
"@miadi/stateloom-protocol": "^0.1.
|
|
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
|
},
|