@statelyai/mcp 0.2.0 → 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/README.md CHANGED
@@ -78,6 +78,30 @@ Most tools accept a machine config object or a source document. Conversion
78
78
  supports XState, SCXML, XGraph, and D2. Validation also supports JSON, YAML, and
79
79
  Mermaid.
80
80
 
81
+ ## `create_state_machine`
82
+
83
+ <!-- create_state_machine registration from src/server.ts and convert/visualize response contracts -->
84
+
85
+ Creates a new XState v5 machine from a natural-language request. The connected
86
+ agent authors the XState source, then Stately normalizes it and returns the
87
+ source. Visualization defaults on and returns native PNG image blocks because a
88
+ diagram usually makes generated behavior easier to verify. Set `visualization`
89
+ to `false` only for code-only requests, clients that cannot display images, or
90
+ machines where a diagram adds no useful information.
91
+
92
+ <details>
93
+ <summary>Summary: create and visualize a machine</summary>
94
+
95
+ ```text
96
+ Make a state machine representing an order checkout flow.
97
+ ```
98
+
99
+ The agent calls Stately with complete XState v5 source. Omit `visualization` to
100
+ render PNG diagrams, customize it with `direction` or `parentNodeIds`, or set it
101
+ to `false` to return only the normalized XState source.
102
+
103
+ </details>
104
+
81
105
  ## `validate_machine`
82
106
 
83
107
  <!-- validate_machine registration from src/server.ts and validate response contract -->
package/dist/index.cjs ADDED
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
+ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
4
+ let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ let zod = require("zod");
6
+
7
+ //#region src/server.ts
8
+ const readOnlyAnnotations = {
9
+ readOnlyHint: true,
10
+ destructiveHint: false,
11
+ idempotentHint: true,
12
+ openWorldHint: false
13
+ };
14
+ const apiFormats = [
15
+ "d2",
16
+ "scxml",
17
+ "xgraph",
18
+ "xstate"
19
+ ];
20
+ const validationFormats = [
21
+ ...apiFormats,
22
+ "json",
23
+ "yaml",
24
+ "mermaid"
25
+ ];
26
+ const apiFormatSchema = zod.z.enum(apiFormats);
27
+ const validationFormatSchema = zod.z.enum(validationFormats);
28
+ const machineSchema = zod.z.union([zod.z.record(zod.z.string(), zod.z.unknown()), zod.z.string()]);
29
+ const sourceSchema = zod.z.object({
30
+ format: validationFormatSchema.optional(),
31
+ content: machineSchema
32
+ });
33
+ const pngImageSchema = zod.z.object({
34
+ format: zod.z.literal("png"),
35
+ mediaType: zod.z.literal("image/png"),
36
+ encoding: zod.z.literal("base64"),
37
+ content: zod.z.string()
38
+ });
39
+ const visualizeApiResponseSchema = zod.z.array(zod.z.object({
40
+ parentNodeId: zod.z.string(),
41
+ parentNodeLabel: zod.z.string(),
42
+ hierarchyLevel: zod.z.number().int().nonnegative(),
43
+ images: zod.z.array(pngImageSchema).min(1),
44
+ width: zod.z.number(),
45
+ height: zod.z.number(),
46
+ warnings: zod.z.array(zod.z.string())
47
+ }));
48
+ const xstateMachineResultSchema = zod.z.object({
49
+ format: zod.z.literal("xstate"),
50
+ content: zod.z.string(),
51
+ warnings: zod.z.array(zod.z.string())
52
+ });
53
+ const visualizationMetadataSchema = zod.z.object({
54
+ parentNodeId: zod.z.string(),
55
+ parentNodeLabel: zod.z.string(),
56
+ hierarchyLevel: zod.z.number().int().nonnegative(),
57
+ width: zod.z.number(),
58
+ height: zod.z.number(),
59
+ warnings: zod.z.array(zod.z.string())
60
+ });
61
+ const createMachineOutputSchema = zod.z.object({
62
+ machine: xstateMachineResultSchema,
63
+ visualizations: zod.z.array(visualizationMetadataSchema)
64
+ });
65
+ const createMachineVisualizationSchema = zod.z.union([zod.z.literal(false), zod.z.object({
66
+ direction: zod.z.enum(["right", "down"]).optional(),
67
+ parentNodeIds: zod.z.array(zod.z.string()).min(1).optional()
68
+ })]).default({}).describe("Visualization defaults on. Set false only when the user explicitly wants code only, the client cannot display images, or a diagram adds no useful information.");
69
+ const patchSchema = zod.z.object({ op: zod.z.string() }).passthrough();
70
+ function generateTestCode(machineCode, paths) {
71
+ return [
72
+ [
73
+ "// Generated by generate_test_code (Stately MCP).",
74
+ "// Guards and actions are stubbed; assertions use state paths.",
75
+ "import { createActor } from 'xstate';",
76
+ "import { describe, expect, it } from 'vitest';",
77
+ "",
78
+ machineCode.trim(),
79
+ ""
80
+ ].join("\n"),
81
+ "describe('generated paths', () => {",
82
+ paths.map((path) => {
83
+ const events = path.steps.filter((step) => step.eventType !== null && step.eventType !== "" && !step.eventType.startsWith("xstate.")).map((step) => ` actor.send({ type: ${JSON.stringify(step.eventType)} });`);
84
+ return [
85
+ ` it(${JSON.stringify(path.name)}, () => {`,
86
+ " const actor = createActor(machine).start();",
87
+ ...events,
88
+ ` expect(actor.getSnapshot().matches(${JSON.stringify(path.finalStatePath)})).toBe(true);`,
89
+ " });"
90
+ ].join("\n");
91
+ }).join("\n\n"),
92
+ "});",
93
+ ""
94
+ ].join("\n");
95
+ }
96
+ function toolText$1(body) {
97
+ return JSON.stringify(body, null, 2);
98
+ }
99
+ function toolResult(body) {
100
+ return {
101
+ structuredContent: body,
102
+ content: [{
103
+ type: "text",
104
+ text: toolText$1(body)
105
+ }]
106
+ };
107
+ }
108
+ function visualizationMetadata(visualizations) {
109
+ return visualizations.map(({ parentNodeId, parentNodeLabel, hierarchyLevel, width, height, warnings }) => ({
110
+ parentNodeId,
111
+ parentNodeLabel,
112
+ hierarchyLevel,
113
+ width,
114
+ height,
115
+ warnings
116
+ }));
117
+ }
118
+ function visualizationImageContent(visualizations) {
119
+ return visualizations.map((visualization) => ({
120
+ type: "image",
121
+ data: visualization.images[0].content,
122
+ mimeType: "image/png"
123
+ }));
124
+ }
125
+ function createMachineToolResult(body) {
126
+ const machine = xstateMachineResultSchema.parse(body.machine);
127
+ const visualizations = visualizeApiResponseSchema.parse(body.visualizations);
128
+ const count = visualizations.length;
129
+ return {
130
+ structuredContent: createMachineOutputSchema.parse({
131
+ machine,
132
+ visualizations: visualizationMetadata(visualizations)
133
+ }),
134
+ content: [{
135
+ type: "text",
136
+ text: `${count > 0 ? `Created an XState machine and rendered ${count} hierarchy ${count === 1 ? "visualization" : "visualizations"}.` : "Created an XState machine."}\n\n${machine.content}`
137
+ }, ...visualizationImageContent(visualizations)]
138
+ };
139
+ }
140
+ function visualizeToolResult(body) {
141
+ const visualizations = visualizeApiResponseSchema.parse(body.visualizations);
142
+ const count = visualizations.length;
143
+ return {
144
+ structuredContent: { visualizations: visualizationMetadata(visualizations) },
145
+ content: [{
146
+ type: "text",
147
+ text: `Rendered ${count} machine hierarchy ${count === 1 ? "visualization" : "visualizations"}.`
148
+ }, ...visualizationImageContent(visualizations)]
149
+ };
150
+ }
151
+ function toolError(body) {
152
+ return {
153
+ isError: true,
154
+ ...toolResult(body)
155
+ };
156
+ }
157
+ function normalizeApiBaseUrl(apiBaseUrl) {
158
+ return new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/$/, "");
159
+ }
160
+ function normalizeSource(input) {
161
+ const content = input.source?.content ?? input.machine;
162
+ if (content === void 0) throw new Error("Expected machine or source.content.");
163
+ return {
164
+ format: input.source?.format ?? input.format ?? (typeof content === "string" ? "xstate" : "json"),
165
+ content
166
+ };
167
+ }
168
+ function toConversionSource(source) {
169
+ if (!apiFormats.includes(source.format)) {
170
+ if (source.format === "json" && typeof source.content === "object") return {
171
+ format: "xstate",
172
+ content: `createMachine(${JSON.stringify(source.content, null, 2)})`
173
+ };
174
+ if (source.format === "json" && typeof source.content === "string") return {
175
+ format: "xstate",
176
+ content: source.content
177
+ };
178
+ throw new Error(`Format ${source.format} is only supported by validate_machine.`);
179
+ }
180
+ return source;
181
+ }
182
+ async function callApi(options, pathname, body) {
183
+ const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
184
+ const headers = new Headers(configuredHeaders);
185
+ const authMode = options.authMode ?? "auto";
186
+ if (authMode !== "none" && !headers.has("authorization")) {
187
+ const credential = await options.credentialProvider?.();
188
+ const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
189
+ if (token) headers.set("authorization", `Bearer ${token}`);
190
+ else if (authMode === "bearer") throw new Error("No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.");
191
+ }
192
+ const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
193
+ method: "POST",
194
+ headers: {
195
+ "content-type": "application/json",
196
+ ...Object.fromEntries(headers.entries())
197
+ },
198
+ body: JSON.stringify(body)
199
+ });
200
+ const result = await response.json().catch(() => ({ error: `Invalid JSON response from ${pathname}.` }));
201
+ if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
202
+ return result;
203
+ }
204
+ function registerTool(server, name, config, handler, formatResult = toolResult) {
205
+ server.registerTool(name, {
206
+ ...config,
207
+ annotations: readOnlyAnnotations
208
+ }, async (args) => {
209
+ try {
210
+ return formatResult(await handler(args));
211
+ } catch (error) {
212
+ return toolError({ error: error instanceof Error ? error.message : String(error) });
213
+ }
214
+ });
215
+ }
216
+ function registerStatelyMcpTools(server, options = {}) {
217
+ registerTool(server, "create_state_machine", {
218
+ title: "Create state machine",
219
+ description: "Create a new XState v5 state machine from the user's description. The calling model must author complete XState v5 code and pass it as xstateCode. Return the XState source and, by default, directly viewable PNG diagrams. Prefer visualization because it makes behavior easier to verify; disable it only when the user explicitly wants code only, the client cannot display images, or a diagram adds no useful information.",
220
+ inputSchema: {
221
+ xstateCode: zod.z.string().min(1).describe("Complete XState v5 source with imports and createMachine(...) or setup(...).createMachine(...)."),
222
+ visualization: createMachineVisualizationSchema
223
+ },
224
+ outputSchema: createMachineOutputSchema
225
+ }, async (input) => {
226
+ const machine = xstateMachineResultSchema.parse(await callApi(options, "/v1/convert", {
227
+ source: {
228
+ format: "xstate",
229
+ content: input.xstateCode
230
+ },
231
+ target: { format: "xstate" }
232
+ }));
233
+ return {
234
+ machine,
235
+ visualizations: input.visualization === false ? [] : await callApi(options, "/v1/visualize", {
236
+ source: {
237
+ format: machine.format,
238
+ content: machine.content
239
+ },
240
+ options: {
241
+ direction: input.visualization.direction,
242
+ formats: ["png"],
243
+ parentNodeIds: input.visualization.parentNodeIds
244
+ }
245
+ })
246
+ };
247
+ }, createMachineToolResult);
248
+ registerTool(server, "validate_machine", {
249
+ title: "Validate machine",
250
+ description: "Validate an XState machine config or source string through the Stately API.",
251
+ inputSchema: {
252
+ machine: machineSchema.optional(),
253
+ format: validationFormatSchema.optional(),
254
+ source: sourceSchema.optional()
255
+ }
256
+ }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
257
+ registerTool(server, "convert_machine", {
258
+ title: "Convert machine",
259
+ description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
260
+ inputSchema: {
261
+ machine: machineSchema.optional(),
262
+ format: apiFormatSchema.optional(),
263
+ source: sourceSchema.optional(),
264
+ to: apiFormatSchema
265
+ }
266
+ }, (input) => callApi(options, "/v1/convert", {
267
+ source: toConversionSource(normalizeSource(input)),
268
+ target: { format: input.to }
269
+ }));
270
+ registerTool(server, "diff_machines", {
271
+ title: "Diff machines",
272
+ description: "Compute a semantic diff between two machine documents through the Stately API.",
273
+ inputSchema: {
274
+ base: sourceSchema,
275
+ compare: sourceSchema,
276
+ includePatches: zod.z.boolean().optional()
277
+ }
278
+ }, (input) => callApi(options, "/v1/diff", {
279
+ base: toConversionSource(normalizeSource({ source: input.base })),
280
+ compare: toConversionSource(normalizeSource({ source: input.compare })),
281
+ includePatches: input.includePatches
282
+ }));
283
+ registerTool(server, "apply_machine_patches", {
284
+ title: "Apply machine patches",
285
+ description: "Apply semantic machine patches through the Stately API.",
286
+ inputSchema: {
287
+ machine: machineSchema.optional(),
288
+ format: apiFormatSchema.optional(),
289
+ source: sourceSchema.optional(),
290
+ patches: zod.z.array(patchSchema),
291
+ to: apiFormatSchema.optional()
292
+ }
293
+ }, (input) => callApi(options, "/v1/apply-patches", {
294
+ source: toConversionSource(normalizeSource(input)),
295
+ patches: input.patches,
296
+ target: input.to ? { format: input.to } : void 0
297
+ }));
298
+ registerTool(server, "generate_graph_paths", {
299
+ title: "Generate graph paths",
300
+ description: "Generate shortest or simple paths through the Stately API.",
301
+ inputSchema: {
302
+ machine: machineSchema.optional(),
303
+ format: apiFormatSchema.optional(),
304
+ source: sourceSchema.optional(),
305
+ graph: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
306
+ kind: zod.z.enum(["shortest", "simple"]).default("shortest"),
307
+ max: zod.z.number().int().positive().optional()
308
+ }
309
+ }, async (input) => {
310
+ return callApi(options, "/v1/graph-paths", {
311
+ graph: input.graph ?? (await callApi(options, "/v1/convert", {
312
+ source: toConversionSource(normalizeSource(input)),
313
+ target: { format: "xgraph" }
314
+ })).content,
315
+ options: {
316
+ strategy: input.kind,
317
+ limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
318
+ }
319
+ });
320
+ });
321
+ registerTool(server, "simulate_machine", {
322
+ title: "Simulate machine",
323
+ description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
324
+ inputSchema: {
325
+ machine: machineSchema.optional(),
326
+ format: apiFormatSchema.optional(),
327
+ source: sourceSchema.optional(),
328
+ events: zod.z.array(zod.z.object({ type: zod.z.string() }).passthrough()).min(1).max(100)
329
+ }
330
+ }, (input) => callApi(options, "/v1/simulate", {
331
+ source: toConversionSource(normalizeSource(input)),
332
+ events: input.events
333
+ }));
334
+ registerTool(server, "visualize_machine", {
335
+ title: "Visualize machine",
336
+ description: "Render machine hierarchy levels as directly viewable PNG images. Optionally select parent node ids.",
337
+ inputSchema: {
338
+ machine: machineSchema.optional(),
339
+ format: apiFormatSchema.optional(),
340
+ source: sourceSchema.optional(),
341
+ direction: zod.z.enum(["right", "down"]).optional(),
342
+ parentNodeIds: zod.z.array(zod.z.string()).min(1).optional()
343
+ }
344
+ }, (input) => {
345
+ return callApi(options, "/v1/visualize", {
346
+ source: toConversionSource(normalizeSource(input)),
347
+ options: {
348
+ direction: input.direction,
349
+ formats: ["png"],
350
+ parentNodeIds: input.parentNodeIds
351
+ }
352
+ }).then((visualizations) => ({ visualizations }));
353
+ }, visualizeToolResult);
354
+ registerTool(server, "analyze_machine", {
355
+ title: "Analyze machine",
356
+ description: "Deep analysis of machine logic (merges deterministic graph checks with AI review); may require an API key — configure via server options; for purely deterministic checks use validate_machine.",
357
+ inputSchema: {
358
+ machine: machineSchema.optional(),
359
+ format: apiFormatSchema.optional(),
360
+ source: sourceSchema.optional(),
361
+ intent: zod.z.string().optional()
362
+ }
363
+ }, (input) => callApi(options, "/v1/analyze", {
364
+ source: toConversionSource(normalizeSource(input)),
365
+ intent: input.intent
366
+ }));
367
+ registerTool(server, "generate_test_code", {
368
+ title: "Generate test code",
369
+ description: "Generate a runnable vitest test file from the machine, one test per generated path. Guards and actions are stubbed; assertions use state paths.",
370
+ inputSchema: {
371
+ machine: machineSchema.optional(),
372
+ format: apiFormatSchema.optional(),
373
+ source: sourceSchema.optional(),
374
+ kind: zod.z.enum(["shortest", "simple"]).default("shortest"),
375
+ max: zod.z.number().int().positive().optional()
376
+ }
377
+ }, async (input) => {
378
+ const source = toConversionSource(normalizeSource(input));
379
+ const graph = (await callApi(options, "/v1/convert", {
380
+ source,
381
+ target: { format: "xgraph" }
382
+ })).content;
383
+ const pathsResponse = await callApi(options, "/v1/graph-paths", {
384
+ graph,
385
+ options: {
386
+ strategy: input.kind,
387
+ limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
388
+ }
389
+ });
390
+ return {
391
+ code: generateTestCode(String((await callApi(options, "/v1/convert", {
392
+ source,
393
+ target: { format: "xstate" }
394
+ })).content), pathsResponse.paths),
395
+ paths: pathsResponse.paths,
396
+ coverage: pathsResponse.coverage
397
+ };
398
+ });
399
+ registerTool(server, "extract_machines_from_code", {
400
+ title: "Extract machines from code",
401
+ description: "Parse XState source code into XGraph through the Stately API.",
402
+ inputSchema: { code: zod.z.string().min(1) }
403
+ }, async (input) => {
404
+ return { machines: [{
405
+ format: "xgraph",
406
+ graph: (await callApi(options, "/v1/convert", {
407
+ source: {
408
+ format: "xstate",
409
+ content: input.code
410
+ },
411
+ target: { format: "xgraph" }
412
+ })).content
413
+ }] };
414
+ });
415
+ return server;
416
+ }
417
+ function createStatelyMcpServer(options = {}) {
418
+ const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
419
+ name: "stately",
420
+ version: "0.1.0"
421
+ });
422
+ registerStatelyMcpTools(server, options);
423
+ return server;
424
+ }
425
+
426
+ //#endregion
427
+ //#region src/index.ts
428
+ async function loadNode() {
429
+ const [fs, os, path] = await Promise.all([
430
+ import("node:fs/promises"),
431
+ import("node:os"),
432
+ import("node:path")
433
+ ]);
434
+ return {
435
+ fs,
436
+ os,
437
+ path
438
+ };
439
+ }
440
+ function workspaceFromArgv() {
441
+ const index = process.argv.indexOf("--workspace");
442
+ return index >= 0 ? process.argv[index + 1] : process.cwd();
443
+ }
444
+ function isCandidate(pathname) {
445
+ return /\.(?:[cm]?[jt]sx?|json|ya?ml|scxml|mmd|mermaid)$/i.test(pathname);
446
+ }
447
+ function formatForPath(pathname, text) {
448
+ if (/\.(?:mmd|mermaid)$/i.test(pathname)) return "mermaid";
449
+ if (/\.scxml$/i.test(pathname)) return "scxml";
450
+ if (/\.ya?ml$/i.test(pathname)) return "yaml";
451
+ if (/\.json$/i.test(pathname)) return "json";
452
+ if (/^\s*</.test(text)) return "scxml";
453
+ return "xstate";
454
+ }
455
+ async function walkWorkspace(root, glob) {
456
+ const { fs, path } = await loadNode();
457
+ const results = [];
458
+ async function walk(dir) {
459
+ const entries = await fs.readdir(dir, { withFileTypes: true });
460
+ for (const entry of entries) {
461
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
462
+ const full = path.join(dir, entry.name);
463
+ if (entry.isDirectory()) await walk(full);
464
+ else if (isCandidate(full)) {
465
+ const relative = path.relative(root, full);
466
+ if (!glob || relative.includes(glob)) results.push(relative);
467
+ }
468
+ }
469
+ }
470
+ await walk(root);
471
+ return results;
472
+ }
473
+ async function readWorkspaceMachine(input) {
474
+ const { fs, path } = await loadNode();
475
+ const root = workspaceFromArgv();
476
+ const fullPath = path.resolve(root, input.path);
477
+ const relative = path.relative(root, fullPath);
478
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path must stay inside workspace.");
479
+ const sourceText = await fs.readFile(fullPath, "utf8");
480
+ return {
481
+ machine: sourceText,
482
+ format: formatForPath(fullPath, sourceText),
483
+ source: { path: relative },
484
+ ...input.includeSourceText ? { sourceText } : {}
485
+ };
486
+ }
487
+ async function readStoredCliCredential() {
488
+ const { fs, os, path } = await loadNode();
489
+ const configDir = process.env.STATELYAI_CONFIG_DIR ? path.resolve(process.env.STATELYAI_CONFIG_DIR) : process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "statelyai") : process.platform === "win32" ? path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "statelyai") : path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "statelyai");
490
+ try {
491
+ const raw = await fs.readFile(path.join(configDir, "credentials.json"), "utf8");
492
+ const parsed = JSON.parse(raw);
493
+ if (typeof parsed.apiKey === "string" && parsed.apiKey) return {
494
+ type: "api_key",
495
+ token: parsed.apiKey
496
+ };
497
+ if (typeof parsed.credential === "object" && parsed.credential !== null && "type" in parsed.credential) {
498
+ const credential = parsed.credential;
499
+ if (credential.type === "api_key" && typeof credential.token === "string") return {
500
+ type: "api_key",
501
+ token: credential.token
502
+ };
503
+ if (credential.type === "oauth" && typeof credential.accessToken === "string") return {
504
+ type: "oauth",
505
+ accessToken: credential.accessToken,
506
+ ...typeof credential.refreshToken === "string" ? { refreshToken: credential.refreshToken } : {},
507
+ ...typeof credential.expiresAt === "number" ? { expiresAt: credential.expiresAt } : {}
508
+ };
509
+ }
510
+ } catch {
511
+ return;
512
+ }
513
+ }
514
+ async function getDefaultCredential() {
515
+ if (process.env.STATELY_ACCESS_TOKEN) return {
516
+ type: "oauth",
517
+ accessToken: process.env.STATELY_ACCESS_TOKEN
518
+ };
519
+ if (process.env.STATELY_API_KEY) return {
520
+ type: "api_key",
521
+ token: process.env.STATELY_API_KEY
522
+ };
523
+ return readStoredCliCredential();
524
+ }
525
+ function toolText(body) {
526
+ return JSON.stringify(body, null, 2);
527
+ }
528
+ function createStatelyMcpServer$1(options = {}) {
529
+ const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
530
+ name: "stately",
531
+ version: "0.1.0"
532
+ });
533
+ if (options.includeWorkspaceTools !== false) {
534
+ server.registerTool("list_machines", {
535
+ title: "list_machines",
536
+ description: "List candidate machine sources in the workspace.",
537
+ inputSchema: { glob: zod.z.string().optional() },
538
+ annotations: readOnlyAnnotations
539
+ }, async (request) => {
540
+ const machines = (await walkWorkspace(workspaceFromArgv(), request.glob)).map((pathname) => ({
541
+ source: { path: pathname },
542
+ format: "xstate"
543
+ }));
544
+ return {
545
+ structuredContent: { machines },
546
+ content: [{
547
+ type: "text",
548
+ text: toolText({ machines })
549
+ }]
550
+ };
551
+ });
552
+ server.registerTool("get_machine", {
553
+ title: "get_machine",
554
+ description: "Read a machine source from the workspace and return it.",
555
+ inputSchema: {
556
+ path: zod.z.string(),
557
+ includeSourceText: zod.z.boolean().optional()
558
+ },
559
+ annotations: readOnlyAnnotations
560
+ }, async (request) => {
561
+ const result = await readWorkspaceMachine(request);
562
+ return {
563
+ structuredContent: result,
564
+ content: [{
565
+ type: "text",
566
+ text: toolText(result)
567
+ }]
568
+ };
569
+ });
570
+ }
571
+ registerStatelyMcpTools(server, {
572
+ ...options,
573
+ authMode: options.authMode ?? (process.env.STATELY_AUTH_MODE === "none" ? "none" : void 0),
574
+ credentialProvider: options.credentialProvider ?? getDefaultCredential
575
+ });
576
+ return server;
577
+ }
578
+ async function main() {
579
+ const server = createStatelyMcpServer$1();
580
+ const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
581
+ await server.connect(transport);
582
+ }
583
+ if (require("url").pathToFileURL(__filename).href === `file://${process.argv[1]}`) main();
584
+
585
+ //#endregion
586
+ exports.createHostedStatelyMcpServer = createStatelyMcpServer;
587
+ exports.createStatelyMcpServer = createStatelyMcpServer$1;
588
+ exports.formatForPath = formatForPath;
589
+ exports.generateTestCode = generateTestCode;
590
+ exports.getDefaultCredential = getDefaultCredential;
591
+ exports.isCandidate = isCandidate;
592
+ exports.readOnlyAnnotations = readOnlyAnnotations;
593
+ exports.readWorkspaceMachine = readWorkspaceMachine;
594
+ exports.registerStatelyMcpTools = registerStatelyMcpTools;
595
+ exports.walkWorkspace = walkWorkspace;
596
+ exports.workspaceFromArgv = workspaceFromArgv;
@@ -0,0 +1,72 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ //#region src/server.d.ts
4
+ declare const readOnlyAnnotations: {
5
+ readOnlyHint: boolean;
6
+ destructiveHint: boolean;
7
+ idempotentHint: boolean;
8
+ openWorldHint: boolean;
9
+ };
10
+ type StatelyMcpServerOptions = {
11
+ includeWorkspaceTools?: boolean;
12
+ apiBaseUrl?: string | URL;
13
+ apiHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
14
+ credentialProvider?: () => StatelyCredential | undefined | Promise<StatelyCredential | undefined>;
15
+ authMode?: 'auto' | 'none' | 'bearer';
16
+ anonymousGraphPathLimit?: number;
17
+ };
18
+ type StatelyCredential = {
19
+ type: 'api_key';
20
+ token: string;
21
+ } | {
22
+ type: 'oauth';
23
+ accessToken: string;
24
+ refreshToken?: string;
25
+ expiresAt?: number;
26
+ };
27
+ type GraphPathStep = {
28
+ stateIds: string[];
29
+ statePath: string;
30
+ eventType: string | null;
31
+ edgeId: string | null;
32
+ };
33
+ type GraphPath = {
34
+ id: string;
35
+ name: string;
36
+ viaLabel: string | null;
37
+ steps: GraphPathStep[];
38
+ finalStatePath: string;
39
+ finalStateIds: string[];
40
+ };
41
+ declare function generateTestCode(machineCode: string, paths: GraphPath[]): string;
42
+ declare function registerStatelyMcpTools(server: McpServer, options?: StatelyMcpServerOptions): McpServer;
43
+ declare function createStatelyMcpServer(options?: StatelyMcpServerOptions): McpServer;
44
+ //#endregion
45
+ //#region src/index.d.ts
46
+ declare function workspaceFromArgv(): string;
47
+ declare function isCandidate(pathname: string): boolean;
48
+ declare function formatForPath(pathname: string, text: string): "mermaid" | "scxml" | "yaml" | "json" | "xstate";
49
+ declare function walkWorkspace(root: string, glob?: string): Promise<string[]>;
50
+ declare function readWorkspaceMachine(input: {
51
+ path: string;
52
+ includeSourceText?: boolean;
53
+ }): Promise<{
54
+ sourceText?: string | undefined;
55
+ machine: string;
56
+ format: "mermaid" | "scxml" | "yaml" | "json" | "xstate";
57
+ source: {
58
+ path: string;
59
+ };
60
+ }>;
61
+ declare function getDefaultCredential(): Promise<{
62
+ type: "oauth";
63
+ accessToken: string;
64
+ refreshToken?: string;
65
+ expiresAt?: number;
66
+ } | {
67
+ type: "api_key";
68
+ token: string;
69
+ } | undefined>;
70
+ declare function createStatelyMcpServer$1(options?: StatelyMcpServerOptions): McpServer;
71
+ //#endregion
72
+ export { type StatelyMcpServerOptions, createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };
@@ -0,0 +1,72 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ //#region src/server.d.ts
4
+ declare const readOnlyAnnotations: {
5
+ readOnlyHint: boolean;
6
+ destructiveHint: boolean;
7
+ idempotentHint: boolean;
8
+ openWorldHint: boolean;
9
+ };
10
+ type StatelyMcpServerOptions = {
11
+ includeWorkspaceTools?: boolean;
12
+ apiBaseUrl?: string | URL;
13
+ apiHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
14
+ credentialProvider?: () => StatelyCredential | undefined | Promise<StatelyCredential | undefined>;
15
+ authMode?: 'auto' | 'none' | 'bearer';
16
+ anonymousGraphPathLimit?: number;
17
+ };
18
+ type StatelyCredential = {
19
+ type: 'api_key';
20
+ token: string;
21
+ } | {
22
+ type: 'oauth';
23
+ accessToken: string;
24
+ refreshToken?: string;
25
+ expiresAt?: number;
26
+ };
27
+ type GraphPathStep = {
28
+ stateIds: string[];
29
+ statePath: string;
30
+ eventType: string | null;
31
+ edgeId: string | null;
32
+ };
33
+ type GraphPath = {
34
+ id: string;
35
+ name: string;
36
+ viaLabel: string | null;
37
+ steps: GraphPathStep[];
38
+ finalStatePath: string;
39
+ finalStateIds: string[];
40
+ };
41
+ declare function generateTestCode(machineCode: string, paths: GraphPath[]): string;
42
+ declare function registerStatelyMcpTools(server: McpServer, options?: StatelyMcpServerOptions): McpServer;
43
+ declare function createStatelyMcpServer(options?: StatelyMcpServerOptions): McpServer;
44
+ //#endregion
45
+ //#region src/index.d.ts
46
+ declare function workspaceFromArgv(): string;
47
+ declare function isCandidate(pathname: string): boolean;
48
+ declare function formatForPath(pathname: string, text: string): "mermaid" | "scxml" | "yaml" | "json" | "xstate";
49
+ declare function walkWorkspace(root: string, glob?: string): Promise<string[]>;
50
+ declare function readWorkspaceMachine(input: {
51
+ path: string;
52
+ includeSourceText?: boolean;
53
+ }): Promise<{
54
+ sourceText?: string | undefined;
55
+ machine: string;
56
+ format: "mermaid" | "scxml" | "yaml" | "json" | "xstate";
57
+ source: {
58
+ path: string;
59
+ };
60
+ }>;
61
+ declare function getDefaultCredential(): Promise<{
62
+ type: "oauth";
63
+ accessToken: string;
64
+ refreshToken?: string;
65
+ expiresAt?: number;
66
+ } | {
67
+ type: "api_key";
68
+ token: string;
69
+ } | undefined>;
70
+ declare function createStatelyMcpServer$1(options?: StatelyMcpServerOptions): McpServer;
71
+ //#endregion
72
+ export { type StatelyMcpServerOptions, createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };
package/dist/index.mjs ADDED
@@ -0,0 +1,585 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+
6
+ //#region src/server.ts
7
+ const readOnlyAnnotations = {
8
+ readOnlyHint: true,
9
+ destructiveHint: false,
10
+ idempotentHint: true,
11
+ openWorldHint: false
12
+ };
13
+ const apiFormats = [
14
+ "d2",
15
+ "scxml",
16
+ "xgraph",
17
+ "xstate"
18
+ ];
19
+ const validationFormats = [
20
+ ...apiFormats,
21
+ "json",
22
+ "yaml",
23
+ "mermaid"
24
+ ];
25
+ const apiFormatSchema = z.enum(apiFormats);
26
+ const validationFormatSchema = z.enum(validationFormats);
27
+ const machineSchema = z.union([z.record(z.string(), z.unknown()), z.string()]);
28
+ const sourceSchema = z.object({
29
+ format: validationFormatSchema.optional(),
30
+ content: machineSchema
31
+ });
32
+ const pngImageSchema = z.object({
33
+ format: z.literal("png"),
34
+ mediaType: z.literal("image/png"),
35
+ encoding: z.literal("base64"),
36
+ content: z.string()
37
+ });
38
+ const visualizeApiResponseSchema = z.array(z.object({
39
+ parentNodeId: z.string(),
40
+ parentNodeLabel: z.string(),
41
+ hierarchyLevel: z.number().int().nonnegative(),
42
+ images: z.array(pngImageSchema).min(1),
43
+ width: z.number(),
44
+ height: z.number(),
45
+ warnings: z.array(z.string())
46
+ }));
47
+ const xstateMachineResultSchema = z.object({
48
+ format: z.literal("xstate"),
49
+ content: z.string(),
50
+ warnings: z.array(z.string())
51
+ });
52
+ const visualizationMetadataSchema = z.object({
53
+ parentNodeId: z.string(),
54
+ parentNodeLabel: z.string(),
55
+ hierarchyLevel: z.number().int().nonnegative(),
56
+ width: z.number(),
57
+ height: z.number(),
58
+ warnings: z.array(z.string())
59
+ });
60
+ const createMachineOutputSchema = z.object({
61
+ machine: xstateMachineResultSchema,
62
+ visualizations: z.array(visualizationMetadataSchema)
63
+ });
64
+ const createMachineVisualizationSchema = z.union([z.literal(false), z.object({
65
+ direction: z.enum(["right", "down"]).optional(),
66
+ parentNodeIds: z.array(z.string()).min(1).optional()
67
+ })]).default({}).describe("Visualization defaults on. Set false only when the user explicitly wants code only, the client cannot display images, or a diagram adds no useful information.");
68
+ const patchSchema = z.object({ op: z.string() }).passthrough();
69
+ function generateTestCode(machineCode, paths) {
70
+ return [
71
+ [
72
+ "// Generated by generate_test_code (Stately MCP).",
73
+ "// Guards and actions are stubbed; assertions use state paths.",
74
+ "import { createActor } from 'xstate';",
75
+ "import { describe, expect, it } from 'vitest';",
76
+ "",
77
+ machineCode.trim(),
78
+ ""
79
+ ].join("\n"),
80
+ "describe('generated paths', () => {",
81
+ paths.map((path) => {
82
+ const events = path.steps.filter((step) => step.eventType !== null && step.eventType !== "" && !step.eventType.startsWith("xstate.")).map((step) => ` actor.send({ type: ${JSON.stringify(step.eventType)} });`);
83
+ return [
84
+ ` it(${JSON.stringify(path.name)}, () => {`,
85
+ " const actor = createActor(machine).start();",
86
+ ...events,
87
+ ` expect(actor.getSnapshot().matches(${JSON.stringify(path.finalStatePath)})).toBe(true);`,
88
+ " });"
89
+ ].join("\n");
90
+ }).join("\n\n"),
91
+ "});",
92
+ ""
93
+ ].join("\n");
94
+ }
95
+ function toolText$1(body) {
96
+ return JSON.stringify(body, null, 2);
97
+ }
98
+ function toolResult(body) {
99
+ return {
100
+ structuredContent: body,
101
+ content: [{
102
+ type: "text",
103
+ text: toolText$1(body)
104
+ }]
105
+ };
106
+ }
107
+ function visualizationMetadata(visualizations) {
108
+ return visualizations.map(({ parentNodeId, parentNodeLabel, hierarchyLevel, width, height, warnings }) => ({
109
+ parentNodeId,
110
+ parentNodeLabel,
111
+ hierarchyLevel,
112
+ width,
113
+ height,
114
+ warnings
115
+ }));
116
+ }
117
+ function visualizationImageContent(visualizations) {
118
+ return visualizations.map((visualization) => ({
119
+ type: "image",
120
+ data: visualization.images[0].content,
121
+ mimeType: "image/png"
122
+ }));
123
+ }
124
+ function createMachineToolResult(body) {
125
+ const machine = xstateMachineResultSchema.parse(body.machine);
126
+ const visualizations = visualizeApiResponseSchema.parse(body.visualizations);
127
+ const count = visualizations.length;
128
+ return {
129
+ structuredContent: createMachineOutputSchema.parse({
130
+ machine,
131
+ visualizations: visualizationMetadata(visualizations)
132
+ }),
133
+ content: [{
134
+ type: "text",
135
+ text: `${count > 0 ? `Created an XState machine and rendered ${count} hierarchy ${count === 1 ? "visualization" : "visualizations"}.` : "Created an XState machine."}\n\n${machine.content}`
136
+ }, ...visualizationImageContent(visualizations)]
137
+ };
138
+ }
139
+ function visualizeToolResult(body) {
140
+ const visualizations = visualizeApiResponseSchema.parse(body.visualizations);
141
+ const count = visualizations.length;
142
+ return {
143
+ structuredContent: { visualizations: visualizationMetadata(visualizations) },
144
+ content: [{
145
+ type: "text",
146
+ text: `Rendered ${count} machine hierarchy ${count === 1 ? "visualization" : "visualizations"}.`
147
+ }, ...visualizationImageContent(visualizations)]
148
+ };
149
+ }
150
+ function toolError(body) {
151
+ return {
152
+ isError: true,
153
+ ...toolResult(body)
154
+ };
155
+ }
156
+ function normalizeApiBaseUrl(apiBaseUrl) {
157
+ return new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/$/, "");
158
+ }
159
+ function normalizeSource(input) {
160
+ const content = input.source?.content ?? input.machine;
161
+ if (content === void 0) throw new Error("Expected machine or source.content.");
162
+ return {
163
+ format: input.source?.format ?? input.format ?? (typeof content === "string" ? "xstate" : "json"),
164
+ content
165
+ };
166
+ }
167
+ function toConversionSource(source) {
168
+ if (!apiFormats.includes(source.format)) {
169
+ if (source.format === "json" && typeof source.content === "object") return {
170
+ format: "xstate",
171
+ content: `createMachine(${JSON.stringify(source.content, null, 2)})`
172
+ };
173
+ if (source.format === "json" && typeof source.content === "string") return {
174
+ format: "xstate",
175
+ content: source.content
176
+ };
177
+ throw new Error(`Format ${source.format} is only supported by validate_machine.`);
178
+ }
179
+ return source;
180
+ }
181
+ async function callApi(options, pathname, body) {
182
+ const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
183
+ const headers = new Headers(configuredHeaders);
184
+ const authMode = options.authMode ?? "auto";
185
+ if (authMode !== "none" && !headers.has("authorization")) {
186
+ const credential = await options.credentialProvider?.();
187
+ const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
188
+ if (token) headers.set("authorization", `Bearer ${token}`);
189
+ else if (authMode === "bearer") throw new Error("No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.");
190
+ }
191
+ const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
192
+ method: "POST",
193
+ headers: {
194
+ "content-type": "application/json",
195
+ ...Object.fromEntries(headers.entries())
196
+ },
197
+ body: JSON.stringify(body)
198
+ });
199
+ const result = await response.json().catch(() => ({ error: `Invalid JSON response from ${pathname}.` }));
200
+ if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
201
+ return result;
202
+ }
203
+ function registerTool(server, name, config, handler, formatResult = toolResult) {
204
+ server.registerTool(name, {
205
+ ...config,
206
+ annotations: readOnlyAnnotations
207
+ }, async (args) => {
208
+ try {
209
+ return formatResult(await handler(args));
210
+ } catch (error) {
211
+ return toolError({ error: error instanceof Error ? error.message : String(error) });
212
+ }
213
+ });
214
+ }
215
+ function registerStatelyMcpTools(server, options = {}) {
216
+ registerTool(server, "create_state_machine", {
217
+ title: "Create state machine",
218
+ description: "Create a new XState v5 state machine from the user's description. The calling model must author complete XState v5 code and pass it as xstateCode. Return the XState source and, by default, directly viewable PNG diagrams. Prefer visualization because it makes behavior easier to verify; disable it only when the user explicitly wants code only, the client cannot display images, or a diagram adds no useful information.",
219
+ inputSchema: {
220
+ xstateCode: z.string().min(1).describe("Complete XState v5 source with imports and createMachine(...) or setup(...).createMachine(...)."),
221
+ visualization: createMachineVisualizationSchema
222
+ },
223
+ outputSchema: createMachineOutputSchema
224
+ }, async (input) => {
225
+ const machine = xstateMachineResultSchema.parse(await callApi(options, "/v1/convert", {
226
+ source: {
227
+ format: "xstate",
228
+ content: input.xstateCode
229
+ },
230
+ target: { format: "xstate" }
231
+ }));
232
+ return {
233
+ machine,
234
+ visualizations: input.visualization === false ? [] : await callApi(options, "/v1/visualize", {
235
+ source: {
236
+ format: machine.format,
237
+ content: machine.content
238
+ },
239
+ options: {
240
+ direction: input.visualization.direction,
241
+ formats: ["png"],
242
+ parentNodeIds: input.visualization.parentNodeIds
243
+ }
244
+ })
245
+ };
246
+ }, createMachineToolResult);
247
+ registerTool(server, "validate_machine", {
248
+ title: "Validate machine",
249
+ description: "Validate an XState machine config or source string through the Stately API.",
250
+ inputSchema: {
251
+ machine: machineSchema.optional(),
252
+ format: validationFormatSchema.optional(),
253
+ source: sourceSchema.optional()
254
+ }
255
+ }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
256
+ registerTool(server, "convert_machine", {
257
+ title: "Convert machine",
258
+ description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
259
+ inputSchema: {
260
+ machine: machineSchema.optional(),
261
+ format: apiFormatSchema.optional(),
262
+ source: sourceSchema.optional(),
263
+ to: apiFormatSchema
264
+ }
265
+ }, (input) => callApi(options, "/v1/convert", {
266
+ source: toConversionSource(normalizeSource(input)),
267
+ target: { format: input.to }
268
+ }));
269
+ registerTool(server, "diff_machines", {
270
+ title: "Diff machines",
271
+ description: "Compute a semantic diff between two machine documents through the Stately API.",
272
+ inputSchema: {
273
+ base: sourceSchema,
274
+ compare: sourceSchema,
275
+ includePatches: z.boolean().optional()
276
+ }
277
+ }, (input) => callApi(options, "/v1/diff", {
278
+ base: toConversionSource(normalizeSource({ source: input.base })),
279
+ compare: toConversionSource(normalizeSource({ source: input.compare })),
280
+ includePatches: input.includePatches
281
+ }));
282
+ registerTool(server, "apply_machine_patches", {
283
+ title: "Apply machine patches",
284
+ description: "Apply semantic machine patches through the Stately API.",
285
+ inputSchema: {
286
+ machine: machineSchema.optional(),
287
+ format: apiFormatSchema.optional(),
288
+ source: sourceSchema.optional(),
289
+ patches: z.array(patchSchema),
290
+ to: apiFormatSchema.optional()
291
+ }
292
+ }, (input) => callApi(options, "/v1/apply-patches", {
293
+ source: toConversionSource(normalizeSource(input)),
294
+ patches: input.patches,
295
+ target: input.to ? { format: input.to } : void 0
296
+ }));
297
+ registerTool(server, "generate_graph_paths", {
298
+ title: "Generate graph paths",
299
+ description: "Generate shortest or simple paths through the Stately API.",
300
+ inputSchema: {
301
+ machine: machineSchema.optional(),
302
+ format: apiFormatSchema.optional(),
303
+ source: sourceSchema.optional(),
304
+ graph: z.record(z.string(), z.unknown()).optional(),
305
+ kind: z.enum(["shortest", "simple"]).default("shortest"),
306
+ max: z.number().int().positive().optional()
307
+ }
308
+ }, async (input) => {
309
+ return callApi(options, "/v1/graph-paths", {
310
+ graph: input.graph ?? (await callApi(options, "/v1/convert", {
311
+ source: toConversionSource(normalizeSource(input)),
312
+ target: { format: "xgraph" }
313
+ })).content,
314
+ options: {
315
+ strategy: input.kind,
316
+ limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
317
+ }
318
+ });
319
+ });
320
+ registerTool(server, "simulate_machine", {
321
+ title: "Simulate machine",
322
+ description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
323
+ inputSchema: {
324
+ machine: machineSchema.optional(),
325
+ format: apiFormatSchema.optional(),
326
+ source: sourceSchema.optional(),
327
+ events: z.array(z.object({ type: z.string() }).passthrough()).min(1).max(100)
328
+ }
329
+ }, (input) => callApi(options, "/v1/simulate", {
330
+ source: toConversionSource(normalizeSource(input)),
331
+ events: input.events
332
+ }));
333
+ registerTool(server, "visualize_machine", {
334
+ title: "Visualize machine",
335
+ description: "Render machine hierarchy levels as directly viewable PNG images. Optionally select parent node ids.",
336
+ inputSchema: {
337
+ machine: machineSchema.optional(),
338
+ format: apiFormatSchema.optional(),
339
+ source: sourceSchema.optional(),
340
+ direction: z.enum(["right", "down"]).optional(),
341
+ parentNodeIds: z.array(z.string()).min(1).optional()
342
+ }
343
+ }, (input) => {
344
+ return callApi(options, "/v1/visualize", {
345
+ source: toConversionSource(normalizeSource(input)),
346
+ options: {
347
+ direction: input.direction,
348
+ formats: ["png"],
349
+ parentNodeIds: input.parentNodeIds
350
+ }
351
+ }).then((visualizations) => ({ visualizations }));
352
+ }, visualizeToolResult);
353
+ registerTool(server, "analyze_machine", {
354
+ title: "Analyze machine",
355
+ description: "Deep analysis of machine logic (merges deterministic graph checks with AI review); may require an API key — configure via server options; for purely deterministic checks use validate_machine.",
356
+ inputSchema: {
357
+ machine: machineSchema.optional(),
358
+ format: apiFormatSchema.optional(),
359
+ source: sourceSchema.optional(),
360
+ intent: z.string().optional()
361
+ }
362
+ }, (input) => callApi(options, "/v1/analyze", {
363
+ source: toConversionSource(normalizeSource(input)),
364
+ intent: input.intent
365
+ }));
366
+ registerTool(server, "generate_test_code", {
367
+ title: "Generate test code",
368
+ description: "Generate a runnable vitest test file from the machine, one test per generated path. Guards and actions are stubbed; assertions use state paths.",
369
+ inputSchema: {
370
+ machine: machineSchema.optional(),
371
+ format: apiFormatSchema.optional(),
372
+ source: sourceSchema.optional(),
373
+ kind: z.enum(["shortest", "simple"]).default("shortest"),
374
+ max: z.number().int().positive().optional()
375
+ }
376
+ }, async (input) => {
377
+ const source = toConversionSource(normalizeSource(input));
378
+ const graph = (await callApi(options, "/v1/convert", {
379
+ source,
380
+ target: { format: "xgraph" }
381
+ })).content;
382
+ const pathsResponse = await callApi(options, "/v1/graph-paths", {
383
+ graph,
384
+ options: {
385
+ strategy: input.kind,
386
+ limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
387
+ }
388
+ });
389
+ return {
390
+ code: generateTestCode(String((await callApi(options, "/v1/convert", {
391
+ source,
392
+ target: { format: "xstate" }
393
+ })).content), pathsResponse.paths),
394
+ paths: pathsResponse.paths,
395
+ coverage: pathsResponse.coverage
396
+ };
397
+ });
398
+ registerTool(server, "extract_machines_from_code", {
399
+ title: "Extract machines from code",
400
+ description: "Parse XState source code into XGraph through the Stately API.",
401
+ inputSchema: { code: z.string().min(1) }
402
+ }, async (input) => {
403
+ return { machines: [{
404
+ format: "xgraph",
405
+ graph: (await callApi(options, "/v1/convert", {
406
+ source: {
407
+ format: "xstate",
408
+ content: input.code
409
+ },
410
+ target: { format: "xgraph" }
411
+ })).content
412
+ }] };
413
+ });
414
+ return server;
415
+ }
416
+ function createStatelyMcpServer(options = {}) {
417
+ const server = new McpServer({
418
+ name: "stately",
419
+ version: "0.1.0"
420
+ });
421
+ registerStatelyMcpTools(server, options);
422
+ return server;
423
+ }
424
+
425
+ //#endregion
426
+ //#region src/index.ts
427
+ async function loadNode() {
428
+ const [fs, os, path] = await Promise.all([
429
+ import("node:fs/promises"),
430
+ import("node:os"),
431
+ import("node:path")
432
+ ]);
433
+ return {
434
+ fs,
435
+ os,
436
+ path
437
+ };
438
+ }
439
+ function workspaceFromArgv() {
440
+ const index = process.argv.indexOf("--workspace");
441
+ return index >= 0 ? process.argv[index + 1] : process.cwd();
442
+ }
443
+ function isCandidate(pathname) {
444
+ return /\.(?:[cm]?[jt]sx?|json|ya?ml|scxml|mmd|mermaid)$/i.test(pathname);
445
+ }
446
+ function formatForPath(pathname, text) {
447
+ if (/\.(?:mmd|mermaid)$/i.test(pathname)) return "mermaid";
448
+ if (/\.scxml$/i.test(pathname)) return "scxml";
449
+ if (/\.ya?ml$/i.test(pathname)) return "yaml";
450
+ if (/\.json$/i.test(pathname)) return "json";
451
+ if (/^\s*</.test(text)) return "scxml";
452
+ return "xstate";
453
+ }
454
+ async function walkWorkspace(root, glob) {
455
+ const { fs, path } = await loadNode();
456
+ const results = [];
457
+ async function walk(dir) {
458
+ const entries = await fs.readdir(dir, { withFileTypes: true });
459
+ for (const entry of entries) {
460
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
461
+ const full = path.join(dir, entry.name);
462
+ if (entry.isDirectory()) await walk(full);
463
+ else if (isCandidate(full)) {
464
+ const relative = path.relative(root, full);
465
+ if (!glob || relative.includes(glob)) results.push(relative);
466
+ }
467
+ }
468
+ }
469
+ await walk(root);
470
+ return results;
471
+ }
472
+ async function readWorkspaceMachine(input) {
473
+ const { fs, path } = await loadNode();
474
+ const root = workspaceFromArgv();
475
+ const fullPath = path.resolve(root, input.path);
476
+ const relative = path.relative(root, fullPath);
477
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path must stay inside workspace.");
478
+ const sourceText = await fs.readFile(fullPath, "utf8");
479
+ return {
480
+ machine: sourceText,
481
+ format: formatForPath(fullPath, sourceText),
482
+ source: { path: relative },
483
+ ...input.includeSourceText ? { sourceText } : {}
484
+ };
485
+ }
486
+ async function readStoredCliCredential() {
487
+ const { fs, os, path } = await loadNode();
488
+ const configDir = process.env.STATELYAI_CONFIG_DIR ? path.resolve(process.env.STATELYAI_CONFIG_DIR) : process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "statelyai") : process.platform === "win32" ? path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "statelyai") : path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "statelyai");
489
+ try {
490
+ const raw = await fs.readFile(path.join(configDir, "credentials.json"), "utf8");
491
+ const parsed = JSON.parse(raw);
492
+ if (typeof parsed.apiKey === "string" && parsed.apiKey) return {
493
+ type: "api_key",
494
+ token: parsed.apiKey
495
+ };
496
+ if (typeof parsed.credential === "object" && parsed.credential !== null && "type" in parsed.credential) {
497
+ const credential = parsed.credential;
498
+ if (credential.type === "api_key" && typeof credential.token === "string") return {
499
+ type: "api_key",
500
+ token: credential.token
501
+ };
502
+ if (credential.type === "oauth" && typeof credential.accessToken === "string") return {
503
+ type: "oauth",
504
+ accessToken: credential.accessToken,
505
+ ...typeof credential.refreshToken === "string" ? { refreshToken: credential.refreshToken } : {},
506
+ ...typeof credential.expiresAt === "number" ? { expiresAt: credential.expiresAt } : {}
507
+ };
508
+ }
509
+ } catch {
510
+ return;
511
+ }
512
+ }
513
+ async function getDefaultCredential() {
514
+ if (process.env.STATELY_ACCESS_TOKEN) return {
515
+ type: "oauth",
516
+ accessToken: process.env.STATELY_ACCESS_TOKEN
517
+ };
518
+ if (process.env.STATELY_API_KEY) return {
519
+ type: "api_key",
520
+ token: process.env.STATELY_API_KEY
521
+ };
522
+ return readStoredCliCredential();
523
+ }
524
+ function toolText(body) {
525
+ return JSON.stringify(body, null, 2);
526
+ }
527
+ function createStatelyMcpServer$1(options = {}) {
528
+ const server = new McpServer({
529
+ name: "stately",
530
+ version: "0.1.0"
531
+ });
532
+ if (options.includeWorkspaceTools !== false) {
533
+ server.registerTool("list_machines", {
534
+ title: "list_machines",
535
+ description: "List candidate machine sources in the workspace.",
536
+ inputSchema: { glob: z.string().optional() },
537
+ annotations: readOnlyAnnotations
538
+ }, async (request) => {
539
+ const machines = (await walkWorkspace(workspaceFromArgv(), request.glob)).map((pathname) => ({
540
+ source: { path: pathname },
541
+ format: "xstate"
542
+ }));
543
+ return {
544
+ structuredContent: { machines },
545
+ content: [{
546
+ type: "text",
547
+ text: toolText({ machines })
548
+ }]
549
+ };
550
+ });
551
+ server.registerTool("get_machine", {
552
+ title: "get_machine",
553
+ description: "Read a machine source from the workspace and return it.",
554
+ inputSchema: {
555
+ path: z.string(),
556
+ includeSourceText: z.boolean().optional()
557
+ },
558
+ annotations: readOnlyAnnotations
559
+ }, async (request) => {
560
+ const result = await readWorkspaceMachine(request);
561
+ return {
562
+ structuredContent: result,
563
+ content: [{
564
+ type: "text",
565
+ text: toolText(result)
566
+ }]
567
+ };
568
+ });
569
+ }
570
+ registerStatelyMcpTools(server, {
571
+ ...options,
572
+ authMode: options.authMode ?? (process.env.STATELY_AUTH_MODE === "none" ? "none" : void 0),
573
+ credentialProvider: options.credentialProvider ?? getDefaultCredential
574
+ });
575
+ return server;
576
+ }
577
+ async function main() {
578
+ const server = createStatelyMcpServer$1();
579
+ const transport = new StdioServerTransport();
580
+ await server.connect(transport);
581
+ }
582
+ if (import.meta.url === `file://${process.argv[1]}`) main();
583
+
584
+ //#endregion
585
+ export { createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@statelyai/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "license": "UNLICENSED",
5
- "type": "module",
6
- "files": [
7
- "dist"
8
- ],
9
5
  "bin": {
10
6
  "stately-mcp": "dist/index.mjs"
11
7
  },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "type": "module",
12
12
  "main": "./dist/index.cjs",
13
13
  "module": "./dist/index.mjs",
14
14
  "types": "./dist/index.d.mts",
@@ -31,6 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsdown",
33
33
  "test": "vitest run",
34
- "test:watch": "vitest"
34
+ "test:watch": "vitest",
35
+ "verify:package": "node scripts/verify-package.mjs"
35
36
  }
36
37
  }