@openfairygui/mcp 0.3.1 → 0.5.0-alpha.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.
@@ -1,8 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { ListToolsRequestSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
3
4
  import { createNodeBackendRuntime } from "@openfairygui/backend/node";
4
- import { BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_CONTRACT_VERSION } from "@openfairygui/backend";
5
5
  import { z } from "zod";
6
+ import { BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_CONTRACT_VERSION, BACKEND_DIAGNOSTICS_URI, BACKEND_DIAGNOSTIC_TEMPLATE, getBackendDiagnosticCatalog, getBackendDiagnosticGuide } from "@openfairygui/backend";
7
+ import { OPENFAIRYGUI_DOCS_INDEX_URI, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, getInstalledContractSnapshot, getInstalledDocumentationIndex, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema, readInstalledDocumentation } from "@openfairygui/backend/docs";
6
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
9
  import path from "node:path";
8
10
  import { pathToFileURL } from "node:url";
@@ -53,6 +55,11 @@ const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS = [
53
55
  description: "Guide a client through backend-owned revision checks without inventing operation grammar.",
54
56
  text: [
55
57
  "Use openfairygui_backend_get_session to read the current revision before mutation.",
58
+ "Use openfairygui_backend_get_project_outline for identities, then openfairygui_backend_query_entity for current properties at the returned revision.",
59
+ "For settings edits, query target {kind:\"project\"} or {kind:\"package\",selector:{packageId}}; copy entity.properties.settings, change the requested fields, and submit the complete settings to updateProjectSettings or updatePackageSettings.",
60
+ "Read openfairygui://contracts/operations and openfairygui://contracts/operations/{kind} for the current operation names and exact JSON parameters.",
61
+ "Call openfairygui_backend_preflight_transaction with the queried revision and planned operations to execute and discard an isolated preview; inspect the backend diagnostics.",
62
+ "A successful preview does not reserve a revision or guarantee save. Apply the same batch with expectedRevision set to the preview baseRevision; refresh properties and re-plan on stale revision.",
56
63
  "Call openfairygui_backend_apply_transaction with sessionId, expectedRevision, and backend/UAM-owned operations.",
57
64
  "If the backend returns a stale revision error, refresh the session snapshot and re-plan against the new revision.",
58
65
  "Do not invent selector grammar, transaction grammar, or operation payload semantics at the MCP layer."
@@ -96,6 +103,32 @@ function registerOpenFairyGuiBackendPrompts(server) {
96
103
  }, () => promptResult(definition.text));
97
104
  }
98
105
  //#endregion
106
+ //#region src/contract-schema.ts
107
+ const CONTRACT_SNAPSHOT = getInstalledContractSnapshot();
108
+ function contractObjectSchema(schema) {
109
+ const result = z.fromJSONSchema({
110
+ ...schema,
111
+ $defs: CONTRACT_SNAPSHOT.$defs
112
+ });
113
+ if (!(result instanceof z.ZodObject)) throw new TypeError("Tool contract must be an object");
114
+ return result;
115
+ }
116
+ /** Decode only generated Uint8Array locations; arbitrary JSON metadata is not rewritten. */
117
+ function decodeToolBytes(input, paths) {
118
+ if (!paths.length) return input;
119
+ const result = structuredClone(input);
120
+ function visit(value, parts) {
121
+ if (!parts.length) return value === null ? value : Uint8Array.from(value);
122
+ if (!value || typeof value !== "object") return value;
123
+ const [key, ...rest] = parts;
124
+ const record = value;
125
+ for (const name of key === "*" ? Object.keys(record) : [key]) if (Object.hasOwn(record, name)) record[name] = visit(record[name], rest);
126
+ return value;
127
+ }
128
+ for (const parts of paths) visit(result, parts);
129
+ return result;
130
+ }
131
+ //#endregion
99
132
  //#region src/resource-definitions.ts
100
133
  const JSON_MIME_TYPE = "application/json";
101
134
  function firstVariable(value) {
@@ -110,12 +143,69 @@ function jsonResource(uri, backendResult) {
110
143
  }
111
144
  const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
112
145
  const OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES = [
146
+ "openfairygui://docs/methods/{method}",
147
+ "openfairygui://docs/cli/{command}",
148
+ BACKEND_DIAGNOSTIC_TEMPLATE,
149
+ OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE,
113
150
  "openfairygui://backend/session/{sessionId}",
114
151
  "openfairygui://backend/session/{sessionId}/outline",
115
152
  "openfairygui://backend/cache/{sessionId}",
116
153
  "openfairygui://backend/job/{sessionId}/{jobId}"
117
154
  ];
118
155
  function registerOpenFairyGuiBackendResources(server, runtime) {
156
+ server.registerResource("openfairygui_docs_index", OPENFAIRYGUI_DOCS_INDEX_URI, {
157
+ title: "Installed Documentation",
158
+ description: "Offline documentation IDs, URIs, installed package version and contract digest shared with the CLI.",
159
+ mimeType: JSON_MIME_TYPE
160
+ }, (uri) => jsonResource(uri, getInstalledDocumentationIndex()));
161
+ function installedDocument(uri, id) {
162
+ const document = readInstalledDocumentation(id);
163
+ return { contents: [{
164
+ uri: uri.toString(),
165
+ mimeType: document.mimeType,
166
+ text: document.text
167
+ }] };
168
+ }
169
+ for (const id of [
170
+ "workflow",
171
+ "restore-limits",
172
+ "skill",
173
+ "contracts"
174
+ ]) server.registerResource(`openfairygui_docs_${id}`, `openfairygui://docs/${id}`, {
175
+ title: `Installed ${id}`,
176
+ description: "Read the installed-version corpus without repository or network access.",
177
+ mimeType: id === "contracts" ? JSON_MIME_TYPE : "text/markdown"
178
+ }, (uri) => installedDocument(uri, id));
179
+ server.registerResource("openfairygui_docs_method", new ResourceTemplate("openfairygui://docs/methods/{method}", { list: void 0 }), {
180
+ title: "Installed Method Contract",
181
+ description: "Read self-contained Backend/MCP wire input/output schemas and metadata.",
182
+ mimeType: JSON_MIME_TYPE
183
+ }, (uri, variables) => installedDocument(uri, `methods/${firstVariable(variables.method)}`));
184
+ server.registerResource("openfairygui_diagnostic_catalog", BACKEND_DIAGNOSTICS_URI, {
185
+ title: "Diagnostic Recovery Catalog",
186
+ description: "Complete formal diagnostic ownership and recovery guidance; never automatic repair.",
187
+ mimeType: JSON_MIME_TYPE
188
+ }, (uri) => jsonResource(uri, getBackendDiagnosticCatalog()));
189
+ server.registerResource("openfairygui_docs_cli", new ResourceTemplate("openfairygui://docs/cli/{command}", { list: void 0 }), {
190
+ title: "Installed CLI Output Contract",
191
+ description: "Read a generated, self-contained CLI JSON envelope schema.",
192
+ mimeType: JSON_MIME_TYPE
193
+ }, (uri, variables) => installedDocument(uri, `cli/${decodeURIComponent(firstVariable(variables.command))}`));
194
+ server.registerResource("openfairygui_diagnostic_guide", new ResourceTemplate(BACKEND_DIAGNOSTIC_TEMPLATE, { list: void 0 }), {
195
+ title: "Diagnostic Recovery Guide",
196
+ description: "Read the recovery boundary for one stable diagnostic code.",
197
+ mimeType: JSON_MIME_TYPE
198
+ }, (uri, variables) => jsonResource(uri, getBackendDiagnosticGuide(firstVariable(variables.code))));
199
+ server.registerResource("openfairygui_operation_catalog", OPENFAIRYGUI_OPERATION_CATALOG_URI, {
200
+ title: "UAM Operation Catalog",
201
+ description: "Discover current operations and their generated JSON schemas.",
202
+ mimeType: JSON_MIME_TYPE
203
+ }, (uri) => jsonResource(uri, getOpenFairyGuiOperationCatalog()));
204
+ server.registerResource("openfairygui_operation_schema", new ResourceTemplate(OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, { list: void 0 }), {
205
+ title: "UAM Operation Schema",
206
+ description: "Read the precise Core-derived JSON wire schema for one operation. Structure is not semantic preflight.",
207
+ mimeType: JSON_MIME_TYPE
208
+ }, (uri, variables) => jsonResource(uri, getOpenFairyGuiOperationSchema(firstVariable(variables.kind))));
119
209
  server.registerResource("openfairygui_backend_capabilities", OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, {
120
210
  title: "OpenFairyGUI Backend Capabilities",
121
211
  description: "Read the backend capability and version envelope as JSON.",
@@ -146,348 +236,13 @@ function registerOpenFairyGuiBackendResources(server, runtime) {
146
236
  })));
147
237
  }
148
238
  //#endregion
149
- //#region src/tool-definitions.ts
150
- const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
151
- const OPENFAIRYGUI_BACKEND_TOOL_NAMES = [
152
- "openfairygui_backend_get_capabilities",
153
- "openfairygui_backend_open_session",
154
- "openfairygui_backend_open_project_session",
155
- "openfairygui_backend_get_session",
156
- "openfairygui_backend_get_project_outline",
157
- "openfairygui_backend_validate_session",
158
- "openfairygui_backend_apply_transaction",
159
- "openfairygui_backend_save_session",
160
- "openfairygui_backend_materialize_session",
161
- "openfairygui_backend_close_session",
162
- "openfairygui_backend_get_events",
163
- "openfairygui_backend_get_job",
164
- "openfairygui_backend_list_jobs",
165
- "openfairygui_backend_cancel_job",
166
- "openfairygui_backend_get_cache_snapshot",
167
- "openfairygui_backend_refresh_cache"
168
- ];
169
- const sessionId = z.string().min(1);
170
- const jobId = z.string().min(1);
171
- const expectedRevision = z.number().int().nonnegative();
172
- const limit = z.number().int().nonnegative().optional();
173
- const identifier = z.string().min(1).max(256);
174
- function isOpenFairyGuiMcpPayloadWithinBudget(root) {
175
- const pending = [{
176
- value: root,
177
- depth: 0
178
- }];
179
- let nodes = 0;
180
- while (pending.length > 0) {
181
- const { value, depth } = pending.pop();
182
- nodes += 1;
183
- if (nodes > 1e5 || depth > 32) return false;
184
- if (value === null || typeof value === "boolean") continue;
185
- if (typeof value === "number") {
186
- if (!Number.isFinite(value)) return false;
187
- continue;
188
- }
189
- if (typeof value === "string") {
190
- if (value.length > 1e6) return false;
191
- continue;
192
- }
193
- if (value instanceof Uint8Array) {
194
- if (value.byteLength > 8 * 1024 * 1024) return false;
195
- continue;
196
- }
197
- if (Array.isArray(value)) {
198
- if (value.length > 1e4) return false;
199
- for (const child of value) pending.push({
200
- value: child,
201
- depth: depth + 1
202
- });
203
- continue;
204
- }
205
- if (typeof value !== "object") return false;
206
- const entries = Object.entries(value);
207
- if (entries.length > 1e4 || entries.some(([key]) => key.length > 256)) return false;
208
- for (const [, child] of entries) pending.push({
209
- value: child,
210
- depth: depth + 1
211
- });
212
- }
213
- return true;
214
- }
215
- const boundedPayload = z.json();
216
- const bytes = z.array(z.number().int().min(0).max(255)).max(8 * 1024 * 1024);
217
- const packageSelector = z.object({ packageId: identifier });
218
- const resourceSelector = z.object({
219
- packageId: identifier,
220
- resourceId: identifier
221
- });
222
- const componentSelector = z.object({
223
- packageId: identifier,
224
- componentResourceId: identifier
225
- });
226
- const displayNodeSelector = componentSelector.extend({ displayNodeId: identifier });
227
- const controllerSelector = componentSelector.extend({ controllerName: identifier });
228
- const transitionSelector = componentSelector.extend({ transitionName: identifier });
229
- const folderSelector = packageSelector.extend({
230
- branch: z.string().max(256).optional(),
231
- path: z.string().min(1).max(4096)
232
- });
233
- const operationBase = { opId: identifier.optional() };
234
- const operation = z.discriminatedUnion("kind", [
235
- z.object({
236
- ...operationBase,
237
- kind: z.literal("updateProjectSettings"),
238
- settings: boundedPayload
239
- }),
240
- z.object({
241
- ...operationBase,
242
- kind: z.literal("updatePackageSettings"),
243
- selector: packageSelector,
244
- settings: boundedPayload
245
- }),
246
- z.object({
247
- ...operationBase,
248
- kind: z.literal("renameResource"),
249
- selector: resourceSelector,
250
- newName: identifier
251
- }),
252
- z.object({
253
- ...operationBase,
254
- kind: z.literal("moveResource"),
255
- selector: resourceSelector,
256
- toPath: z.string().max(4096)
257
- }),
258
- z.object({
259
- ...operationBase,
260
- kind: z.literal("setResourceFavorite"),
261
- selector: resourceSelector,
262
- favorite: z.boolean()
263
- }),
264
- z.object({
265
- ...operationBase,
266
- kind: z.literal("setResourceFolderFavorite"),
267
- selector: folderSelector,
268
- favorite: z.boolean()
269
- }),
270
- z.object({
271
- ...operationBase,
272
- kind: z.literal("setResourceFolderAtlas"),
273
- selector: folderSelector,
274
- atlas: z.string().max(32)
275
- }),
276
- z.object({
277
- ...operationBase,
278
- kind: z.literal("setResourceExported"),
279
- selector: resourceSelector,
280
- exported: z.boolean()
281
- }),
282
- z.object({
283
- ...operationBase,
284
- kind: z.literal("addResourceFolder"),
285
- selector: packageSelector,
286
- path: z.string().max(4096),
287
- branch: z.string().max(256).optional(),
288
- favorite: z.boolean().optional(),
289
- atlas: z.string().max(32).optional()
290
- }),
291
- z.object({
292
- ...operationBase,
293
- kind: z.literal("renameResourceFolder"),
294
- selector: folderSelector,
295
- newName: identifier
296
- }),
297
- z.object({
298
- ...operationBase,
299
- kind: z.literal("moveResourceFolder"),
300
- selector: folderSelector,
301
- toPath: z.string().max(4096)
302
- }),
303
- z.object({
304
- ...operationBase,
305
- kind: z.literal("removeResourceFolder"),
306
- selector: folderSelector
307
- }),
308
- z.object({
309
- ...operationBase,
310
- kind: z.literal("setImageResourceProps"),
311
- selector: resourceSelector,
312
- props: boundedPayload
313
- }),
314
- z.object({
315
- ...operationBase,
316
- kind: z.literal("addResource"),
317
- selector: packageSelector,
318
- resource: boundedPayload,
319
- atIndex: z.number().int().nonnegative().optional()
320
- }),
321
- z.object({
322
- ...operationBase,
323
- kind: z.literal("addBranch"),
324
- branch: identifier
325
- }),
326
- z.object({
327
- ...operationBase,
328
- kind: z.literal("renameBranch"),
329
- selector: z.object({ branch: identifier }),
330
- newName: identifier
331
- }),
332
- z.object({
333
- ...operationBase,
334
- kind: z.literal("removeBranch"),
335
- selector: z.object({ branch: identifier })
336
- }),
337
- z.object({
338
- ...operationBase,
339
- kind: z.literal("addPackage"),
340
- package: boundedPayload,
341
- atIndex: z.number().int().nonnegative()
342
- }),
343
- z.object({
344
- ...operationBase,
345
- kind: z.literal("renamePackage"),
346
- selector: packageSelector,
347
- newName: identifier
348
- }),
349
- z.object({
350
- ...operationBase,
351
- kind: z.literal("removePackage"),
352
- selector: packageSelector
353
- }),
354
- z.object({
355
- ...operationBase,
356
- kind: z.literal("addComponent"),
357
- selector: packageSelector,
358
- component: boundedPayload,
359
- atIndex: z.number().int().nonnegative()
360
- }),
361
- z.object({
362
- ...operationBase,
363
- kind: z.literal("removeComponent"),
364
- selector: componentSelector
365
- }),
366
- z.object({
367
- ...operationBase,
368
- kind: z.literal("moveComponent"),
369
- selector: componentSelector,
370
- toPackageId: identifier,
371
- toIndex: z.number().int().nonnegative()
372
- }),
373
- z.object({
374
- ...operationBase,
375
- kind: z.literal("replaceResourceBytes"),
376
- selector: resourceSelector,
377
- sourceBytes: bytes
378
- }),
379
- z.object({
380
- ...operationBase,
381
- kind: z.literal("removeResource"),
382
- selector: resourceSelector
383
- }),
384
- z.object({
385
- ...operationBase,
386
- kind: z.literal("setDisplayNodeProps"),
387
- selector: displayNodeSelector,
388
- props: boundedPayload
389
- }),
390
- z.object({
391
- ...operationBase,
392
- kind: z.literal("setComponentProps"),
393
- selector: componentSelector,
394
- props: boundedPayload
395
- }),
396
- z.object({
397
- ...operationBase,
398
- kind: z.literal("attachDisplayNode"),
399
- selector: componentSelector,
400
- atIndex: z.number().int().nonnegative(),
401
- node: boundedPayload
402
- }),
403
- z.object({
404
- ...operationBase,
405
- kind: z.literal("detachDisplayNode"),
406
- selector: displayNodeSelector
407
- }),
408
- ...["addController", "updateController"].map((kind) => z.object({
409
- ...operationBase,
410
- kind: z.literal(kind),
411
- selector: controllerSelector,
412
- controller: boundedPayload
413
- })),
414
- z.object({
415
- ...operationBase,
416
- kind: z.literal("removeController"),
417
- selector: controllerSelector
418
- }),
419
- ...["addTransition", "updateTransition"].map((kind) => z.object({
420
- ...operationBase,
421
- kind: z.literal(kind),
422
- selector: transitionSelector,
423
- transition: boundedPayload
424
- })),
425
- z.object({
426
- ...operationBase,
427
- kind: z.literal("removeTransition"),
428
- selector: transitionSelector
429
- }),
430
- ...[
431
- "addLookGear",
432
- "updateLookGear",
433
- "addGear",
434
- "updateGear"
435
- ].map((kind) => z.object({
436
- ...operationBase,
437
- kind: z.literal(kind),
438
- selector: displayNodeSelector.extend({
439
- kind: identifier,
440
- controllerName: identifier
441
- }),
442
- gear: boundedPayload
443
- })),
444
- ...["removeLookGear", "removeGear"].map((kind) => z.object({
445
- ...operationBase,
446
- kind: z.literal(kind),
447
- selector: displayNodeSelector.extend({
448
- kind: identifier,
449
- controllerName: identifier
450
- })
451
- }))
452
- ]);
453
- const project = z.object({
454
- projectId: identifier,
455
- projectType: z.number().int(),
456
- version: z.string().max(256),
457
- branches: z.array(z.string().max(256)).max(256),
458
- settings: boundedPayload,
459
- packages: z.array(z.object({
460
- id: identifier,
461
- name: identifier,
462
- compressPNG: z.boolean().nullable(),
463
- jpegQuality: z.number().finite().nullable(),
464
- publish: boundedPayload.nullable(),
465
- branchNames: z.array(z.string().max(256)).max(256),
466
- folders: z.array(boundedPayload).max(1e4),
467
- resources: z.array(boundedPayload).max(1e5)
468
- })).max(1e3)
469
- });
470
- const OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA = z.object({ backendResult: z.discriminatedUnion("ok", [z.object({
471
- ok: z.literal(true),
472
- data: boundedPayload,
473
- meta: boundedPayload
474
- }), z.object({
475
- ok: z.literal(false),
476
- error: z.object({
477
- code: identifier,
478
- message: z.string().max(1e6)
479
- }).passthrough(),
480
- meta: boundedPayload,
481
- session: boundedPayload.optional()
482
- })]) });
483
- const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
239
+ //#region src/tool-metadata.ts
240
+ const OPENFAIRYGUI_BACKEND_TOOL_METADATA = [
484
241
  {
485
242
  name: "openfairygui_backend_get_capabilities",
486
243
  backendMethod: "getCapabilities",
487
244
  title: "Get Backend Capabilities",
488
245
  description: "Return the OpenFairyGUI backend capability, version, and service-plane snapshot.",
489
- inputSchema: z.object({}),
490
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
491
246
  annotations: {
492
247
  readOnlyHint: true,
493
248
  idempotentHint: true,
@@ -499,8 +254,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
499
254
  backendMethod: "openSession",
500
255
  title: "Open Backend Session",
501
256
  description: "Open a FairyGUI project through BackendRuntime and acquire its backend-local session lock.",
502
- inputSchema: z.object({ projectPath: z.string().min(1) }),
503
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
504
257
  annotations: {
505
258
  readOnlyHint: false,
506
259
  idempotentHint: false,
@@ -512,13 +265,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
512
265
  backendMethod: "openProjectSession",
513
266
  title: "Open Project Session",
514
267
  description: "Open a browser-safe backend session from an already loaded UAM project without filesystem access.",
515
- inputSchema: z.object({
516
- project,
517
- sessionId: z.string().min(1).optional(),
518
- canonicalProjectPath: z.string().min(1).optional(),
519
- canonicalPathKey: z.string().min(1).optional()
520
- }),
521
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
522
268
  annotations: {
523
269
  readOnlyHint: false,
524
270
  idempotentHint: false,
@@ -530,8 +276,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
530
276
  backendMethod: "getSession",
531
277
  title: "Get Backend Session",
532
278
  description: "Return a backend session snapshot by session id.",
533
- inputSchema: z.object({ sessionId }),
534
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
535
279
  annotations: {
536
280
  readOnlyHint: true,
537
281
  idempotentHint: true,
@@ -543,8 +287,41 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
543
287
  backendMethod: "getProjectOutline",
544
288
  title: "Get Project Outline",
545
289
  description: "Return a revision-bound project/package/resource/component identity outline without source bytes or full property payloads.",
546
- inputSchema: z.object({ sessionId }),
547
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
290
+ annotations: {
291
+ readOnlyHint: true,
292
+ idempotentHint: true,
293
+ openWorldHint: false
294
+ }
295
+ },
296
+ {
297
+ name: "openfairygui_backend_query_entity",
298
+ backendMethod: "queryEntity",
299
+ title: "Query Entity Properties",
300
+ description: "Read revision-bound project/package settings, resource, component-property, display-node, controller (including pages/actions), or transition (including items) snapshots. Project queries use only kind; other queries use formal selectors. Settings snapshots include the complete settings payload for updateProjectSettings/updatePackageSettings. No source bytes; fixed projection with explicit response limits.",
301
+ annotations: {
302
+ readOnlyHint: true,
303
+ idempotentHint: true,
304
+ openWorldHint: false
305
+ }
306
+ },
307
+ {
308
+ name: "openfairygui_backend_read_session_state",
309
+ backendMethod: "readSessionState",
310
+ title: "Read Session State",
311
+ description: "Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.",
312
+ maxResponseBytes: 16777216,
313
+ annotations: {
314
+ readOnlyHint: true,
315
+ idempotentHint: true,
316
+ openWorldHint: false
317
+ }
318
+ },
319
+ {
320
+ name: "openfairygui_backend_read_resource_bytes",
321
+ backendMethod: "readResourceBytes",
322
+ title: "Read Resource Bytes",
323
+ description: "Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.",
324
+ maxResponseBytes: 16777216,
548
325
  annotations: {
549
326
  readOnlyHint: true,
550
327
  idempotentHint: true,
@@ -556,8 +333,17 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
556
333
  backendMethod: "validateSession",
557
334
  title: "Validate Project Session",
558
335
  description: "Validate the current session project structure, references, paths, and available source bytes without writing files.",
559
- inputSchema: z.object({ sessionId }),
560
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
336
+ annotations: {
337
+ readOnlyHint: true,
338
+ idempotentHint: true,
339
+ openWorldHint: false
340
+ }
341
+ },
342
+ {
343
+ name: "openfairygui_backend_preflight_transaction",
344
+ backendMethod: "preflightTransaction",
345
+ title: "Preview UAM Transaction",
346
+ description: "Execute a revision-checked operation batch on an isolated project snapshot and discard the result. Returns the base revision and Core diagnostics; does not write, reserve a revision, or guarantee a later apply/save.",
561
347
  annotations: {
562
348
  readOnlyHint: true,
563
349
  idempotentHint: true,
@@ -569,12 +355,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
569
355
  backendMethod: "applyTransaction",
570
356
  title: "Apply UAM Transaction",
571
357
  description: "Apply a bounded, revision-checked UAM operation batch using the Core transaction discriminants.",
572
- inputSchema: z.object({
573
- sessionId,
574
- expectedRevision,
575
- operations: z.array(operation).min(1).max(1e3)
576
- }),
577
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
578
358
  annotations: {
579
359
  readOnlyHint: false,
580
360
  destructiveHint: true,
@@ -587,14 +367,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
587
367
  backendMethod: "saveSession",
588
368
  title: "Save Backend Session",
589
369
  description: "Write the current backend session through its coordinated save path; Node uses an atomic staged directory swap.",
590
- inputSchema: z.object({
591
- sessionId,
592
- expectedRevision: expectedRevision.optional(),
593
- targetPath: z.string().min(1).optional(),
594
- force: z.boolean().optional(),
595
- mode: z.literal("materializeCleanSession").optional()
596
- }),
597
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
598
370
  annotations: {
599
371
  readOnlyHint: false,
600
372
  destructiveHint: true,
@@ -607,13 +379,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
607
379
  backendMethod: "materializeSession",
608
380
  title: "Materialize Backend Session",
609
381
  description: "Force materialize the current backend session project through the configured project storage without requiring a dirty edit revision.",
610
- inputSchema: z.object({
611
- sessionId,
612
- expectedRevision: expectedRevision.optional(),
613
- mode: z.literal("fullProject").optional(),
614
- reason: z.string().min(1).optional()
615
- }),
616
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
617
382
  annotations: {
618
383
  readOnlyHint: false,
619
384
  destructiveHint: true,
@@ -626,8 +391,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
626
391
  backendMethod: "closeSession",
627
392
  title: "Close Backend Session",
628
393
  description: "Close a backend session and release its backend-local session lock.",
629
- inputSchema: z.object({ sessionId }),
630
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
631
394
  annotations: {
632
395
  readOnlyHint: false,
633
396
  idempotentHint: false,
@@ -639,12 +402,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
639
402
  backendMethod: "getEvents",
640
403
  title: "Get Runtime Events",
641
404
  description: "Poll backend runtime events for a session using the backend P2 event cursor contract.",
642
- inputSchema: z.object({
643
- sessionId,
644
- after: z.string().optional(),
645
- limit
646
- }),
647
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
648
405
  annotations: {
649
406
  readOnlyHint: true,
650
407
  idempotentHint: true,
@@ -656,11 +413,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
656
413
  backendMethod: "getJob",
657
414
  title: "Get Runtime Job",
658
415
  description: "Return a backend runtime job snapshot by session and backend-local job id.",
659
- inputSchema: z.object({
660
- sessionId,
661
- jobId
662
- }),
663
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
664
416
  annotations: {
665
417
  readOnlyHint: true,
666
418
  idempotentHint: true,
@@ -672,21 +424,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
672
424
  backendMethod: "listJobs",
673
425
  title: "List Runtime Jobs",
674
426
  description: "List backend runtime jobs for a session with backend P2 status/kind filters.",
675
- inputSchema: z.object({
676
- sessionId,
677
- status: z.enum([
678
- "queued",
679
- "running",
680
- "completed",
681
- "failed",
682
- "cancelled",
683
- "active",
684
- "terminal"
685
- ]).optional(),
686
- kind: z.literal("cache.refresh").optional(),
687
- limit
688
- }),
689
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
690
427
  annotations: {
691
428
  readOnlyHint: true,
692
429
  idempotentHint: true,
@@ -698,11 +435,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
698
435
  backendMethod: "cancelJob",
699
436
  title: "Cancel Runtime Job",
700
437
  description: "Request cooperative cancellation for a backend runtime job.",
701
- inputSchema: z.object({
702
- sessionId,
703
- jobId
704
- }),
705
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
706
438
  annotations: {
707
439
  readOnlyHint: false,
708
440
  idempotentHint: false,
@@ -714,8 +446,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
714
446
  backendMethod: "getCacheSnapshot",
715
447
  title: "Get Cache Snapshot",
716
448
  description: "Return the backend P2 derived read-only cache snapshot for a session.",
717
- inputSchema: z.object({ sessionId }),
718
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
719
449
  annotations: {
720
450
  readOnlyHint: true,
721
451
  idempotentHint: true,
@@ -727,15 +457,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
727
457
  backendMethod: "refreshCache",
728
458
  title: "Refresh Cache",
729
459
  description: "Create a backend P2 cache.refresh job for the session cache snapshot.",
730
- inputSchema: z.object({
731
- sessionId,
732
- reason: z.enum([
733
- "manual",
734
- "session_open",
735
- "after_save"
736
- ]).optional()
737
- }),
738
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
739
460
  annotations: {
740
461
  readOnlyHint: false,
741
462
  idempotentHint: false,
@@ -744,9 +465,62 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
744
465
  }
745
466
  ];
746
467
  //#endregion
468
+ //#region src/tool-definitions.ts
469
+ const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
470
+ const OPENFAIRYGUI_BACKEND_TOOL_NAMES = OPENFAIRYGUI_BACKEND_TOOL_METADATA.map((entry) => entry.name);
471
+ function isOpenFairyGuiMcpPayloadWithinBudget(root) {
472
+ const pending = [{
473
+ value: root,
474
+ depth: 0
475
+ }];
476
+ let nodes = 0;
477
+ while (pending.length > 0) {
478
+ const { value, depth } = pending.pop();
479
+ nodes += 1;
480
+ if (nodes > 1e5 || depth > 32) return false;
481
+ if (value === null || typeof value === "boolean") continue;
482
+ if (typeof value === "number") {
483
+ if (!Number.isFinite(value)) return false;
484
+ continue;
485
+ }
486
+ if (typeof value === "string") {
487
+ if (value.length > 1e6) return false;
488
+ continue;
489
+ }
490
+ if (value instanceof Uint8Array) {
491
+ if (value.byteLength > 8 * 1024 * 1024) return false;
492
+ continue;
493
+ }
494
+ if (Array.isArray(value)) {
495
+ if (value.length > 1e4) return false;
496
+ for (const child of value) pending.push({
497
+ value: child,
498
+ depth: depth + 1
499
+ });
500
+ continue;
501
+ }
502
+ if (typeof value !== "object") return false;
503
+ const entries = Object.entries(value);
504
+ if (entries.length > 1e4 || entries.some(([key]) => key.length > 256)) return false;
505
+ for (const [, child] of entries) pending.push({
506
+ value: child,
507
+ depth: depth + 1
508
+ });
509
+ }
510
+ return true;
511
+ }
512
+ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = OPENFAIRYGUI_BACKEND_TOOL_METADATA.map((metadata) => {
513
+ const contract = CONTRACT_SNAPSHOT.tools[metadata.backendMethod];
514
+ return {
515
+ ...metadata,
516
+ inputSchema: contractObjectSchema(contract.input),
517
+ outputSchema: contractObjectSchema(contract.output)
518
+ };
519
+ });
520
+ //#endregion
747
521
  //#region src/tool-handler.ts
748
- function jsonResult(payload, isError = false) {
749
- const text = JSON.stringify(payload, null, 2);
522
+ function jsonResult(payload, isError = false, compact = false) {
523
+ const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, compact ? void 0 : 2);
750
524
  const wirePayload = JSON.parse(text);
751
525
  return {
752
526
  content: [{
@@ -780,113 +554,32 @@ function unhandledBackendFailure(startedAt) {
780
554
  }
781
555
  async function callOpenFairyGuiBackendTool(runtime, name, input) {
782
556
  if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
557
+ const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
558
+ if (!definition) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
559
+ const decoded = decodeToolBytes(definition.inputSchema.parse(input), CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
783
560
  const startedAt = Date.now();
784
- let result;
785
561
  try {
786
- switch (name) {
787
- case "openfairygui_backend_get_capabilities":
788
- result = runtime.getCapabilities();
789
- break;
790
- case "openfairygui_backend_open_session":
791
- result = await runtime.openSession({ projectPath: String(input.projectPath) });
792
- break;
793
- case "openfairygui_backend_open_project_session":
794
- result = runtime.openProjectSession({
795
- project: input.project,
796
- sessionId: input.sessionId === void 0 ? void 0 : String(input.sessionId),
797
- canonicalProjectPath: input.canonicalProjectPath === void 0 ? void 0 : String(input.canonicalProjectPath),
798
- canonicalPathKey: input.canonicalPathKey === void 0 ? void 0 : String(input.canonicalPathKey)
799
- });
800
- break;
801
- case "openfairygui_backend_get_session":
802
- result = runtime.getSession({ sessionId: String(input.sessionId) });
803
- break;
804
- case "openfairygui_backend_get_project_outline":
805
- result = runtime.getProjectOutline({ sessionId: String(input.sessionId) });
806
- break;
807
- case "openfairygui_backend_validate_session":
808
- result = runtime.validateSession({ sessionId: String(input.sessionId) });
809
- break;
810
- case "openfairygui_backend_apply_transaction": {
811
- const operations = input.operations.map((operation) => operation.kind === "replaceResourceBytes" ? {
812
- ...operation,
813
- sourceBytes: new Uint8Array(operation.sourceBytes)
814
- } : operation);
815
- result = await runtime.applyTransaction({
816
- sessionId: String(input.sessionId),
817
- expectedRevision: Number(input.expectedRevision),
818
- operations
819
- });
820
- break;
562
+ const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
563
+ let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== void 0);
564
+ if (definition.maxResponseBytes !== void 0 && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes) response = jsonResult({
565
+ ...unhandledBackendFailure(startedAt),
566
+ error: {
567
+ code: "mcp_response_budget_exceeded",
568
+ message: "The complete MCP tool response exceeds its byte limit.",
569
+ maxBytes: definition.maxResponseBytes
821
570
  }
822
- case "openfairygui_backend_save_session":
823
- result = await runtime.saveSession({
824
- sessionId: String(input.sessionId),
825
- expectedRevision: input.expectedRevision === void 0 ? void 0 : Number(input.expectedRevision),
826
- targetPath: input.targetPath === void 0 ? void 0 : String(input.targetPath),
827
- force: input.force === void 0 ? void 0 : Boolean(input.force),
828
- mode: input.mode
829
- });
830
- break;
831
- case "openfairygui_backend_materialize_session":
832
- result = await runtime.materializeSession({
833
- sessionId: String(input.sessionId),
834
- expectedRevision: input.expectedRevision === void 0 ? void 0 : Number(input.expectedRevision),
835
- mode: input.mode,
836
- reason: input.reason === void 0 ? void 0 : String(input.reason)
837
- });
838
- break;
839
- case "openfairygui_backend_close_session":
840
- result = await runtime.closeSession({ sessionId: String(input.sessionId) });
841
- break;
842
- case "openfairygui_backend_get_events":
843
- result = runtime.getEvents({
844
- sessionId: String(input.sessionId),
845
- after: input.after === void 0 ? void 0 : String(input.after),
846
- limit: input.limit === void 0 ? void 0 : Number(input.limit)
847
- });
848
- break;
849
- case "openfairygui_backend_get_job":
850
- result = runtime.getJob({
851
- sessionId: String(input.sessionId),
852
- jobId: String(input.jobId)
853
- });
854
- break;
855
- case "openfairygui_backend_list_jobs":
856
- result = runtime.listJobs({
857
- sessionId: String(input.sessionId),
858
- status: input.status,
859
- kind: input.kind,
860
- limit: input.limit === void 0 ? void 0 : Number(input.limit)
861
- });
862
- break;
863
- case "openfairygui_backend_cancel_job":
864
- result = runtime.cancelJob({
865
- sessionId: String(input.sessionId),
866
- jobId: String(input.jobId)
867
- });
868
- break;
869
- case "openfairygui_backend_get_cache_snapshot":
870
- result = runtime.getCacheSnapshot({ sessionId: String(input.sessionId) });
871
- break;
872
- case "openfairygui_backend_refresh_cache":
873
- result = runtime.refreshCache({
874
- sessionId: String(input.sessionId),
875
- reason: input.reason
876
- });
877
- break;
878
- default: throw new Error(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
879
- }
571
+ }, true);
572
+ definition.outputSchema.parse(response.structuredContent);
573
+ return response;
880
574
  } catch {
881
575
  return jsonResult(unhandledBackendFailure(startedAt), true);
882
576
  }
883
- return jsonResult(result, isBackendFailure(result));
884
577
  }
885
578
  //#endregion
886
579
  //#region src/server.ts
887
580
  const require = createRequire(import.meta.url);
888
581
  function getInjectedPackageVersion() {
889
- const version = "0.3.1";
582
+ const version = "0.5.0-alpha.1";
890
583
  return typeof version === "string" && true ? version : null;
891
584
  }
892
585
  function readPackageVersion() {
@@ -905,17 +598,39 @@ function createOpenFairyGuiMcpServer(options = {}) {
905
598
  name: options.name ?? "openfairygui-mcp",
906
599
  version: options.version ?? PACKAGE_VERSION
907
600
  });
908
- for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) server.registerTool(definition.name, {
909
- title: definition.title,
910
- description: definition.description,
911
- inputSchema: definition.inputSchema,
912
- outputSchema: definition.outputSchema,
913
- annotations: definition.annotations,
914
- _meta: {
915
- "openfairygui/backendMethod": definition.backendMethod,
916
- "openfairygui/adapter": "thin-backend-p2"
917
- }
918
- }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
601
+ const tools = [];
602
+ for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) {
603
+ const metadata = {
604
+ name: definition.name,
605
+ title: definition.title,
606
+ description: definition.description,
607
+ annotations: definition.annotations,
608
+ _meta: {
609
+ "openfairygui/backendMethod": definition.backendMethod,
610
+ "openfairygui/adapter": "thin-backend-p2",
611
+ "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
612
+ }
613
+ };
614
+ server.registerTool(definition.name, {
615
+ ...metadata,
616
+ inputSchema: definition.inputSchema,
617
+ outputSchema: definition.outputSchema
618
+ }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
619
+ tools.push(ToolSchema.parse({
620
+ ...metadata,
621
+ inputSchema: z.toJSONSchema(definition.inputSchema, {
622
+ target: "draft-07",
623
+ io: "input",
624
+ reused: "ref"
625
+ }),
626
+ outputSchema: z.toJSONSchema(definition.outputSchema, {
627
+ target: "draft-07",
628
+ io: "output",
629
+ reused: "ref"
630
+ })
631
+ }));
632
+ }
633
+ server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
919
634
  registerOpenFairyGuiBackendResources(server, runtime);
920
635
  registerOpenFairyGuiBackendPrompts(server);
921
636
  return server;
@@ -931,4 +646,4 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
931
646
  process.exitCode = 1;
932
647
  });
933
648
  //#endregion
934
- export { OPENFAIRYGUI_BACKEND_TOOL_NAMES as a, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI as c, OPENFAIRYGUI_BACKEND_PROMPT_NAMES as d, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS as i, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES as l, createOpenFairyGuiMcpServer as n, OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA as o, callOpenFairyGuiBackendTool as r, OPENFAIRYGUI_BACKEND_TOOL_PREFIX as s, connectOpenFairyGuiMcpStdio as t, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS as u };
649
+ export { OPENFAIRYGUI_BACKEND_TOOL_NAMES as a, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES as c, getOpenFairyGuiOperationCatalog as d, getOpenFairyGuiOperationSchema as f, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS as i, OPENFAIRYGUI_OPERATION_CATALOG_URI as l, OPENFAIRYGUI_BACKEND_PROMPT_NAMES as m, createOpenFairyGuiMcpServer as n, OPENFAIRYGUI_BACKEND_TOOL_PREFIX as o, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS as p, callOpenFairyGuiBackendTool as r, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI as s, connectOpenFairyGuiMcpStdio as t, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE as u };