@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.cjs CHANGED
@@ -4,6 +4,472 @@ let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk
4
4
  let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
5
5
  let zod = require("zod");
6
6
 
7
+ //#region ../api-contract/dist/errors.mjs
8
+ /**
9
+ * Error shape returned by every `/api/v1` machine operation (and by the shared
10
+ * auth guard). Each operation registers its own error schema component in the
11
+ * OpenAPI document, so this is a factory rather than a single shared instance.
12
+ */
13
+ function createApiErrorResponseSchema() {
14
+ return zod.z.object({
15
+ error: zod.z.string(),
16
+ issues: zod.z.array(zod.z.unknown()).optional()
17
+ });
18
+ }
19
+ const apiErrorResponseSchema = createApiErrorResponseSchema();
20
+ /**
21
+ * Canonical error codes for the `/api/v1` resource operations. Machine
22
+ * operations keep the flat `{ error: string }` shape above unchanged; the
23
+ * nested envelope below applies to resource operations only.
24
+ */
25
+ const resourceErrorCodeSchema = zod.z.enum([
26
+ "bad_request",
27
+ "unauthenticated",
28
+ "forbidden",
29
+ "not_found",
30
+ "conflict",
31
+ "payload_too_large",
32
+ "rate_limited",
33
+ "capability_disabled",
34
+ "upstream_error",
35
+ "internal_error"
36
+ ]);
37
+ /** Factory, matching `createApiErrorResponseSchema`, for per-route OpenAPI components. */
38
+ function createResourceApiErrorResponseSchema() {
39
+ return zod.z.object({ error: zod.z.object({
40
+ code: resourceErrorCodeSchema,
41
+ message: zod.z.string(),
42
+ issues: zod.z.array(zod.z.unknown()).optional(),
43
+ retryable: zod.z.boolean().optional()
44
+ }) });
45
+ }
46
+ const resourceApiErrorResponseSchema = createResourceApiErrorResponseSchema();
47
+
48
+ //#endregion
49
+ //#region ../api-contract/dist/resources.mjs
50
+ /**
51
+ * Canonical schemas for persistent `/api/v1` resources plus the owned,
52
+ * deterministic code-extraction operation.
53
+ *
54
+ * These models are canonical, not upstream-shaped: providers (e.g. the Stately
55
+ * Registry adapter) are responsible for translating upstream envelopes,
56
+ * identifier quirks and casing into these shapes. No `{ data }` envelope
57
+ * appears anywhere; all timestamps are ISO strings.
58
+ */
59
+ /**
60
+ * XState major versions the editor targets (see
61
+ * `packages/editor/src/formats/xstate.ts`).
62
+ */
63
+ const xstateVersionSchema = zod.z.union([zod.z.literal(5), zod.z.literal(6)]);
64
+ const projectVisibilitySchema = zod.z.enum([
65
+ "public",
66
+ "unlisted",
67
+ "private"
68
+ ]);
69
+ const verifyResultSchema = zod.z.object({
70
+ valid: zod.z.literal(true),
71
+ tokenScope: zod.z.enum(["user", "project"])
72
+ });
73
+ const projectSummarySchema = zod.z.object({
74
+ projectId: zod.z.string(),
75
+ projectVersionId: zod.z.string(),
76
+ name: zod.z.string(),
77
+ description: zod.z.string().nullable(),
78
+ visibility: projectVisibilitySchema,
79
+ createdAt: zod.z.string(),
80
+ updatedAt: zod.z.string()
81
+ });
82
+ const projectSchema = projectSummarySchema.extend({
83
+ rootMachineId: zod.z.string().nullable(),
84
+ machines: zod.z.array(zod.z.object({ machineId: zod.z.string() }))
85
+ });
86
+ /**
87
+ * `name` is optional: the Registry project envelope only returns machine ids.
88
+ * Providers that can supply names cheaply should.
89
+ */
90
+ const projectMachineSummarySchema = zod.z.object({
91
+ machineId: zod.z.string(),
92
+ name: zod.z.string().optional()
93
+ });
94
+ const machineSchema$1 = zod.z.object({
95
+ machineId: zod.z.string(),
96
+ name: zod.z.string(),
97
+ projectId: zod.z.string().optional(),
98
+ projectVersionId: zod.z.string(),
99
+ definition: zod.z.unknown(),
100
+ createdAt: zod.z.string().optional(),
101
+ updatedAt: zod.z.string().optional()
102
+ });
103
+ const machineVersionSchema = zod.z.object({
104
+ machineVersionId: zod.z.string(),
105
+ machineId: zod.z.string(),
106
+ versionNumber: zod.z.number(),
107
+ name: zod.z.string(),
108
+ definition: zod.z.unknown(),
109
+ createdAt: zod.z.string(),
110
+ updatedAt: zod.z.string()
111
+ });
112
+ const extractedMachinesSchema = zod.z.object({ machines: zod.z.array(zod.z.object({ config: zod.z.record(zod.z.string(), zod.z.unknown()) })) });
113
+ const projectsCreateRequestSchema = zod.z.object({
114
+ name: zod.z.string().min(1),
115
+ visibility: projectVisibilitySchema
116
+ });
117
+ const projectsUpdateRequestSchema = zod.z.object({
118
+ name: zod.z.string().min(1).optional(),
119
+ description: zod.z.string().nullable().optional(),
120
+ visibility: projectVisibilitySchema.optional()
121
+ }).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." });
122
+ const machinesCreateRequestSchema = zod.z.object({
123
+ projectVersionId: zod.z.string().min(1),
124
+ xstateVersion: xstateVersionSchema,
125
+ name: zod.z.string().min(1).optional(),
126
+ definition: zod.z.unknown().optional()
127
+ });
128
+ const machinesUpdateRequestSchema = zod.z.object({
129
+ name: zod.z.string().min(1).optional(),
130
+ definition: zod.z.unknown().optional()
131
+ }).refine((value) => value.name !== void 0 || value.definition !== void 0, { message: "At least one of `name` or `definition` is required." });
132
+ const codeExtractMachinesRequestSchema = zod.z.object({
133
+ code: zod.z.string().min(1),
134
+ xstateVersion: xstateVersionSchema.optional()
135
+ });
136
+ const authVerifyResponseSchema = verifyResultSchema;
137
+ const projectsListResponseSchema = zod.z.array(projectSummarySchema);
138
+ const projectsGetResponseSchema = projectSchema;
139
+ const projectsCreateResponseSchema = projectSchema;
140
+ const projectsUpdateResponseSchema = projectSchema;
141
+ const projectsDeleteResponseSchema = zod.z.object({ projectId: zod.z.string() });
142
+ const projectsListMachinesResponseSchema = zod.z.array(projectMachineSummarySchema);
143
+ const machinesCreateResponseSchema = machineSchema$1;
144
+ const machinesGetResponseSchema = machineSchema$1;
145
+ const machinesUpdateResponseSchema = machineSchema$1;
146
+ const machinesGetVersionResponseSchema = machineVersionSchema;
147
+ const codeExtractMachinesResponseSchema = extractedMachinesSchema;
148
+ /**
149
+ * Request/response schema pairs keyed by resource capability name. Kept here
150
+ * rather than on the `Capability` metadata entries so that the capability
151
+ * registry stays plain, serialisable data.
152
+ */
153
+ const resourceOperationSchemas = {
154
+ "auth.verify": { response: authVerifyResponseSchema },
155
+ "projects.list": { response: projectsListResponseSchema },
156
+ "projects.create": {
157
+ request: projectsCreateRequestSchema,
158
+ response: projectsCreateResponseSchema
159
+ },
160
+ "projects.get": { response: projectsGetResponseSchema },
161
+ "projects.update": {
162
+ request: projectsUpdateRequestSchema,
163
+ response: projectsUpdateResponseSchema
164
+ },
165
+ "projects.delete": { response: projectsDeleteResponseSchema },
166
+ "projects.listMachines": { response: projectsListMachinesResponseSchema },
167
+ "machines.create": {
168
+ request: machinesCreateRequestSchema,
169
+ response: machinesCreateResponseSchema
170
+ },
171
+ "machines.get": { response: machinesGetResponseSchema },
172
+ "machines.getVersion": { response: machinesGetVersionResponseSchema },
173
+ "machines.update": {
174
+ request: machinesUpdateRequestSchema,
175
+ response: machinesUpdateResponseSchema
176
+ }
177
+ };
178
+ /** Provider-independent operations implemented by this repository. */
179
+ const codeOperationSchemas = { "code.extractMachines": {
180
+ request: codeExtractMachinesRequestSchema,
181
+ response: codeExtractMachinesResponseSchema
182
+ } };
183
+ /** Every owned API operation with a canonical request/response contract. */
184
+ const operationSchemas = {
185
+ ...resourceOperationSchemas,
186
+ ...codeOperationSchemas
187
+ };
188
+
189
+ //#endregion
190
+ //#region src/resourceClient.ts
191
+ /**
192
+ * `ResourceClient` port: the only way MCP resource tools reach hosted Stately
193
+ * projects and machines.
194
+ *
195
+ * Implementations speak canonical `@statelyai/api-contract` values and throw
196
+ * {@link ResourceClientError} with a canonical `{ code, message }`. Two
197
+ * implementations exist:
198
+ *
199
+ * - HTTP-backed ({@link createHttpResourceClient}), used by Studio's MCP proxy,
200
+ * which already forwards `apiBaseUrl` and an `Authorization` header.
201
+ * - in-process, provided by the editor MCP route directly over its resource
202
+ * service (see `packages/editor/src/lib/resources/mcpResourceClient.ts`).
203
+ *
204
+ * Resource tools must never import Registry code or re-implement the contract.
205
+ */
206
+ /** Canonical error surfaced by every `ResourceClient` implementation. */
207
+ var ResourceClientError = class extends Error {
208
+ code;
209
+ issues;
210
+ retryable;
211
+ constructor(code, message, options = {}) {
212
+ super(message, options.cause ? { cause: options.cause } : void 0);
213
+ this.name = "ResourceClientError";
214
+ this.code = code;
215
+ this.issues = options.issues;
216
+ this.retryable = options.retryable;
217
+ }
218
+ };
219
+ function isResourceClientError(value) {
220
+ return value instanceof ResourceClientError;
221
+ }
222
+ const statusToResourceErrorCode = {
223
+ 400: "bad_request",
224
+ 401: "unauthenticated",
225
+ 403: "forbidden",
226
+ 404: "not_found",
227
+ 409: "conflict",
228
+ 413: "payload_too_large",
229
+ 429: "rate_limited",
230
+ 500: "internal_error",
231
+ 501: "capability_disabled",
232
+ 502: "upstream_error"
233
+ };
234
+ function resourceErrorCodeForStatus(status) {
235
+ return statusToResourceErrorCode[status] ?? (status >= 500 ? "upstream_error" : "bad_request");
236
+ }
237
+ /**
238
+ * Base URL of the owned v1 resource routes, derived from the same
239
+ * `apiBaseUrl` option the machine tools use.
240
+ *
241
+ * `apiBaseUrl` conventionally already ends in `/api` (the default is
242
+ * `https://editor.stately.ai/api`, and Studio's proxy passes that), so the
243
+ * rules are explicit and never double a segment:
244
+ *
245
+ * - ends in `/v1` -> used as-is
246
+ * - ends in `/api` -> `+ /v1`
247
+ * - anything else (e.g. a bare origin) -> `+ /api/v1`
248
+ */
249
+ function resourceApiV1BaseUrl(apiBaseUrl) {
250
+ const normalized = new URL(String(apiBaseUrl ?? "https://editor.stately.ai/api")).toString().replace(/\/+$/, "");
251
+ if (/\/v1$/.test(normalized)) return normalized;
252
+ if (/\/api$/.test(normalized)) return `${normalized}/v1`;
253
+ return `${normalized}/api/v1`;
254
+ }
255
+ async function resolveHeaders(options) {
256
+ const headers = new Headers(options.headers);
257
+ if (options.getHeaders) new Headers(await options.getHeaders()).forEach((value, key) => {
258
+ headers.set(key, value);
259
+ });
260
+ headers.set("accept", "application/json");
261
+ return headers;
262
+ }
263
+ function parseErrorBody(body, status) {
264
+ const nested = typeof body === "object" && body !== null ? body.error : void 0;
265
+ if (typeof nested === "object" && nested !== null) {
266
+ const envelope = nested;
267
+ const code = resourceErrorCodeSchema.safeParse(envelope.code);
268
+ return new ResourceClientError(code.success ? code.data : resourceErrorCodeForStatus(status), typeof envelope.message === "string" ? envelope.message : `Request failed with HTTP ${status}.`, {
269
+ issues: Array.isArray(envelope.issues) ? envelope.issues : void 0,
270
+ retryable: typeof envelope.retryable === "boolean" ? envelope.retryable : void 0
271
+ });
272
+ }
273
+ return new ResourceClientError(resourceErrorCodeForStatus(status), typeof nested === "string" ? nested : `Request failed with HTTP ${status}.`);
274
+ }
275
+ /**
276
+ * HTTP-backed `ResourceClient` over the owned routes:
277
+ * `GET /api/v1/projects`, `/api/v1/projects/:projectId`,
278
+ * `/api/v1/projects/:projectId/machines` and `/api/v1/machines/:machineId`.
279
+ */
280
+ function createHttpResourceClient(options = {}) {
281
+ const baseUrl = resourceApiV1BaseUrl(options.apiBaseUrl);
282
+ const doFetch = options.fetch ?? fetch;
283
+ async function get(pathname, schema, call) {
284
+ const headers = await resolveHeaders(options);
285
+ let response;
286
+ try {
287
+ response = await doFetch(`${baseUrl}${pathname}`, {
288
+ method: "GET",
289
+ headers,
290
+ signal: call?.signal
291
+ });
292
+ } catch (error) {
293
+ if (error instanceof Error && error.name === "AbortError") throw error;
294
+ throw new ResourceClientError("upstream_error", error instanceof Error ? error.message : `GET ${pathname} failed.`, {
295
+ retryable: true,
296
+ cause: error
297
+ });
298
+ }
299
+ const body = await response.json().catch(() => void 0);
300
+ if (!response.ok) throw parseErrorBody(body, response.status);
301
+ const parsed = schema.safeParse(body);
302
+ if (!parsed.success) throw new ResourceClientError("upstream_error", `Unexpected response shape for GET ${pathname}.`);
303
+ return parsed.data;
304
+ }
305
+ return {
306
+ listProjects: (call) => get("/projects", projectsListResponseSchema, call),
307
+ getProject: (projectId, call) => get(`/projects/${encodeURIComponent(projectId)}`, projectsGetResponseSchema, call),
308
+ listProjectMachines: (projectId, call) => get(`/projects/${encodeURIComponent(projectId)}/machines`, projectsListMachinesResponseSchema, call),
309
+ getMachine: (machineId, call) => get(`/machines/${encodeURIComponent(machineId)}`, machinesGetResponseSchema, call)
310
+ };
311
+ }
312
+
313
+ //#endregion
314
+ //#region src/toolCallObserver.ts
315
+ const observerStates = /* @__PURE__ */ new WeakMap();
316
+ const errorCodes = /* @__PURE__ */ new WeakMap();
317
+ function notifyToolCall(observer, event) {
318
+ try {
319
+ Promise.resolve(observer(event)).catch(() => void 0);
320
+ } catch {}
321
+ }
322
+ function isErrorResult(result) {
323
+ return typeof result === "object" && result !== null && "isError" in result && result.isError === true;
324
+ }
325
+ function getRequestMethod(requestSchema) {
326
+ if (typeof requestSchema !== "object" || requestSchema === null) return;
327
+ const shape = requestSchema.shape;
328
+ if (typeof shape !== "object" || shape === null) return void 0;
329
+ const method = shape.method;
330
+ if (typeof method !== "object" || method === null) return void 0;
331
+ return method.value;
332
+ }
333
+ function installToolCallObserver(server, state) {
334
+ try {
335
+ server.server.assertCanSetRequestHandler("tools/call");
336
+ } catch {
337
+ throw new Error("onToolCall must be configured before any tools are registered on this McpServer.");
338
+ }
339
+ const setRequestHandler = server.server.setRequestHandler.bind(server.server);
340
+ server.server.setRequestHandler = ((requestSchema, handler) => {
341
+ if (getRequestMethod(requestSchema) !== "tools/call") return setRequestHandler(requestSchema, handler);
342
+ return setRequestHandler(requestSchema, async (request, extra) => {
343
+ const observer = state.observers.get(request.params.name);
344
+ if (!observer) return handler(request, extra);
345
+ const startedAt = Date.now();
346
+ try {
347
+ const result = await handler(request, extra);
348
+ const failed = isErrorResult(result);
349
+ notifyToolCall(observer, {
350
+ toolName: request.params.name,
351
+ outcome: failed ? "error" : "success",
352
+ durationMs: Date.now() - startedAt,
353
+ ...failed ? { errorCode: errorCodes.get(result) ?? "tool_error" } : {}
354
+ });
355
+ return result;
356
+ } catch (error) {
357
+ notifyToolCall(observer, {
358
+ toolName: request.params.name,
359
+ outcome: "error",
360
+ durationMs: Date.now() - startedAt,
361
+ errorCode: "tool_error"
362
+ });
363
+ throw error;
364
+ }
365
+ });
366
+ });
367
+ }
368
+ function registerToolCallObserver(server, toolName, observer) {
369
+ if (!observer) return;
370
+ let state = observerStates.get(server);
371
+ if (!state) {
372
+ state = { observers: /* @__PURE__ */ new Map() };
373
+ installToolCallObserver(server, state);
374
+ observerStates.set(server, state);
375
+ }
376
+ state.observers.set(toolName, observer);
377
+ }
378
+ function markToolCallError(result, errorCode) {
379
+ errorCodes.set(result, errorCode);
380
+ return result;
381
+ }
382
+
383
+ //#endregion
384
+ //#region src/resourceTools.ts
385
+ /** Read-only, non-destructive; hosted resources are an open world. */
386
+ const resourceReadOnlyAnnotations = {
387
+ readOnlyHint: true,
388
+ destructiveHint: false,
389
+ idempotentHint: true,
390
+ openWorldHint: true
391
+ };
392
+ const projectIdSchema = zod.z.string().min(1).describe("Stately project id (`projectId`, never a project version id).");
393
+ const machineIdSchema = zod.z.string().min(1).describe("Stately machine id (`machineId`).");
394
+ const projectsOutputSchema = zod.z.object({ projects: projectsListResponseSchema });
395
+ const projectOutputSchema = zod.z.object({ project: projectsGetResponseSchema });
396
+ const projectMachinesOutputSchema = zod.z.object({ machines: projectsListMachinesResponseSchema });
397
+ const machineOutputSchema = zod.z.object({ machine: machinesGetResponseSchema });
398
+ function toolText$2(body) {
399
+ return JSON.stringify(body, null, 2);
400
+ }
401
+ function resourceToolResult(body) {
402
+ return {
403
+ structuredContent: body,
404
+ content: [{
405
+ type: "text",
406
+ text: toolText$2(body)
407
+ }]
408
+ };
409
+ }
410
+ function resourceToolError(error) {
411
+ return {
412
+ isError: true,
413
+ content: [{
414
+ type: "text",
415
+ text: toolText$2({ error: {
416
+ code: error.code,
417
+ message: error.message,
418
+ ...error.issues ? { issues: error.issues } : {},
419
+ ...error.retryable === void 0 ? {} : { retryable: error.retryable }
420
+ } })
421
+ }]
422
+ };
423
+ }
424
+ function toResourceClientError(error) {
425
+ if (isResourceClientError(error)) return error;
426
+ return new ResourceClientError("internal_error", error instanceof Error ? error.message : String(error), { cause: error });
427
+ }
428
+ function registerResourceTool(server, name, config, resolveClient, handler, options) {
429
+ registerToolCallObserver(server, name, options.onToolCall);
430
+ server.registerTool(name, {
431
+ ...config,
432
+ annotations: resourceReadOnlyAnnotations
433
+ }, async (args) => {
434
+ try {
435
+ const client = await resolveClient();
436
+ if (!client) throw new ResourceClientError("capability_disabled", "Hosted Stately resources are not available on this deployment.");
437
+ return resourceToolResult(await handler(client, args));
438
+ } catch (error) {
439
+ const resourceError = toResourceClientError(error);
440
+ return markToolCallError(resourceToolError(resourceError), resourceError.code);
441
+ }
442
+ });
443
+ }
444
+ function registerStatelyResourceTools(server, resolveClient, options = {}) {
445
+ registerResourceTool(server, "list_stately_projects", {
446
+ title: "List Stately projects",
447
+ description: "List the hosted Stately projects the caller can access. Hosted resources only; use list_machines for workspace files.",
448
+ inputSchema: {},
449
+ outputSchema: projectsOutputSchema
450
+ }, resolveClient, async (client) => ({ projects: await client.listProjects() }), options);
451
+ registerResourceTool(server, "get_stately_project", {
452
+ title: "Get Stately project",
453
+ description: "Read one hosted Stately project, including its current project version id and machine ids.",
454
+ inputSchema: { projectId: projectIdSchema },
455
+ outputSchema: projectOutputSchema
456
+ }, resolveClient, async (client, args) => ({ project: await client.getProject(args.projectId) }), options);
457
+ registerResourceTool(server, "list_stately_project_machines", {
458
+ title: "List Stately project machines",
459
+ description: "List the machines belonging to a hosted Stately project.",
460
+ inputSchema: { projectId: projectIdSchema },
461
+ outputSchema: projectMachinesOutputSchema
462
+ }, resolveClient, async (client, args) => ({ machines: await client.listProjectMachines(args.projectId) }), options);
463
+ registerResourceTool(server, "get_stately_machine", {
464
+ title: "Get Stately machine",
465
+ description: "Read one hosted Stately machine, including its definition.",
466
+ inputSchema: { machineId: machineIdSchema },
467
+ outputSchema: machineOutputSchema
468
+ }, resolveClient, async (client, args) => ({ machine: await client.getMachine(args.machineId) }), options);
469
+ return server;
470
+ }
471
+
472
+ //#endregion
7
473
  //#region src/server.ts
8
474
  const readOnlyAnnotations = {
9
475
  readOnlyHint: true,
@@ -50,6 +516,10 @@ const xstateMachineResultSchema = zod.z.object({
50
516
  content: zod.z.string(),
51
517
  warnings: zod.z.array(zod.z.string())
52
518
  });
519
+ const xstateConversionCheckSchema = zod.z.object({
520
+ format: zod.z.literal("xgraph"),
521
+ warnings: zod.z.array(zod.z.string())
522
+ });
53
523
  const visualizationMetadataSchema = zod.z.object({
54
524
  parentNodeId: zod.z.string(),
55
525
  parentNodeLabel: zod.z.string(),
@@ -179,7 +649,8 @@ function toConversionSource(source) {
179
649
  }
180
650
  return source;
181
651
  }
182
- async function callApi(options, pathname, body) {
652
+ const missingCredentialMessage = "No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.";
653
+ async function resolveApiHeaders(options, onMissingCredential) {
183
654
  const configuredHeaders = typeof options.apiHeaders === "function" ? await options.apiHeaders() : options.apiHeaders;
184
655
  const headers = new Headers(configuredHeaders);
185
656
  const authMode = options.authMode ?? "auto";
@@ -187,8 +658,28 @@ async function callApi(options, pathname, body) {
187
658
  const credential = await options.credentialProvider?.();
188
659
  const token = credential?.type === "oauth" ? credential.accessToken : credential?.token;
189
660
  if (token) headers.set("authorization", `Bearer ${token}`);
190
- else if (authMode === "bearer") throw new Error("No Stately credential configured. Run `statelyai login` or set STATELY_ACCESS_TOKEN.");
661
+ else if (authMode === "bearer" || onMissingCredential === "throw") throw new Error(missingCredentialMessage);
191
662
  }
663
+ return headers;
664
+ }
665
+ /**
666
+ * Resource tools are always registered so tool listing keeps working; the
667
+ * client is resolved per call. Missing credentials surface as a canonical
668
+ * `unauthenticated` tool error rather than a crash.
669
+ */
670
+ function createResourceClientResolver(options) {
671
+ return async () => {
672
+ if (options.resourceClient) return options.resourceClient;
673
+ const headers = await resolveApiHeaders(options, "allow");
674
+ if ((options.authMode ?? "auto") !== "none" && !headers.has("authorization")) throw new ResourceClientError("unauthenticated", `Hosted Stately resources require authentication. ${missingCredentialMessage}`);
675
+ return createHttpResourceClient({
676
+ apiBaseUrl: options.apiBaseUrl,
677
+ headers
678
+ });
679
+ };
680
+ }
681
+ async function callApi(options, pathname, body) {
682
+ const headers = await resolveApiHeaders(options, "allow");
192
683
  const response = await fetch(`${normalizeApiBaseUrl(options.apiBaseUrl)}${pathname}`, {
193
684
  method: "POST",
194
685
  headers: {
@@ -201,7 +692,8 @@ async function callApi(options, pathname, body) {
201
692
  if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `${pathname} failed with HTTP ${response.status}`);
202
693
  return result;
203
694
  }
204
- function registerTool(server, name, config, handler, formatResult = toolResult) {
695
+ function registerTool(server, options, name, config, handler, formatResult = toolResult) {
696
+ registerToolCallObserver(server, name, options.onToolCall);
205
697
  server.registerTool(name, {
206
698
  ...config,
207
699
  annotations: readOnlyAnnotations
@@ -209,12 +701,12 @@ function registerTool(server, name, config, handler, formatResult = toolResult)
209
701
  try {
210
702
  return formatResult(await handler(args));
211
703
  } catch (error) {
212
- return toolError({ error: error instanceof Error ? error.message : String(error) });
704
+ return markToolCallError(toolError({ error: error instanceof Error ? error.message : String(error) }), "internal_error");
213
705
  }
214
706
  });
215
707
  }
216
708
  function registerStatelyMcpTools(server, options = {}) {
217
- registerTool(server, "create_state_machine", {
709
+ registerTool(server, options, "create_state_machine", {
218
710
  title: "Create state machine",
219
711
  description: "Create a new XState v5 state machine from the user's description. The calling model must author complete XState v5 code and pass it as xstateCode. Return the XState source and, by default, directly viewable PNG diagrams. Prefer visualization because it makes behavior easier to verify; disable it only when the user explicitly wants code only, the client cannot display images, or a diagram adds no useful information.",
220
712
  inputSchema: {
@@ -223,13 +715,18 @@ function registerStatelyMcpTools(server, options = {}) {
223
715
  },
224
716
  outputSchema: createMachineOutputSchema
225
717
  }, async (input) => {
226
- const machine = xstateMachineResultSchema.parse(await callApi(options, "/v1/convert", {
718
+ const conversion = xstateConversionCheckSchema.parse(await callApi(options, "/v1/convert", {
227
719
  source: {
228
720
  format: "xstate",
229
721
  content: input.xstateCode
230
722
  },
231
- target: { format: "xstate" }
723
+ target: { format: "xgraph" }
232
724
  }));
725
+ const machine = {
726
+ format: "xstate",
727
+ content: input.xstateCode,
728
+ warnings: conversion.warnings
729
+ };
233
730
  return {
234
731
  machine,
235
732
  visualizations: input.visualization === false ? [] : await callApi(options, "/v1/visualize", {
@@ -245,7 +742,7 @@ function registerStatelyMcpTools(server, options = {}) {
245
742
  })
246
743
  };
247
744
  }, createMachineToolResult);
248
- registerTool(server, "validate_machine", {
745
+ registerTool(server, options, "validate_machine", {
249
746
  title: "Validate machine",
250
747
  description: "Validate an XState machine config or source string through the Stately API.",
251
748
  inputSchema: {
@@ -254,7 +751,7 @@ function registerStatelyMcpTools(server, options = {}) {
254
751
  source: sourceSchema.optional()
255
752
  }
256
753
  }, (input) => callApi(options, "/v1/validate", { source: normalizeSource(input) }));
257
- registerTool(server, "convert_machine", {
754
+ registerTool(server, options, "convert_machine", {
258
755
  title: "Convert machine",
259
756
  description: "Convert between XState, SCXML, XGraph, and D2 through the Stately API.",
260
757
  inputSchema: {
@@ -267,7 +764,7 @@ function registerStatelyMcpTools(server, options = {}) {
267
764
  source: toConversionSource(normalizeSource(input)),
268
765
  target: { format: input.to }
269
766
  }));
270
- registerTool(server, "diff_machines", {
767
+ registerTool(server, options, "diff_machines", {
271
768
  title: "Diff machines",
272
769
  description: "Compute a semantic diff between two machine documents through the Stately API.",
273
770
  inputSchema: {
@@ -280,7 +777,7 @@ function registerStatelyMcpTools(server, options = {}) {
280
777
  compare: toConversionSource(normalizeSource({ source: input.compare })),
281
778
  includePatches: input.includePatches
282
779
  }));
283
- registerTool(server, "apply_machine_patches", {
780
+ registerTool(server, options, "apply_machine_patches", {
284
781
  title: "Apply machine patches",
285
782
  description: "Apply semantic machine patches through the Stately API.",
286
783
  inputSchema: {
@@ -295,7 +792,7 @@ function registerStatelyMcpTools(server, options = {}) {
295
792
  patches: input.patches,
296
793
  target: input.to ? { format: input.to } : void 0
297
794
  }));
298
- registerTool(server, "generate_graph_paths", {
795
+ registerTool(server, options, "generate_graph_paths", {
299
796
  title: "Generate graph paths",
300
797
  description: "Generate shortest or simple paths through the Stately API.",
301
798
  inputSchema: {
@@ -318,7 +815,7 @@ function registerStatelyMcpTools(server, options = {}) {
318
815
  }
319
816
  });
320
817
  });
321
- registerTool(server, "simulate_machine", {
818
+ registerTool(server, options, "simulate_machine", {
322
819
  title: "Simulate machine",
323
820
  description: "Simulate an XState machine through a sequence of events; returns per-step state, context, and executed actions. Deterministic; guards are stubbed.",
324
821
  inputSchema: {
@@ -331,7 +828,7 @@ function registerStatelyMcpTools(server, options = {}) {
331
828
  source: toConversionSource(normalizeSource(input)),
332
829
  events: input.events
333
830
  }));
334
- registerTool(server, "visualize_machine", {
831
+ registerTool(server, options, "visualize_machine", {
335
832
  title: "Visualize machine",
336
833
  description: "Render machine hierarchy levels as directly viewable PNG images. Optionally select parent node ids.",
337
834
  inputSchema: {
@@ -351,7 +848,16 @@ function registerStatelyMcpTools(server, options = {}) {
351
848
  }
352
849
  }).then((visualizations) => ({ visualizations }));
353
850
  }, visualizeToolResult);
354
- registerTool(server, "analyze_machine", {
851
+ registerTool(server, options, "extract_machines_from_code", {
852
+ title: "Extract machines from code",
853
+ description: "Deterministically extract canonical XState machine configs from JavaScript or TypeScript source. XState 5/6 syntax is inferred unless explicitly provided.",
854
+ inputSchema: {
855
+ code: zod.z.string().min(1),
856
+ xstateVersion: xstateVersionSchema.optional().describe("Optional XState major-version override. Omit to infer from source syntax.")
857
+ },
858
+ outputSchema: codeExtractMachinesResponseSchema
859
+ }, (input) => callApi(options, "/v1/code/extract-machines", input));
860
+ registerTool(server, options, "analyze_machine", {
355
861
  title: "Analyze machine",
356
862
  description: "Deep analysis of machine logic (merges deterministic graph checks with AI review); may require an API key — configure via server options; for purely deterministic checks use validate_machine.",
357
863
  inputSchema: {
@@ -364,7 +870,7 @@ function registerStatelyMcpTools(server, options = {}) {
364
870
  source: toConversionSource(normalizeSource(input)),
365
871
  intent: input.intent
366
872
  }));
367
- registerTool(server, "generate_test_code", {
873
+ registerTool(server, options, "generate_test_code", {
368
874
  title: "Generate test code",
369
875
  description: "Generate a runnable vitest test file from the machine, one test per generated path. Guards and actions are stubbed; assertions use state paths.",
370
876
  inputSchema: {
@@ -396,22 +902,7 @@ function registerStatelyMcpTools(server, options = {}) {
396
902
  coverage: pathsResponse.coverage
397
903
  };
398
904
  });
399
- registerTool(server, "extract_machines_from_code", {
400
- title: "Extract machines from code",
401
- description: "Parse XState source code into XGraph through the Stately API.",
402
- inputSchema: { code: zod.z.string().min(1) }
403
- }, async (input) => {
404
- return { machines: [{
405
- format: "xgraph",
406
- graph: (await callApi(options, "/v1/convert", {
407
- source: {
408
- format: "xstate",
409
- content: input.code
410
- },
411
- target: { format: "xgraph" }
412
- })).content
413
- }] };
414
- });
905
+ if (options.includeResourceTools !== false) registerStatelyResourceTools(server, createResourceClientResolver(options), { onToolCall: options.onToolCall });
415
906
  return server;
416
907
  }
417
908
  function createStatelyMcpServer(options = {}) {
@@ -531,6 +1022,7 @@ function createStatelyMcpServer$1(options = {}) {
531
1022
  version: "0.1.0"
532
1023
  });
533
1024
  if (options.includeWorkspaceTools !== false) {
1025
+ registerToolCallObserver(server, "list_machines", options.onToolCall);
534
1026
  server.registerTool("list_machines", {
535
1027
  title: "list_machines",
536
1028
  description: "List candidate machine sources in the workspace.",
@@ -549,6 +1041,7 @@ function createStatelyMcpServer$1(options = {}) {
549
1041
  }]
550
1042
  };
551
1043
  });
1044
+ registerToolCallObserver(server, "get_machine", options.onToolCall);
552
1045
  server.registerTool("get_machine", {
553
1046
  title: "get_machine",
554
1047
  description: "Read a machine source from the workspace and return it.",
@@ -583,14 +1076,20 @@ async function main() {
583
1076
  if (require("url").pathToFileURL(__filename).href === `file://${process.argv[1]}`) main();
584
1077
 
585
1078
  //#endregion
1079
+ exports.ResourceClientError = ResourceClientError;
586
1080
  exports.createHostedStatelyMcpServer = createStatelyMcpServer;
1081
+ exports.createHttpResourceClient = createHttpResourceClient;
587
1082
  exports.createStatelyMcpServer = createStatelyMcpServer$1;
588
1083
  exports.formatForPath = formatForPath;
589
1084
  exports.generateTestCode = generateTestCode;
590
1085
  exports.getDefaultCredential = getDefaultCredential;
591
1086
  exports.isCandidate = isCandidate;
1087
+ exports.isResourceClientError = isResourceClientError;
592
1088
  exports.readOnlyAnnotations = readOnlyAnnotations;
593
1089
  exports.readWorkspaceMachine = readWorkspaceMachine;
594
1090
  exports.registerStatelyMcpTools = registerStatelyMcpTools;
1091
+ exports.registerStatelyResourceTools = registerStatelyResourceTools;
1092
+ exports.resourceApiV1BaseUrl = resourceApiV1BaseUrl;
1093
+ exports.resourceReadOnlyAnnotations = resourceReadOnlyAnnotations;
595
1094
  exports.walkWorkspace = walkWorkspace;
596
1095
  exports.workspaceFromArgv = workspaceFromArgv;