@miadi/stateloom-mcp 0.2.6 → 0.3.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.
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 (.smdf.json or .erdf.json)
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
- * ERD tools (Spec 80, ./erd.ts) — the active document's type is its extension:
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
- * Notes (either document type): set_notes, get_notes — what a person or an
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 render_diagram answer for an ERD when
32
- * the active document is one.
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 (isErdfPath(PROJECT_FILE))
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 and load_definition
93
- // write without reading first, so this is the one place that can refuse.
94
- if (isErdfPath(PROJECT_FILE)) {
95
- throw new Error(`The active document ${PROJECT_FILE} is an ERD. Use the ERD tools on it, or set_project_file to a .smdf.json document for a state machine.`);
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("### States");
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;
@@ -681,7 +746,7 @@ function buildServer() {
681
746
  const def = readDef();
682
747
  if (!def)
683
748
  return {
684
- content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}. Use create_state_machine first.` }],
749
+ content: [{ type: "text", text: noMachine(" Use create_state_machine first.") }],
685
750
  isError: true,
686
751
  };
687
752
  const parentName = parent ?? "Root";
@@ -706,7 +771,7 @@ function buildServer() {
706
771
  const def = readDef();
707
772
  if (!def)
708
773
  return {
709
- content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }],
774
+ content: [{ type: "text", text: noMachine() }],
710
775
  isError: true,
711
776
  };
712
777
  const source = def.events[0];
@@ -727,7 +792,7 @@ function buildServer() {
727
792
  const def = readDef();
728
793
  if (!def)
729
794
  return {
730
- content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }],
795
+ content: [{ type: "text", text: noMachine() }],
731
796
  isError: true,
732
797
  };
733
798
  const target = findState(def.state, state);
@@ -749,7 +814,7 @@ function buildServer() {
749
814
  server.tool("remove_state", "Remove a state from the definition", { name: z.string() }, async ({ name }) => {
750
815
  const def = readDef();
751
816
  if (!def)
752
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
817
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
753
818
  if (name === "Root")
754
819
  return { content: [{ type: "text", text: "Cannot remove Root state." }], isError: true };
755
820
  def.state = removeState(def.state, name);
@@ -760,7 +825,7 @@ function buildServer() {
760
825
  server.tool("validate_definition", "Validate the current state machine definition against all rules", {}, async () => {
761
826
  const def = readDef();
762
827
  if (!def)
763
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
828
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
764
829
  const errors = validate(def);
765
830
  if (errors.length === 0)
766
831
  return { content: [{ type: "text", text: "✓ Definition is valid. No errors found." }] };
@@ -772,7 +837,7 @@ function buildServer() {
772
837
  server.tool("generate_code", "Generate Python or TypeScript code from the current definition", { language: z.enum(["python", "typescript"]) }, async ({ language }) => {
773
838
  const def = readDef();
774
839
  if (!def)
775
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
840
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
776
841
  const errors = validate(def);
777
842
  if (errors.length > 0) {
778
843
  const lines = errors.map((e) => `[${e.ruleId}] ${e.message}`).join("\n");
@@ -784,19 +849,27 @@ function buildServer() {
784
849
  const code = generateViaSmcg(def, language);
785
850
  return { content: [{ type: "text", text: code }] };
786
851
  });
787
- server.tool("get_definition", "Get the current definition as JSON — the state machine, or the ERD when the active document is a .erdf.json", {}, async () => {
852
+ 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
853
  if (isErdfPath(PROJECT_FILE))
789
854
  return erdGetDefinition(erdHost);
855
+ if (isSqdfPath(PROJECT_FILE))
856
+ return sequenceGetDefinition(sequenceHost);
857
+ if (isSysdfPath(PROJECT_FILE))
858
+ return systemGetDefinition(systemHost);
790
859
  const def = readDef();
791
860
  if (!def)
792
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
861
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
793
862
  return {
794
863
  content: [{ type: "text", text: JSON.stringify({ stateMachine: def }, null, 2) }],
795
864
  };
796
865
  });
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 }) => {
866
+ 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
867
  if (isErdfPath(PROJECT_FILE))
799
868
  return erdLoadDefinition(erdHost, json);
869
+ if (isSqdfPath(PROJECT_FILE))
870
+ return sequenceLoadDefinition(sequenceHost, json);
871
+ if (isSysdfPath(PROJECT_FILE))
872
+ return systemLoadDefinition(systemHost, json);
800
873
  try {
801
874
  const parsed = JSON.parse(json);
802
875
  const def = parsed.stateMachine ?? parsed.StateMachine ?? parsed;
@@ -820,7 +893,7 @@ function buildServer() {
820
893
  server.tool("list_states", "List all states in the current definition with their kind and transitions", {}, async () => {
821
894
  const def = readDef();
822
895
  if (!def)
823
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
896
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
824
897
  const lines = [];
825
898
  const walk = (state, depth) => {
826
899
  const indent = " ".repeat(depth);
@@ -838,13 +911,13 @@ function buildServer() {
838
911
  server.tool("list_events", "List all events in the current definition", {}, async () => {
839
912
  const def = readDef();
840
913
  if (!def)
841
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
914
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
842
915
  const events = def.events.flatMap((src) => src.events.map((e) => `${e.id}${e.description ? ` — ${e.description}` : ""}`));
843
916
  if (events.length === 0)
844
917
  return { content: [{ type: "text", text: "No events defined yet." }] };
845
918
  return { content: [{ type: "text", text: events.join("\n") }] };
846
919
  });
847
- server.tool("set_project_file", "Choose the active document the loom weaves — two modalities. STANDALONE: any .smdf.json path (a state machine) or .erdf.json path (an ERD — the ERD tools act on it). 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 }) => {
920
+ 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
921
  try {
849
922
  const r = resolveProjectSwitch(path, PROJECT_FILE);
850
923
  const denial = denyProjectSwitch(r.path);
@@ -855,14 +928,25 @@ function buildServer() {
855
928
  bindBridge(PROJECT_FILE);
856
929
  joinBridge();
857
930
  }
931
+ if (isSysdfPath(PROJECT_FILE))
932
+ LAST_SYSTEM = PROJECT_FILE;
858
933
  const def = r.exists ? readDef() : null;
859
- const summary = isErdfPath(PROJECT_FILE)
860
- ? erdSummary(PROJECT_FILE)
861
- : def
862
- ? `existing machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
863
- : r.exists
864
- ? "file exists but is not a readable definition"
865
- : "no file yet — create_state_machine or load_definition will write it";
934
+ const summary = [
935
+ isErdfPath(PROJECT_FILE)
936
+ ? erdSummary(PROJECT_FILE)
937
+ : isSqdfPath(PROJECT_FILE)
938
+ ? sequenceSummary(PROJECT_FILE)
939
+ : isSysdfPath(PROJECT_FILE)
940
+ ? systemSummary(PROJECT_FILE)
941
+ : def
942
+ ? `existing machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
943
+ : r.exists
944
+ ? "file exists but is not a readable definition"
945
+ : "no file yet — create_state_machine or load_definition will write it",
946
+ reconcileModeLine(systemHost, PROJECT_FILE),
947
+ ]
948
+ .filter(Boolean)
949
+ .join("\n");
866
950
  const bridgeNote = BRIDGE_URL
867
951
  ? r.unchanged
868
952
  ? `bridge unchanged (${bridge?.status ?? "not configured"})`
@@ -884,15 +968,24 @@ function buildServer() {
884
968
  };
885
969
  }
886
970
  });
887
- server.tool("get_project_file", "Report the active .smdf.json document path, whether it exists on disk, and live-bridge status", {}, async () => {
971
+ 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
972
  const def = readDef();
889
- const summary = isErdfPath(PROJECT_FILE)
890
- ? erdSummary(PROJECT_FILE)
891
- : def
892
- ? `machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
893
- : existsSync(PROJECT_FILE)
894
- ? "file exists but is not a readable definition"
895
- : "file does not exist yet";
973
+ const summary = [
974
+ isErdfPath(PROJECT_FILE)
975
+ ? erdSummary(PROJECT_FILE)
976
+ : isSqdfPath(PROJECT_FILE)
977
+ ? sequenceSummary(PROJECT_FILE)
978
+ : isSysdfPath(PROJECT_FILE)
979
+ ? systemSummary(PROJECT_FILE)
980
+ : def
981
+ ? `machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
982
+ : existsSync(PROJECT_FILE)
983
+ ? "file exists but is not a readable definition"
984
+ : "file does not exist yet",
985
+ reconcileModeLine(systemHost, PROJECT_FILE),
986
+ ]
987
+ .filter(Boolean)
988
+ .join("\n");
896
989
  const bridgeNote = BRIDGE_URL
897
990
  ? `bridge: ${bridge?.status ?? "not initialized"} → ${BRIDGE_URL}`
898
991
  : "bridge not configured (STATELOOM_BRIDGE_URL unset)";
@@ -902,10 +995,12 @@ function buildServer() {
902
995
  ],
903
996
  };
904
997
  });
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 }) => {
998
+ 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 }) => {
999
+ if (system || isSysdfPath(PROJECT_FILE))
1000
+ return systemRispec(systemHost, { system, intent });
906
1001
  const def = readDef();
907
1002
  if (!def)
908
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
1003
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
909
1004
  const md = generateRispec(def, intent);
910
1005
  return { content: [{ type: "text", text: md }] };
911
1006
  });
@@ -932,7 +1027,7 @@ function buildServer() {
932
1027
  return "no desktop opener available";
933
1028
  }
934
1029
  }
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.", {
1030
+ 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
1031
  format: z.enum(["png", "svg", "mermaid", "ascii"]).optional(),
937
1032
  path: z.string().optional(),
938
1033
  scale: z.number().optional(),
@@ -942,10 +1037,14 @@ function buildServer() {
942
1037
  }, async ({ format, path, scale, theme, stamp, open }) => {
943
1038
  if (isErdfPath(PROJECT_FILE))
944
1039
  return erdRender(erdHost, { format, path, stamp });
1040
+ if (isSqdfPath(PROJECT_FILE))
1041
+ return sequenceRender(sequenceHost, { format, path, stamp });
1042
+ if (isSysdfPath(PROJECT_FILE))
1043
+ return systemRender(systemHost, { format, path, stamp });
945
1044
  const def = readDef();
946
1045
  if (!def)
947
1046
  return {
948
- content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }],
1047
+ content: [{ type: "text", text: noMachine() }],
949
1048
  isError: true,
950
1049
  };
951
1050
  const chosen = format ?? "png";
@@ -1009,13 +1108,19 @@ function buildServer() {
1009
1108
  }
1010
1109
  });
1011
1110
  registerErdTools(server, erdHost);
1111
+ registerSequenceTools(server, sequenceHost);
1112
+ registerSystemTools(server, systemHost);
1012
1113
  // 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) or an entity name (ERD). 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 }) => {
1114
+ 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
1115
  if (isErdfPath(PROJECT_FILE))
1015
1116
  return erdSetNotes(erdHost, target, notes);
1117
+ if (isSqdfPath(PROJECT_FILE))
1118
+ return sequenceSetNotes(sequenceHost, target, notes);
1119
+ if (isSysdfPath(PROJECT_FILE))
1120
+ return systemSetNotes(systemHost, target, notes);
1016
1121
  const def = readDef();
1017
1122
  if (!def)
1018
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
1123
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
1019
1124
  const holder = target ? findState(def.state, target) : def.settings;
1020
1125
  if (!holder)
1021
1126
  return { content: [{ type: "text", text: `State '${target}' not found.` }], isError: true };
@@ -1039,9 +1144,13 @@ function buildServer() {
1039
1144
  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
1145
  if (isErdfPath(PROJECT_FILE))
1041
1146
  return erdGetNotes(erdHost);
1147
+ if (isSqdfPath(PROJECT_FILE))
1148
+ return sequenceGetNotes(sequenceHost);
1149
+ if (isSysdfPath(PROJECT_FILE))
1150
+ return systemGetNotes(systemHost);
1042
1151
  const def = readDef();
1043
1152
  if (!def)
1044
- return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
1153
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
1045
1154
  const entries = collectNotes(def);
1046
1155
  return { content: [{ type: "text", text: formatNotes(def.settings.name, PROJECT_FILE, entries) }] };
1047
1156
  });
@@ -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 {};