@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 +18 -7
- package/dist/index.cjs +596 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.mjs +585 -0
- package/package.json +8 -7
package/LICENSE
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Copyright (c) 2026 Stately Software, Inc.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.mts
ADDED
|
@@ -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.
|
|
4
|
-
"license": "
|
|
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
|
}
|