@miadi/stateloom-mcp 0.1.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/projectSwitch.d.ts +11 -0
- package/dist/projectSwitch.js +18 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.js +866 -0
- package/dist/tests/projectSwitch.test.d.ts +1 -0
- package/dist/tests/projectSwitch.test.js +41 -0
- package/package.json +26 -0
- package/src/projectSwitch.ts +32 -0
- package/src/server.ts +1080 -0
- package/src/tests/projectSwitch.test.ts +47 -0
- package/tsconfig.json +15 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SMCraft MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides tools for LLM agents to design, validate, and generate
|
|
6
|
+
* state machines through conversation.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - create_state_machine: Create a new state machine definition
|
|
10
|
+
* - add_state: Add a state to the definition
|
|
11
|
+
* - add_event: Add an event to the definition
|
|
12
|
+
* - add_transition: Add a transition to a state
|
|
13
|
+
* - remove_state: Remove a state from the definition
|
|
14
|
+
* - validate_definition: Validate the current definition
|
|
15
|
+
* - generate_code: Generate Python or TypeScript code
|
|
16
|
+
* - get_definition: Get the current definition as JSON
|
|
17
|
+
* - load_definition: Load a definition from JSON
|
|
18
|
+
* - list_states: List all states in the current definition
|
|
19
|
+
* - list_events: List all events in the current definition
|
|
20
|
+
* - set_project_file: Choose which .smdf.json path is the active document
|
|
21
|
+
* - get_project_file: Report the active document path and bridge status
|
|
22
|
+
*
|
|
23
|
+
* Resources:
|
|
24
|
+
* - smcraft://definition — Current state machine definition
|
|
25
|
+
*
|
|
26
|
+
* Prompts:
|
|
27
|
+
* - design-state-machine — Guided state machine design conversation
|
|
28
|
+
*/
|
|
29
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
30
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { execFileSync } from "child_process";
|
|
33
|
+
import { writeFileSync, readFileSync, mkdtempSync, rmSync, existsSync, statSync } from "fs";
|
|
34
|
+
import { join, resolve } from "path";
|
|
35
|
+
import { tmpdir } from "os";
|
|
36
|
+
import { createBridgeClient } from "@miadi/stateloom-client";
|
|
37
|
+
import { envAlias } from "@miadi/stateloom-protocol";
|
|
38
|
+
import { resolveProjectSwitch } from "./projectSwitch.js";
|
|
39
|
+
// STATELOOM_* read first, SMCRAFT_* legacy twin honored — the live MCP
|
|
40
|
+
// registration bakes SMCRAFT_PROJECT_FILE and must keep working (2026-07-27
|
|
41
|
+
// rename brief). Resolved once at boot, like the direct reads they replace.
|
|
42
|
+
const BRIDGE_URL = envAlias("BRIDGE_URL");
|
|
43
|
+
const AGENT_NAME = envAlias("AGENT_NAME");
|
|
44
|
+
const BRIDGE_TOKEN = envAlias("BRIDGE_TOKEN");
|
|
45
|
+
// ─── File-backed shared store ────────────────────────────────────────
|
|
46
|
+
// Both web/ and mcp/ resolve the same path from STATELOOM_PROJECT_FILE
|
|
47
|
+
// (legacy twin SMCRAFT_PROJECT_FILE still honored).
|
|
48
|
+
// Web reads/writes via /api/file; mcp reads/writes here. fs.watch on
|
|
49
|
+
// the web side broadcasts disk changes back to the canvas.
|
|
50
|
+
// Mutable: `set_project_file` lets the agent re-point the loom at another
|
|
51
|
+
// .smdf.json document mid-session (the hub keys live rooms by this path).
|
|
52
|
+
let PROJECT_FILE = resolve(envAlias("PROJECT_FILE") ?? "./statemachine.smdf.json");
|
|
53
|
+
function readDef() {
|
|
54
|
+
if (!existsSync(PROJECT_FILE))
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync(PROJECT_FILE, "utf8");
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
return parsed.stateMachine ?? parsed.StateMachine ?? parsed;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`[smcraft-mcp] Failed to read ${PROJECT_FILE}:`, e);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeDef(def) {
|
|
67
|
+
writeFileSync(PROJECT_FILE, JSON.stringify({ stateMachine: def }, null, 2), "utf8");
|
|
68
|
+
}
|
|
69
|
+
// ─── Optional real-time design bridge (env-gated, best-effort) ───────
|
|
70
|
+
// When STATELOOM_BRIDGE_URL (or legacy SMCRAFT_BRIDGE_URL) is set, each
|
|
71
|
+
// mutation is mirrored to the bridge
|
|
72
|
+
// hub so a live web canvas updates as the agent edits. Fully backward-
|
|
73
|
+
// compatible: the file on disk remains the durable truth, every bridge
|
|
74
|
+
// call is wrapped so any failure degrades to a no-op, and standalone
|
|
75
|
+
// operation (no bridge URL) is untouched.
|
|
76
|
+
let bridge;
|
|
77
|
+
if (!BRIDGE_URL) {
|
|
78
|
+
console.error("[smcraft-mcp] STATELOOM_BRIDGE_URL is not set — edits persist to disk but " +
|
|
79
|
+
"are NOT broadcast; a live canvas will not update. Set STATELOOM_BRIDGE_URL " +
|
|
80
|
+
"(legacy twin: SMCRAFT_BRIDGE_URL; e.g. http://127.0.0.1:4599) on this MCP " +
|
|
81
|
+
"process to go live.");
|
|
82
|
+
}
|
|
83
|
+
/** (Re)create the bridge client bound to `docId`, dropping any previous one. */
|
|
84
|
+
function bindBridge(docId) {
|
|
85
|
+
if (!BRIDGE_URL)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
bridge?.disconnect();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* best-effort */
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
bridge = createBridgeClient({
|
|
95
|
+
url: BRIDGE_URL,
|
|
96
|
+
role: "agent",
|
|
97
|
+
docId,
|
|
98
|
+
name: AGENT_NAME ?? "mcp-agent",
|
|
99
|
+
token: BRIDGE_TOKEN,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error("[smcraft-mcp] bridge init failed (continuing standalone):", e);
|
|
104
|
+
bridge = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Best-effort, non-blocking join. Failure never blocks tool handling. */
|
|
108
|
+
function joinBridge() {
|
|
109
|
+
if (!bridge)
|
|
110
|
+
return;
|
|
111
|
+
bridge
|
|
112
|
+
.join()
|
|
113
|
+
.then(() => {
|
|
114
|
+
console.error(`[smcraft-mcp] bridge connected (${BRIDGE_URL}) docId=${PROJECT_FILE}`);
|
|
115
|
+
})
|
|
116
|
+
.catch((e) => {
|
|
117
|
+
console.error("[smcraft-mcp] bridge join failed (continuing standalone):", e?.message ?? e);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
bindBridge(PROJECT_FILE);
|
|
121
|
+
if (BRIDGE_URL) {
|
|
122
|
+
// Best-effort teardown on process exit.
|
|
123
|
+
let bridgeClosed = false;
|
|
124
|
+
const closeBridge = () => {
|
|
125
|
+
if (bridgeClosed)
|
|
126
|
+
return;
|
|
127
|
+
bridgeClosed = true;
|
|
128
|
+
try {
|
|
129
|
+
bridge?.disconnect();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* best-effort */
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
process.on("exit", closeBridge);
|
|
136
|
+
process.on("SIGINT", () => {
|
|
137
|
+
closeBridge();
|
|
138
|
+
process.exit(0);
|
|
139
|
+
});
|
|
140
|
+
process.on("SIGTERM", () => {
|
|
141
|
+
closeBridge();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Emit precise granular ops (never diffs) AFTER writeDef. The mtime is
|
|
146
|
+
// stamped from the file we just wrote so the hub can dedup its own
|
|
147
|
+
// file-watch echo of this same change.
|
|
148
|
+
let droppedEmitWarned = false;
|
|
149
|
+
function warnDroppedEmit(kind) {
|
|
150
|
+
if (droppedEmitWarned)
|
|
151
|
+
return;
|
|
152
|
+
droppedEmitWarned = true;
|
|
153
|
+
console.error(`[smcraft-mcp] dropped ${kind} emit — bridge ` +
|
|
154
|
+
(bridge ? `status=${bridge.status}` : "not configured (STATELOOM_BRIDGE_URL unset)") +
|
|
155
|
+
"; disk write succeeded, live canvas did not update. (warned once)");
|
|
156
|
+
}
|
|
157
|
+
function bridgeEmitPatch(ops) {
|
|
158
|
+
if (!bridge || bridge.status !== "connected") {
|
|
159
|
+
warnDroppedEmit("patch");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const mtime = statSync(PROJECT_FILE).mtimeMs;
|
|
164
|
+
bridge.emitPatch(ops, mtime);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* best-effort */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function bridgeEmitFull(def) {
|
|
171
|
+
if (!bridge || bridge.status !== "connected") {
|
|
172
|
+
warnDroppedEmit("full-def");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const mtime = statSync(PROJECT_FILE).mtimeMs;
|
|
177
|
+
// Local Definition/StateDef differ slightly from the protocol's
|
|
178
|
+
// StateMachineDefinition; the payload is serialized as-is over the wire.
|
|
179
|
+
bridge.emitFull(def, mtime);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* best-effort */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function createEmpty(namespace, name) {
|
|
186
|
+
return {
|
|
187
|
+
settings: { namespace, name, asynchronous: false },
|
|
188
|
+
events: [{ name: "Internal", events: [] }],
|
|
189
|
+
state: { name: "Root", states: [] },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function findState(root, name) {
|
|
193
|
+
if (root.name === name)
|
|
194
|
+
return root;
|
|
195
|
+
for (const child of root.states ?? []) {
|
|
196
|
+
const found = findState(child, name);
|
|
197
|
+
if (found)
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
function removeState(root, name) {
|
|
203
|
+
return {
|
|
204
|
+
...root,
|
|
205
|
+
states: root.states
|
|
206
|
+
?.filter((s) => s.name !== name)
|
|
207
|
+
.map((s) => removeState(s, name)),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function collectStateNames(root) {
|
|
211
|
+
const names = [root.name];
|
|
212
|
+
for (const child of root.states ?? []) {
|
|
213
|
+
names.push(...collectStateNames(child));
|
|
214
|
+
}
|
|
215
|
+
return names;
|
|
216
|
+
}
|
|
217
|
+
function collectEventIds(def) {
|
|
218
|
+
return def.events.flatMap((src) => src.events.map((e) => e.id));
|
|
219
|
+
}
|
|
220
|
+
function validate(def) {
|
|
221
|
+
const errors = [];
|
|
222
|
+
const stateNames = collectStateNames(def.state);
|
|
223
|
+
const eventIds = collectEventIds(def);
|
|
224
|
+
if (eventIds.length === 0) {
|
|
225
|
+
errors.push({ ruleId: "V001", message: "No events defined" });
|
|
226
|
+
}
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
for (const name of stateNames) {
|
|
229
|
+
if (seen.has(name))
|
|
230
|
+
errors.push({
|
|
231
|
+
ruleId: "V002",
|
|
232
|
+
message: `Duplicate state name: ${name}`,
|
|
233
|
+
});
|
|
234
|
+
seen.add(name);
|
|
235
|
+
}
|
|
236
|
+
const checkTransitions = (state) => {
|
|
237
|
+
for (const t of state.transitions ?? []) {
|
|
238
|
+
if (!eventIds.includes(t.event))
|
|
239
|
+
errors.push({
|
|
240
|
+
ruleId: "V003",
|
|
241
|
+
message: `State '${state.name}' references unknown event '${t.event}'`,
|
|
242
|
+
});
|
|
243
|
+
if (t.nextState && !stateNames.includes(t.nextState))
|
|
244
|
+
errors.push({
|
|
245
|
+
ruleId: "V004",
|
|
246
|
+
message: `State '${state.name}' targets unknown state '${t.nextState}'`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
(state.states ?? []).forEach(checkTransitions);
|
|
250
|
+
};
|
|
251
|
+
checkTransitions(def.state);
|
|
252
|
+
if (!def.state.states || def.state.states.length === 0)
|
|
253
|
+
errors.push({
|
|
254
|
+
ruleId: "V005",
|
|
255
|
+
message: "Root must have at least one child state",
|
|
256
|
+
});
|
|
257
|
+
return errors;
|
|
258
|
+
}
|
|
259
|
+
function generateViaSmcg(def, language) {
|
|
260
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "smcraft-"));
|
|
261
|
+
// Sanitize the input filename — settings.name is agent-controlled free text.
|
|
262
|
+
const safeName = String(def.settings.name ?? "machine").replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
263
|
+
const inputFile = join(tmpDir, `${safeName}.smdf.json`);
|
|
264
|
+
const outputDir = join(tmpDir, "output");
|
|
265
|
+
try {
|
|
266
|
+
// Write definition as SMDF JSON
|
|
267
|
+
const smdf = { settings: def.settings, events: def.events, state: def.state };
|
|
268
|
+
writeFileSync(inputFile, JSON.stringify(smdf, null, 2));
|
|
269
|
+
// Call real smcg CLI — execFileSync (no shell) so a crafted name cannot
|
|
270
|
+
// inject shell commands.
|
|
271
|
+
execFileSync("smcg", [inputFile, "-l", language, "-o", outputDir], {
|
|
272
|
+
encoding: "utf-8",
|
|
273
|
+
timeout: 30000,
|
|
274
|
+
});
|
|
275
|
+
// Read generated output
|
|
276
|
+
const ext = language === "python" ? "py" : "ts";
|
|
277
|
+
const name = def.settings.name ?? "machine";
|
|
278
|
+
const outputFile = join(outputDir, `${name}_fsm.${ext}`);
|
|
279
|
+
return readFileSync(outputFile, "utf-8");
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
// Fallback to lightweight inline generation if smcg fails
|
|
283
|
+
return language === "python"
|
|
284
|
+
? generatePythonFallback(def)
|
|
285
|
+
: generateTypeScriptFallback(def);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
try {
|
|
289
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Fallback lightweight generators (used when smcg CLI unavailable)
|
|
295
|
+
function generatePythonFallback(def) {
|
|
296
|
+
const lines = [];
|
|
297
|
+
const name = def.settings.name ?? "StateMachine";
|
|
298
|
+
const states = def.state.states ?? [];
|
|
299
|
+
const events = collectEventIds(def);
|
|
300
|
+
lines.push(`"""Generated by SMCraft"""`);
|
|
301
|
+
lines.push(`from enum import Enum, auto`);
|
|
302
|
+
lines.push(`from smcraft.runtime import Context, State, TransitionHelper`);
|
|
303
|
+
lines.push(``);
|
|
304
|
+
lines.push(`class StateId(Enum):`);
|
|
305
|
+
for (const s of collectStateNames(def.state)) {
|
|
306
|
+
lines.push(` ${s} = auto()`);
|
|
307
|
+
}
|
|
308
|
+
lines.push(``);
|
|
309
|
+
for (const s of states) {
|
|
310
|
+
lines.push(`class State${s.name}(State):`);
|
|
311
|
+
lines.push(` """State: ${s.name}"""`);
|
|
312
|
+
if (s.onEntry)
|
|
313
|
+
lines.push(` def on_entry(self, context):\n ${s.onEntry}`);
|
|
314
|
+
if (s.onExit)
|
|
315
|
+
lines.push(` def on_exit(self, context):\n ${s.onExit}`);
|
|
316
|
+
for (const t of s.transitions ?? []) {
|
|
317
|
+
lines.push(` def on_${t.event.toLowerCase()}(self, context):`);
|
|
318
|
+
if (t.condition)
|
|
319
|
+
lines.push(` if not (${t.condition}): return`);
|
|
320
|
+
if (t.nextState)
|
|
321
|
+
lines.push(` TransitionHelper.do_transition(context, context.state_${t.nextState.toLowerCase()})`);
|
|
322
|
+
else
|
|
323
|
+
lines.push(` pass # internal transition`);
|
|
324
|
+
}
|
|
325
|
+
lines.push(``);
|
|
326
|
+
}
|
|
327
|
+
lines.push(`class ${name}Context(Context):`);
|
|
328
|
+
lines.push(` """Context for ${name}"""`);
|
|
329
|
+
lines.push(` def __init__(self):`);
|
|
330
|
+
lines.push(` super().__init__()`);
|
|
331
|
+
for (const s of states) {
|
|
332
|
+
lines.push(` self.state_${s.name.toLowerCase()} = State${s.name}()`);
|
|
333
|
+
}
|
|
334
|
+
lines.push(` self.state_initial = self.state_${(states[0]?.name ?? "Root").toLowerCase()}`);
|
|
335
|
+
lines.push(``);
|
|
336
|
+
return lines.join("\n");
|
|
337
|
+
}
|
|
338
|
+
function generateTypeScriptFallback(def) {
|
|
339
|
+
const lines = [];
|
|
340
|
+
const name = def.settings.name ?? "StateMachine";
|
|
341
|
+
const states = def.state.states ?? [];
|
|
342
|
+
lines.push(`/** Generated by SMCraft */`);
|
|
343
|
+
lines.push(`import { Context, State, TransitionHelper } from "smcraft/runtime";`);
|
|
344
|
+
lines.push(``);
|
|
345
|
+
lines.push(`export enum StateId {`);
|
|
346
|
+
for (const s of collectStateNames(def.state)) {
|
|
347
|
+
lines.push(` ${s} = "${s}",`);
|
|
348
|
+
}
|
|
349
|
+
lines.push(`}`);
|
|
350
|
+
lines.push(``);
|
|
351
|
+
for (const s of states) {
|
|
352
|
+
lines.push(`export class State${s.name} extends State {`);
|
|
353
|
+
for (const t of s.transitions ?? []) {
|
|
354
|
+
lines.push(` on${t.event}(context: ${name}Context): void {`);
|
|
355
|
+
if (t.condition)
|
|
356
|
+
lines.push(` if (!(${t.condition})) return;`);
|
|
357
|
+
if (t.nextState)
|
|
358
|
+
lines.push(` TransitionHelper.doTransition(context, (context as any).state${t.nextState});`);
|
|
359
|
+
lines.push(` }`);
|
|
360
|
+
}
|
|
361
|
+
lines.push(`}`);
|
|
362
|
+
lines.push(``);
|
|
363
|
+
}
|
|
364
|
+
lines.push(`export class ${name}Context extends Context {`);
|
|
365
|
+
for (const s of states) {
|
|
366
|
+
lines.push(` state${s.name} = new State${s.name}();`);
|
|
367
|
+
}
|
|
368
|
+
if (states.length > 0)
|
|
369
|
+
lines.push(` stateInitial = this.state${states[0].name};`);
|
|
370
|
+
lines.push(`}`);
|
|
371
|
+
return lines.join("\n");
|
|
372
|
+
}
|
|
373
|
+
function loadPde(def) {
|
|
374
|
+
const folder = def.settings._source?.pdeFolder;
|
|
375
|
+
const id = def.settings._source?.pdeId;
|
|
376
|
+
if (!folder || !id)
|
|
377
|
+
return null;
|
|
378
|
+
const root = resolve(PROJECT_FILE, "..");
|
|
379
|
+
const candidates = [
|
|
380
|
+
resolve(root, folder, `pde-${id}.json`),
|
|
381
|
+
resolve(folder, `pde-${id}.json`),
|
|
382
|
+
];
|
|
383
|
+
for (const path of candidates) {
|
|
384
|
+
if (!existsSync(path))
|
|
385
|
+
continue;
|
|
386
|
+
try {
|
|
387
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
388
|
+
return {
|
|
389
|
+
prompt: data.prompt,
|
|
390
|
+
primary: data.result?.primary,
|
|
391
|
+
secondary: data.result?.secondary,
|
|
392
|
+
directions: data.result?.directions,
|
|
393
|
+
actionStack: data.result?.actionStack,
|
|
394
|
+
ambiguities: data.result?.ambiguities,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
/* try next */
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
function generateRispec(def, intent) {
|
|
404
|
+
const pde = loadPde(def);
|
|
405
|
+
const name = def.settings.name;
|
|
406
|
+
const ns = def.settings.namespace;
|
|
407
|
+
const lines = [];
|
|
408
|
+
const states = def.state.states ?? [];
|
|
409
|
+
const tagline = intent ??
|
|
410
|
+
pde?.primary?.target ??
|
|
411
|
+
def.settings._source?.originalPrompt ??
|
|
412
|
+
`${name} state machine`;
|
|
413
|
+
lines.push(`# ${name} — RISE rispec`);
|
|
414
|
+
lines.push("");
|
|
415
|
+
lines.push(`> ${tagline}`);
|
|
416
|
+
lines.push("");
|
|
417
|
+
lines.push(`**Namespace:** \`${ns}\` · **Async:** ${def.settings.asynchronous}`);
|
|
418
|
+
if (def.settings._source?.pdeId) {
|
|
419
|
+
lines.push(`**Source PDE:** \`${def.settings._source.pdeId}\``);
|
|
420
|
+
}
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push("---");
|
|
423
|
+
lines.push("");
|
|
424
|
+
// Reverse Engineering
|
|
425
|
+
lines.push("## R — Reverse Engineering");
|
|
426
|
+
lines.push("");
|
|
427
|
+
lines.push(`${name} is modeled as a state machine with ${collectStateNames(def.state).length - 1} non-root states across ${depth(def.state)} level(s) of composition. ` +
|
|
428
|
+
`Events: ${collectEventIds(def).length}. Transitions: ${countTransitions(def.state)}.`);
|
|
429
|
+
lines.push("");
|
|
430
|
+
if (pde?.prompt) {
|
|
431
|
+
lines.push(`Originating prompt:`);
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push(`> ${pde.prompt}`);
|
|
434
|
+
lines.push("");
|
|
435
|
+
}
|
|
436
|
+
// Intent
|
|
437
|
+
lines.push("## I — Intent");
|
|
438
|
+
lines.push("");
|
|
439
|
+
lines.push("**Desired Outcome**");
|
|
440
|
+
lines.push("");
|
|
441
|
+
lines.push(intent ?? pde?.primary?.target ?? "_(no intent supplied — pass `intent` arg or set _source.originalPrompt)_");
|
|
442
|
+
lines.push("");
|
|
443
|
+
if (pde?.directions?.east?.length) {
|
|
444
|
+
lines.push("**Vision (🌅 East)**");
|
|
445
|
+
lines.push("");
|
|
446
|
+
for (const v of pde.directions.east)
|
|
447
|
+
lines.push(`- ${v.text}`);
|
|
448
|
+
lines.push("");
|
|
449
|
+
}
|
|
450
|
+
lines.push("**Structural Tension**");
|
|
451
|
+
lines.push("");
|
|
452
|
+
lines.push(`- *Current:* ${currentReality(def, pde)}`);
|
|
453
|
+
lines.push(`- *Desired:* ${desiredOutcome(def, pde, intent)}`);
|
|
454
|
+
lines.push("");
|
|
455
|
+
// Specifications
|
|
456
|
+
lines.push("## S — Specifications");
|
|
457
|
+
lines.push("");
|
|
458
|
+
lines.push("### States");
|
|
459
|
+
lines.push("");
|
|
460
|
+
for (const s of states) {
|
|
461
|
+
renderStateSpec(s, lines, 0);
|
|
462
|
+
}
|
|
463
|
+
lines.push("");
|
|
464
|
+
lines.push("### Events");
|
|
465
|
+
lines.push("");
|
|
466
|
+
for (const src of def.events) {
|
|
467
|
+
if (!src.events?.length)
|
|
468
|
+
continue;
|
|
469
|
+
lines.push(`**${src.name}**`);
|
|
470
|
+
for (const e of src.events) {
|
|
471
|
+
lines.push(`- \`${e.id}\`${e.description ? ` — ${e.description}` : ""}`);
|
|
472
|
+
}
|
|
473
|
+
lines.push("");
|
|
474
|
+
}
|
|
475
|
+
lines.push("### Transitions (flat)");
|
|
476
|
+
lines.push("");
|
|
477
|
+
for (const t of collectTransitions(def.state)) {
|
|
478
|
+
const arrow = t.next ? `→ ${t.next}` : "→ (internal)";
|
|
479
|
+
lines.push(`- \`${t.from}\` --[${t.event}]-- ${arrow}${t.condition ? ` *(when: ${t.condition})*` : ""}`);
|
|
480
|
+
}
|
|
481
|
+
lines.push("");
|
|
482
|
+
// Action Steps (medicine-wheel walk if South/East/North/West present)
|
|
483
|
+
const mw = states.filter((s) => ["East", "South", "West", "North"].includes(s.name));
|
|
484
|
+
if (mw.length || pde?.actionStack?.length) {
|
|
485
|
+
lines.push("### Action Steps");
|
|
486
|
+
lines.push("");
|
|
487
|
+
if (mw.length) {
|
|
488
|
+
const order = ["South", "East", "North", "West"];
|
|
489
|
+
for (const dirName of order) {
|
|
490
|
+
const dir = states.find((s) => s.name === dirName);
|
|
491
|
+
if (!dir)
|
|
492
|
+
continue;
|
|
493
|
+
lines.push(`**${glyph(dirName)} ${dirName}** — ${dir.description ?? ""}`);
|
|
494
|
+
for (const c of dir.states ?? []) {
|
|
495
|
+
lines.push(`1. ${c.description ?? c.name}`);
|
|
496
|
+
}
|
|
497
|
+
lines.push("");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else if (pde?.actionStack?.length) {
|
|
501
|
+
for (const a of pde.actionStack) {
|
|
502
|
+
lines.push(`- *(${a.direction ?? "?"})* ${a.text}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Ambiguities surfaced from PDE
|
|
508
|
+
if (pde?.ambiguities?.length) {
|
|
509
|
+
lines.push("### Open Questions");
|
|
510
|
+
lines.push("");
|
|
511
|
+
for (const a of pde.ambiguities) {
|
|
512
|
+
lines.push(`- ${a.text}${a.suggestion ? ` \n → *${a.suggestion}*` : ""}`);
|
|
513
|
+
}
|
|
514
|
+
lines.push("");
|
|
515
|
+
}
|
|
516
|
+
// Exportation
|
|
517
|
+
lines.push("## E — Exportation");
|
|
518
|
+
lines.push("");
|
|
519
|
+
lines.push("- Code: `generate_code` (python | typescript) — runtime stubs from this SMDF");
|
|
520
|
+
lines.push(`- Visual: open this file in the stateloom web designer with \`STATELOOM_PROJECT_FILE=${PROJECT_FILE}\``);
|
|
521
|
+
lines.push("- SMDF: `get_definition` returns the canonical JSON");
|
|
522
|
+
lines.push("");
|
|
523
|
+
return lines.join("\n");
|
|
524
|
+
}
|
|
525
|
+
function depth(s, d = 0) {
|
|
526
|
+
if (!s.states?.length)
|
|
527
|
+
return d;
|
|
528
|
+
return Math.max(...s.states.map((c) => depth(c, d + 1)));
|
|
529
|
+
}
|
|
530
|
+
function countTransitions(s) {
|
|
531
|
+
let n = s.transitions?.length ?? 0;
|
|
532
|
+
for (const c of s.states ?? [])
|
|
533
|
+
n += countTransitions(c);
|
|
534
|
+
return n;
|
|
535
|
+
}
|
|
536
|
+
function collectTransitions(s) {
|
|
537
|
+
const out = [];
|
|
538
|
+
for (const t of s.transitions ?? []) {
|
|
539
|
+
out.push({ from: s.name, event: t.event, next: t.nextState, condition: t.condition });
|
|
540
|
+
}
|
|
541
|
+
for (const c of s.states ?? [])
|
|
542
|
+
out.push(...collectTransitions(c));
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
function renderStateSpec(s, lines, depthLevel) {
|
|
546
|
+
const indent = " ".repeat(depthLevel);
|
|
547
|
+
const kind = s.kind && s.kind !== "normal" ? ` *(${s.kind})*` : "";
|
|
548
|
+
lines.push(`${indent}- **${s.name}**${kind}${s.description ? ` — ${s.description}` : ""}`);
|
|
549
|
+
for (const t of s.transitions ?? []) {
|
|
550
|
+
lines.push(`${indent} - on \`${t.event}\` ${t.nextState ? `→ \`${t.nextState}\`` : "(internal)"}`);
|
|
551
|
+
}
|
|
552
|
+
for (const c of s.states ?? [])
|
|
553
|
+
renderStateSpec(c, lines, depthLevel + 1);
|
|
554
|
+
}
|
|
555
|
+
function currentReality(def, _pde) {
|
|
556
|
+
const hasFinal = collectAllStates(def.state).some((s) => s.kind === "final");
|
|
557
|
+
return hasFinal
|
|
558
|
+
? `${def.settings.name} is designed but not implemented; entry state defined, terminal state reachable in principle.`
|
|
559
|
+
: `${def.settings.name} is partially modeled; no terminal state declared yet.`;
|
|
560
|
+
}
|
|
561
|
+
function collectAllStates(s) {
|
|
562
|
+
const out = [s];
|
|
563
|
+
for (const c of s.states ?? [])
|
|
564
|
+
out.push(...collectAllStates(c));
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
function desiredOutcome(def, pde, intent) {
|
|
568
|
+
if (intent)
|
|
569
|
+
return intent;
|
|
570
|
+
if (pde?.primary?.target)
|
|
571
|
+
return pde.primary.target;
|
|
572
|
+
return `${def.settings.name} runs the modeled flow end-to-end and reaches its terminal state.`;
|
|
573
|
+
}
|
|
574
|
+
function glyph(direction) {
|
|
575
|
+
return { South: "🔥", East: "🌅", North: "❄️", West: "🌊" }[direction] ?? "•";
|
|
576
|
+
}
|
|
577
|
+
// ─── MCP Server ──────────────────────────────────────────────────────
|
|
578
|
+
const server = new McpServer({
|
|
579
|
+
name: "smcraft-mcp",
|
|
580
|
+
version: "0.1.0",
|
|
581
|
+
});
|
|
582
|
+
// Tools
|
|
583
|
+
server.tool("create_state_machine", "Create a new state machine definition with namespace and name", { namespace: z.string(), name: z.string(), asynchronous: z.boolean().optional() }, async ({ namespace, name, asynchronous }) => {
|
|
584
|
+
const def = createEmpty(namespace, name);
|
|
585
|
+
if (asynchronous)
|
|
586
|
+
def.settings.asynchronous = true;
|
|
587
|
+
writeDef(def);
|
|
588
|
+
bridgeEmitFull(def);
|
|
589
|
+
return {
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: "text",
|
|
593
|
+
text: `Created state machine '${name}' in namespace '${namespace}' → ${PROJECT_FILE}. Add states and events next.`,
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
server.tool("add_state", "Add a state to the state machine. Parent defaults to Root.", {
|
|
599
|
+
name: z.string(),
|
|
600
|
+
parent: z.string().optional(),
|
|
601
|
+
kind: z.enum(["normal", "final", "history"]).optional(),
|
|
602
|
+
description: z.string().optional(),
|
|
603
|
+
}, async ({ name, parent, kind, description }) => {
|
|
604
|
+
const def = readDef();
|
|
605
|
+
if (!def)
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}. Use create_state_machine first.` }],
|
|
608
|
+
isError: true,
|
|
609
|
+
};
|
|
610
|
+
const parentName = parent ?? "Root";
|
|
611
|
+
const parentState = findState(def.state, parentName);
|
|
612
|
+
if (!parentState)
|
|
613
|
+
return {
|
|
614
|
+
content: [{ type: "text", text: `Parent state '${parentName}' not found.` }],
|
|
615
|
+
isError: true,
|
|
616
|
+
};
|
|
617
|
+
if (!parentState.states)
|
|
618
|
+
parentState.states = [];
|
|
619
|
+
parentState.states.push({ name, kind: kind ?? "normal", description });
|
|
620
|
+
writeDef(def);
|
|
621
|
+
bridgeEmitPatch([
|
|
622
|
+
{ op: "state.add", parent: parentName, state: { name, kind: kind ?? "normal", description } },
|
|
623
|
+
]);
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text", text: `Added state '${name}' under '${parentName}' (${kind ?? "normal"}).` }],
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
server.tool("add_event", "Add an event to the state machine", { id: z.string(), description: z.string().optional() }, async ({ id, description }) => {
|
|
629
|
+
const def = readDef();
|
|
630
|
+
if (!def)
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }],
|
|
633
|
+
isError: true,
|
|
634
|
+
};
|
|
635
|
+
const source = def.events[0];
|
|
636
|
+
source.events.push({ id, description });
|
|
637
|
+
writeDef(def);
|
|
638
|
+
bridgeEmitPatch([{ op: "event.add", sourceIndex: 0, event: { id, description } }]);
|
|
639
|
+
return {
|
|
640
|
+
content: [{ type: "text", text: `Added event '${id}'.` }],
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
server.tool("add_transition", "Add a transition to a state, triggered by an event", {
|
|
644
|
+
state: z.string(),
|
|
645
|
+
event: z.string(),
|
|
646
|
+
nextState: z.string().optional(),
|
|
647
|
+
condition: z.string().optional(),
|
|
648
|
+
description: z.string().optional(),
|
|
649
|
+
}, async ({ state, event, nextState, condition, description }) => {
|
|
650
|
+
const def = readDef();
|
|
651
|
+
if (!def)
|
|
652
|
+
return {
|
|
653
|
+
content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }],
|
|
654
|
+
isError: true,
|
|
655
|
+
};
|
|
656
|
+
const target = findState(def.state, state);
|
|
657
|
+
if (!target)
|
|
658
|
+
return {
|
|
659
|
+
content: [{ type: "text", text: `State '${state}' not found.` }],
|
|
660
|
+
isError: true,
|
|
661
|
+
};
|
|
662
|
+
if (!target.transitions)
|
|
663
|
+
target.transitions = [];
|
|
664
|
+
target.transitions.push({ event, nextState, condition, description });
|
|
665
|
+
writeDef(def);
|
|
666
|
+
bridgeEmitPatch([
|
|
667
|
+
{ op: "transition.add", state, transition: { event, nextState, condition, description } },
|
|
668
|
+
]);
|
|
669
|
+
const desc = nextState ? `${state} --[${event}]--> ${nextState}` : `${state} --[${event}]--> (internal)`;
|
|
670
|
+
return { content: [{ type: "text", text: `Added transition: ${desc}` }] };
|
|
671
|
+
});
|
|
672
|
+
server.tool("remove_state", "Remove a state from the definition", { name: z.string() }, async ({ name }) => {
|
|
673
|
+
const def = readDef();
|
|
674
|
+
if (!def)
|
|
675
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
676
|
+
if (name === "Root")
|
|
677
|
+
return { content: [{ type: "text", text: "Cannot remove Root state." }], isError: true };
|
|
678
|
+
def.state = removeState(def.state, name);
|
|
679
|
+
writeDef(def);
|
|
680
|
+
bridgeEmitPatch([{ op: "state.remove", name }]);
|
|
681
|
+
return { content: [{ type: "text", text: `Removed state '${name}'.` }] };
|
|
682
|
+
});
|
|
683
|
+
server.tool("validate_definition", "Validate the current state machine definition against all rules", {}, async () => {
|
|
684
|
+
const def = readDef();
|
|
685
|
+
if (!def)
|
|
686
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
687
|
+
const errors = validate(def);
|
|
688
|
+
if (errors.length === 0)
|
|
689
|
+
return { content: [{ type: "text", text: "✓ Definition is valid. No errors found." }] };
|
|
690
|
+
const lines = errors.map((e) => `[${e.ruleId}] ${e.message}`).join("\n");
|
|
691
|
+
return {
|
|
692
|
+
content: [{ type: "text", text: `Found ${errors.length} validation error(s):\n${lines}` }],
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
server.tool("generate_code", "Generate Python or TypeScript code from the current definition", { language: z.enum(["python", "typescript"]) }, async ({ language }) => {
|
|
696
|
+
const def = readDef();
|
|
697
|
+
if (!def)
|
|
698
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
699
|
+
const errors = validate(def);
|
|
700
|
+
if (errors.length > 0) {
|
|
701
|
+
const lines = errors.map((e) => `[${e.ruleId}] ${e.message}`).join("\n");
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: `Fix validation errors first:\n${lines}` }],
|
|
704
|
+
isError: true,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const code = generateViaSmcg(def, language);
|
|
708
|
+
return { content: [{ type: "text", text: code }] };
|
|
709
|
+
});
|
|
710
|
+
server.tool("get_definition", "Get the current state machine definition as JSON", {}, async () => {
|
|
711
|
+
const def = readDef();
|
|
712
|
+
if (!def)
|
|
713
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
714
|
+
return {
|
|
715
|
+
content: [{ type: "text", text: JSON.stringify({ stateMachine: def }, null, 2) }],
|
|
716
|
+
};
|
|
717
|
+
});
|
|
718
|
+
server.tool("load_definition", "Load a state machine definition from JSON", { json: z.string() }, async ({ json }) => {
|
|
719
|
+
try {
|
|
720
|
+
const parsed = JSON.parse(json);
|
|
721
|
+
const def = parsed.stateMachine ?? parsed.StateMachine ?? parsed;
|
|
722
|
+
writeDef(def);
|
|
723
|
+
bridgeEmitFull(def);
|
|
724
|
+
const stateCount = collectStateNames(def.state).length;
|
|
725
|
+
const eventCount = collectEventIds(def).length;
|
|
726
|
+
return {
|
|
727
|
+
content: [
|
|
728
|
+
{
|
|
729
|
+
type: "text",
|
|
730
|
+
text: `Loaded definition → ${PROJECT_FILE}: ${def.settings.name} (${stateCount} states, ${eventCount} events)`,
|
|
731
|
+
},
|
|
732
|
+
],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
catch (e) {
|
|
736
|
+
return { content: [{ type: "text", text: `Invalid JSON: ${e}` }], isError: true };
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
server.tool("list_states", "List all states in the current definition with their kind and transitions", {}, async () => {
|
|
740
|
+
const def = readDef();
|
|
741
|
+
if (!def)
|
|
742
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
743
|
+
const lines = [];
|
|
744
|
+
const walk = (state, depth) => {
|
|
745
|
+
const indent = " ".repeat(depth);
|
|
746
|
+
const kind = state.kind ? ` (${state.kind})` : "";
|
|
747
|
+
const transCount = state.transitions?.length ?? 0;
|
|
748
|
+
lines.push(`${indent}${state.name}${kind} — ${transCount} transition(s)`);
|
|
749
|
+
for (const t of state.transitions ?? []) {
|
|
750
|
+
lines.push(`${indent} → [${t.event}] → ${t.nextState ?? "(internal)"}`);
|
|
751
|
+
}
|
|
752
|
+
(state.states ?? []).forEach((s) => walk(s, depth + 1));
|
|
753
|
+
};
|
|
754
|
+
walk(def.state, 0);
|
|
755
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
756
|
+
});
|
|
757
|
+
server.tool("list_events", "List all events in the current definition", {}, async () => {
|
|
758
|
+
const def = readDef();
|
|
759
|
+
if (!def)
|
|
760
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
761
|
+
const events = def.events.flatMap((src) => src.events.map((e) => `${e.id}${e.description ? ` — ${e.description}` : ""}`));
|
|
762
|
+
if (events.length === 0)
|
|
763
|
+
return { content: [{ type: "text", text: "No events defined yet." }] };
|
|
764
|
+
return { content: [{ type: "text", text: events.join("\n") }] };
|
|
765
|
+
});
|
|
766
|
+
server.tool("set_project_file", "Choose which .smdf.json path is the active document the loom weaves. Disk persistence AND the live bridge room (the hub keys rooms by this path) both re-point to it. A missing file is legitimate — create_state_machine or load_definition writes it next. Path must end in .json; convention is *.smdf.json.", { path: z.string() }, async ({ path }) => {
|
|
767
|
+
try {
|
|
768
|
+
const r = resolveProjectSwitch(path, PROJECT_FILE);
|
|
769
|
+
PROJECT_FILE = r.path;
|
|
770
|
+
if (!r.unchanged) {
|
|
771
|
+
bindBridge(PROJECT_FILE);
|
|
772
|
+
joinBridge();
|
|
773
|
+
}
|
|
774
|
+
const def = r.exists ? readDef() : null;
|
|
775
|
+
const summary = def
|
|
776
|
+
? `existing machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
|
|
777
|
+
: r.exists
|
|
778
|
+
? "file exists but is not a readable definition"
|
|
779
|
+
: "no file yet — create_state_machine or load_definition will write it";
|
|
780
|
+
const bridgeNote = BRIDGE_URL
|
|
781
|
+
? r.unchanged
|
|
782
|
+
? `bridge unchanged (${bridge?.status ?? "not configured"})`
|
|
783
|
+
: `bridge re-joining room '${r.path}'`
|
|
784
|
+
: "bridge not configured (STATELOOM_BRIDGE_URL unset)";
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: `Active document: ${r.path}\nPrevious: ${r.previous}\n${summary}\n${bridgeNote}`,
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
return {
|
|
796
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
797
|
+
isError: true,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
server.tool("get_project_file", "Report the active .smdf.json document path, whether it exists on disk, and live-bridge status", {}, async () => {
|
|
802
|
+
const def = readDef();
|
|
803
|
+
const summary = def
|
|
804
|
+
? `machine '${def.settings.name}' (${collectStateNames(def.state).length} states, ${collectEventIds(def).length} events)`
|
|
805
|
+
: existsSync(PROJECT_FILE)
|
|
806
|
+
? "file exists but is not a readable definition"
|
|
807
|
+
: "file does not exist yet";
|
|
808
|
+
const bridgeNote = BRIDGE_URL
|
|
809
|
+
? `bridge: ${bridge?.status ?? "not initialized"} → ${BRIDGE_URL}`
|
|
810
|
+
: "bridge not configured (STATELOOM_BRIDGE_URL unset)";
|
|
811
|
+
return {
|
|
812
|
+
content: [
|
|
813
|
+
{ type: "text", text: `Active document: ${PROJECT_FILE}\n${summary}\n${bridgeNote}` },
|
|
814
|
+
],
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
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 }) => {
|
|
818
|
+
const def = readDef();
|
|
819
|
+
if (!def)
|
|
820
|
+
return { content: [{ type: "text", text: `No state machine at ${PROJECT_FILE}.` }], isError: true };
|
|
821
|
+
const md = generateRispec(def, intent);
|
|
822
|
+
return { content: [{ type: "text", text: md }] };
|
|
823
|
+
});
|
|
824
|
+
// Resources
|
|
825
|
+
server.resource("definition", "smcraft://definition", async () => {
|
|
826
|
+
const def = readDef();
|
|
827
|
+
return {
|
|
828
|
+
contents: [
|
|
829
|
+
{
|
|
830
|
+
uri: "smcraft://definition",
|
|
831
|
+
mimeType: "application/json",
|
|
832
|
+
text: def
|
|
833
|
+
? JSON.stringify({ stateMachine: def }, null, 2)
|
|
834
|
+
: `{"error": "No definition at ${PROJECT_FILE}"}`,
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
// Prompts
|
|
840
|
+
server.prompt("design-state-machine", "Guided conversation to design a state machine step by step", { domain: z.string().optional(), name: z.string().optional() }, ({ domain, name }) => ({
|
|
841
|
+
messages: [
|
|
842
|
+
{
|
|
843
|
+
role: "user",
|
|
844
|
+
content: {
|
|
845
|
+
type: "text",
|
|
846
|
+
text: `I want to design a state machine${name ? ` called "${name}"` : ""}${domain ? ` for the "${domain}" domain` : ""}. Guide me through:
|
|
847
|
+
1. First, create the state machine with create_state_machine
|
|
848
|
+
2. Help me identify the key states and add them with add_state
|
|
849
|
+
3. Define the events that trigger transitions with add_event
|
|
850
|
+
4. Wire up transitions between states with add_transition
|
|
851
|
+
5. Validate the design with validate_definition
|
|
852
|
+
6. Generate code with generate_code
|
|
853
|
+
|
|
854
|
+
Start by asking me about the states and events I need.`,
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
}));
|
|
859
|
+
// Start
|
|
860
|
+
async function main() {
|
|
861
|
+
const transport = new StdioServerTransport();
|
|
862
|
+
await server.connect(transport);
|
|
863
|
+
console.error(`SMCraft MCP server running on stdio (project file: ${PROJECT_FILE})`);
|
|
864
|
+
joinBridge();
|
|
865
|
+
}
|
|
866
|
+
main().catch(console.error);
|