@statelyai/mcp 0.3.2 → 0.4.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/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,
@@ -49,6 +515,10 @@ const xstateMachineResultSchema = z.object({
49
515
  content: z.string(),
50
516
  warnings: z.array(z.string())
51
517
  });
518
+ const xstateConversionCheckSchema = z.object({
519
+ format: z.literal("xgraph"),
520
+ warnings: z.array(z.string())
521
+ });
52
522
  const visualizationMetadataSchema = z.object({
53
523
  parentNodeId: z.string(),
54
524
  parentNodeLabel: z.string(),
@@ -178,7 +648,8 @@ function toConversionSource(source) {
178
648
  }
179
649
  return source;
180
650
  }
181
- async function callApi(options, pathname, body) {
651
+ const missingCredentialMessage = "No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.";
652
+ async function resolveApiHeaders(options, onMissingCredential) {
182
653
  const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
183
654
  const headers = new Headers(configuredHeaders);
184
655
  const authMode = options.authMode ?? "auto";
@@ -186,8 +657,28 @@ async function callApi(options, pathname, body) {
186
657
  const credential = await options.credentialProvider?.();
187
658
  const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
188
659
  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.");
660
+ else if (authMode === "bearer" || onMissingCredential === "throw") throw new Error(missingCredentialMessage);
190
661
  }
662
+ return headers;
663
+ }
664
+ /**
665
+ * Resource tools are always registered so tool listing keeps working; the
666
+ * client is resolved per call. Missing credentials surface as a canonical
667
+ * `unauthenticated` tool error rather than a crash.
668
+ */
669
+ function createResourceClientResolver(options) {
670
+ return async () => {
671
+ if (options.resourceClient) return options.resourceClient;
672
+ const headers = await resolveApiHeaders(options, "allow");
673
+ if ((options.authMode ?? "auto") !== "none" && !headers.has("authorization")) throw new ResourceClientError("unauthenticated", `Hosted Stately resources require authentication. ${missingCredentialMessage}`);
674
+ return createHttpResourceClient({
675
+ apiBaseUrl: options.apiBaseUrl,
676
+ headers
677
+ });
678
+ };
679
+ }
680
+ async function callApi(options, pathname, body) {
681
+ const headers = await resolveApiHeaders(options, "allow");
191
682
  const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
192
683
  method: "POST",
193
684
  headers: {
@@ -200,7 +691,8 @@ async function callApi(options, pathname, body) {
200
691
  if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
201
692
  return result;
202
693
  }
203
- function registerTool(server, name, config, handler, formatResult = toolResult) {
694
+ function registerTool(server, options, name, config, handler, formatResult = toolResult) {
695
+ registerToolCallObserver(server, name, options.onToolCall);
204
696
  server.registerTool(name, {
205
697
  ...config,
206
698
  annotations: readOnlyAnnotations
@@ -208,12 +700,12 @@ function registerTool(server, name, config, handler, formatResult = toolResult)
208
700
  try {
209
701
  return formatResult(await handler(args));
210
702
  } catch (error) {
211
- return toolError({ error: error instanceof Error ? error.message : String(error) });
703
+ return markToolCallError(toolError({ error: error instanceof Error ? error.message : String(error) }), "internal_error");
212
704
  }
213
705
  });
214
706
  }
215
707
  function registerStatelyMcpTools(server, options = {}) {
216
- registerTool(server, "create_state_machine", {
708
+ registerTool(server, options, "create_state_machine", {
217
709
  title: "Create state machine",
218
710
  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
711
  inputSchema: {
@@ -222,13 +714,18 @@ function registerStatelyMcpTools(server, options = {}) {
222
714
  },
223
715
  outputSchema: createMachineOutputSchema
224
716
  }, async (input) => {
225
- const machine = xstateMachineResultSchema.parse(await callApi(options, "/v1/convert", {
717
+ const conversion = xstateConversionCheckSchema.parse(await callApi(options, "/v1/convert", {
226
718
  source: {
227
719
  format: "xstate",
228
720
  content: input.xstateCode
229
721
  },
230
- target: { format: "xstate" }
722
+ target: { format: "xgraph" }
231
723
  }));
724
+ const machine = {
725
+ format: "xstate",
726
+ content: input.xstateCode,
727
+ warnings: conversion.warnings
728
+ };
232
729
  return {
233
730
  machine,
234
731
  visualizations: input.visualization === false ? [] : await callApi(options, "/v1/visualize", {
@@ -244,7 +741,7 @@ function registerStatelyMcpTools(server, options = {}) {
244
741
  })
245
742
  };
246
743
  }, createMachineToolResult);
247
- registerTool(server, "validate_machine", {
744
+ registerTool(server, options, "validate_machine", {
248
745
  title: "Validate machine",
249
746
  description: "Validate an XState machine config or source string through the Stately API.",
250
747
  inputSchema: {
@@ -253,7 +750,7 @@ function registerStatelyMcpTools(server, options = {}) {
253
750
  source: sourceSchema.optional()
254
751
  }
255
752
  }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
256
- registerTool(server, "convert_machine", {
753
+ registerTool(server, options, "convert_machine", {
257
754
  title: "Convert machine",
258
755
  description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
259
756
  inputSchema: {
@@ -266,7 +763,7 @@ function registerStatelyMcpTools(server, options = {}) {
266
763
  source: toConversionSource(normalizeSource(input)),
267
764
  target: { format: input.to }
268
765
  }));
269
- registerTool(server, "diff_machines", {
766
+ registerTool(server, options, "diff_machines", {
270
767
  title: "Diff machines",
271
768
  description: "Compute a semantic diff between two machine documents through the Stately API.",
272
769
  inputSchema: {
@@ -279,7 +776,7 @@ function registerStatelyMcpTools(server, options = {}) {
279
776
  compare: toConversionSource(normalizeSource({ source: input.compare })),
280
777
  includePatches: input.includePatches
281
778
  }));
282
- registerTool(server, "apply_machine_patches", {
779
+ registerTool(server, options, "apply_machine_patches", {
283
780
  title: "Apply machine patches",
284
781
  description: "Apply semantic machine patches through the Stately API.",
285
782
  inputSchema: {
@@ -294,7 +791,7 @@ function registerStatelyMcpTools(server, options = {}) {
294
791
  patches: input.patches,
295
792
  target: input.to ? { format: input.to } : void 0
296
793
  }));
297
- registerTool(server, "generate_graph_paths", {
794
+ registerTool(server, options, "generate_graph_paths", {
298
795
  title: "Generate graph paths",
299
796
  description: "Generate shortest or simple paths through the Stately API.",
300
797
  inputSchema: {
@@ -317,7 +814,7 @@ function registerStatelyMcpTools(server, options = {}) {
317
814
  }
318
815
  });
319
816
  });
320
- registerTool(server, "simulate_machine", {
817
+ registerTool(server, options, "simulate_machine", {
321
818
  title: "Simulate machine",
322
819
  description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
323
820
  inputSchema: {
@@ -330,7 +827,7 @@ function registerStatelyMcpTools(server, options = {}) {
330
827
  source: toConversionSource(normalizeSource(input)),
331
828
  events: input.events
332
829
  }));
333
- registerTool(server, "visualize_machine", {
830
+ registerTool(server, options, "visualize_machine", {
334
831
  title: "Visualize machine",
335
832
  description: "Render machine hierarchy levels as directly viewable PNG images. Optionally select parent node ids.",
336
833
  inputSchema: {
@@ -350,7 +847,16 @@ function registerStatelyMcpTools(server, options = {}) {
350
847
  }
351
848
  }).then((visualizations) => ({ visualizations }));
352
849
  }, visualizeToolResult);
353
- registerTool(server, "analyze_machine", {
850
+ registerTool(server, options, "extract_machines_from_code", {
851
+ title: "Extract machines from code",
852
+ description: "Deterministically extract canonical XState machine configs from JavaScript or TypeScript source. XState 5/6 syntax is inferred unless explicitly provided.",
853
+ inputSchema: {
854
+ code: z.string().min(1),
855
+ xstateVersion: xstateVersionSchema.optional().describe("Optional XState major-version override. Omit to infer from source syntax.")
856
+ },
857
+ outputSchema: codeExtractMachinesResponseSchema
858
+ }, (input) => callApi(options, "/v1/code/extract-machines", input));
859
+ registerTool(server, options, "analyze_machine", {
354
860
  title: "Analyze machine",
355
861
  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
862
  inputSchema: {
@@ -363,7 +869,7 @@ function registerStatelyMcpTools(server, options = {}) {
363
869
  source: toConversionSource(normalizeSource(input)),
364
870
  intent: input.intent
365
871
  }));
366
- registerTool(server, "generate_test_code", {
872
+ registerTool(server, options, "generate_test_code", {
367
873
  title: "Generate test code",
368
874
  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
875
  inputSchema: {
@@ -395,22 +901,7 @@ function registerStatelyMcpTools(server, options = {}) {
395
901
  coverage: pathsResponse.coverage
396
902
  };
397
903
  });
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
- });
904
+ if (options.includeResourceTools !== false) registerStatelyResourceTools(server, createResourceClientResolver(options), { onToolCall: options.onToolCall });
414
905
  return server;
415
906
  }
416
907
  function createStatelyMcpServer(options = {}) {
@@ -530,6 +1021,7 @@ function createStatelyMcpServer$1(options = {}) {
530
1021
  version: "0.1.0"
531
1022
  });
532
1023
  if (options.includeWorkspaceTools !== false) {
1024
+ registerToolCallObserver(server, "list_machines", options.onToolCall);
533
1025
  server.registerTool("list_machines", {
534
1026
  title: "list_machines",
535
1027
  description: "List candidate machine sources in the workspace.",
@@ -548,6 +1040,7 @@ function createStatelyMcpServer$1(options = {}) {
548
1040
  }]
549
1041
  };
550
1042
  });
1043
+ registerToolCallObserver(server, "get_machine", options.onToolCall);
551
1044
  server.registerTool("get_machine", {
552
1045
  title: "get_machine",
553
1046
  description: "Read a machine source from the workspace and return it.",
@@ -582,4 +1075,4 @@ async function main() {
582
1075
  if (import.meta.url === `file://${process.argv[1]}`) main();
583
1076
 
584
1077
  //#endregion
585
- export { createStatelyMcpServer as createHostedStatelyMcpServer, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, walkWorkspace, workspaceFromArgv };
1078
+ export { ResourceClientError, createStatelyMcpServer as createHostedStatelyMcpServer, createHttpResourceClient, createStatelyMcpServer$1 as createStatelyMcpServer, formatForPath, generateTestCode, getDefaultCredential, isCandidate, isResourceClientError, readOnlyAnnotations, readWorkspaceMachine, registerStatelyMcpTools, registerStatelyResourceTools, resourceApiV1BaseUrl, resourceReadOnlyAnnotations, walkWorkspace, workspaceFromArgv };