@statelyai/mcp 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3,6 +3,472 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
 
6
+ //#region ../api-contract/dist/errors.mjs
7
+ /**
8
+ * Error shape returned by every `/api/v1` machine operation (and by the shared
9
+ * auth guard). Each operation registers its own error schema component in the
10
+ * OpenAPI document, so this is a factory rather than a single shared instance.
11
+ */
12
+ function createApiErrorResponseSchema() {
13
+ return z.object({
14
+ error: z.string(),
15
+ issues: z.array(z.unknown()).optional()
16
+ });
17
+ }
18
+ const apiErrorResponseSchema = createApiErrorResponseSchema();
19
+ /**
20
+ * Canonical error codes for the `/api/v1` resource operations. Machine
21
+ * operations keep the flat `{ error: string }` shape above unchanged; the
22
+ * nested envelope below applies to resource operations only.
23
+ */
24
+ const resourceErrorCodeSchema = z.enum([
25
+ "bad_request",
26
+ "unauthenticated",
27
+ "forbidden",
28
+ "not_found",
29
+ "conflict",
30
+ "payload_too_large",
31
+ "rate_limited",
32
+ "capability_disabled",
33
+ "upstream_error",
34
+ "internal_error"
35
+ ]);
36
+ /** Factory, matching `createApiErrorResponseSchema`, for per-route OpenAPI components. */
37
+ function createResourceApiErrorResponseSchema() {
38
+ return z.object({ error: z.object({
39
+ code: resourceErrorCodeSchema,
40
+ message: z.string(),
41
+ issues: z.array(z.unknown()).optional(),
42
+ retryable: z.boolean().optional()
43
+ }) });
44
+ }
45
+ const resourceApiErrorResponseSchema = createResourceApiErrorResponseSchema();
46
+
47
+ //#endregion
48
+ //#region ../api-contract/dist/resources.mjs
49
+ /**
50
+ * Canonical schemas for persistent `/api/v1` resources plus the owned,
51
+ * deterministic code-extraction operation.
52
+ *
53
+ * These models are canonical, not upstream-shaped: providers (e.g. the Stately
54
+ * Registry adapter) are responsible for translating upstream envelopes,
55
+ * identifier quirks and casing into these shapes. No `{ data }` envelope
56
+ * appears anywhere; all timestamps are ISO strings.
57
+ */
58
+ /**
59
+ * XState major versions the editor targets (see
60
+ * `packages/editor/src/formats/xstate.ts`).
61
+ */
62
+ const xstateVersionSchema = z.union([z.literal(5), z.literal(6)]);
63
+ const projectVisibilitySchema = z.enum([
64
+ "public",
65
+ "unlisted",
66
+ "private"
67
+ ]);
68
+ const verifyResultSchema = z.object({
69
+ valid: z.literal(true),
70
+ tokenScope: z.enum(["user", "project"])
71
+ });
72
+ const projectSummarySchema = z.object({
73
+ projectId: z.string(),
74
+ projectVersionId: z.string(),
75
+ name: z.string(),
76
+ description: z.string().nullable(),
77
+ visibility: projectVisibilitySchema,
78
+ createdAt: z.string(),
79
+ updatedAt: z.string()
80
+ });
81
+ const projectSchema = projectSummarySchema.extend({
82
+ rootMachineId: z.string().nullable(),
83
+ machines: z.array(z.object({ machineId: z.string() }))
84
+ });
85
+ /**
86
+ * `name` is optional: the Registry project envelope only returns machine ids.
87
+ * Providers that can supply names cheaply should.
88
+ */
89
+ const projectMachineSummarySchema = z.object({
90
+ machineId: z.string(),
91
+ name: z.string().optional()
92
+ });
93
+ const machineSchema$1 = z.object({
94
+ machineId: z.string(),
95
+ name: z.string(),
96
+ projectId: z.string().optional(),
97
+ projectVersionId: z.string(),
98
+ definition: z.unknown(),
99
+ createdAt: z.string().optional(),
100
+ updatedAt: z.string().optional()
101
+ });
102
+ const machineVersionSchema = z.object({
103
+ machineVersionId: z.string(),
104
+ machineId: z.string(),
105
+ versionNumber: z.number(),
106
+ name: z.string(),
107
+ definition: z.unknown(),
108
+ createdAt: z.string(),
109
+ updatedAt: z.string()
110
+ });
111
+ const extractedMachinesSchema = z.object({ machines: z.array(z.object({ config: z.record(z.string(), z.unknown()) })) });
112
+ const projectsCreateRequestSchema = z.object({
113
+ name: z.string().min(1),
114
+ visibility: projectVisibilitySchema
115
+ });
116
+ const projectsUpdateRequestSchema = z.object({
117
+ name: z.string().min(1).optional(),
118
+ description: z.string().nullable().optional(),
119
+ visibility: projectVisibilitySchema.optional()
120
+ }).refine((value) => value.name !== void 0 || value.description !== void 0 || value.visibility !== void 0, { message: "At least one of `name`, `description` or `visibility` is required." });
121
+ const machinesCreateRequestSchema = z.object({
122
+ projectVersionId: z.string().min(1),
123
+ xstateVersion: xstateVersionSchema,
124
+ name: z.string().min(1).optional(),
125
+ definition: z.unknown().optional()
126
+ });
127
+ const machinesUpdateRequestSchema = z.object({
128
+ name: z.string().min(1).optional(),
129
+ definition: z.unknown().optional()
130
+ }).refine((value) => value.name !== void 0 || value.definition !== void 0, { message: "At least one of `name` or `definition` is required." });
131
+ const codeExtractMachinesRequestSchema = z.object({
132
+ code: z.string().min(1),
133
+ xstateVersion: xstateVersionSchema.optional()
134
+ });
135
+ const authVerifyResponseSchema = verifyResultSchema;
136
+ const projectsListResponseSchema = z.array(projectSummarySchema);
137
+ const projectsGetResponseSchema = projectSchema;
138
+ const projectsCreateResponseSchema = projectSchema;
139
+ const projectsUpdateResponseSchema = projectSchema;
140
+ const projectsDeleteResponseSchema = z.object({ projectId: z.string() });
141
+ const projectsListMachinesResponseSchema = z.array(projectMachineSummarySchema);
142
+ const machinesCreateResponseSchema = machineSchema$1;
143
+ const machinesGetResponseSchema = machineSchema$1;
144
+ const machinesUpdateResponseSchema = machineSchema$1;
145
+ const machinesGetVersionResponseSchema = machineVersionSchema;
146
+ const codeExtractMachinesResponseSchema = extractedMachinesSchema;
147
+ /**
148
+ * Request/response schema pairs keyed by resource capability name. Kept here
149
+ * rather than on the `Capability` metadata entries so that the capability
150
+ * registry stays plain, serialisable data.
151
+ */
152
+ const resourceOperationSchemas = {
153
+ "auth.verify": { response: authVerifyResponseSchema },
154
+ "projects.list": { response: projectsListResponseSchema },
155
+ "projects.create": {
156
+ request: projectsCreateRequestSchema,
157
+ response: projectsCreateResponseSchema
158
+ },
159
+ "projects.get": { response: projectsGetResponseSchema },
160
+ "projects.update": {
161
+ request: projectsUpdateRequestSchema,
162
+ response: projectsUpdateResponseSchema
163
+ },
164
+ "projects.delete": { response: projectsDeleteResponseSchema },
165
+ "projects.listMachines": { response: projectsListMachinesResponseSchema },
166
+ "machines.create": {
167
+ request: machinesCreateRequestSchema,
168
+ response: machinesCreateResponseSchema
169
+ },
170
+ "machines.get": { response: machinesGetResponseSchema },
171
+ "machines.getVersion": { response: machinesGetVersionResponseSchema },
172
+ "machines.update": {
173
+ request: machinesUpdateRequestSchema,
174
+ response: machinesUpdateResponseSchema
175
+ }
176
+ };
177
+ /** Provider-independent operations implemented by this repository. */
178
+ const codeOperationSchemas = { "code.extractMachines": {
179
+ request: codeExtractMachinesRequestSchema,
180
+ response: codeExtractMachinesResponseSchema
181
+ } };
182
+ /** Every owned API operation with a canonical request/response contract. */
183
+ const operationSchemas = {
184
+ ...resourceOperationSchemas,
185
+ ...codeOperationSchemas
186
+ };
187
+
188
+ //#endregion
189
+ //#region src/resourceClient.ts
190
+ /**
191
+ * `ResourceClient` port: the only way MCP resource tools reach hosted Stately
192
+ * projects and machines.
193
+ *
194
+ * Implementations speak canonical `@statelyai/api-contract` values and throw
195
+ * {@link ResourceClientError} with a canonical `{ code, message }`. Two
196
+ * implementations exist:
197
+ *
198
+ * - HTTP-backed ({@link createHttpResourceClient}), used by Studio's MCP proxy,
199
+ * which already forwards `apiBaseUrl` and an `Authorization` header.
200
+ * - in-process, provided by the editor MCP route directly over its resource
201
+ * service (see `packages/editor/src/lib/resources/mcpResourceClient.ts`).
202
+ *
203
+ * Resource tools must never import Registry code or re-implement the contract.
204
+ */
205
+ /** Canonical error surfaced by every `ResourceClient` implementation. */
206
+ var ResourceClientError = class extends Error {
207
+ code;
208
+ issues;
209
+ retryable;
210
+ constructor(code, message, options = {}) {
211
+ super(message, options.cause ? { cause: options.cause } : void 0);
212
+ this.name = "ResourceClientError";
213
+ this.code = code;
214
+ this.issues = options.issues;
215
+ this.retryable = options.retryable;
216
+ }
217
+ };
218
+ function isResourceClientError(value) {
219
+ return value instanceof ResourceClientError;
220
+ }
221
+ const statusToResourceErrorCode = {
222
+ 400: "bad_request",
223
+ 401: "unauthenticated",
224
+ 403: "forbidden",
225
+ 404: "not_found",
226
+ 409: "conflict",
227
+ 413: "payload_too_large",
228
+ 429: "rate_limited",
229
+ 500: "internal_error",
230
+ 501: "capability_disabled",
231
+ 502: "upstream_error"
232
+ };
233
+ function resourceErrorCodeForStatus(status) {
234
+ return statusToResourceErrorCode[status] ?? (status >= 500 ? "upstream_error" : "bad_request");
235
+ }
236
+ /**
237
+ * Base URL of the owned v1 resource routes, derived from the same
238
+ * `apiBaseUrl` option the machine tools use.
239
+ *
240
+ * `apiBaseUrl` conventionally already ends in `/api` (the default is
241
+ * `https://editor.stately.ai/api`, and Studio's proxy passes that), so the
242
+ * rules are explicit and never double a segment:
243
+ *
244
+ * - ends in `/v1` -> used as-is
245
+ * - ends in `/api` -> `+ /v1`
246
+ * - anything else (e.g. a bare origin) -> `+ /api/v1`
247
+ */
248
+ function resourceApiV1BaseUrl(apiBaseUrl) {
249
+ const normalized = new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/+$/, "");
250
+ if (/\/v1$/.test(normalized)) return normalized;
251
+ if (/\/api$/.test(normalized)) return `${normalized}/v1`;
252
+ return `${normalized}/api/v1`;
253
+ }
254
+ async function resolveHeaders(options) {
255
+ const headers = new Headers(options.headers);
256
+ if (options.getHeaders) new Headers(await options.getHeaders()).forEach((value, key) => {
257
+ headers.set(key, value);
258
+ });
259
+ headers.set("accept", "application/json");
260
+ return headers;
261
+ }
262
+ function parseErrorBody(body, status) {
263
+ const nested = typeof body === "object" && body !== null ? body.error : void 0;
264
+ if (typeof nested === "object" && nested !== null) {
265
+ const envelope = nested;
266
+ const code = resourceErrorCodeSchema.safeParse(envelope.code);
267
+ return new ResourceClientError(code.success ? code.data : resourceErrorCodeForStatus(status), typeof envelope.message === "string" ? envelope.message : `Request failed with HTTP ${status}.`, {
268
+ issues: Array.isArray(envelope.issues) ? envelope.issues : void 0,
269
+ retryable: typeof envelope.retryable === "boolean" ? envelope.retryable : void 0
270
+ });
271
+ }
272
+ return new ResourceClientError(resourceErrorCodeForStatus(status), typeof nested === "string" ? nested : `Request failed with HTTP ${status}.`);
273
+ }
274
+ /**
275
+ * HTTP-backed `ResourceClient` over the owned routes:
276
+ * `GET /api/v1/projects`, `/api/v1/projects/:projectId`,
277
+ * `/api/v1/projects/:projectId/machines` and `/api/v1/machines/:machineId`.
278
+ */
279
+ function createHttpResourceClient(options = {}) {
280
+ const baseUrl = resourceApiV1BaseUrl(options.apiBaseUrl);
281
+ const doFetch = options.fetch ?? fetch;
282
+ async function get(pathname, schema, call) {
283
+ const headers = await resolveHeaders(options);
284
+ let response;
285
+ try {
286
+ response = await doFetch(`${baseUrl}${pathname}`, {
287
+ method: "GET",
288
+ headers,
289
+ signal: call?.signal
290
+ });
291
+ } catch (error) {
292
+ if (error instanceof Error && error.name === "AbortError") throw error;
293
+ throw new ResourceClientError("upstream_error", error instanceof Error ? error.message : `GET ${pathname} failed.`, {
294
+ retryable: true,
295
+ cause: error
296
+ });
297
+ }
298
+ const body = await response.json().catch(() => void 0);
299
+ if (!response.ok) throw parseErrorBody(body, response.status);
300
+ const parsed = schema.safeParse(body);
301
+ if (!parsed.success) throw new ResourceClientError("upstream_error", `Unexpected response shape for GET ${pathname}.`);
302
+ return parsed.data;
303
+ }
304
+ return {
305
+ listProjects: (call) => get("/projects", projectsListResponseSchema, call),
306
+ getProject: (projectId, call) => get(`/projects/${encodeURIComponent(projectId)}`, projectsGetResponseSchema, call),
307
+ listProjectMachines: (projectId, call) => get(`/projects/${encodeURIComponent(projectId)}/machines`, projectsListMachinesResponseSchema, call),
308
+ getMachine: (machineId, call) => get(`/machines/${encodeURIComponent(machineId)}`, machinesGetResponseSchema, call)
309
+ };
310
+ }
311
+
312
+ //#endregion
313
+ //#region src/toolCallObserver.ts
314
+ const observerStates = /* @__PURE__ */ new WeakMap();
315
+ const errorCodes = /* @__PURE__ */ new WeakMap();
316
+ function notifyToolCall(observer, event) {
317
+ try {
318
+ Promise.resolve(observer(event)).catch(() => void 0);
319
+ } catch {}
320
+ }
321
+ function isErrorResult(result) {
322
+ return typeof result === "object" && result !== null && "isError" in result && result.isError === true;
323
+ }
324
+ function getRequestMethod(requestSchema) {
325
+ if (typeof requestSchema !== "object" || requestSchema === null) return;
326
+ const shape = requestSchema.shape;
327
+ if (typeof shape !== "object" || shape === null) return void 0;
328
+ const method = shape.method;
329
+ if (typeof method !== "object" || method === null) return void 0;
330
+ return method.value;
331
+ }
332
+ function installToolCallObserver(server, state) {
333
+ try {
334
+ server.server.assertCanSetRequestHandler("tools/call");
335
+ } catch {
336
+ throw new Error("onToolCall must be configured before any tools are registered on this McpServer.");
337
+ }
338
+ const setRequestHandler = server.server.setRequestHandler.bind(server.server);
339
+ server.server.setRequestHandler = ((requestSchema, handler) => {
340
+ if (getRequestMethod(requestSchema) !== "tools/call") return setRequestHandler(requestSchema, handler);
341
+ return setRequestHandler(requestSchema, async (request, extra) => {
342
+ const observer = state.observers.get(request.params.name);
343
+ if (!observer) return handler(request, extra);
344
+ const startedAt = Date.now();
345
+ try {
346
+ const result = await handler(request, extra);
347
+ const failed = isErrorResult(result);
348
+ notifyToolCall(observer, {
349
+ toolName: request.params.name,
350
+ outcome: failed ? "error" : "success",
351
+ durationMs: Date.now() - startedAt,
352
+ ...failed ? { errorCode: errorCodes.get(result) ?? "tool_error" } : {}
353
+ });
354
+ return result;
355
+ } catch (error) {
356
+ notifyToolCall(observer, {
357
+ toolName: request.params.name,
358
+ outcome: "error",
359
+ durationMs: Date.now() - startedAt,
360
+ errorCode: "tool_error"
361
+ });
362
+ throw error;
363
+ }
364
+ });
365
+ });
366
+ }
367
+ function registerToolCallObserver(server, toolName, observer) {
368
+ if (!observer) return;
369
+ let state = observerStates.get(server);
370
+ if (!state) {
371
+ state = { observers: /* @__PURE__ */ new Map() };
372
+ installToolCallObserver(server, state);
373
+ observerStates.set(server, state);
374
+ }
375
+ state.observers.set(toolName, observer);
376
+ }
377
+ function markToolCallError(result, errorCode) {
378
+ errorCodes.set(result, errorCode);
379
+ return result;
380
+ }
381
+
382
+ //#endregion
383
+ //#region src/resourceTools.ts
384
+ /** Read-only, non-destructive; hosted resources are an open world. */
385
+ const resourceReadOnlyAnnotations = {
386
+ readOnlyHint: true,
387
+ destructiveHint: false,
388
+ idempotentHint: true,
389
+ openWorldHint: true
390
+ };
391
+ const projectIdSchema = z.string().min(1).describe("Stately project id (`projectId`, never a project version id).");
392
+ const machineIdSchema = z.string().min(1).describe("Stately machine id (`machineId`).");
393
+ const projectsOutputSchema = z.object({ projects: projectsListResponseSchema });
394
+ const projectOutputSchema = z.object({ project: projectsGetResponseSchema });
395
+ const projectMachinesOutputSchema = z.object({ machines: projectsListMachinesResponseSchema });
396
+ const machineOutputSchema = z.object({ machine: machinesGetResponseSchema });
397
+ function toolText$2(body) {
398
+ return JSON.stringify(body, null, 2);
399
+ }
400
+ function resourceToolResult(body) {
401
+ return {
402
+ structuredContent: body,
403
+ content: [{
404
+ type: "text",
405
+ text: toolText$2(body)
406
+ }]
407
+ };
408
+ }
409
+ function resourceToolError(error) {
410
+ return {
411
+ isError: true,
412
+ content: [{
413
+ type: "text",
414
+ text: toolText$2({ error: {
415
+ code: error.code,
416
+ message: error.message,
417
+ ...error.issues ? { issues: error.issues } : {},
418
+ ...error.retryable === void 0 ? {} : { retryable: error.retryable }
419
+ } })
420
+ }]
421
+ };
422
+ }
423
+ function toResourceClientError(error) {
424
+ if (isResourceClientError(error)) return error;
425
+ return new ResourceClientError("internal_error", error instanceof Error ? error.message : String(error), { cause: error });
426
+ }
427
+ function registerResourceTool(server, name, config, resolveClient, handler, options) {
428
+ registerToolCallObserver(server, name, options.onToolCall);
429
+ server.registerTool(name, {
430
+ ...config,
431
+ annotations: resourceReadOnlyAnnotations
432
+ }, async (args) => {
433
+ try {
434
+ const client = await resolveClient();
435
+ if (!client) throw new ResourceClientError("capability_disabled", "Hosted Stately resources are not available on this deployment.");
436
+ return resourceToolResult(await handler(client, args));
437
+ } catch (error) {
438
+ const resourceError = toResourceClientError(error);
439
+ return markToolCallError(resourceToolError(resourceError), resourceError.code);
440
+ }
441
+ });
442
+ }
443
+ function registerStatelyResourceTools(server, resolveClient, options = {}) {
444
+ registerResourceTool(server, "list_stately_projects", {
445
+ title: "List Stately projects",
446
+ description: "List the hosted Stately projects the caller can access. Hosted resources only; use list_machines for workspace files.",
447
+ inputSchema: {},
448
+ outputSchema: projectsOutputSchema
449
+ }, resolveClient, async (client) => ({ projects: await client.listProjects() }), options);
450
+ registerResourceTool(server, "get_stately_project", {
451
+ title: "Get Stately project",
452
+ description: "Read one hosted Stately project, including its current project version id and machine ids.",
453
+ inputSchema: { projectId: projectIdSchema },
454
+ outputSchema: projectOutputSchema
455
+ }, resolveClient, async (client, args) => ({ project: await client.getProject(args.projectId) }), options);
456
+ registerResourceTool(server, "list_stately_project_machines", {
457
+ title: "List Stately project machines",
458
+ description: "List the machines belonging to a hosted Stately project.",
459
+ inputSchema: { projectId: projectIdSchema },
460
+ outputSchema: projectMachinesOutputSchema
461
+ }, resolveClient, async (client, args) => ({ machines: await client.listProjectMachines(args.projectId) }), options);
462
+ registerResourceTool(server, "get_stately_machine", {
463
+ title: "Get Stately machine",
464
+ description: "Read one hosted Stately machine, including its definition.",
465
+ inputSchema: { machineId: machineIdSchema },
466
+ outputSchema: machineOutputSchema
467
+ }, resolveClient, async (client, args) => ({ machine: await client.getMachine(args.machineId) }), options);
468
+ return server;
469
+ }
470
+
471
+ //#endregion
6
472
  //#region src/server.ts
7
473
  const readOnlyAnnotations = {
8
474
  readOnlyHint: true,
@@ -178,7 +644,8 @@ function toConversionSource(source) {
178
644
  }
179
645
  return source;
180
646
  }
181
- async function callApi(options, pathname, body) {
647
+ const missingCredentialMessage = "No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.";
648
+ async function resolveApiHeaders(options, onMissingCredential) {
182
649
  const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
183
650
  const headers = new Headers(configuredHeaders);
184
651
  const authMode = options.authMode ?? "auto";
@@ -186,8 +653,28 @@ async function callApi(options, pathname, body) {
186
653
  const credential = await options.credentialProvider?.();
187
654
  const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
188
655
  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.");
656
+ else if (authMode === "bearer" || onMissingCredential === "throw") throw new Error(missingCredentialMessage);
190
657
  }
658
+ return headers;
659
+ }
660
+ /**
661
+ * Resource tools are always registered so tool listing keeps working; the
662
+ * client is resolved per call. Missing credentials surface as a canonical
663
+ * `unauthenticated` tool error rather than a crash.
664
+ */
665
+ function createResourceClientResolver(options) {
666
+ return async () => {
667
+ if (options.resourceClient) return options.resourceClient;
668
+ const headers = await resolveApiHeaders(options, "allow");
669
+ if ((options.authMode ?? "auto") !== "none" && !headers.has("authorization")) throw new ResourceClientError("unauthenticated", `Hosted Stately resources require authentication. ${missingCredentialMessage}`);
670
+ return createHttpResourceClient({
671
+ apiBaseUrl: options.apiBaseUrl,
672
+ headers
673
+ });
674
+ };
675
+ }
676
+ async function callApi(options, pathname, body) {
677
+ const headers = await resolveApiHeaders(options, "allow");
191
678
  const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
192
679
  method: "POST",
193
680
  headers: {
@@ -200,7 +687,8 @@ async function callApi(options, pathname, body) {
200
687
  if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
201
688
  return result;
202
689
  }
203
- function registerTool(server, name, config, handler, formatResult = toolResult) {
690
+ function registerTool(server, options, name, config, handler, formatResult = toolResult) {
691
+ registerToolCallObserver(server, name, options.onToolCall);
204
692
  server.registerTool(name, {
205
693
  ...config,
206
694
  annotations: readOnlyAnnotations
@@ -208,12 +696,12 @@ function registerTool(server, name, config, handler, formatResult = toolResult)
208
696
  try {
209
697
  return formatResult(await handler(args));
210
698
  } catch (error) {
211
- return toolError({ error: error instanceof Error ? error.message : String(error) });
699
+ return markToolCallError(toolError({ error: error instanceof Error ? error.message : String(error) }), "internal_error");
212
700
  }
213
701
  });
214
702
  }
215
703
  function registerStatelyMcpTools(server, options = {}) {
216
- registerTool(server, "create_state_machine", {
704
+ registerTool(server, options, "create_state_machine", {
217
705
  title: "Create state machine",
218
706
  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
707
  inputSchema: {
@@ -244,7 +732,7 @@ function registerStatelyMcpTools(server, options = {}) {
244
732
  })
245
733
  };
246
734
  }, createMachineToolResult);
247
- registerTool(server, "validate_machine", {
735
+ registerTool(server, options, "validate_machine", {
248
736
  title: "Validate machine",
249
737
  description: "Validate an XState machine config or source string through the Stately API.",
250
738
  inputSchema: {
@@ -253,7 +741,7 @@ function registerStatelyMcpTools(server, options = {}) {
253
741
  source: sourceSchema.optional()
254
742
  }
255
743
  }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
256
- registerTool(server, "convert_machine", {
744
+ registerTool(server, options, "convert_machine", {
257
745
  title: "Convert machine",
258
746
  description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
259
747
  inputSchema: {
@@ -266,7 +754,7 @@ function registerStatelyMcpTools(server, options = {}) {
266
754
  source: toConversionSource(normalizeSource(input)),
267
755
  target: { format: input.to }
268
756
  }));
269
- registerTool(server, "diff_machines", {
757
+ registerTool(server, options, "diff_machines", {
270
758
  title: "Diff machines",
271
759
  description: "Compute a semantic diff between two machine documents through the Stately API.",
272
760
  inputSchema: {
@@ -279,7 +767,7 @@ function registerStatelyMcpTools(server, options = {}) {
279
767
  compare: toConversionSource(normalizeSource({ source: input.compare })),
280
768
  includePatches: input.includePatches
281
769
  }));
282
- registerTool(server, "apply_machine_patches", {
770
+ registerTool(server, options, "apply_machine_patches", {
283
771
  title: "Apply machine patches",
284
772
  description: "Apply semantic machine patches through the Stately API.",
285
773
  inputSchema: {
@@ -294,7 +782,7 @@ function registerStatelyMcpTools(server, options = {}) {
294
782
  patches: input.patches,
295
783
  target: input.to ? { format: input.to } : void 0
296
784
  }));
297
- registerTool(server, "generate_graph_paths", {
785
+ registerTool(server, options, "generate_graph_paths", {
298
786
  title: "Generate graph paths",
299
787
  description: "Generate shortest or simple paths through the Stately API.",
300
788
  inputSchema: {
@@ -317,7 +805,7 @@ function registerStatelyMcpTools(server, options = {}) {
317
805
  }
318
806
  });
319
807
  });
320
- registerTool(server, "simulate_machine", {
808
+ registerTool(server, options, "simulate_machine", {
321
809
  title: "Simulate machine",
322
810
  description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
323
811
  inputSchema: {
@@ -330,7 +818,7 @@ function registerStatelyMcpTools(server, options = {}) {
330
818
  source: toConversionSource(normalizeSource(input)),
331
819
  events: input.events
332
820
  }));
333
- registerTool(server, "visualize_machine", {
821
+ registerTool(server, options, "visualize_machine", {
334
822
  title: "Visualize machine",
335
823
  description: "Render machine hierarchy levels as directly viewable PNG images. Optionally select parent node ids.",
336
824
  inputSchema: {
@@ -350,7 +838,16 @@ function registerStatelyMcpTools(server, options = {}) {
350
838
  }
351
839
  }).then((visualizations) => ({ visualizations }));
352
840
  }, visualizeToolResult);
353
- registerTool(server, "analyze_machine", {
841
+ registerTool(server, options, "extract_machines_from_code", {
842
+ title: "Extract machines from code",
843
+ description: "Deterministically extract canonical XState machine configs from JavaScript or TypeScript source. XState 5/6 syntax is inferred unless explicitly provided.",
844
+ inputSchema: {
845
+ code: z.string().min(1),
846
+ xstateVersion: xstateVersionSchema.optional().describe("Optional XState major-version override. Omit to infer from source syntax.")
847
+ },
848
+ outputSchema: codeExtractMachinesResponseSchema
849
+ }, (input) => callApi(options, "/v1/code/extract-machines", input));
850
+ registerTool(server, options, "analyze_machine", {
354
851
  title: "Analyze machine",
355
852
  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
853
  inputSchema: {
@@ -363,7 +860,7 @@ function registerStatelyMcpTools(server, options = {}) {
363
860
  source: toConversionSource(normalizeSource(input)),
364
861
  intent: input.intent
365
862
  }));
366
- registerTool(server, "generate_test_code", {
863
+ registerTool(server, options, "generate_test_code", {
367
864
  title: "Generate test code",
368
865
  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
866
  inputSchema: {
@@ -395,22 +892,7 @@ function registerStatelyMcpTools(server, options = {}) {
395
892
  coverage: pathsResponse.coverage
396
893
  };
397
894
  });
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
- });
895
+ if (options.includeResourceTools !== false) registerStatelyResourceTools(server, createResourceClientResolver(options), { onToolCall: options.onToolCall });
414
896
  return server;
415
897
  }
416
898
  function createStatelyMcpServer(options = {}) {
@@ -530,6 +1012,7 @@ function createStatelyMcpServer$1(options = {}) {
530
1012
  version: "0.1.0"
531
1013
  });
532
1014
  if (options.includeWorkspaceTools !== false) {
1015
+ registerToolCallObserver(server, "list_machines", options.onToolCall);
533
1016
  server.registerTool("list_machines", {
534
1017
  title: "list_machines",
535
1018
  description: "List candidate machine sources in the workspace.",
@@ -548,6 +1031,7 @@ function createStatelyMcpServer$1(options = {}) {
548
1031
  }]
549
1032
  };
550
1033
  });
1034
+ registerToolCallObserver(server, "get_machine", options.onToolCall);
551
1035
  server.registerTool("get_machine", {
552
1036
  title: "get_machine",
553
1037
  description: "Read a machine source from the workspace and return it.",
@@ -582,4 +1066,4 @@ async function main() {
582
1066
  if (import.meta.url === `file://${process.argv[1]}`) main();
583
1067
 
584
1068
  //#endregion
585
- export { createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };
1069
+ export { ResourceClientError, createStatelyMcpServer as createHostedStatelyMcpServer, createHttpResourceClient, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, isResourceClientError, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, registerStatelyResourceTools, resourceApiV1BaseUrl, resourceReadOnlyAnnotations, walkWorkspace, workspaceFromArgv };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@statelyai/mcp",
3
- "version": "0.3.1",
4
- "license": "UNLICENSED",
3
+ "version": "0.4.0",
4
+ "license": "MIT",
5
5
  "bin": {
6
6
  "stately-mcp": "dist/index.mjs"
7
7
  },
@@ -26,10 +26,11 @@
26
26
  "devDependencies": {
27
27
  "@types/node": "25.2.1",
28
28
  "tsdown": "0.21.0-beta.2",
29
- "vitest": "^3.2.4"
29
+ "vitest": "^3.2.4",
30
+ "@statelyai/api-contract": "0.0.1"
30
31
  },
31
32
  "scripts": {
32
- "build": "tsdown",
33
+ "build": "pnpm --filter @statelyai/api-contract run build && tsdown",
33
34
  "test": "vitest run",
34
35
  "test:watch": "vitest",
35
36
  "verify:package": "node scripts/verify-package.mjs"