@miadi/stateloom-mcp 0.3.0 → 0.3.2

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/erd.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "fs";
3
3
  import { dirname, join, resolve } from "path";
4
- import { addAttribute, addEntity, addRelationship, checkLinks, checkStateOf, collectNotes, emptyErd, ERD_CARDINALITIES, isErdDefinition, isErdfPath, isSqdfPath, isSysdfPath, removeAttribute, removeEntity, removeRelationship, renderMermaidEr, updateEntity, updateErdSettings, summarizeErd, validateErd, } from "@miadi/stateloom-protocol";
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
6
  const ok = (text) => ({ content: [{ type: "text", text }] });
7
7
  const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
@@ -9,7 +9,7 @@ export function readErd(path) {
9
9
  if (!existsSync(path))
10
10
  return null;
11
11
  try {
12
- const parsed = JSON.parse(readFileSync(path, "utf8"));
12
+ const parsed = normalizeNotes(JSON.parse(readFileSync(path, "utf8")));
13
13
  return isErdDefinition(parsed) ? parsed : null;
14
14
  }
15
15
  catch {
@@ -123,7 +123,7 @@ function readSmdf(path) {
123
123
  return null;
124
124
  try {
125
125
  const parsed = JSON.parse(readFileSync(path, "utf8"));
126
- const def = parsed.stateMachine ?? parsed.StateMachine ?? parsed;
126
+ const def = normalizeNotes(parsed.stateMachine ?? parsed.StateMachine ?? parsed);
127
127
  return def && typeof def === "object" && def.state ? def : null;
128
128
  }
129
129
  catch {
package/dist/sequence.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { existsSync, readFileSync, writeFileSync } from "fs";
3
3
  import { dirname, join, resolve } from "path";
4
- import { SQD_FRAGMENT_KINDS, addFragment, addFragmentMessage, addMessage, addParticipant, allMessages, docKindOfPath, emptySequence, isSequenceDefinition, isSqdfPath, moveMessage, moveParticipant, parseMessageRef, removeFragment, removeMessage, removeParticipant, renameParticipant, renderMermaidSequence, summarizeSequence, updateFragment, updateMessage, updateParticipant, updateSqdSettings, validateSequence, } from "@miadi/stateloom-protocol";
4
+ import { SQD_FRAGMENT_KINDS, addFragment, addFragmentMessage, addMessage, addParticipant, allMessages, docKindOfPath, emptySequence, isSequenceDefinition, isSqdfPath, moveMessage, moveParticipant, parseMessageRef, removeFragment, removeMessage, removeParticipant, renameParticipant, renderMermaidSequence, summarizeSequence, updateFragment, updateMessage, updateParticipant, updateSqdSettings, validateSequence, normalizeNotes } from "@miadi/stateloom-protocol";
5
5
  import { stampedOutputPath } from "@miadi/stateloom-cli/render";
6
6
  import { formatNotes } from "./erd.js";
7
7
  import { writeFileAtomic } from "./atomicWrite.js";
@@ -13,7 +13,7 @@ export function readSequence(path) {
13
13
  if (!existsSync(path))
14
14
  return null;
15
15
  try {
16
- const parsed = JSON.parse(readFileSync(path, "utf8"));
16
+ const parsed = normalizeNotes(JSON.parse(readFileSync(path, "utf8")));
17
17
  return isSequenceDefinition(parsed) ? parsed : null;
18
18
  }
19
19
  catch {
package/dist/server.js CHANGED
@@ -65,7 +65,7 @@ import { basename, dirname, join, resolve } from "path";
65
65
  import { fileURLToPath } from "node:url";
66
66
  import { tmpdir } from "os";
67
67
  import { createBridgeClient } from "@miadi/stateloom-client";
68
- import { collectNotes, docKindOfPath, envAlias, isErdfPath, isSqdfPath, isSysdfPath, } from "@miadi/stateloom-protocol";
68
+ import { collectNotes, docKindOfPath, envAlias, isErdfPath, isSqdfPath, isSysdfPath, normalizeNotes, } from "@miadi/stateloom-protocol";
69
69
  import { renderDiagramToFile, defaultOutputPath } from "@miadi/stateloom-cli/render";
70
70
  import { resolveProjectSwitch } from "./projectSwitch.js";
71
71
  import { registerErdTools, erdSummary, erdGetDefinition, erdLoadDefinition, erdRender, erdGetNotes, erdSetNotes, formatNotes, } from "./erd.js";
@@ -108,7 +108,7 @@ function readDef() {
108
108
  try {
109
109
  const raw = readFileSync(PROJECT_FILE, "utf8");
110
110
  const parsed = JSON.parse(raw);
111
- return parsed.stateMachine ?? parsed.StateMachine ?? parsed;
111
+ return normalizeNotes(parsed.stateMachine ?? parsed.StateMachine ?? parsed);
112
112
  }
113
113
  catch (e) {
114
114
  console.error(`[smcraft-mcp] Failed to read ${PROJECT_FILE}:`, e);
@@ -120,7 +120,17 @@ function writeDef(def) {
120
120
  // and load_definition write without reading first, so this is the one place that can refuse.
121
121
  if (docKindOfPath(PROJECT_FILE) !== "machine")
122
122
  throw new Error(noMachine());
123
- writeFileSync(PROJECT_FILE, JSON.stringify({ stateMachine: def }, null, 2), "utf8");
123
+ // Keep the file's own shape: a machine written bare stays bare, so one edit is
124
+ // a small diff and not the whole file re-indented under `stateMachine`.
125
+ let bare = false;
126
+ try {
127
+ const current = JSON.parse(readFileSync(PROJECT_FILE, "utf8"));
128
+ bare = !!current && typeof current === "object" && "state" in current && !("stateMachine" in current) && !("StateMachine" in current);
129
+ }
130
+ catch {
131
+ // A new or unreadable file is written in the loom's own shape.
132
+ }
133
+ writeFileSync(PROJECT_FILE, JSON.stringify(bare ? def : { stateMachine: def }, null, 2) + (bare ? "\n" : ""), "utf8");
124
134
  }
125
135
  // ─── Optional real-time design bridge (env-gated, best-effort) ───────
126
136
  // When STATELOOM_BRIDGE_URL (or legacy SMCRAFT_BRIDGE_URL) is set, each
@@ -657,6 +667,11 @@ function renderStateSpec(s, lines, depthLevel) {
657
667
  const indent = " ".repeat(depthLevel);
658
668
  const kind = s.kind && s.kind !== "normal" ? ` *(${s.kind})*` : "";
659
669
  lines.push(`${indent}- **${s.name}**${kind}${s.description ? ` — ${s.description}` : ""}`);
670
+ if (s.prompt?.trim()) {
671
+ lines.push(`${indent} - *the agent resolves here:*`);
672
+ for (const line of s.prompt.trim().split("\n"))
673
+ lines.push(`${indent} > ${line}`);
674
+ }
660
675
  for (const t of s.transitions ?? []) {
661
676
  lines.push(`${indent} - on \`${t.event}\` ${t.nextState ? `→ \`${t.nextState}\`` : "(internal)"}`);
662
677
  }
@@ -737,12 +752,13 @@ function buildServer() {
737
752
  ],
738
753
  };
739
754
  });
740
- server.tool("add_state", "Add a state to the state machine. Parent defaults to Root.", {
755
+ 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).", {
741
756
  name: z.string(),
742
757
  parent: z.string().optional(),
743
758
  kind: z.enum(["normal", "final", "history"]).optional(),
744
759
  description: z.string().optional(),
745
- }, async ({ name, parent, kind, description }) => {
760
+ prompt: z.string().optional(),
761
+ }, async ({ name, parent, kind, description, prompt }) => {
746
762
  const def = readDef();
747
763
  if (!def)
748
764
  return {
@@ -758,15 +774,33 @@ function buildServer() {
758
774
  };
759
775
  if (!parentState.states)
760
776
  parentState.states = [];
761
- parentState.states.push({ name, kind: kind ?? "normal", description });
777
+ const added = { name, kind: kind ?? "normal", description, ...(prompt?.trim() ? { prompt } : {}) };
778
+ parentState.states.push(added);
762
779
  writeDef(def);
763
780
  bridgeEmitPatch([
764
- { op: "state.add", parent: parentName, state: { name, kind: kind ?? "normal", description } },
781
+ { op: "state.add", parent: parentName, state: { name, kind: kind ?? "normal", description, ...(added.prompt ? { prompt: added.prompt } : {}) } },
765
782
  ]);
766
783
  return {
767
784
  content: [{ type: "text", text: `Added state '${name}' under '${parentName}' (${kind ?? "normal"}).` }],
768
785
  };
769
786
  });
787
+ 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
+ const def = readDef();
789
+ if (!def)
790
+ return { content: [{ type: "text", text: noMachine() }], isError: true };
791
+ const target = findState(def.state, state);
792
+ if (!target)
793
+ return { content: [{ type: "text", text: `State '${state}' not found.` }], isError: true };
794
+ if (prompt.trim())
795
+ target.prompt = prompt;
796
+ else
797
+ delete target.prompt;
798
+ writeDef(def);
799
+ bridgeEmitPatch([{ op: "state.update", name: state, patch: { prompt: prompt.trim() ? prompt : "" } }]);
800
+ return {
801
+ content: [{ type: "text", text: prompt.trim() ? `Prompt set on '${state}' (${prompt.length} characters).` : `Prompt cleared on '${state}'.` }],
802
+ };
803
+ });
770
804
  server.tool("add_event", "Add an event to the state machine", { id: z.string(), description: z.string().optional() }, async ({ id, description }) => {
771
805
  const def = readDef();
772
806
  if (!def)
package/dist/system.js CHANGED
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
3
3
  import { writeFileAtomic } from "./atomicWrite.js";
4
4
  import { basename, dirname, join, resolve } from "path";
5
- import { RECONCILE_MODES, SYSTEM_ACTOR_KINDS, addActor, addMember, checkSystem, docKindOfPath, emptySystem, formatReplay, isSqdfPath, isSysdfPath, isSystemDefinition, machineName, membersByKind, parseFocus, parseMessageRef, reconcileModeOf, reconcileSystem, relativeMemberPath, removeActor, removeMember, renderMermaid, renderMermaidEr, renderMermaidSequence, replayScenario, resolveMemberPath, setReconcileMode, stateOfList, summarizeReconcile, summarizeSystem, updateMember, updateSystemSettings, } from "@miadi/stateloom-protocol";
5
+ import { RECONCILE_MODES, SYSTEM_ACTOR_KINDS, addActor, addMember, checkSystem, docKindOfPath, emptySystem, formatReplay, isSqdfPath, isSysdfPath, isSystemDefinition, machineName, membersByKind, parseFocus, parseMessageRef, reconcileModeOf, reconcileSystem, relativeMemberPath, removeActor, removeMember, renderMermaid, renderMermaidEr, renderMermaidSequence, replayScenario, resolveMemberPath, setReconcileMode, stateOfList, summarizeReconcile, summarizeSystem, updateMember, updateSystemSettings, normalizeNotes } from "@miadi/stateloom-protocol";
6
6
  import { stampedOutputPath } from "@miadi/stateloom-cli/render";
7
7
  import { formatNotes } from "./erd.js";
8
8
  const ok = (text) => ({ content: [{ type: "text", text }] });
@@ -14,7 +14,7 @@ export function readSystem(path) {
14
14
  if (!existsSync(path))
15
15
  return null;
16
16
  try {
17
- const parsed = JSON.parse(readFileSync(path, "utf8"));
17
+ const parsed = normalizeNotes(JSON.parse(readFileSync(path, "utf8")));
18
18
  return isSystemDefinition(parsed) ? parsed : null;
19
19
  }
20
20
  catch {
@@ -76,7 +76,7 @@ function loadMember(host, path, absolute) {
76
76
  catch {
77
77
  return { path, kind, def: null, error: "it is not valid JSON" };
78
78
  }
79
- const def = kind === "machine" ? (parsed?.stateMachine ?? parsed?.StateMachine ?? parsed) : parsed;
79
+ const def = normalizeNotes(kind === "machine" ? (parsed?.stateMachine ?? parsed?.StateMachine ?? parsed) : parsed);
80
80
  return { path, kind, def };
81
81
  }
82
82
  /** One line for set_project_file / get_project_file when the active document is a system. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",
@@ -40,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@miadi/stateloom-cli": "^0.1.3",
42
42
  "@miadi/stateloom-client": "^0.1.3",
43
- "@miadi/stateloom-protocol": "^0.1.9",
43
+ "@miadi/stateloom-protocol": "^0.1.11",
44
44
  "@modelcontextprotocol/sdk": "^1.12.1",
45
45
  "zod": "^4.3.6"
46
46
  },