@statelyai/mcp 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2025 Stately Software, Inc.
2
+
3
+ All rights reserved.
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.
9
+
10
+ For licensing inquiries, contact team@stately.ai.
package/README.md CHANGED
@@ -1,58 +1,207 @@
1
- # @statelyai/mcp
1
+ # Stately MCP
2
2
 
3
- Reusable Stately MCP server for XState machine tooling.
3
+ Use Stately's machine tools from an MCP-compatible agent.
4
4
 
5
- ## Hosted HTTP usage
5
+ ## Connect
6
6
 
7
- <!-- hosted server factory and options from src/server.ts -->
7
+ <!-- public Stately MCP URL -->
8
8
 
9
- ```ts
10
- import { createStatelyMcpServer } from '@statelyai/mcp';
9
+ Add this remote MCP server URL to your client:
11
10
 
12
- const server = createStatelyMcpServer({
13
- includeWorkspaceTools: false,
14
- apiBaseUrl: 'https://editor.stately.ai/api',
15
- credentialProvider: async () => ({
16
- type: 'oauth',
17
- accessToken,
18
- }),
19
- anonymousGraphPathLimit: 5,
20
- });
11
+ ```text
12
+ https://stately.ai/mcp
21
13
  ```
22
14
 
23
- The host owns the transport, auth, CORS, rate limiting, and deployment. This
24
- package owns MCP tool schemas, handlers, annotations, and calls to the Stately
25
- HTTP API. Use `authMode: 'none'` only when the API is protected elsewhere.
15
+ The client will open Stately sign-in when authentication is required.
26
16
 
27
- ## Local stdio usage
17
+ ## Machine input
28
18
 
29
- <!-- stdio CLI bin from package.json and workspace tools from src/index.ts -->
19
+ <!-- shared machine input schemas from src/server.ts -->
30
20
 
31
- ```bash
32
- stately-mcp --workspace /path/to/project
21
+ Most tools accept either `machine` plus `format`, or a `source` object with
22
+ `format` and `content`. Conversion-based tools support `xstate`, `scxml`,
23
+ `xgraph`, and `d2`. Validation also supports `json`, `yaml`, and `mermaid`.
24
+
25
+ The examples below use `<machine source>` as a placeholder for the complete
26
+ machine document supplied by the agent.
27
+
28
+ ## `validate_machine`
29
+
30
+ <!-- validate_machine registration from src/server.ts and validate response contract -->
31
+
32
+ Checks whether a machine can be parsed and reports structural issues such as
33
+ unreachable states. A successful response contains `ok` and an `issues` array.
34
+
35
+ Example prompt: "Check this machine for logic problems."
36
+
37
+ ```json
38
+ {
39
+ "machine": "<machine source>",
40
+ "format": "xstate"
41
+ }
42
+ ```
43
+
44
+ ## `convert_machine`
45
+
46
+ <!-- convert_machine registration from src/server.ts and convert response contract -->
47
+
48
+ Converts a machine between XState, SCXML, XGraph, and D2. The response contains
49
+ the target `format`, converted `content`, and any `warnings`.
50
+
51
+ Example prompt: "Convert this machine to SCXML."
52
+
53
+ ```json
54
+ {
55
+ "machine": "<machine source>",
56
+ "format": "xstate",
57
+ "to": "scxml"
58
+ }
59
+ ```
60
+
61
+ ## `diff_machines`
62
+
63
+ <!-- diff_machines registration from src/server.ts and diff response contract -->
64
+
65
+ Compares two machine documents structurally instead of comparing their source
66
+ text. It returns a summary and semantic diff; set `includePatches` to request
67
+ applicable patches.
68
+
69
+ Example prompt: "What changed between these two machines?"
70
+
71
+ ```json
72
+ {
73
+ "base": { "format": "xstate", "content": "<old machine source>" },
74
+ "compare": { "format": "xstate", "content": "<new machine source>" },
75
+ "includePatches": true
76
+ }
77
+ ```
78
+
79
+ ## `apply_machine_patches`
80
+
81
+ <!-- apply_machine_patches registration from src/server.ts and apply-patches response contract -->
82
+
83
+ Applies semantic patches to a machine and returns the updated document. Use
84
+ `to` to choose the returned format.
85
+
86
+ Example prompt: "Apply these suggested changes and return XState code."
87
+
88
+ ```json
89
+ {
90
+ "machine": "<machine source>",
91
+ "format": "xstate",
92
+ "patches": [
93
+ { "op": "updateState", "stateId": "active", "key": "complete" }
94
+ ],
95
+ "to": "xstate"
96
+ }
97
+ ```
98
+
99
+ ## `generate_graph_paths`
100
+
101
+ <!-- generate_graph_paths registration from src/server.ts and graph-paths response contract -->
102
+
103
+ Generates shortest or simple paths through a machine. The response includes
104
+ the event steps for each path plus state and transition coverage. Stately MCP
105
+ returns at most five paths per call.
106
+
107
+ Example prompt: "Find the shortest paths through this machine."
108
+
109
+ ```json
110
+ {
111
+ "machine": "<machine source>",
112
+ "format": "xstate",
113
+ "kind": "shortest",
114
+ "max": 5
115
+ }
116
+ ```
117
+
118
+ ## `simulate_machine`
119
+
120
+ <!-- simulate_machine registration from src/server.ts and simulate response contract -->
121
+
122
+ Runs up to 100 events through a machine and returns a step-by-step trace of
123
+ states, context, and executed action names. Guards are stubbed during this
124
+ deterministic simulation.
125
+
126
+ Example prompt: "Simulate PLAY followed by STOP."
127
+
128
+ ```json
129
+ {
130
+ "machine": "<machine source>",
131
+ "format": "xstate",
132
+ "events": [{ "type": "PLAY" }, { "type": "STOP" }]
133
+ }
33
134
  ```
34
135
 
35
- Local stdio mode includes workspace tools:
136
+ ## `visualize_machine`
137
+
138
+ <!-- visualize_machine registration from src/server.ts and visualize response contract -->
139
+
140
+ Renders a machine as an SVG diagram. The response contains the SVG string,
141
+ its width and height, and any warnings.
142
+
143
+ Example prompt: "Draw this machine from left to right."
144
+
145
+ ```json
146
+ {
147
+ "machine": "<machine source>",
148
+ "format": "xstate",
149
+ "direction": "right"
150
+ }
151
+ ```
36
152
 
37
- - `list_machines`
38
- - `get_machine`
153
+ ## `analyze_machine`
39
154
 
40
- API-backed tools read `STATELY_ACCESS_TOKEN`, stored CLI credentials, or
41
- `STATELY_API_KEY`. Use `STATELY_AUTH_MODE=none` for no-auth self-hosted APIs.
155
+ <!-- analyze_machine registration from src/server.ts and analyze response contract -->
42
156
 
43
- ## Tools
157
+ Combines deterministic checks with AI review. Provide optional intent to assess
158
+ the machine against its expected behavior. The response contains issues and may
159
+ include suggested fixes. Use `validate_machine` when only deterministic checks
160
+ are needed.
44
161
 
45
- <!-- hosted MCP tool registrations from src/server.ts -->
162
+ Example prompt: "Check that STOP always ends playback permanently."
46
163
 
47
- - `validate_machine`
48
- - `convert_machine`
49
- - `diff_machines`
50
- - `apply_machine_patches`
51
- - `generate_graph_paths`
52
- - `simulate_machine`
53
- - `visualize_machine`
54
- - `analyze_machine`
55
- - `generate_test_code`
56
- - `extract_machines_from_code`
164
+ ```json
165
+ {
166
+ "machine": "<machine source>",
167
+ "format": "xstate",
168
+ "intent": "STOP must reach a final state, and playback must not restart"
169
+ }
170
+ ```
171
+
172
+ ## `generate_test_code`
173
+
174
+ <!-- generate_test_code registration and generateTestCode implementation from src/server.ts -->
175
+
176
+ Generates a runnable Vitest file with one test per generated path. Each test
177
+ sends the path's events and asserts its final state. Guards and actions are
178
+ stubbed.
179
+
180
+ Example prompt: "Generate tests for the shortest paths through this machine."
181
+
182
+ ```json
183
+ {
184
+ "machine": "<machine source>",
185
+ "format": "xstate",
186
+ "kind": "shortest",
187
+ "max": 5
188
+ }
189
+ ```
190
+
191
+ ## `extract_machines_from_code`
192
+
193
+ <!-- extract_machines_from_code registration from src/server.ts -->
194
+
195
+ Parses XState source code and returns the extracted machine as an XGraph
196
+ document.
197
+
198
+ Example prompt: "Extract the XState machine from this TypeScript file."
199
+
200
+ ```json
201
+ {
202
+ "code": "import { createMachine } from 'xstate';\nexport const player = createMachine({ /* complete config */ });"
203
+ }
204
+ ```
57
205
 
58
- All tools are annotated read-only, non-destructive, idempotent, and closed-world.
206
+ All Stately MCP tools are registered as read-only, non-destructive, idempotent,
207
+ and closed-world.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statelyai/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "files": [
@@ -19,11 +19,6 @@
19
19
  "require": "./dist/index.cjs"
20
20
  }
21
21
  },
22
- "scripts": {
23
- "build": "tsdown",
24
- "test": "vitest run",
25
- "test:watch": "vitest"
26
- },
27
22
  "dependencies": {
28
23
  "@modelcontextprotocol/sdk": "1.26.0",
29
24
  "zod": "^4.4.1"
@@ -32,5 +27,10 @@
32
27
  "@types/node": "25.2.1",
33
28
  "tsdown": "0.21.0-beta.2",
34
29
  "vitest": "^3.2.4"
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest"
35
35
  }
36
- }
36
+ }
package/dist/index.cjs DELETED
@@ -1,483 +0,0 @@
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 patchSchema = zod.z.object({
34
- op: zod.z.string(),
35
- path: zod.z.string(),
36
- value: zod.z.unknown().optional()
37
- });
38
- function generateTestCode(machineCode, paths) {
39
- return [
40
- [
41
- "// Generated by generate_test_code (Stately MCP).",
42
- "// Guards and actions are stubbed; assertions use state paths.",
43
- "import { createActor } from 'xstate';",
44
- "import { describe, expect, it } from 'vitest';",
45
- "",
46
- machineCode.trim(),
47
- ""
48
- ].join("\n"),
49
- "describe('generated paths', () => {",
50
- paths.map((path) => {
51
- const events = path.steps.filter((step) => step.eventType !== null && step.eventType !== "" && !step.eventType.startsWith("xstate.")).map((step) => ` actor.send({ type: ${JSON.stringify(step.eventType)} });`);
52
- return [
53
- ` it(${JSON.stringify(path.name)}, () => {`,
54
- " const actor = createActor(machine).start();",
55
- ...events,
56
- ` expect(actor.getSnapshot().matches(${JSON.stringify(path.finalStatePath)})).toBe(true);`,
57
- " });"
58
- ].join("\n");
59
- }).join("\n\n"),
60
- "});",
61
- ""
62
- ].join("\n");
63
- }
64
- function toolText$1(body) {
65
- return JSON.stringify(body, null, 2);
66
- }
67
- function toolResult(body) {
68
- return {
69
- structuredContent: body,
70
- content: [{
71
- type: "text",
72
- text: toolText$1(body)
73
- }]
74
- };
75
- }
76
- function toolError(body) {
77
- return {
78
- isError: true,
79
- ...toolResult(body)
80
- };
81
- }
82
- function normalizeApiBaseUrl(apiBaseUrl) {
83
- return new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/$/, "");
84
- }
85
- function normalizeSource(input) {
86
- const content = input.source?.content ?? input.machine;
87
- if (content === void 0) throw new Error("Expected machine or source.content.");
88
- return {
89
- format: input.source?.format ?? input.format ?? (typeof content === "string" ? "xstate" : "json"),
90
- content
91
- };
92
- }
93
- function toConversionSource(source) {
94
- if (!apiFormats.includes(source.format)) {
95
- if (source.format === "json" && typeof source.content === "object") return {
96
- format: "xstate",
97
- content: `createMachine(${JSON.stringify(source.content, null, 2)})`
98
- };
99
- if (source.format === "json" && typeof source.content === "string") return {
100
- format: "xstate",
101
- content: source.content
102
- };
103
- throw new Error(`Format ${source.format} is only supported by validate_machine.`);
104
- }
105
- return source;
106
- }
107
- async function callApi(options, pathname, body) {
108
- const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
109
- const headers = new Headers(configuredHeaders);
110
- const authMode = options.authMode ?? "auto";
111
- if (authMode !== "none" && !headers.has("authorization")) {
112
- const credential = await options.credentialProvider?.();
113
- const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
114
- if (token) headers.set("authorization", `Bearer ${token}`);
115
- else if (authMode === "bearer") throw new Error("No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.");
116
- }
117
- const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
118
- method: "POST",
119
- headers: {
120
- "content-type": "application/json",
121
- ...Object.fromEntries(headers.entries())
122
- },
123
- body: JSON.stringify(body)
124
- });
125
- const result = await response.json().catch(() => ({ error: `Invalid JSON response from ${pathname}.` }));
126
- if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
127
- return result;
128
- }
129
- function registerTool(server, name, config, handler) {
130
- server.registerTool(name, {
131
- ...config,
132
- annotations: readOnlyAnnotations
133
- }, async (args) => {
134
- try {
135
- return toolResult(await handler(args));
136
- } catch (error) {
137
- return toolError({ error: error instanceof Error ? error.message : String(error) });
138
- }
139
- });
140
- }
141
- function registerStatelyMcpTools(server, options = {}) {
142
- registerTool(server, "validate_machine", {
143
- title: "Validate machine",
144
- description: "Validate an XState machine config or source string through the Stately API.",
145
- inputSchema: {
146
- machine: machineSchema.optional(),
147
- format: validationFormatSchema.optional(),
148
- source: sourceSchema.optional()
149
- }
150
- }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
151
- registerTool(server, "convert_machine", {
152
- title: "Convert machine",
153
- description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
154
- inputSchema: {
155
- machine: machineSchema.optional(),
156
- format: apiFormatSchema.optional(),
157
- source: sourceSchema.optional(),
158
- to: apiFormatSchema
159
- }
160
- }, (input) => callApi(options, "/v1/convert", {
161
- source: toConversionSource(normalizeSource(input)),
162
- target: { format: input.to }
163
- }));
164
- registerTool(server, "diff_machines", {
165
- title: "Diff machines",
166
- description: "Compute a semantic diff between two machine documents through the Stately API.",
167
- inputSchema: {
168
- base: sourceSchema,
169
- compare: sourceSchema,
170
- includePatches: zod.z.boolean().optional()
171
- }
172
- }, (input) => callApi(options, "/v1/diff", {
173
- base: toConversionSource(normalizeSource({ source: input.base })),
174
- compare: toConversionSource(normalizeSource({ source: input.compare })),
175
- includePatches: input.includePatches
176
- }));
177
- registerTool(server, "apply_machine_patches", {
178
- title: "Apply machine patches",
179
- description: "Apply semantic machine patches through the Stately API.",
180
- inputSchema: {
181
- machine: machineSchema.optional(),
182
- format: apiFormatSchema.optional(),
183
- source: sourceSchema.optional(),
184
- patches: zod.z.array(patchSchema),
185
- to: apiFormatSchema.optional()
186
- }
187
- }, (input) => callApi(options, "/v1/apply-patches", {
188
- source: toConversionSource(normalizeSource(input)),
189
- patches: input.patches,
190
- target: input.to ? { format: input.to } : void 0
191
- }));
192
- registerTool(server, "generate_graph_paths", {
193
- title: "Generate graph paths",
194
- description: "Generate shortest or simple paths through the Stately API.",
195
- inputSchema: {
196
- machine: machineSchema.optional(),
197
- format: apiFormatSchema.optional(),
198
- source: sourceSchema.optional(),
199
- graph: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
200
- kind: zod.z.enum(["shortest", "simple"]).default("shortest"),
201
- max: zod.z.number().int().positive().optional()
202
- }
203
- }, async (input) => {
204
- return callApi(options, "/v1/graph-paths", {
205
- graph: input.graph ?? (await callApi(options, "/v1/convert", {
206
- source: toConversionSource(normalizeSource(input)),
207
- target: { format: "xgraph" }
208
- })).content,
209
- options: {
210
- strategy: input.kind,
211
- limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
212
- }
213
- });
214
- });
215
- registerTool(server, "simulate_machine", {
216
- title: "Simulate machine",
217
- description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
218
- inputSchema: {
219
- machine: machineSchema.optional(),
220
- format: apiFormatSchema.optional(),
221
- source: sourceSchema.optional(),
222
- events: zod.z.array(zod.z.object({ type: zod.z.string() }).passthrough()).min(1).max(100)
223
- }
224
- }, (input) => callApi(options, "/v1/simulate", {
225
- source: toConversionSource(normalizeSource(input)),
226
- events: input.events
227
- }));
228
- registerTool(server, "visualize_machine", {
229
- title: "Visualize machine",
230
- description: "Render the machine as an SVG diagram (returns svg string + dimensions).",
231
- inputSchema: {
232
- machine: machineSchema.optional(),
233
- format: apiFormatSchema.optional(),
234
- source: sourceSchema.optional(),
235
- direction: zod.z.enum(["right", "down"]).optional()
236
- }
237
- }, (input) => callApi(options, "/v1/visualize", {
238
- source: toConversionSource(normalizeSource(input)),
239
- options: input.direction ? { direction: input.direction } : void 0
240
- }));
241
- registerTool(server, "analyze_machine", {
242
- title: "Analyze machine",
243
- 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.",
244
- inputSchema: {
245
- machine: machineSchema.optional(),
246
- format: apiFormatSchema.optional(),
247
- source: sourceSchema.optional(),
248
- intent: zod.z.string().optional()
249
- }
250
- }, (input) => callApi(options, "/v1/analyze", {
251
- source: toConversionSource(normalizeSource(input)),
252
- intent: input.intent
253
- }));
254
- registerTool(server, "generate_test_code", {
255
- title: "Generate test code",
256
- description: "Generate a runnable vitest test file from the machine, one test per generated path. Guards and actions are stubbed; assertions use state paths.",
257
- inputSchema: {
258
- machine: machineSchema.optional(),
259
- format: apiFormatSchema.optional(),
260
- source: sourceSchema.optional(),
261
- kind: zod.z.enum(["shortest", "simple"]).default("shortest"),
262
- max: zod.z.number().int().positive().optional()
263
- }
264
- }, async (input) => {
265
- const source = toConversionSource(normalizeSource(input));
266
- const graph = (await callApi(options, "/v1/convert", {
267
- source,
268
- target: { format: "xgraph" }
269
- })).content;
270
- const pathsResponse = await callApi(options, "/v1/graph-paths", {
271
- graph,
272
- options: {
273
- strategy: input.kind,
274
- limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
275
- }
276
- });
277
- return {
278
- code: generateTestCode(String((await callApi(options, "/v1/convert", {
279
- source,
280
- target: { format: "xstate" }
281
- })).content), pathsResponse.paths),
282
- paths: pathsResponse.paths,
283
- coverage: pathsResponse.coverage
284
- };
285
- });
286
- registerTool(server, "extract_machines_from_code", {
287
- title: "Extract machines from code",
288
- description: "Parse XState source code into XGraph through the Stately API.",
289
- inputSchema: { code: zod.z.string().min(1) }
290
- }, async (input) => {
291
- return { machines: [{
292
- format: "xgraph",
293
- graph: (await callApi(options, "/v1/convert", {
294
- source: {
295
- format: "xstate",
296
- content: input.code
297
- },
298
- target: { format: "xgraph" }
299
- })).content
300
- }] };
301
- });
302
- return server;
303
- }
304
- function createStatelyMcpServer(options = {}) {
305
- const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
306
- name: "stately",
307
- version: "0.1.0"
308
- });
309
- registerStatelyMcpTools(server, options);
310
- return server;
311
- }
312
-
313
- //#endregion
314
- //#region src/index.ts
315
- async function loadNode() {
316
- const [fs, os, path] = await Promise.all([
317
- import("node:fs/promises"),
318
- import("node:os"),
319
- import("node:path")
320
- ]);
321
- return {
322
- fs,
323
- os,
324
- path
325
- };
326
- }
327
- function workspaceFromArgv() {
328
- const index = process.argv.indexOf("--workspace");
329
- return index >= 0 ? process.argv[index + 1] : process.cwd();
330
- }
331
- function isCandidate(pathname) {
332
- return /\.(?:[cm]?[jt]sx?|json|ya?ml|scxml|mmd|mermaid)$/i.test(pathname);
333
- }
334
- function formatForPath(pathname, text) {
335
- if (/\.(?:mmd|mermaid)$/i.test(pathname)) return "mermaid";
336
- if (/\.scxml$/i.test(pathname)) return "scxml";
337
- if (/\.ya?ml$/i.test(pathname)) return "yaml";
338
- if (/\.json$/i.test(pathname)) return "json";
339
- if (/^\s*</.test(text)) return "scxml";
340
- return "xstate";
341
- }
342
- async function walkWorkspace(root, glob) {
343
- const { fs, path } = await loadNode();
344
- const results = [];
345
- async function walk(dir) {
346
- const entries = await fs.readdir(dir, { withFileTypes: true });
347
- for (const entry of entries) {
348
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
349
- const full = path.join(dir, entry.name);
350
- if (entry.isDirectory()) await walk(full);
351
- else if (isCandidate(full)) {
352
- const relative = path.relative(root, full);
353
- if (!glob || relative.includes(glob)) results.push(relative);
354
- }
355
- }
356
- }
357
- await walk(root);
358
- return results;
359
- }
360
- async function readWorkspaceMachine(input) {
361
- const { fs, path } = await loadNode();
362
- const root = workspaceFromArgv();
363
- const fullPath = path.resolve(root, input.path);
364
- const relative = path.relative(root, fullPath);
365
- if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path must stay inside workspace.");
366
- const sourceText = await fs.readFile(fullPath, "utf8");
367
- return {
368
- machine: sourceText,
369
- format: formatForPath(fullPath, sourceText),
370
- source: { path: relative },
371
- ...input.includeSourceText ? { sourceText } : {}
372
- };
373
- }
374
- async function readStoredCliCredential() {
375
- const { fs, os, path } = await loadNode();
376
- 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");
377
- try {
378
- const raw = await fs.readFile(path.join(configDir, "credentials.json"), "utf8");
379
- const parsed = JSON.parse(raw);
380
- if (typeof parsed.apiKey === "string" && parsed.apiKey) return {
381
- type: "api_key",
382
- token: parsed.apiKey
383
- };
384
- if (typeof parsed.credential === "object" && parsed.credential !== null && "type" in parsed.credential) {
385
- const credential = parsed.credential;
386
- if (credential.type === "api_key" && typeof credential.token === "string") return {
387
- type: "api_key",
388
- token: credential.token
389
- };
390
- if (credential.type === "oauth" && typeof credential.accessToken === "string") return {
391
- type: "oauth",
392
- accessToken: credential.accessToken,
393
- ...typeof credential.refreshToken === "string" ? { refreshToken: credential.refreshToken } : {},
394
- ...typeof credential.expiresAt === "number" ? { expiresAt: credential.expiresAt } : {}
395
- };
396
- }
397
- } catch {
398
- return;
399
- }
400
- }
401
- async function getDefaultCredential() {
402
- if (process.env.STATELY_ACCESS_TOKEN) return {
403
- type: "oauth",
404
- accessToken: process.env.STATELY_ACCESS_TOKEN
405
- };
406
- if (process.env.STATELY_API_KEY) return {
407
- type: "api_key",
408
- token: process.env.STATELY_API_KEY
409
- };
410
- return readStoredCliCredential();
411
- }
412
- function toolText(body) {
413
- return JSON.stringify(body, null, 2);
414
- }
415
- function createStatelyMcpServer$1(options = {}) {
416
- const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
417
- name: "stately",
418
- version: "0.1.0"
419
- });
420
- if (options.includeWorkspaceTools !== false) {
421
- server.registerTool("list_machines", {
422
- title: "list_machines",
423
- description: "List candidate machine sources in the workspace.",
424
- inputSchema: { glob: zod.z.string().optional() },
425
- annotations: readOnlyAnnotations
426
- }, async (request) => {
427
- const machines = (await walkWorkspace(workspaceFromArgv(), request.glob)).map((pathname) => ({
428
- source: { path: pathname },
429
- format: "xstate"
430
- }));
431
- return {
432
- structuredContent: { machines },
433
- content: [{
434
- type: "text",
435
- text: toolText({ machines })
436
- }]
437
- };
438
- });
439
- server.registerTool("get_machine", {
440
- title: "get_machine",
441
- description: "Read a machine source from the workspace and return it.",
442
- inputSchema: {
443
- path: zod.z.string(),
444
- includeSourceText: zod.z.boolean().optional()
445
- },
446
- annotations: readOnlyAnnotations
447
- }, async (request) => {
448
- const result = await readWorkspaceMachine(request);
449
- return {
450
- structuredContent: result,
451
- content: [{
452
- type: "text",
453
- text: toolText(result)
454
- }]
455
- };
456
- });
457
- }
458
- registerStatelyMcpTools(server, {
459
- ...options,
460
- authMode: options.authMode ?? (process.env.STATELY_AUTH_MODE === "none" ? "none" : void 0),
461
- credentialProvider: options.credentialProvider ?? getDefaultCredential
462
- });
463
- return server;
464
- }
465
- async function main() {
466
- const server = createStatelyMcpServer$1();
467
- const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
468
- await server.connect(transport);
469
- }
470
- if (require("url").pathToFileURL(__filename).href === `file://${process.argv[1]}`) main();
471
-
472
- //#endregion
473
- exports.createHostedStatelyMcpServer = createStatelyMcpServer;
474
- exports.createStatelyMcpServer = createStatelyMcpServer$1;
475
- exports.formatForPath = formatForPath;
476
- exports.generateTestCode = generateTestCode;
477
- exports.getDefaultCredential = getDefaultCredential;
478
- exports.isCandidate = isCandidate;
479
- exports.readOnlyAnnotations = readOnlyAnnotations;
480
- exports.readWorkspaceMachine = readWorkspaceMachine;
481
- exports.registerStatelyMcpTools = registerStatelyMcpTools;
482
- exports.walkWorkspace = walkWorkspace;
483
- exports.workspaceFromArgv = workspaceFromArgv;
package/dist/index.d.cts DELETED
@@ -1,72 +0,0 @@
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 DELETED
@@ -1,72 +0,0 @@
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 DELETED
@@ -1,472 +0,0 @@
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 patchSchema = z.object({
33
- op: z.string(),
34
- path: z.string(),
35
- value: z.unknown().optional()
36
- });
37
- function generateTestCode(machineCode, paths) {
38
- return [
39
- [
40
- "// Generated by generate_test_code (Stately MCP).",
41
- "// Guards and actions are stubbed; assertions use state paths.",
42
- "import { createActor } from 'xstate';",
43
- "import { describe, expect, it } from 'vitest';",
44
- "",
45
- machineCode.trim(),
46
- ""
47
- ].join("\n"),
48
- "describe('generated paths', () => {",
49
- paths.map((path) => {
50
- const events = path.steps.filter((step) => step.eventType !== null && step.eventType !== "" && !step.eventType.startsWith("xstate.")).map((step) => ` actor.send({ type: ${JSON.stringify(step.eventType)} });`);
51
- return [
52
- ` it(${JSON.stringify(path.name)}, () => {`,
53
- " const actor = createActor(machine).start();",
54
- ...events,
55
- ` expect(actor.getSnapshot().matches(${JSON.stringify(path.finalStatePath)})).toBe(true);`,
56
- " });"
57
- ].join("\n");
58
- }).join("\n\n"),
59
- "});",
60
- ""
61
- ].join("\n");
62
- }
63
- function toolText$1(body) {
64
- return JSON.stringify(body, null, 2);
65
- }
66
- function toolResult(body) {
67
- return {
68
- structuredContent: body,
69
- content: [{
70
- type: "text",
71
- text: toolText$1(body)
72
- }]
73
- };
74
- }
75
- function toolError(body) {
76
- return {
77
- isError: true,
78
- ...toolResult(body)
79
- };
80
- }
81
- function normalizeApiBaseUrl(apiBaseUrl) {
82
- return new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/$/, "");
83
- }
84
- function normalizeSource(input) {
85
- const content = input.source?.content ?? input.machine;
86
- if (content === void 0) throw new Error("Expected machine or source.content.");
87
- return {
88
- format: input.source?.format ?? input.format ?? (typeof content === "string" ? "xstate" : "json"),
89
- content
90
- };
91
- }
92
- function toConversionSource(source) {
93
- if (!apiFormats.includes(source.format)) {
94
- if (source.format === "json" && typeof source.content === "object") return {
95
- format: "xstate",
96
- content: `createMachine(${JSON.stringify(source.content, null, 2)})`
97
- };
98
- if (source.format === "json" && typeof source.content === "string") return {
99
- format: "xstate",
100
- content: source.content
101
- };
102
- throw new Error(`Format ${source.format} is only supported by validate_machine.`);
103
- }
104
- return source;
105
- }
106
- async function callApi(options, pathname, body) {
107
- const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
108
- const headers = new Headers(configuredHeaders);
109
- const authMode = options.authMode ?? "auto";
110
- if (authMode !== "none" && !headers.has("authorization")) {
111
- const credential = await options.credentialProvider?.();
112
- const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
113
- if (token) headers.set("authorization", `Bearer ${token}`);
114
- else if (authMode === "bearer") throw new Error("No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.");
115
- }
116
- const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
117
- method: "POST",
118
- headers: {
119
- "content-type": "application/json",
120
- ...Object.fromEntries(headers.entries())
121
- },
122
- body: JSON.stringify(body)
123
- });
124
- const result = await response.json().catch(() => ({ error: `Invalid JSON response from ${pathname}.` }));
125
- if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
126
- return result;
127
- }
128
- function registerTool(server, name, config, handler) {
129
- server.registerTool(name, {
130
- ...config,
131
- annotations: readOnlyAnnotations
132
- }, async (args) => {
133
- try {
134
- return toolResult(await handler(args));
135
- } catch (error) {
136
- return toolError({ error: error instanceof Error ? error.message : String(error) });
137
- }
138
- });
139
- }
140
- function registerStatelyMcpTools(server, options = {}) {
141
- registerTool(server, "validate_machine", {
142
- title: "Validate machine",
143
- description: "Validate an XState machine config or source string through the Stately API.",
144
- inputSchema: {
145
- machine: machineSchema.optional(),
146
- format: validationFormatSchema.optional(),
147
- source: sourceSchema.optional()
148
- }
149
- }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
150
- registerTool(server, "convert_machine", {
151
- title: "Convert machine",
152
- description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
153
- inputSchema: {
154
- machine: machineSchema.optional(),
155
- format: apiFormatSchema.optional(),
156
- source: sourceSchema.optional(),
157
- to: apiFormatSchema
158
- }
159
- }, (input) => callApi(options, "/v1/convert", {
160
- source: toConversionSource(normalizeSource(input)),
161
- target: { format: input.to }
162
- }));
163
- registerTool(server, "diff_machines", {
164
- title: "Diff machines",
165
- description: "Compute a semantic diff between two machine documents through the Stately API.",
166
- inputSchema: {
167
- base: sourceSchema,
168
- compare: sourceSchema,
169
- includePatches: z.boolean().optional()
170
- }
171
- }, (input) => callApi(options, "/v1/diff", {
172
- base: toConversionSource(normalizeSource({ source: input.base })),
173
- compare: toConversionSource(normalizeSource({ source: input.compare })),
174
- includePatches: input.includePatches
175
- }));
176
- registerTool(server, "apply_machine_patches", {
177
- title: "Apply machine patches",
178
- description: "Apply semantic machine patches through the Stately API.",
179
- inputSchema: {
180
- machine: machineSchema.optional(),
181
- format: apiFormatSchema.optional(),
182
- source: sourceSchema.optional(),
183
- patches: z.array(patchSchema),
184
- to: apiFormatSchema.optional()
185
- }
186
- }, (input) => callApi(options, "/v1/apply-patches", {
187
- source: toConversionSource(normalizeSource(input)),
188
- patches: input.patches,
189
- target: input.to ? { format: input.to } : void 0
190
- }));
191
- registerTool(server, "generate_graph_paths", {
192
- title: "Generate graph paths",
193
- description: "Generate shortest or simple paths through the Stately API.",
194
- inputSchema: {
195
- machine: machineSchema.optional(),
196
- format: apiFormatSchema.optional(),
197
- source: sourceSchema.optional(),
198
- graph: z.record(z.string(), z.unknown()).optional(),
199
- kind: z.enum(["shortest", "simple"]).default("shortest"),
200
- max: z.number().int().positive().optional()
201
- }
202
- }, async (input) => {
203
- return callApi(options, "/v1/graph-paths", {
204
- graph: input.graph ?? (await callApi(options, "/v1/convert", {
205
- source: toConversionSource(normalizeSource(input)),
206
- target: { format: "xgraph" }
207
- })).content,
208
- options: {
209
- strategy: input.kind,
210
- limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
211
- }
212
- });
213
- });
214
- registerTool(server, "simulate_machine", {
215
- title: "Simulate machine",
216
- description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
217
- inputSchema: {
218
- machine: machineSchema.optional(),
219
- format: apiFormatSchema.optional(),
220
- source: sourceSchema.optional(),
221
- events: z.array(z.object({ type: z.string() }).passthrough()).min(1).max(100)
222
- }
223
- }, (input) => callApi(options, "/v1/simulate", {
224
- source: toConversionSource(normalizeSource(input)),
225
- events: input.events
226
- }));
227
- registerTool(server, "visualize_machine", {
228
- title: "Visualize machine",
229
- description: "Render the machine as an SVG diagram (returns svg string + dimensions).",
230
- inputSchema: {
231
- machine: machineSchema.optional(),
232
- format: apiFormatSchema.optional(),
233
- source: sourceSchema.optional(),
234
- direction: z.enum(["right", "down"]).optional()
235
- }
236
- }, (input) => callApi(options, "/v1/visualize", {
237
- source: toConversionSource(normalizeSource(input)),
238
- options: input.direction ? { direction: input.direction } : void 0
239
- }));
240
- registerTool(server, "analyze_machine", {
241
- title: "Analyze machine",
242
- 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.",
243
- inputSchema: {
244
- machine: machineSchema.optional(),
245
- format: apiFormatSchema.optional(),
246
- source: sourceSchema.optional(),
247
- intent: z.string().optional()
248
- }
249
- }, (input) => callApi(options, "/v1/analyze", {
250
- source: toConversionSource(normalizeSource(input)),
251
- intent: input.intent
252
- }));
253
- registerTool(server, "generate_test_code", {
254
- title: "Generate test code",
255
- description: "Generate a runnable vitest test file from the machine, one test per generated path. Guards and actions are stubbed; assertions use state paths.",
256
- inputSchema: {
257
- machine: machineSchema.optional(),
258
- format: apiFormatSchema.optional(),
259
- source: sourceSchema.optional(),
260
- kind: z.enum(["shortest", "simple"]).default("shortest"),
261
- max: z.number().int().positive().optional()
262
- }
263
- }, async (input) => {
264
- const source = toConversionSource(normalizeSource(input));
265
- const graph = (await callApi(options, "/v1/convert", {
266
- source,
267
- target: { format: "xgraph" }
268
- })).content;
269
- const pathsResponse = await callApi(options, "/v1/graph-paths", {
270
- graph,
271
- options: {
272
- strategy: input.kind,
273
- limit: Math.min(input.max ?? 5, options.anonymousGraphPathLimit ?? 5)
274
- }
275
- });
276
- return {
277
- code: generateTestCode(String((await callApi(options, "/v1/convert", {
278
- source,
279
- target: { format: "xstate" }
280
- })).content), pathsResponse.paths),
281
- paths: pathsResponse.paths,
282
- coverage: pathsResponse.coverage
283
- };
284
- });
285
- registerTool(server, "extract_machines_from_code", {
286
- title: "Extract machines from code",
287
- description: "Parse XState source code into XGraph through the Stately API.",
288
- inputSchema: { code: z.string().min(1) }
289
- }, async (input) => {
290
- return { machines: [{
291
- format: "xgraph",
292
- graph: (await callApi(options, "/v1/convert", {
293
- source: {
294
- format: "xstate",
295
- content: input.code
296
- },
297
- target: { format: "xgraph" }
298
- })).content
299
- }] };
300
- });
301
- return server;
302
- }
303
- function createStatelyMcpServer(options = {}) {
304
- const server = new McpServer({
305
- name: "stately",
306
- version: "0.1.0"
307
- });
308
- registerStatelyMcpTools(server, options);
309
- return server;
310
- }
311
-
312
- //#endregion
313
- //#region src/index.ts
314
- async function loadNode() {
315
- const [fs, os, path] = await Promise.all([
316
- import("node:fs/promises"),
317
- import("node:os"),
318
- import("node:path")
319
- ]);
320
- return {
321
- fs,
322
- os,
323
- path
324
- };
325
- }
326
- function workspaceFromArgv() {
327
- const index = process.argv.indexOf("--workspace");
328
- return index >= 0 ? process.argv[index + 1] : process.cwd();
329
- }
330
- function isCandidate(pathname) {
331
- return /\.(?:[cm]?[jt]sx?|json|ya?ml|scxml|mmd|mermaid)$/i.test(pathname);
332
- }
333
- function formatForPath(pathname, text) {
334
- if (/\.(?:mmd|mermaid)$/i.test(pathname)) return "mermaid";
335
- if (/\.scxml$/i.test(pathname)) return "scxml";
336
- if (/\.ya?ml$/i.test(pathname)) return "yaml";
337
- if (/\.json$/i.test(pathname)) return "json";
338
- if (/^\s*</.test(text)) return "scxml";
339
- return "xstate";
340
- }
341
- async function walkWorkspace(root, glob) {
342
- const { fs, path } = await loadNode();
343
- const results = [];
344
- async function walk(dir) {
345
- const entries = await fs.readdir(dir, { withFileTypes: true });
346
- for (const entry of entries) {
347
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
348
- const full = path.join(dir, entry.name);
349
- if (entry.isDirectory()) await walk(full);
350
- else if (isCandidate(full)) {
351
- const relative = path.relative(root, full);
352
- if (!glob || relative.includes(glob)) results.push(relative);
353
- }
354
- }
355
- }
356
- await walk(root);
357
- return results;
358
- }
359
- async function readWorkspaceMachine(input) {
360
- const { fs, path } = await loadNode();
361
- const root = workspaceFromArgv();
362
- const fullPath = path.resolve(root, input.path);
363
- const relative = path.relative(root, fullPath);
364
- if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("Path must stay inside workspace.");
365
- const sourceText = await fs.readFile(fullPath, "utf8");
366
- return {
367
- machine: sourceText,
368
- format: formatForPath(fullPath, sourceText),
369
- source: { path: relative },
370
- ...input.includeSourceText ? { sourceText } : {}
371
- };
372
- }
373
- async function readStoredCliCredential() {
374
- const { fs, os, path } = await loadNode();
375
- 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");
376
- try {
377
- const raw = await fs.readFile(path.join(configDir, "credentials.json"), "utf8");
378
- const parsed = JSON.parse(raw);
379
- if (typeof parsed.apiKey === "string" && parsed.apiKey) return {
380
- type: "api_key",
381
- token: parsed.apiKey
382
- };
383
- if (typeof parsed.credential === "object" && parsed.credential !== null && "type" in parsed.credential) {
384
- const credential = parsed.credential;
385
- if (credential.type === "api_key" && typeof credential.token === "string") return {
386
- type: "api_key",
387
- token: credential.token
388
- };
389
- if (credential.type === "oauth" && typeof credential.accessToken === "string") return {
390
- type: "oauth",
391
- accessToken: credential.accessToken,
392
- ...typeof credential.refreshToken === "string" ? { refreshToken: credential.refreshToken } : {},
393
- ...typeof credential.expiresAt === "number" ? { expiresAt: credential.expiresAt } : {}
394
- };
395
- }
396
- } catch {
397
- return;
398
- }
399
- }
400
- async function getDefaultCredential() {
401
- if (process.env.STATELY_ACCESS_TOKEN) return {
402
- type: "oauth",
403
- accessToken: process.env.STATELY_ACCESS_TOKEN
404
- };
405
- if (process.env.STATELY_API_KEY) return {
406
- type: "api_key",
407
- token: process.env.STATELY_API_KEY
408
- };
409
- return readStoredCliCredential();
410
- }
411
- function toolText(body) {
412
- return JSON.stringify(body, null, 2);
413
- }
414
- function createStatelyMcpServer$1(options = {}) {
415
- const server = new McpServer({
416
- name: "stately",
417
- version: "0.1.0"
418
- });
419
- if (options.includeWorkspaceTools !== false) {
420
- server.registerTool("list_machines", {
421
- title: "list_machines",
422
- description: "List candidate machine sources in the workspace.",
423
- inputSchema: { glob: z.string().optional() },
424
- annotations: readOnlyAnnotations
425
- }, async (request) => {
426
- const machines = (await walkWorkspace(workspaceFromArgv(), request.glob)).map((pathname) => ({
427
- source: { path: pathname },
428
- format: "xstate"
429
- }));
430
- return {
431
- structuredContent: { machines },
432
- content: [{
433
- type: "text",
434
- text: toolText({ machines })
435
- }]
436
- };
437
- });
438
- server.registerTool("get_machine", {
439
- title: "get_machine",
440
- description: "Read a machine source from the workspace and return it.",
441
- inputSchema: {
442
- path: z.string(),
443
- includeSourceText: z.boolean().optional()
444
- },
445
- annotations: readOnlyAnnotations
446
- }, async (request) => {
447
- const result = await readWorkspaceMachine(request);
448
- return {
449
- structuredContent: result,
450
- content: [{
451
- type: "text",
452
- text: toolText(result)
453
- }]
454
- };
455
- });
456
- }
457
- registerStatelyMcpTools(server, {
458
- ...options,
459
- authMode: options.authMode ?? (process.env.STATELY_AUTH_MODE === "none" ? "none" : void 0),
460
- credentialProvider: options.credentialProvider ?? getDefaultCredential
461
- });
462
- return server;
463
- }
464
- async function main() {
465
- const server = createStatelyMcpServer$1();
466
- const transport = new StdioServerTransport();
467
- await server.connect(transport);
468
- }
469
- if (import.meta.url === `file://${process.argv[1]}`) main();
470
-
471
- //#endregion
472
- export { createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };