@miadi/stateloom-mcp 0.3.2 → 0.3.4
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/atomicWrite.d.ts +12 -0
- package/dist/atomicWrite.js +21 -1
- package/dist/erd.js +2 -1
- package/dist/server.js +79 -2
- package/package.json +3 -3
package/dist/atomicWrite.d.ts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the folder a document is about to be written into, one level only.
|
|
3
|
+
*
|
|
4
|
+
* Earned 2026-10-01 (Episode 550, L1): `miadi-chronicle://550/screenwalk`
|
|
5
|
+
* resolves to `<episode>/diagrams/screenwalk.smdf.json`, and naming a missing
|
|
6
|
+
* file is how an episode's first diagram is created — but a fresh episode has
|
|
7
|
+
* no `diagrams/` yet, so the first write failed with ENOENT. The folder is
|
|
8
|
+
* created only when ITS parent exists (an episode's `diagrams/` room, a new
|
|
9
|
+
* folder beside existing work); a path whose ancestors are missing is a typo,
|
|
10
|
+
* and it still fails loudly instead of growing a tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureParentFolder(path: string): void;
|
|
1
13
|
export declare function writeFileAtomic(path: string, text: string): void;
|
package/dist/atomicWrite.js
CHANGED
|
@@ -6,9 +6,29 @@
|
|
|
6
6
|
* replace it regardless of its mode, and a read-only document is a person's
|
|
7
7
|
* word that it should not change — and the new file keeps the old one's mode.
|
|
8
8
|
*/
|
|
9
|
-
import { accessSync, chmodSync, constants, existsSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
10
10
|
import { basename, dirname, join } from "path";
|
|
11
|
+
/**
|
|
12
|
+
* Create the folder a document is about to be written into, one level only.
|
|
13
|
+
*
|
|
14
|
+
* Earned 2026-10-01 (Episode 550, L1): `miadi-chronicle://550/screenwalk`
|
|
15
|
+
* resolves to `<episode>/diagrams/screenwalk.smdf.json`, and naming a missing
|
|
16
|
+
* file is how an episode's first diagram is created — but a fresh episode has
|
|
17
|
+
* no `diagrams/` yet, so the first write failed with ENOENT. The folder is
|
|
18
|
+
* created only when ITS parent exists (an episode's `diagrams/` room, a new
|
|
19
|
+
* folder beside existing work); a path whose ancestors are missing is a typo,
|
|
20
|
+
* and it still fails loudly instead of growing a tree.
|
|
21
|
+
*/
|
|
22
|
+
export function ensureParentFolder(path) {
|
|
23
|
+
const dir = dirname(path);
|
|
24
|
+
if (existsSync(dir))
|
|
25
|
+
return;
|
|
26
|
+
if (!existsSync(dirname(dir)))
|
|
27
|
+
return;
|
|
28
|
+
mkdirSync(dir);
|
|
29
|
+
}
|
|
11
30
|
export function writeFileAtomic(path, text) {
|
|
31
|
+
ensureParentFolder(path);
|
|
12
32
|
let mode;
|
|
13
33
|
if (existsSync(path)) {
|
|
14
34
|
accessSync(path, constants.W_OK);
|
package/dist/erd.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { addAttribute, addEntity, addRelationship, checkLinks, checkStateOf, collectNotes, emptyErd, ERD_CARDINALITIES, isErdDefinition, isErdfPath, isSqdfPath, isSysdfPath, removeAttribute, removeEntity, removeRelationship, renderMermaidEr, updateEntity, updateErdSettings, summarizeErd, validateErd, normalizeNotes } from "@miadi/stateloom-protocol";
|
|
5
5
|
import { stampedOutputPath } from "@miadi/stateloom-cli/render";
|
|
6
|
+
import { writeFileAtomic } from "./atomicWrite.js";
|
|
6
7
|
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
7
8
|
const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
8
9
|
export function readErd(path) {
|
|
@@ -17,7 +18,7 @@ export function readErd(path) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
function writeErd(path, def) {
|
|
20
|
-
|
|
21
|
+
writeFileAtomic(path, JSON.stringify(def, null, 2) + "\n");
|
|
21
22
|
}
|
|
22
23
|
/** One line for set_project_file / get_project_file when the active document is an ERDF. */
|
|
23
24
|
export function erdSummary(path) {
|
package/dist/server.js
CHANGED
|
@@ -72,6 +72,7 @@ import { registerErdTools, erdSummary, erdGetDefinition, erdLoadDefinition, erdR
|
|
|
72
72
|
import { registerSequenceTools, sequenceSummary, sequenceGetDefinition, sequenceLoadDefinition, sequenceRender, sequenceGetNotes, sequenceSetNotes, } from "./sequence.js";
|
|
73
73
|
import { registerSystemTools, checkSystemReport, reconcileAfterEdit, reconcileModeLine, systemSummary, systemGetDefinition, systemLoadDefinition, systemRender, systemGetNotes, systemSetNotes, systemRispec, } from "./system.js";
|
|
74
74
|
import { liveBridge } from "./live.js";
|
|
75
|
+
import { writeFileAtomic } from "./atomicWrite.js";
|
|
75
76
|
// STATELOOM_* read first, SMCRAFT_* legacy twin honored — the live MCP
|
|
76
77
|
// registration bakes SMCRAFT_PROJECT_FILE and must keep working (2026-07-27
|
|
77
78
|
// rename brief). Resolved once at boot, like the direct reads they replace.
|
|
@@ -130,7 +131,7 @@ function writeDef(def) {
|
|
|
130
131
|
catch {
|
|
131
132
|
// A new or unreadable file is written in the loom's own shape.
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
+
writeFileAtomic(PROJECT_FILE, JSON.stringify(bare ? def : { stateMachine: def }, null, 2) + (bare ? "\n" : ""));
|
|
134
135
|
}
|
|
135
136
|
// ─── Optional real-time design bridge (env-gated, best-effort) ───────
|
|
136
137
|
// When STATELOOM_BRIDGE_URL (or legacy SMCRAFT_BRIDGE_URL) is set, each
|
|
@@ -784,6 +785,78 @@ function buildServer() {
|
|
|
784
785
|
content: [{ type: "text", text: `Added state '${name}' under '${parentName}' (${kind ?? "normal"}).` }],
|
|
785
786
|
};
|
|
786
787
|
});
|
|
788
|
+
server.tool("add_object", "Declare an object the machine is constructed with: `instance` is the name guards use (`walk` in `walk.state`), `class` is its type — an ERD entity when it is data. Fixes L001/L003 without rewriting the machine. Refused when the instance already exists.", { instance: z.string(), class: z.string() }, async ({ instance, class: className }) => {
|
|
789
|
+
const def = readDef();
|
|
790
|
+
if (!def)
|
|
791
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
792
|
+
if (!instance.trim() || !className.trim())
|
|
793
|
+
return { content: [{ type: "text", text: "An object needs an instance and a class." }], isError: true };
|
|
794
|
+
const objects = [...(def.settings.objects ?? [])];
|
|
795
|
+
if (objects.some((o) => o.instance === instance)) {
|
|
796
|
+
return { content: [{ type: "text", text: `The machine already has an object '${instance}'.` }], isError: true };
|
|
797
|
+
}
|
|
798
|
+
objects.push({ instance, class: className });
|
|
799
|
+
def.settings.objects = objects;
|
|
800
|
+
writeDef(def);
|
|
801
|
+
bridgeEmitPatch([{ op: "settings.update", patch: { objects } }]);
|
|
802
|
+
return { content: [{ type: "text", text: `Object '${instance}' : ${className} declared on '${def.settings.name}'.` }] };
|
|
803
|
+
});
|
|
804
|
+
server.tool("update_state", "Change what a state says about itself without rewriting the machine: its description, its kind (normal, final, history) or its prompt. Omitted fields stay as they are; an empty description or prompt clears it. Use it after a scenario built states in auto mode, to say what each one is.", {
|
|
805
|
+
name: z.string(),
|
|
806
|
+
description: z.string().optional(),
|
|
807
|
+
kind: z.enum(["normal", "final", "history"]).optional(),
|
|
808
|
+
prompt: z.string().optional(),
|
|
809
|
+
}, async ({ name, description, kind, prompt }) => {
|
|
810
|
+
const def = readDef();
|
|
811
|
+
if (!def)
|
|
812
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
813
|
+
const target = findState(def.state, name);
|
|
814
|
+
if (!target)
|
|
815
|
+
return { content: [{ type: "text", text: `State '${name}' not found.` }], isError: true };
|
|
816
|
+
const patch = {};
|
|
817
|
+
const said = [];
|
|
818
|
+
if (description !== undefined) {
|
|
819
|
+
if (description.trim())
|
|
820
|
+
target.description = description;
|
|
821
|
+
else
|
|
822
|
+
delete target.description;
|
|
823
|
+
patch.description = description;
|
|
824
|
+
said.push(description.trim() ? "description" : "description cleared");
|
|
825
|
+
}
|
|
826
|
+
if (kind !== undefined) {
|
|
827
|
+
target.kind = kind;
|
|
828
|
+
patch.kind = kind;
|
|
829
|
+
said.push(`kind ${kind}`);
|
|
830
|
+
}
|
|
831
|
+
if (prompt !== undefined) {
|
|
832
|
+
if (prompt.trim())
|
|
833
|
+
target.prompt = prompt;
|
|
834
|
+
else
|
|
835
|
+
delete target.prompt;
|
|
836
|
+
patch.prompt = prompt.trim() ? prompt : "";
|
|
837
|
+
said.push(prompt.trim() ? "prompt" : "prompt cleared");
|
|
838
|
+
}
|
|
839
|
+
if (!said.length)
|
|
840
|
+
return { content: [{ type: "text", text: `Nothing to change on '${name}': give a description, a kind or a prompt.` }], isError: true };
|
|
841
|
+
writeDef(def);
|
|
842
|
+
bridgeEmitPatch([{ op: "state.update", name, patch }]);
|
|
843
|
+
return { content: [{ type: "text", text: `Updated '${name}': ${said.join(", ")}.` }] };
|
|
844
|
+
});
|
|
845
|
+
server.tool("update_event", "Change an event's description without rewriting the machine. An empty description clears it.", { id: z.string(), description: z.string() }, async ({ id, description }) => {
|
|
846
|
+
const def = readDef();
|
|
847
|
+
if (!def)
|
|
848
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
849
|
+
const event = (def.events ?? []).flatMap((s) => s.events ?? []).find((e) => e.id === id);
|
|
850
|
+
if (!event)
|
|
851
|
+
return { content: [{ type: "text", text: `Event '${id}' not found.` }], isError: true };
|
|
852
|
+
if (description.trim())
|
|
853
|
+
event.description = description;
|
|
854
|
+
else
|
|
855
|
+
delete event.description;
|
|
856
|
+
writeDef(def);
|
|
857
|
+
bridgeEmitPatch([{ op: "event.update", id, patch: { description } }]);
|
|
858
|
+
return { content: [{ type: "text", text: description.trim() ? `Described event '${id}'.` : `Cleared the description of '${id}'.` }] };
|
|
859
|
+
});
|
|
787
860
|
server.tool("set_prompt", "Say what an agent is asked to resolve while the machine is in a state: the instruction it receives when the state is entered — an opening message, the frame of every later turn. `description` says what the state is and notes are the conversation about it; the prompt is what to do in it. The text replaces what was there; an empty string clears it. Engines and code generation ignore it; get_definition and generate_rispec show it.", { state: z.string(), prompt: z.string() }, async ({ state, prompt }) => {
|
|
788
861
|
const def = readDef();
|
|
789
862
|
if (!def)
|
|
@@ -1025,7 +1098,11 @@ function buildServer() {
|
|
|
1025
1098
|
: "bridge not configured (STATELOOM_BRIDGE_URL unset)";
|
|
1026
1099
|
return {
|
|
1027
1100
|
content: [
|
|
1028
|
-
{
|
|
1101
|
+
{
|
|
1102
|
+
type: "text",
|
|
1103
|
+
// The version is said so an agent can tell when its pin is behind (Episode 550, L4).
|
|
1104
|
+
text: `Active document: ${PROJECT_FILE}\n${summary}\n${bridgeNote}\nCanvas: ${canvasLink(PROJECT_FILE)}\nLoom: @miadi/stateloom-mcp ${PACKAGE_VERSION} (\`npm view @miadi/stateloom-mcp version\` names the latest)`,
|
|
1105
|
+
},
|
|
1029
1106
|
],
|
|
1030
1107
|
};
|
|
1031
1108
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/stateloom-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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.
|
|
41
|
+
"@miadi/stateloom-cli": "^0.1.4",
|
|
42
42
|
"@miadi/stateloom-client": "^0.1.3",
|
|
43
|
-
"@miadi/stateloom-protocol": "^0.1.
|
|
43
|
+
"@miadi/stateloom-protocol": "^0.1.13",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
45
45
|
"zod": "^4.3.6"
|
|
46
46
|
},
|