@miadi/stateloom-mcp 0.2.6 → 0.3.1
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 +1 -0
- package/dist/atomicWrite.js +28 -0
- package/dist/erd.d.ts +2 -0
- package/dist/erd.js +5 -2
- package/dist/live.d.ts +26 -0
- package/dist/live.js +131 -0
- package/dist/projectSwitch.d.ts +6 -2
- package/dist/projectSwitch.js +18 -6
- package/dist/sequence.d.ts +55 -0
- package/dist/sequence.js +337 -0
- package/dist/server.d.ts +21 -5
- package/dist/server.js +208 -75
- package/dist/system.d.ts +88 -0
- package/dist/system.js +813 -0
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -18,18 +18,34 @@
|
|
|
18
18
|
* - load_definition: Load a definition from JSON
|
|
19
19
|
* - list_states: List all states in the current definition
|
|
20
20
|
* - list_events: List all events in the current definition
|
|
21
|
-
* - set_project_file: Choose which document is active
|
|
21
|
+
* - set_project_file: Choose which document is active — .smdf.json (state
|
|
22
|
+
* machine), .erdf.json (ERD), .sqdf.json (sequence) or .sysdf.json (system)
|
|
22
23
|
* - get_project_file: Report the active document path and bridge status
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
+
* The active document's type is its extension, and the tools for each type
|
|
26
|
+
* refuse to write over another type.
|
|
27
|
+
*
|
|
28
|
+
* ERD tools (Spec 80, ./erd.ts):
|
|
25
29
|
* - create_erd, add_entity, update_entity, add_attribute, add_relationship
|
|
26
30
|
* - remove_entity, remove_attribute, remove_relationship
|
|
27
31
|
* - validate_erd, check_links
|
|
28
32
|
*
|
|
29
|
-
*
|
|
33
|
+
* Sequence tools (Spec 81, ./sequence.ts) — a usage scenario:
|
|
34
|
+
* - create_sequence, add_participant, update_participant, remove_participant
|
|
35
|
+
* - add_message, update_message, remove_message, move_message
|
|
36
|
+
* - add_fragment, add_fragment_message, remove_fragment, validate_sequence
|
|
37
|
+
*
|
|
38
|
+
* System tools (Spec 82, ./system.ts) — the drawings of one thing, together:
|
|
39
|
+
* - create_system, add_member, remove_member, add_actor, remove_actor
|
|
40
|
+
* - set_reconcile_mode, check_system, replay_scenario, reconcile_scenario
|
|
41
|
+
* - show: tell the canvases on a system which member and element to show
|
|
42
|
+
* In reconcile mode auto, each sequence edit also updates the machines, the
|
|
43
|
+
* ERD and the system it implies (./live.ts pushes the changes to their rooms).
|
|
44
|
+
*
|
|
45
|
+
* Notes (every document type): set_notes, get_notes — what a person or an
|
|
30
46
|
* agent wrote down about the diagram or one of its shapes, saved in the file.
|
|
31
|
-
* get_definition, load_definition and
|
|
32
|
-
* the active document
|
|
47
|
+
* get_definition, load_definition, render_diagram and check_links answer for
|
|
48
|
+
* the active document's type; generate_rispec also writes a system's rispec.
|
|
33
49
|
*
|
|
34
50
|
* Resources:
|
|
35
51
|
* - smcraft://definition — Current state machine definition
|
|
@@ -49,10 +65,13 @@ import { basename, dirname, join, resolve } from "path";
|
|
|
49
65
|
import { fileURLToPath } from "node:url";
|
|
50
66
|
import { tmpdir } from "os";
|
|
51
67
|
import { createBridgeClient } from "@miadi/stateloom-client";
|
|
52
|
-
import { collectNotes, envAlias, isErdfPath, } from "@miadi/stateloom-protocol";
|
|
68
|
+
import { collectNotes, docKindOfPath, envAlias, isErdfPath, isSqdfPath, isSysdfPath, } from "@miadi/stateloom-protocol";
|
|
53
69
|
import { renderDiagramToFile, defaultOutputPath } from "@miadi/stateloom-cli/render";
|
|
54
70
|
import { resolveProjectSwitch } from "./projectSwitch.js";
|
|
55
71
|
import { registerErdTools, erdSummary, erdGetDefinition, erdLoadDefinition, erdRender, erdGetNotes, erdSetNotes, formatNotes, } from "./erd.js";
|
|
72
|
+
import { registerSequenceTools, sequenceSummary, sequenceGetDefinition, sequenceLoadDefinition, sequenceRender, sequenceGetNotes, sequenceSetNotes, } from "./sequence.js";
|
|
73
|
+
import { registerSystemTools, checkSystemReport, reconcileAfterEdit, reconcileModeLine, systemSummary, systemGetDefinition, systemLoadDefinition, systemRender, systemGetNotes, systemSetNotes, systemRispec, } from "./system.js";
|
|
74
|
+
import { liveBridge } from "./live.js";
|
|
56
75
|
// STATELOOM_* read first, SMCRAFT_* legacy twin honored — the live MCP
|
|
57
76
|
// registration bakes SMCRAFT_PROJECT_FILE and must keep working (2026-07-27
|
|
58
77
|
// rename brief). Resolved once at boot, like the direct reads they replace.
|
|
@@ -72,9 +91,17 @@ const canvasLink = (path) => `${CANVAS_URL}/?doc=${encodeURIComponent(path)}`;
|
|
|
72
91
|
// Mutable: `set_project_file` lets the agent re-point the loom at another
|
|
73
92
|
// .smdf.json document mid-session (the hub keys live rooms by this path).
|
|
74
93
|
let PROJECT_FILE = resolve(envAlias("PROJECT_FILE") ?? "./statemachine.smdf.json");
|
|
94
|
+
const KIND_WORDS = { machine: "a state machine", erd: "an ERD", sequence: "a sequence", system: "a system" };
|
|
95
|
+
/** What the state-machine tools say when there is no machine to act on. */
|
|
96
|
+
function noMachine(then = "") {
|
|
97
|
+
const kind = docKindOfPath(PROJECT_FILE);
|
|
98
|
+
if (kind === "machine")
|
|
99
|
+
return `No state machine at ${PROJECT_FILE}.${then}`;
|
|
100
|
+
return `The active document ${PROJECT_FILE} is ${KIND_WORDS[kind]}, not a state machine. Use the ${kind === "erd" ? "ERD" : kind} tools on it, or set_project_file to a .smdf.json document.`;
|
|
101
|
+
}
|
|
75
102
|
function readDef() {
|
|
76
|
-
// An ERD is not a machine; the state-machine tools see no definition there.
|
|
77
|
-
if (
|
|
103
|
+
// An ERD, a sequence or a system is not a machine; the state-machine tools see no definition there.
|
|
104
|
+
if (docKindOfPath(PROJECT_FILE) !== "machine")
|
|
78
105
|
return null;
|
|
79
106
|
if (!existsSync(PROJECT_FILE))
|
|
80
107
|
return null;
|
|
@@ -89,11 +116,10 @@ function readDef() {
|
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
function writeDef(def) {
|
|
92
|
-
// Never write a machine over an ERD: create_state_machine
|
|
93
|
-
// write without reading first, so this is the one place that can refuse.
|
|
94
|
-
if (
|
|
95
|
-
throw new Error(
|
|
96
|
-
}
|
|
119
|
+
// Never write a machine over an ERD, a sequence or a system: create_state_machine
|
|
120
|
+
// and load_definition write without reading first, so this is the one place that can refuse.
|
|
121
|
+
if (docKindOfPath(PROJECT_FILE) !== "machine")
|
|
122
|
+
throw new Error(noMachine());
|
|
97
123
|
writeFileSync(PROJECT_FILE, JSON.stringify({ stateMachine: def }, null, 2), "utf8");
|
|
98
124
|
}
|
|
99
125
|
// ─── Optional real-time design bridge (env-gated, best-effort) ───────
|
|
@@ -227,6 +253,34 @@ const erdHost = {
|
|
|
227
253
|
},
|
|
228
254
|
emitFull: (def) => bridgeEmitFull(def),
|
|
229
255
|
outsideRoot: (path) => outsideRoot(path),
|
|
256
|
+
// From a sequence or a system, the links are the whole system's: check_system.
|
|
257
|
+
checkSystem: async () => checkSystemReport(systemHost),
|
|
258
|
+
};
|
|
259
|
+
// Pushes to rooms other than the active document's: a reconciled machine, an
|
|
260
|
+
// ERD, a view on a system. Each is a short-lived client of its own (./live.ts).
|
|
261
|
+
const live = liveBridge({ url: BRIDGE_URL, token: BRIDGE_TOKEN, name: AGENT_NAME ?? "mcp-agent" });
|
|
262
|
+
// The system this server last created, set or checked — where `show` and the
|
|
263
|
+
// system tools go when neither an argument nor the active document names one.
|
|
264
|
+
let LAST_SYSTEM = isSysdfPath(PROJECT_FILE) ? PROJECT_FILE : undefined;
|
|
265
|
+
const systemHost = {
|
|
266
|
+
projectFile: () => PROJECT_FILE,
|
|
267
|
+
outsideRoot: (path) => outsideRoot(path),
|
|
268
|
+
emitFull: (def) => bridgeEmitFull(def),
|
|
269
|
+
live,
|
|
270
|
+
reconcileOverride: () => envAlias("RECONCILE"),
|
|
271
|
+
validateMachine: (def) => validate(def),
|
|
272
|
+
machineSpec: (def, level) => machineSpecLines(def, level),
|
|
273
|
+
lastSystem: () => LAST_SYSTEM,
|
|
274
|
+
rememberSystem: (path) => {
|
|
275
|
+
LAST_SYSTEM = path;
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
const sequenceHost = {
|
|
279
|
+
projectFile: () => PROJECT_FILE,
|
|
280
|
+
switchTo: (path) => erdHost.switchTo(path),
|
|
281
|
+
emitFull: (def) => bridgeEmitFull(def),
|
|
282
|
+
outsideRoot: (path) => outsideRoot(path),
|
|
283
|
+
afterEdit: (path) => reconcileAfterEdit(systemHost, path),
|
|
230
284
|
};
|
|
231
285
|
function createEmpty(namespace, name) {
|
|
232
286
|
return {
|
|
@@ -501,30 +555,7 @@ function generateRispec(def, intent) {
|
|
|
501
555
|
// Specifications
|
|
502
556
|
lines.push("## S — Specifications");
|
|
503
557
|
lines.push("");
|
|
504
|
-
lines.push(
|
|
505
|
-
lines.push("");
|
|
506
|
-
for (const s of states) {
|
|
507
|
-
renderStateSpec(s, lines, 0);
|
|
508
|
-
}
|
|
509
|
-
lines.push("");
|
|
510
|
-
lines.push("### Events");
|
|
511
|
-
lines.push("");
|
|
512
|
-
for (const src of def.events) {
|
|
513
|
-
if (!src.events?.length)
|
|
514
|
-
continue;
|
|
515
|
-
lines.push(`**${src.name}**`);
|
|
516
|
-
for (const e of src.events) {
|
|
517
|
-
lines.push(`- \`${e.id}\`${e.description ? ` — ${e.description}` : ""}`);
|
|
518
|
-
}
|
|
519
|
-
lines.push("");
|
|
520
|
-
}
|
|
521
|
-
lines.push("### Transitions (flat)");
|
|
522
|
-
lines.push("");
|
|
523
|
-
for (const t of collectTransitions(def.state)) {
|
|
524
|
-
const arrow = t.next ? `→ ${t.next}` : "→ (internal)";
|
|
525
|
-
lines.push(`- \`${t.from}\` --[${t.event}]-- ${arrow}${t.condition ? ` *(when: ${t.condition})*` : ""}`);
|
|
526
|
-
}
|
|
527
|
-
lines.push("");
|
|
558
|
+
lines.push(...machineSpecLines(def, 3));
|
|
528
559
|
// Action Steps (medicine-wheel walk if South/East/North/West present)
|
|
529
560
|
const mw = states.filter((s) => ["East", "South", "West", "North"].includes(s.name));
|
|
530
561
|
if (mw.length || pde?.actionStack?.length) {
|
|
@@ -568,6 +599,40 @@ function generateRispec(def, intent) {
|
|
|
568
599
|
lines.push("");
|
|
569
600
|
return lines.join("\n");
|
|
570
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* A machine's states, events and flat transitions as markdown, headings at
|
|
604
|
+
* `level` — the Specifications of a machine's rispec, and each machine's
|
|
605
|
+
* Behaviour in a system's rispec.
|
|
606
|
+
*/
|
|
607
|
+
function machineSpecLines(def, level) {
|
|
608
|
+
const h = "#".repeat(level);
|
|
609
|
+
const lines = [];
|
|
610
|
+
lines.push(`${h} States`);
|
|
611
|
+
lines.push("");
|
|
612
|
+
for (const s of def.state?.states ?? []) {
|
|
613
|
+
renderStateSpec(s, lines, 0);
|
|
614
|
+
}
|
|
615
|
+
lines.push("");
|
|
616
|
+
lines.push(`${h} Events`);
|
|
617
|
+
lines.push("");
|
|
618
|
+
for (const src of def.events ?? []) {
|
|
619
|
+
if (!src.events?.length)
|
|
620
|
+
continue;
|
|
621
|
+
lines.push(`**${src.name}**`);
|
|
622
|
+
for (const e of src.events) {
|
|
623
|
+
lines.push(`- \`${e.id}\`${e.description ? ` — ${e.description}` : ""}`);
|
|
624
|
+
}
|
|
625
|
+
lines.push("");
|
|
626
|
+
}
|
|
627
|
+
lines.push(`${h} Transitions (flat)`);
|
|
628
|
+
lines.push("");
|
|
629
|
+
for (const t of def.state ? collectTransitions(def.state) : []) {
|
|
630
|
+
const arrow = t.next ? `→ ${t.next}` : "→ (internal)";
|
|
631
|
+
lines.push(`- \`${t.from}\` --[${t.event}]-- ${arrow}${t.condition ? ` *(when: ${t.condition})*` : ""}`);
|
|
632
|
+
}
|
|
633
|
+
lines.push("");
|
|
634
|
+
return lines;
|
|
635
|
+
}
|
|
571
636
|
function depth(s, d = 0) {
|
|
572
637
|
if (!s.states?.length)
|
|
573
638
|
return d;
|
|
@@ -592,6 +657,11 @@ function renderStateSpec(s, lines, depthLevel) {
|
|
|
592
657
|
const indent = " ".repeat(depthLevel);
|
|
593
658
|
const kind = s.kind && s.kind !== "normal" ? ` *(${s.kind})*` : "";
|
|
594
659
|
lines.push(`${indent}- **${s.name}**${kind}${s.description ? ` — ${s.description}` : ""}`);
|
|
660
|
+
if (s.prompt?.trim()) {
|
|
661
|
+
lines.push(`${indent} - *the agent resolves here:*`);
|
|
662
|
+
for (const line of s.prompt.trim().split("\n"))
|
|
663
|
+
lines.push(`${indent} > ${line}`);
|
|
664
|
+
}
|
|
595
665
|
for (const t of s.transitions ?? []) {
|
|
596
666
|
lines.push(`${indent} - on \`${t.event}\` ${t.nextState ? `→ \`${t.nextState}\`` : "(internal)"}`);
|
|
597
667
|
}
|
|
@@ -672,16 +742,17 @@ function buildServer() {
|
|
|
672
742
|
],
|
|
673
743
|
};
|
|
674
744
|
});
|
|
675
|
-
server.tool("add_state", "Add a state to the state machine. Parent defaults to Root.", {
|
|
745
|
+
server.tool("add_state", "Add a state to the state machine. Parent defaults to Root. `prompt` is what an agent is asked to resolve while the machine is in this state (see set_prompt).", {
|
|
676
746
|
name: z.string(),
|
|
677
747
|
parent: z.string().optional(),
|
|
678
748
|
kind: z.enum(["normal", "final", "history"]).optional(),
|
|
679
749
|
description: z.string().optional(),
|
|
680
|
-
|
|
750
|
+
prompt: z.string().optional(),
|
|
751
|
+
}, async ({ name, parent, kind, description, prompt }) => {
|
|
681
752
|
const def = readDef();
|
|
682
753
|
if (!def)
|
|
683
754
|
return {
|
|
684
|
-
content: [{ type: "text", text:
|
|
755
|
+
content: [{ type: "text", text: noMachine(" Use create_state_machine first.") }],
|
|
685
756
|
isError: true,
|
|
686
757
|
};
|
|
687
758
|
const parentName = parent ?? "Root";
|
|
@@ -693,20 +764,38 @@ function buildServer() {
|
|
|
693
764
|
};
|
|
694
765
|
if (!parentState.states)
|
|
695
766
|
parentState.states = [];
|
|
696
|
-
|
|
767
|
+
const added = { name, kind: kind ?? "normal", description, ...(prompt?.trim() ? { prompt } : {}) };
|
|
768
|
+
parentState.states.push(added);
|
|
697
769
|
writeDef(def);
|
|
698
770
|
bridgeEmitPatch([
|
|
699
|
-
{ op: "state.add", parent: parentName, state: { name, kind: kind ?? "normal", description } },
|
|
771
|
+
{ op: "state.add", parent: parentName, state: { name, kind: kind ?? "normal", description, ...(added.prompt ? { prompt: added.prompt } : {}) } },
|
|
700
772
|
]);
|
|
701
773
|
return {
|
|
702
774
|
content: [{ type: "text", text: `Added state '${name}' under '${parentName}' (${kind ?? "normal"}).` }],
|
|
703
775
|
};
|
|
704
776
|
});
|
|
777
|
+
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 }) => {
|
|
778
|
+
const def = readDef();
|
|
779
|
+
if (!def)
|
|
780
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
781
|
+
const target = findState(def.state, state);
|
|
782
|
+
if (!target)
|
|
783
|
+
return { content: [{ type: "text", text: `State '${state}' not found.` }], isError: true };
|
|
784
|
+
if (prompt.trim())
|
|
785
|
+
target.prompt = prompt;
|
|
786
|
+
else
|
|
787
|
+
delete target.prompt;
|
|
788
|
+
writeDef(def);
|
|
789
|
+
bridgeEmitPatch([{ op: "state.update", name: state, patch: { prompt: prompt.trim() ? prompt : "" } }]);
|
|
790
|
+
return {
|
|
791
|
+
content: [{ type: "text", text: prompt.trim() ? `Prompt set on '${state}' (${prompt.length} characters).` : `Prompt cleared on '${state}'.` }],
|
|
792
|
+
};
|
|
793
|
+
});
|
|
705
794
|
server.tool("add_event", "Add an event to the state machine", { id: z.string(), description: z.string().optional() }, async ({ id, description }) => {
|
|
706
795
|
const def = readDef();
|
|
707
796
|
if (!def)
|
|
708
797
|
return {
|
|
709
|
-
content: [{ type: "text", text:
|
|
798
|
+
content: [{ type: "text", text: noMachine() }],
|
|
710
799
|
isError: true,
|
|
711
800
|
};
|
|
712
801
|
const source = def.events[0];
|
|
@@ -727,7 +816,7 @@ function buildServer() {
|
|
|
727
816
|
const def = readDef();
|
|
728
817
|
if (!def)
|
|
729
818
|
return {
|
|
730
|
-
content: [{ type: "text", text:
|
|
819
|
+
content: [{ type: "text", text: noMachine() }],
|
|
731
820
|
isError: true,
|
|
732
821
|
};
|
|
733
822
|
const target = findState(def.state, state);
|
|
@@ -749,7 +838,7 @@ function buildServer() {
|
|
|
749
838
|
server.tool("remove_state", "Remove a state from the definition", { name: z.string() }, async ({ name }) => {
|
|
750
839
|
const def = readDef();
|
|
751
840
|
if (!def)
|
|
752
|
-
return { content: [{ type: "text", text:
|
|
841
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
753
842
|
if (name === "Root")
|
|
754
843
|
return { content: [{ type: "text", text: "Cannot remove Root state." }], isError: true };
|
|
755
844
|
def.state = removeState(def.state, name);
|
|
@@ -760,7 +849,7 @@ function buildServer() {
|
|
|
760
849
|
server.tool("validate_definition", "Validate the current state machine definition against all rules", {}, async () => {
|
|
761
850
|
const def = readDef();
|
|
762
851
|
if (!def)
|
|
763
|
-
return { content: [{ type: "text", text:
|
|
852
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
764
853
|
const errors = validate(def);
|
|
765
854
|
if (errors.length === 0)
|
|
766
855
|
return { content: [{ type: "text", text: "✓ Definition is valid. No errors found." }] };
|
|
@@ -772,7 +861,7 @@ function buildServer() {
|
|
|
772
861
|
server.tool("generate_code", "Generate Python or TypeScript code from the current definition", { language: z.enum(["python", "typescript"]) }, async ({ language }) => {
|
|
773
862
|
const def = readDef();
|
|
774
863
|
if (!def)
|
|
775
|
-
return { content: [{ type: "text", text:
|
|
864
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
776
865
|
const errors = validate(def);
|
|
777
866
|
if (errors.length > 0) {
|
|
778
867
|
const lines = errors.map((e) => `[${e.ruleId}] ${e.message}`).join("\n");
|
|
@@ -784,19 +873,27 @@ function buildServer() {
|
|
|
784
873
|
const code = generateViaSmcg(def, language);
|
|
785
874
|
return { content: [{ type: "text", text: code }] };
|
|
786
875
|
});
|
|
787
|
-
server.tool("get_definition", "Get the
|
|
876
|
+
server.tool("get_definition", "Get the active document as JSON — a state machine, or the ERD, sequence or system when the active document is a .erdf.json, .sqdf.json or .sysdf.json", {}, async () => {
|
|
788
877
|
if (isErdfPath(PROJECT_FILE))
|
|
789
878
|
return erdGetDefinition(erdHost);
|
|
879
|
+
if (isSqdfPath(PROJECT_FILE))
|
|
880
|
+
return sequenceGetDefinition(sequenceHost);
|
|
881
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
882
|
+
return systemGetDefinition(systemHost);
|
|
790
883
|
const def = readDef();
|
|
791
884
|
if (!def)
|
|
792
|
-
return { content: [{ type: "text", text:
|
|
885
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
793
886
|
return {
|
|
794
887
|
content: [{ type: "text", text: JSON.stringify({ stateMachine: def }, null, 2) }],
|
|
795
888
|
};
|
|
796
889
|
});
|
|
797
|
-
server.tool("load_definition", "Load a definition from JSON into the active document — a state machine, or an ERD when the active document is a .erdf.json", { json: z.string() }, async ({ json }) => {
|
|
890
|
+
server.tool("load_definition", "Load a definition from JSON into the active document — a state machine, or an ERD, a sequence or a system when the active document is a .erdf.json, .sqdf.json or .sysdf.json", { json: z.string() }, async ({ json }) => {
|
|
798
891
|
if (isErdfPath(PROJECT_FILE))
|
|
799
892
|
return erdLoadDefinition(erdHost, json);
|
|
893
|
+
if (isSqdfPath(PROJECT_FILE))
|
|
894
|
+
return sequenceLoadDefinition(sequenceHost, json);
|
|
895
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
896
|
+
return systemLoadDefinition(systemHost, json);
|
|
800
897
|
try {
|
|
801
898
|
const parsed = JSON.parse(json);
|
|
802
899
|
const def = parsed.stateMachine ?? parsed.StateMachine ?? parsed;
|
|
@@ -820,7 +917,7 @@ function buildServer() {
|
|
|
820
917
|
server.tool("list_states", "List all states in the current definition with their kind and transitions", {}, async () => {
|
|
821
918
|
const def = readDef();
|
|
822
919
|
if (!def)
|
|
823
|
-
return { content: [{ type: "text", text:
|
|
920
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
824
921
|
const lines = [];
|
|
825
922
|
const walk = (state, depth) => {
|
|
826
923
|
const indent = " ".repeat(depth);
|
|
@@ -838,13 +935,13 @@ function buildServer() {
|
|
|
838
935
|
server.tool("list_events", "List all events in the current definition", {}, async () => {
|
|
839
936
|
const def = readDef();
|
|
840
937
|
if (!def)
|
|
841
|
-
return { content: [{ type: "text", text:
|
|
938
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
842
939
|
const events = def.events.flatMap((src) => src.events.map((e) => `${e.id}${e.description ? ` — ${e.description}` : ""}`));
|
|
843
940
|
if (events.length === 0)
|
|
844
941
|
return { content: [{ type: "text", text: "No events defined yet." }] };
|
|
845
942
|
return { content: [{ type: "text", text: events.join("\n") }] };
|
|
846
943
|
});
|
|
847
|
-
server.tool("set_project_file", "Choose the active document the loom weaves — two modalities. STANDALONE:
|
|
944
|
+
server.tool("set_project_file", "Choose the active document the loom weaves — two modalities. STANDALONE: a path whose extension says what it is: .smdf.json (a state machine), .erdf.json (an ERD — the ERD tools act on it), .sqdf.json (a sequence — the sequence tools act on it) or .sysdf.json (a system — the system tools, check_system and show act on it). RELATIONAL: a chronicle address `miadi-chronicle://<episode>/<diagram>` resolving over $MIADI_CHRONICLE_ROOT to <episode-dir>/diagrams/<diagram>.smdf.json; name another type by its extension (`miadi-chronicle://103/flow.sqdf.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 }) => {
|
|
848
945
|
try {
|
|
849
946
|
const r = resolveProjectSwitch(path, PROJECT_FILE);
|
|
850
947
|
const denial = denyProjectSwitch(r.path);
|
|
@@ -855,14 +952,25 @@ function buildServer() {
|
|
|
855
952
|
bindBridge(PROJECT_FILE);
|
|
856
953
|
joinBridge();
|
|
857
954
|
}
|
|
955
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
956
|
+
LAST_SYSTEM = PROJECT_FILE;
|
|
858
957
|
const def = r.exists ? readDef() : null;
|
|
859
|
-
const summary =
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
958
|
+
const summary = [
|
|
959
|
+
isErdfPath(PROJECT_FILE)
|
|
960
|
+
? erdSummary(PROJECT_FILE)
|
|
961
|
+
: isSqdfPath(PROJECT_FILE)
|
|
962
|
+
? sequenceSummary(PROJECT_FILE)
|
|
963
|
+
: isSysdfPath(PROJECT_FILE)
|
|
964
|
+
? systemSummary(PROJECT_FILE)
|
|
965
|
+
: def
|
|
966
|
+
? `existing machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
|
|
967
|
+
: r.exists
|
|
968
|
+
? "file exists but is not a readable definition"
|
|
969
|
+
: "no file yet — create_state_machine or load_definition will write it",
|
|
970
|
+
reconcileModeLine(systemHost, PROJECT_FILE),
|
|
971
|
+
]
|
|
972
|
+
.filter(Boolean)
|
|
973
|
+
.join("\n");
|
|
866
974
|
const bridgeNote = BRIDGE_URL
|
|
867
975
|
? r.unchanged
|
|
868
976
|
? `bridge unchanged (${bridge?.status ?? "not configured"})`
|
|
@@ -884,15 +992,24 @@ function buildServer() {
|
|
|
884
992
|
};
|
|
885
993
|
}
|
|
886
994
|
});
|
|
887
|
-
server.tool("get_project_file", "Report the active .smdf.json
|
|
995
|
+
server.tool("get_project_file", "Report the active document's path and what it is (a .smdf.json state machine, .erdf.json ERD, .sqdf.json sequence or .sysdf.json system), whether it exists on disk, the reconcile mode for a sequence or a system, and live-bridge status", {}, async () => {
|
|
888
996
|
const def = readDef();
|
|
889
|
-
const summary =
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
997
|
+
const summary = [
|
|
998
|
+
isErdfPath(PROJECT_FILE)
|
|
999
|
+
? erdSummary(PROJECT_FILE)
|
|
1000
|
+
: isSqdfPath(PROJECT_FILE)
|
|
1001
|
+
? sequenceSummary(PROJECT_FILE)
|
|
1002
|
+
: isSysdfPath(PROJECT_FILE)
|
|
1003
|
+
? systemSummary(PROJECT_FILE)
|
|
1004
|
+
: def
|
|
1005
|
+
? `machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
|
|
1006
|
+
: existsSync(PROJECT_FILE)
|
|
1007
|
+
? "file exists but is not a readable definition"
|
|
1008
|
+
: "file does not exist yet",
|
|
1009
|
+
reconcileModeLine(systemHost, PROJECT_FILE),
|
|
1010
|
+
]
|
|
1011
|
+
.filter(Boolean)
|
|
1012
|
+
.join("\n");
|
|
896
1013
|
const bridgeNote = BRIDGE_URL
|
|
897
1014
|
? `bridge: ${bridge?.status ?? "not initialized"} → ${BRIDGE_URL}`
|
|
898
1015
|
: "bridge not configured (STATELOOM_BRIDGE_URL unset)";
|
|
@@ -902,10 +1019,12 @@ function buildServer() {
|
|
|
902
1019
|
],
|
|
903
1020
|
};
|
|
904
1021
|
});
|
|
905
|
-
server.tool("generate_rispec", "Generate a RISE rispec (markdown) from the current state machine. If the SMDF was sourced from a PDE (settings._source.pdeId/pdeFolder), the PDE's intent, directions, and ambiguities are folded in. Pass `intent` to override the desired outcome.", { intent: z.string().optional() }, async ({ intent }) => {
|
|
1022
|
+
server.tool("generate_rispec", "Generate a RISE rispec (markdown) from the current state machine. If the SMDF was sourced from a PDE (settings._source.pdeId/pdeFolder), the PDE's intent, directions, and ambiguities are folded in. Pass `intent` to override the desired outcome. When the active document is a system (.sysdf.json), or `system` names one, the rispec is the whole system's: Creative Intent, Data from its ERDs, Behaviour per machine, each scenario path as a Creative Advancement Scenario (from the replay), and Open questions. Returns markdown; writes nothing.", { intent: z.string().optional(), system: z.string().optional() }, async ({ intent, system }) => {
|
|
1023
|
+
if (system || isSysdfPath(PROJECT_FILE))
|
|
1024
|
+
return systemRispec(systemHost, { system, intent });
|
|
906
1025
|
const def = readDef();
|
|
907
1026
|
if (!def)
|
|
908
|
-
return { content: [{ type: "text", text:
|
|
1027
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
909
1028
|
const md = generateRispec(def, intent);
|
|
910
1029
|
return { content: [{ type: "text", text: md }] };
|
|
911
1030
|
});
|
|
@@ -932,7 +1051,7 @@ function buildServer() {
|
|
|
932
1051
|
return "no desktop opener available";
|
|
933
1052
|
}
|
|
934
1053
|
}
|
|
935
|
-
server.tool("render_diagram", "Draw the current state machine as a picture. Writes the file next to the project document (or at `path`) and returns its absolute path — plus the image itself for png, so it can be looked at without leaving the conversation. Formats: png (raster, needs librsvg/Inkscape/ImageMagick/Chrome on the host), svg (needs nothing), mermaid, ascii. Pass stamp:true to keep every render instead of overwriting one file.", {
|
|
1054
|
+
server.tool("render_diagram", "Draw the current state machine as a picture. Writes the file next to the project document (or at `path`) and returns its absolute path — plus the image itself for png, so it can be looked at without leaving the conversation. Formats: png (raster, needs librsvg/Inkscape/ImageMagick/Chrome on the host), svg (needs nothing), mermaid, ascii. Pass stamp:true to keep every render instead of overwriting one file. An ERD renders as mermaid (<name>.erd.mmd), a sequence as mermaid (<name>.seq.mmd), and a system as one markdown file of its members' mermaid (<name>.sys.md).", {
|
|
936
1055
|
format: z.enum(["png", "svg", "mermaid", "ascii"]).optional(),
|
|
937
1056
|
path: z.string().optional(),
|
|
938
1057
|
scale: z.number().optional(),
|
|
@@ -942,10 +1061,14 @@ function buildServer() {
|
|
|
942
1061
|
}, async ({ format, path, scale, theme, stamp, open }) => {
|
|
943
1062
|
if (isErdfPath(PROJECT_FILE))
|
|
944
1063
|
return erdRender(erdHost, { format, path, stamp });
|
|
1064
|
+
if (isSqdfPath(PROJECT_FILE))
|
|
1065
|
+
return sequenceRender(sequenceHost, { format, path, stamp });
|
|
1066
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
1067
|
+
return systemRender(systemHost, { format, path, stamp });
|
|
945
1068
|
const def = readDef();
|
|
946
1069
|
if (!def)
|
|
947
1070
|
return {
|
|
948
|
-
content: [{ type: "text", text:
|
|
1071
|
+
content: [{ type: "text", text: noMachine() }],
|
|
949
1072
|
isError: true,
|
|
950
1073
|
};
|
|
951
1074
|
const chosen = format ?? "png";
|
|
@@ -1009,13 +1132,19 @@ function buildServer() {
|
|
|
1009
1132
|
}
|
|
1010
1133
|
});
|
|
1011
1134
|
registerErdTools(server, erdHost);
|
|
1135
|
+
registerSequenceTools(server, sequenceHost);
|
|
1136
|
+
registerSystemTools(server, systemHost);
|
|
1012
1137
|
// Notes — the same two tools for either document type.
|
|
1013
|
-
server.tool("set_notes", "Save working notes on the active diagram or on one of its shapes — what was said about it, an open question, a decision — so whoever opens the document next (a person or an agent) finds them. Omit `target` for the whole diagram; otherwise `target` is a state name (state machine)
|
|
1138
|
+
server.tool("set_notes", "Save working notes on the active diagram or on one of its shapes — what was said about it, an open question, a decision — so whoever opens the document next (a person or an agent) finds them. Omit `target` for the whole diagram; otherwise `target` is a state name (state machine), an entity name (ERD), a participant name, a message ('9', 'f1.2') or 'fragment N' (sequence), or a member's path (system). The text replaces what was there; an empty string clears it. Notes are kept in the document and are not part of the model: engines and code generation ignore them.", { target: z.string().optional(), notes: z.string() }, async ({ target, notes }) => {
|
|
1014
1139
|
if (isErdfPath(PROJECT_FILE))
|
|
1015
1140
|
return erdSetNotes(erdHost, target, notes);
|
|
1141
|
+
if (isSqdfPath(PROJECT_FILE))
|
|
1142
|
+
return sequenceSetNotes(sequenceHost, target, notes);
|
|
1143
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
1144
|
+
return systemSetNotes(systemHost, target, notes);
|
|
1016
1145
|
const def = readDef();
|
|
1017
1146
|
if (!def)
|
|
1018
|
-
return { content: [{ type: "text", text:
|
|
1147
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
1019
1148
|
const holder = target ? findState(def.state, target) : def.settings;
|
|
1020
1149
|
if (!holder)
|
|
1021
1150
|
return { content: [{ type: "text", text: `State '${target}' not found.` }], isError: true };
|
|
@@ -1039,9 +1168,13 @@ function buildServer() {
|
|
|
1039
1168
|
server.tool("get_notes", "Read every note in the active document in one call — the diagram's own first, then each shape that has one. Read them before changing a diagram somebody else has been working on: they are where a person leaves what they were thinking.", {}, async () => {
|
|
1040
1169
|
if (isErdfPath(PROJECT_FILE))
|
|
1041
1170
|
return erdGetNotes(erdHost);
|
|
1171
|
+
if (isSqdfPath(PROJECT_FILE))
|
|
1172
|
+
return sequenceGetNotes(sequenceHost);
|
|
1173
|
+
if (isSysdfPath(PROJECT_FILE))
|
|
1174
|
+
return systemGetNotes(systemHost);
|
|
1042
1175
|
const def = readDef();
|
|
1043
1176
|
if (!def)
|
|
1044
|
-
return { content: [{ type: "text", text:
|
|
1177
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
1045
1178
|
const entries = collectNotes(def);
|
|
1046
1179
|
return { content: [{ type: "text", text: formatNotes(def.settings.name, PROJECT_FILE, entries) }] };
|
|
1047
1180
|
});
|
package/dist/system.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The system half of the MCP server (Spec 82).
|
|
3
|
+
*
|
|
4
|
+
* A system (`.sysdf.json`) names the drawings of one thing being built — its
|
|
5
|
+
* ERDs, its machines, its scenarios — and the actors its scenarios share. The
|
|
6
|
+
* tools here edit it, check every name that crosses between its members, replay
|
|
7
|
+
* a scenario through the machines, and reconcile the members from a scenario.
|
|
8
|
+
*
|
|
9
|
+
* Reconcile mode (`settings.reconcile`, overridden for a session by
|
|
10
|
+
* STATELOOM_RECONCILE): in `propose`, a sequence edit reports in one line what
|
|
11
|
+
* the scenario implies for the other members; in `auto`, the edit applies it —
|
|
12
|
+
* machines are written and patched live, ERDs and the system are written and
|
|
13
|
+
* sent whole — and reports in one line what changed.
|
|
14
|
+
*
|
|
15
|
+
* Which system: the `system` argument, else the active document when it is a
|
|
16
|
+
* `.sysdf.json`, else the system that lists the sequence (or member) at hand —
|
|
17
|
+
* the last one this server created, set or checked, or one beside it — else the
|
|
18
|
+
* last system. Which sequence: the `sequence` argument, else the active document
|
|
19
|
+
* when it is a `.sqdf.json`. A sequence no system lists is checked, replayed and
|
|
20
|
+
* reconciled against the machines (and ERDs) beside it.
|
|
21
|
+
*
|
|
22
|
+
* Everything that touches the server's own state comes in through `SystemHost`.
|
|
23
|
+
*/
|
|
24
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
|
+
import { type StateMachineDefinition, type SystemDefinition } from "@miadi/stateloom-protocol";
|
|
26
|
+
import type { Live } from "./live.js";
|
|
27
|
+
export interface SystemHost {
|
|
28
|
+
/** The active document's absolute path. */
|
|
29
|
+
projectFile(): string;
|
|
30
|
+
/** The refusal for a path outside the permitted document root, if any. */
|
|
31
|
+
outsideRoot(path: string): string | undefined;
|
|
32
|
+
/** Mirror a document to the server's own room (it is the active document). Best-effort. */
|
|
33
|
+
emitFull(def: unknown): void;
|
|
34
|
+
/** Pushes to other rooms: a reconciled machine, an ERD, a view. */
|
|
35
|
+
live: Live;
|
|
36
|
+
/** STATELOOM_RECONCILE, when set: wins over every system's own mode for this session. */
|
|
37
|
+
reconcileOverride(): string | undefined;
|
|
38
|
+
/** A machine's own V rules — the validator behind validate_definition. */
|
|
39
|
+
validateMachine(def: StateMachineDefinition): {
|
|
40
|
+
ruleId: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}[];
|
|
43
|
+
/** A machine's states, events and transitions as markdown, headings at `level` — generate_rispec's content. */
|
|
44
|
+
machineSpec(def: StateMachineDefinition, level: number): string[];
|
|
45
|
+
/** The system this server last created, set or checked. */
|
|
46
|
+
lastSystem(): string | undefined;
|
|
47
|
+
rememberSystem(path: string): void;
|
|
48
|
+
}
|
|
49
|
+
type ToolResult = {
|
|
50
|
+
content: {
|
|
51
|
+
type: "text";
|
|
52
|
+
text: string;
|
|
53
|
+
}[];
|
|
54
|
+
isError?: boolean;
|
|
55
|
+
};
|
|
56
|
+
export declare function readSystem(path: string): SystemDefinition | null;
|
|
57
|
+
/** One line for set_project_file / get_project_file when the active document is a system. */
|
|
58
|
+
export declare function systemSummary(path: string): string;
|
|
59
|
+
/** The reconcile line for get_project_file: a system's mode, or the mode of the system that lists a sequence. */
|
|
60
|
+
export declare function reconcileModeLine(host: SystemHost, path: string): string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* After a sequence edit (sequence.ts `afterEdit`): reconcile the system that
|
|
63
|
+
* lists it, and return the ONE line the edit's result carries. In auto mode the
|
|
64
|
+
* changes are applied; in propose mode — or when no system lists the sequence —
|
|
65
|
+
* they are counted.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reconcileAfterEdit(host: SystemHost, sequencePath: string): Promise<string | undefined>;
|
|
68
|
+
export declare function checkSystemReport(host: SystemHost, args?: {
|
|
69
|
+
system?: string;
|
|
70
|
+
sequence?: string;
|
|
71
|
+
}): ToolResult;
|
|
72
|
+
export declare function systemGetDefinition(host: SystemHost): ToolResult;
|
|
73
|
+
export declare function systemLoadDefinition(host: SystemHost, json: string): ToolResult;
|
|
74
|
+
/** Every member's mermaid in one markdown file: `<name>.sys.md` beside the system. */
|
|
75
|
+
export declare function systemRender(host: SystemHost, opts: {
|
|
76
|
+
format?: string;
|
|
77
|
+
path?: string;
|
|
78
|
+
stamp?: boolean;
|
|
79
|
+
}): ToolResult;
|
|
80
|
+
export declare function systemGetNotes(host: SystemHost): ToolResult;
|
|
81
|
+
/** `target` is a member's path (as the system writes it, or absolute); omitted, the whole system. */
|
|
82
|
+
export declare function systemSetNotes(host: SystemHost, target: string | undefined, notes: string): ToolResult;
|
|
83
|
+
export declare function systemRispec(host: SystemHost, args: {
|
|
84
|
+
system?: string;
|
|
85
|
+
intent?: string;
|
|
86
|
+
}): ToolResult;
|
|
87
|
+
export declare function registerSystemTools(server: McpServer, host: SystemHost): void;
|
|
88
|
+
export {};
|