@statelyai/mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,10 +1,21 @@
1
- Copyright (c) 2025 Stately Software, Inc.
1
+ MIT License
2
2
 
3
- All rights reserved.
3
+ Copyright (c) 2026 Stately Software, Inc.
4
4
 
5
- This repository and the Stately Viz software are proprietary to Stately
6
- Software, Inc. No rights are granted to use, copy, modify, distribute, host, or
7
- sublicense this software except under a separate written agreement with Stately
8
- Software, Inc.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
9
11
 
10
- For licensing inquiries, contact team@stately.ai.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.3.0",
4
- "license": "UNLICENSED",
5
- "type": "module",
6
- "files": [
7
- "dist"
8
- ],
3
+ "version": "0.3.2",
4
+ "license": "MIT",
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
  }