@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/README.md +67 -11
- package/dist/index.cjs +531 -32
- package/dist/index.d.cts +169 -4
- package/dist/index.d.mts +169 -4
- package/dist/index.mjs +526 -33
- package/package.json +4 -3
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
|
-
|
|
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(
|
|
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
|
|
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: "
|
|
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, "
|
|
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
|
-
|
|
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 };
|