@openfairygui/mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
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
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";
5
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import path from "node:path";
6
10
  import { pathToFileURL } from "node:url";
7
11
  //#region src/prompt-definitions.ts
8
12
  const OPENFAIRYGUI_BACKEND_PROMPT_NAMES = [
@@ -51,6 +55,11 @@ const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS = [
51
55
  description: "Guide a client through backend-owned revision checks without inventing operation grammar.",
52
56
  text: [
53
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.",
54
63
  "Call openfairygui_backend_apply_transaction with sessionId, expectedRevision, and backend/UAM-owned operations.",
55
64
  "If the backend returns a stale revision error, refresh the session snapshot and re-plan against the new revision.",
56
65
  "Do not invent selector grammar, transaction grammar, or operation payload semantics at the MCP layer."
@@ -94,6 +103,32 @@ function registerOpenFairyGuiBackendPrompts(server) {
94
103
  }, () => promptResult(definition.text));
95
104
  }
96
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
97
132
  //#region src/resource-definitions.ts
98
133
  const JSON_MIME_TYPE = "application/json";
99
134
  function firstVariable(value) {
@@ -108,12 +143,69 @@ function jsonResource(uri, backendResult) {
108
143
  }
109
144
  const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
110
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,
111
150
  "openfairygui://backend/session/{sessionId}",
112
151
  "openfairygui://backend/session/{sessionId}/outline",
113
152
  "openfairygui://backend/cache/{sessionId}",
114
153
  "openfairygui://backend/job/{sessionId}/{jobId}"
115
154
  ];
116
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))));
117
209
  server.registerResource("openfairygui_backend_capabilities", OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, {
118
210
  title: "OpenFairyGUI Backend Capabilities",
119
211
  description: "Read the backend capability and version envelope as JSON.",
@@ -144,152 +236,13 @@ function registerOpenFairyGuiBackendResources(server, runtime) {
144
236
  })));
145
237
  }
146
238
  //#endregion
147
- //#region src/tool-handler.ts
148
- function jsonResult(payload, isError = false) {
149
- return {
150
- content: [{
151
- type: "text",
152
- text: JSON.stringify(payload, null, 2)
153
- }],
154
- structuredContent: { backendResult: payload },
155
- isError
156
- };
157
- }
158
- function isBackendFailure(value) {
159
- return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
160
- }
161
- async function callOpenFairyGuiBackendTool(runtime, name, input) {
162
- let result;
163
- switch (name) {
164
- case "openfairygui_backend_get_capabilities":
165
- result = runtime.getCapabilities();
166
- break;
167
- case "openfairygui_backend_open_session":
168
- result = await runtime.openSession({ projectPath: String(input.projectPath) });
169
- break;
170
- case "openfairygui_backend_open_project_session":
171
- result = runtime.openProjectSession({
172
- project: input.project,
173
- sessionId: input.sessionId === void 0 ? void 0 : String(input.sessionId),
174
- canonicalProjectPath: input.canonicalProjectPath === void 0 ? void 0 : String(input.canonicalProjectPath),
175
- canonicalPathKey: input.canonicalPathKey === void 0 ? void 0 : String(input.canonicalPathKey)
176
- });
177
- break;
178
- case "openfairygui_backend_get_session":
179
- result = runtime.getSession({ sessionId: String(input.sessionId) });
180
- break;
181
- case "openfairygui_backend_get_project_outline":
182
- result = runtime.getProjectOutline({ sessionId: String(input.sessionId) });
183
- break;
184
- case "openfairygui_backend_validate_session":
185
- result = runtime.validateSession({ sessionId: String(input.sessionId) });
186
- break;
187
- case "openfairygui_backend_apply_transaction":
188
- result = await runtime.applyTransaction({
189
- sessionId: String(input.sessionId),
190
- expectedRevision: Number(input.expectedRevision),
191
- operations: input.operations
192
- });
193
- break;
194
- case "openfairygui_backend_save_session":
195
- result = await runtime.saveSession({
196
- sessionId: String(input.sessionId),
197
- expectedRevision: input.expectedRevision === void 0 ? void 0 : Number(input.expectedRevision),
198
- targetPath: input.targetPath === void 0 ? void 0 : String(input.targetPath),
199
- force: input.force === void 0 ? void 0 : Boolean(input.force),
200
- mode: input.mode
201
- });
202
- break;
203
- case "openfairygui_backend_materialize_session":
204
- result = await runtime.materializeSession({
205
- sessionId: String(input.sessionId),
206
- expectedRevision: input.expectedRevision === void 0 ? void 0 : Number(input.expectedRevision),
207
- mode: input.mode,
208
- reason: input.reason === void 0 ? void 0 : String(input.reason)
209
- });
210
- break;
211
- case "openfairygui_backend_close_session":
212
- result = await runtime.closeSession({ sessionId: String(input.sessionId) });
213
- break;
214
- case "openfairygui_backend_get_events":
215
- result = runtime.getEvents({
216
- sessionId: String(input.sessionId),
217
- after: input.after === void 0 ? void 0 : String(input.after),
218
- limit: input.limit === void 0 ? void 0 : Number(input.limit)
219
- });
220
- break;
221
- case "openfairygui_backend_get_job":
222
- result = runtime.getJob({
223
- sessionId: String(input.sessionId),
224
- jobId: String(input.jobId)
225
- });
226
- break;
227
- case "openfairygui_backend_list_jobs":
228
- result = runtime.listJobs({
229
- sessionId: String(input.sessionId),
230
- status: input.status,
231
- kind: input.kind,
232
- limit: input.limit === void 0 ? void 0 : Number(input.limit)
233
- });
234
- break;
235
- case "openfairygui_backend_cancel_job":
236
- result = runtime.cancelJob({
237
- sessionId: String(input.sessionId),
238
- jobId: String(input.jobId)
239
- });
240
- break;
241
- case "openfairygui_backend_get_cache_snapshot":
242
- result = runtime.getCacheSnapshot({ sessionId: String(input.sessionId) });
243
- break;
244
- case "openfairygui_backend_refresh_cache":
245
- result = runtime.refreshCache({
246
- sessionId: String(input.sessionId),
247
- reason: input.reason
248
- });
249
- break;
250
- default: throw new Error(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
251
- }
252
- return jsonResult(result, isBackendFailure(result));
253
- }
254
- //#endregion
255
- //#region src/tool-definitions.ts
256
- const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
257
- const OPENFAIRYGUI_BACKEND_TOOL_NAMES = [
258
- "openfairygui_backend_get_capabilities",
259
- "openfairygui_backend_open_session",
260
- "openfairygui_backend_open_project_session",
261
- "openfairygui_backend_get_session",
262
- "openfairygui_backend_get_project_outline",
263
- "openfairygui_backend_validate_session",
264
- "openfairygui_backend_apply_transaction",
265
- "openfairygui_backend_save_session",
266
- "openfairygui_backend_materialize_session",
267
- "openfairygui_backend_close_session",
268
- "openfairygui_backend_get_events",
269
- "openfairygui_backend_get_job",
270
- "openfairygui_backend_list_jobs",
271
- "openfairygui_backend_cancel_job",
272
- "openfairygui_backend_get_cache_snapshot",
273
- "openfairygui_backend_refresh_cache"
274
- ];
275
- const sessionId = z.string().min(1);
276
- const jobId = z.string().min(1);
277
- const expectedRevision = z.number().int().nonnegative();
278
- const limit = z.number().int().nonnegative().optional();
279
- const OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA = z.object({ backendResult: z.object({
280
- ok: z.boolean(),
281
- data: z.unknown().optional(),
282
- error: z.unknown().optional(),
283
- meta: z.unknown().optional()
284
- }).passthrough() });
285
- const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
239
+ //#region src/tool-metadata.ts
240
+ const OPENFAIRYGUI_BACKEND_TOOL_METADATA = [
286
241
  {
287
242
  name: "openfairygui_backend_get_capabilities",
288
243
  backendMethod: "getCapabilities",
289
244
  title: "Get Backend Capabilities",
290
245
  description: "Return the OpenFairyGUI backend capability, version, and service-plane snapshot.",
291
- inputSchema: z.object({}),
292
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
293
246
  annotations: {
294
247
  readOnlyHint: true,
295
248
  idempotentHint: true,
@@ -301,8 +254,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
301
254
  backendMethod: "openSession",
302
255
  title: "Open Backend Session",
303
256
  description: "Open a FairyGUI project through BackendRuntime and acquire its backend-local session lock.",
304
- inputSchema: z.object({ projectPath: z.string().min(1) }),
305
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
306
257
  annotations: {
307
258
  readOnlyHint: false,
308
259
  idempotentHint: false,
@@ -314,13 +265,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
314
265
  backendMethod: "openProjectSession",
315
266
  title: "Open Project Session",
316
267
  description: "Open a browser-safe backend session from an already loaded UAM project without filesystem access.",
317
- inputSchema: z.object({
318
- project: z.unknown(),
319
- sessionId: z.string().min(1).optional(),
320
- canonicalProjectPath: z.string().min(1).optional(),
321
- canonicalPathKey: z.string().min(1).optional()
322
- }),
323
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
324
268
  annotations: {
325
269
  readOnlyHint: false,
326
270
  idempotentHint: false,
@@ -332,8 +276,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
332
276
  backendMethod: "getSession",
333
277
  title: "Get Backend Session",
334
278
  description: "Return a backend session snapshot by session id.",
335
- inputSchema: z.object({ sessionId }),
336
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
337
279
  annotations: {
338
280
  readOnlyHint: true,
339
281
  idempotentHint: true,
@@ -345,8 +287,17 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
345
287
  backendMethod: "getProjectOutline",
346
288
  title: "Get Project Outline",
347
289
  description: "Return a revision-bound project/package/resource/component identity outline without source bytes or full property payloads.",
348
- inputSchema: z.object({ sessionId }),
349
- 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.",
350
301
  annotations: {
351
302
  readOnlyHint: true,
352
303
  idempotentHint: true,
@@ -358,8 +309,17 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
358
309
  backendMethod: "validateSession",
359
310
  title: "Validate Project Session",
360
311
  description: "Validate the current session project structure, references, paths, and available source bytes without writing files.",
361
- inputSchema: z.object({ sessionId }),
362
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
312
+ annotations: {
313
+ readOnlyHint: true,
314
+ idempotentHint: true,
315
+ openWorldHint: false
316
+ }
317
+ },
318
+ {
319
+ name: "openfairygui_backend_preflight_transaction",
320
+ backendMethod: "preflightTransaction",
321
+ title: "Preview UAM Transaction",
322
+ 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.",
363
323
  annotations: {
364
324
  readOnlyHint: true,
365
325
  idempotentHint: true,
@@ -370,13 +330,7 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
370
330
  name: "openfairygui_backend_apply_transaction",
371
331
  backendMethod: "applyTransaction",
372
332
  title: "Apply UAM Transaction",
373
- description: "Apply a backend revision-checked UAM operation batch without redefining selector or operation grammar.",
374
- inputSchema: z.object({
375
- sessionId,
376
- expectedRevision,
377
- operations: z.array(z.unknown())
378
- }),
379
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
333
+ description: "Apply a bounded, revision-checked UAM operation batch using the Core transaction discriminants.",
380
334
  annotations: {
381
335
  readOnlyHint: false,
382
336
  destructiveHint: true,
@@ -388,15 +342,7 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
388
342
  name: "openfairygui_backend_save_session",
389
343
  backendMethod: "saveSession",
390
344
  title: "Save Backend Session",
391
- description: "Write the current backend session back through the backend coordinated non-atomic save path.",
392
- inputSchema: z.object({
393
- sessionId,
394
- expectedRevision: expectedRevision.optional(),
395
- targetPath: z.string().min(1).optional(),
396
- force: z.boolean().optional(),
397
- mode: z.literal("materializeCleanSession").optional()
398
- }),
399
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
345
+ description: "Write the current backend session through its coordinated save path; Node uses an atomic staged directory swap.",
400
346
  annotations: {
401
347
  readOnlyHint: false,
402
348
  destructiveHint: true,
@@ -409,13 +355,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
409
355
  backendMethod: "materializeSession",
410
356
  title: "Materialize Backend Session",
411
357
  description: "Force materialize the current backend session project through the configured project storage without requiring a dirty edit revision.",
412
- inputSchema: z.object({
413
- sessionId,
414
- expectedRevision: expectedRevision.optional(),
415
- mode: z.literal("fullProject").optional(),
416
- reason: z.string().min(1).optional()
417
- }),
418
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
419
358
  annotations: {
420
359
  readOnlyHint: false,
421
360
  destructiveHint: true,
@@ -428,8 +367,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
428
367
  backendMethod: "closeSession",
429
368
  title: "Close Backend Session",
430
369
  description: "Close a backend session and release its backend-local session lock.",
431
- inputSchema: z.object({ sessionId }),
432
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
433
370
  annotations: {
434
371
  readOnlyHint: false,
435
372
  idempotentHint: false,
@@ -441,12 +378,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
441
378
  backendMethod: "getEvents",
442
379
  title: "Get Runtime Events",
443
380
  description: "Poll backend runtime events for a session using the backend P2 event cursor contract.",
444
- inputSchema: z.object({
445
- sessionId,
446
- after: z.string().optional(),
447
- limit
448
- }),
449
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
450
381
  annotations: {
451
382
  readOnlyHint: true,
452
383
  idempotentHint: true,
@@ -458,11 +389,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
458
389
  backendMethod: "getJob",
459
390
  title: "Get Runtime Job",
460
391
  description: "Return a backend runtime job snapshot by session and backend-local job id.",
461
- inputSchema: z.object({
462
- sessionId,
463
- jobId
464
- }),
465
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
466
392
  annotations: {
467
393
  readOnlyHint: true,
468
394
  idempotentHint: true,
@@ -474,21 +400,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
474
400
  backendMethod: "listJobs",
475
401
  title: "List Runtime Jobs",
476
402
  description: "List backend runtime jobs for a session with backend P2 status/kind filters.",
477
- inputSchema: z.object({
478
- sessionId,
479
- status: z.enum([
480
- "queued",
481
- "running",
482
- "completed",
483
- "failed",
484
- "cancelled",
485
- "active",
486
- "terminal"
487
- ]).optional(),
488
- kind: z.literal("cache.refresh").optional(),
489
- limit
490
- }),
491
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
492
403
  annotations: {
493
404
  readOnlyHint: true,
494
405
  idempotentHint: true,
@@ -500,11 +411,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
500
411
  backendMethod: "cancelJob",
501
412
  title: "Cancel Runtime Job",
502
413
  description: "Request cooperative cancellation for a backend runtime job.",
503
- inputSchema: z.object({
504
- sessionId,
505
- jobId
506
- }),
507
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
508
414
  annotations: {
509
415
  readOnlyHint: false,
510
416
  idempotentHint: false,
@@ -516,8 +422,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
516
422
  backendMethod: "getCacheSnapshot",
517
423
  title: "Get Cache Snapshot",
518
424
  description: "Return the backend P2 derived read-only cache snapshot for a session.",
519
- inputSchema: z.object({ sessionId }),
520
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
521
425
  annotations: {
522
426
  readOnlyHint: true,
523
427
  idempotentHint: true,
@@ -529,15 +433,6 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
529
433
  backendMethod: "refreshCache",
530
434
  title: "Refresh Cache",
531
435
  description: "Create a backend P2 cache.refresh job for the session cache snapshot.",
532
- inputSchema: z.object({
533
- sessionId,
534
- reason: z.enum([
535
- "manual",
536
- "session_open",
537
- "after_save"
538
- ]).optional()
539
- }),
540
- outputSchema: OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA,
541
436
  annotations: {
542
437
  readOnlyHint: false,
543
438
  idempotentHint: false,
@@ -546,10 +441,113 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = [
546
441
  }
547
442
  ];
548
443
  //#endregion
444
+ //#region src/tool-definitions.ts
445
+ const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
446
+ const OPENFAIRYGUI_BACKEND_TOOL_NAMES = OPENFAIRYGUI_BACKEND_TOOL_METADATA.map((entry) => entry.name);
447
+ function isOpenFairyGuiMcpPayloadWithinBudget(root) {
448
+ const pending = [{
449
+ value: root,
450
+ depth: 0
451
+ }];
452
+ let nodes = 0;
453
+ while (pending.length > 0) {
454
+ const { value, depth } = pending.pop();
455
+ nodes += 1;
456
+ if (nodes > 1e5 || depth > 32) return false;
457
+ if (value === null || typeof value === "boolean") continue;
458
+ if (typeof value === "number") {
459
+ if (!Number.isFinite(value)) return false;
460
+ continue;
461
+ }
462
+ if (typeof value === "string") {
463
+ if (value.length > 1e6) return false;
464
+ continue;
465
+ }
466
+ if (value instanceof Uint8Array) {
467
+ if (value.byteLength > 8 * 1024 * 1024) return false;
468
+ continue;
469
+ }
470
+ if (Array.isArray(value)) {
471
+ if (value.length > 1e4) return false;
472
+ for (const child of value) pending.push({
473
+ value: child,
474
+ depth: depth + 1
475
+ });
476
+ continue;
477
+ }
478
+ if (typeof value !== "object") return false;
479
+ const entries = Object.entries(value);
480
+ if (entries.length > 1e4 || entries.some(([key]) => key.length > 256)) return false;
481
+ for (const [, child] of entries) pending.push({
482
+ value: child,
483
+ depth: depth + 1
484
+ });
485
+ }
486
+ return true;
487
+ }
488
+ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = OPENFAIRYGUI_BACKEND_TOOL_METADATA.map((metadata) => {
489
+ const contract = CONTRACT_SNAPSHOT.tools[metadata.backendMethod];
490
+ return {
491
+ ...metadata,
492
+ inputSchema: contractObjectSchema(contract.input),
493
+ outputSchema: contractObjectSchema(contract.output)
494
+ };
495
+ });
496
+ //#endregion
497
+ //#region src/tool-handler.ts
498
+ function jsonResult(payload, isError = false) {
499
+ const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, 2);
500
+ const wirePayload = JSON.parse(text);
501
+ return {
502
+ content: [{
503
+ type: "text",
504
+ text
505
+ }],
506
+ structuredContent: { backendResult: wirePayload },
507
+ isError
508
+ };
509
+ }
510
+ function isBackendFailure(value) {
511
+ return typeof value === "object" && value !== null && "ok" in value && value.ok === false;
512
+ }
513
+ function unhandledBackendFailure(startedAt) {
514
+ return {
515
+ ok: false,
516
+ meta: {
517
+ requestId: crypto.randomUUID(),
518
+ durationMs: Math.max(0, Date.now() - startedAt),
519
+ warnings: [],
520
+ diagnostics: [],
521
+ stage: "runtime",
522
+ contractVersion: BACKEND_CONTRACT_VERSION,
523
+ capabilitySchemaVersion: BACKEND_CAPABILITY_SCHEMA_VERSION
524
+ },
525
+ error: {
526
+ code: "backend_unhandled_error",
527
+ message: "Backend tool execution failed."
528
+ }
529
+ };
530
+ }
531
+ async function callOpenFairyGuiBackendTool(runtime, name, input) {
532
+ if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
533
+ const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
534
+ if (!definition) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
535
+ const decoded = decodeToolBytes(definition.inputSchema.parse(input), CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
536
+ const startedAt = Date.now();
537
+ try {
538
+ const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
539
+ const response = jsonResult(result, isBackendFailure(result));
540
+ definition.outputSchema.parse(response.structuredContent);
541
+ return response;
542
+ } catch {
543
+ return jsonResult(unhandledBackendFailure(startedAt), true);
544
+ }
545
+ }
546
+ //#endregion
549
547
  //#region src/server.ts
550
548
  const require = createRequire(import.meta.url);
551
549
  function getInjectedPackageVersion() {
552
- const version = "0.3.0";
550
+ const version = "0.4.0";
553
551
  return typeof version === "string" && true ? version : null;
554
552
  }
555
553
  function readPackageVersion() {
@@ -563,22 +561,44 @@ function readPackageVersion() {
563
561
  }
564
562
  const PACKAGE_VERSION = readPackageVersion();
565
563
  function createOpenFairyGuiMcpServer(options = {}) {
566
- const runtime = options.runtime ?? createNodeBackendRuntime();
564
+ const runtime = options.runtime ?? createNodeBackendRuntime({ allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()] });
567
565
  const server = new McpServer({
568
566
  name: options.name ?? "openfairygui-mcp",
569
567
  version: options.version ?? PACKAGE_VERSION
570
568
  });
571
- for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) server.registerTool(definition.name, {
572
- title: definition.title,
573
- description: definition.description,
574
- inputSchema: definition.inputSchema,
575
- outputSchema: definition.outputSchema,
576
- annotations: definition.annotations,
577
- _meta: {
578
- "openfairygui/backendMethod": definition.backendMethod,
579
- "openfairygui/adapter": "thin-backend-p2"
580
- }
581
- }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
569
+ const tools = [];
570
+ for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) {
571
+ const metadata = {
572
+ name: definition.name,
573
+ title: definition.title,
574
+ description: definition.description,
575
+ annotations: definition.annotations,
576
+ _meta: {
577
+ "openfairygui/backendMethod": definition.backendMethod,
578
+ "openfairygui/adapter": "thin-backend-p2",
579
+ "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
580
+ }
581
+ };
582
+ server.registerTool(definition.name, {
583
+ ...metadata,
584
+ inputSchema: definition.inputSchema,
585
+ outputSchema: definition.outputSchema
586
+ }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
587
+ tools.push(ToolSchema.parse({
588
+ ...metadata,
589
+ inputSchema: z.toJSONSchema(definition.inputSchema, {
590
+ target: "draft-07",
591
+ io: "input",
592
+ reused: "ref"
593
+ }),
594
+ outputSchema: z.toJSONSchema(definition.outputSchema, {
595
+ target: "draft-07",
596
+ io: "output",
597
+ reused: "ref"
598
+ })
599
+ }));
600
+ }
601
+ server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
582
602
  registerOpenFairyGuiBackendResources(server, runtime);
583
603
  registerOpenFairyGuiBackendPrompts(server);
584
604
  return server;
@@ -586,11 +606,12 @@ function createOpenFairyGuiMcpServer(options = {}) {
586
606
  //#endregion
587
607
  //#region src/stdio.ts
588
608
  async function connectOpenFairyGuiMcpStdio() {
589
- await createOpenFairyGuiMcpServer().connect(new StdioServerTransport());
609
+ const configuredRoots = process.env.OPENFAIRYGUI_ALLOWED_PROJECT_ROOTS?.split(path.delimiter).map((value) => value.trim()).filter(Boolean);
610
+ await createOpenFairyGuiMcpServer({ allowedProjectRoots: configuredRoots }).connect(new StdioServerTransport());
590
611
  }
591
612
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) connectOpenFairyGuiMcpStdio().catch((error) => {
592
613
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
593
614
  process.exitCode = 1;
594
615
  });
595
616
  //#endregion
596
- export { OPENFAIRYGUI_BACKEND_TOOL_OUTPUT_SCHEMA as a, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI as c, OPENFAIRYGUI_BACKEND_PROMPT_NAMES as d, OPENFAIRYGUI_BACKEND_TOOL_NAMES as i, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES as l, createOpenFairyGuiMcpServer as n, OPENFAIRYGUI_BACKEND_TOOL_PREFIX as o, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS as r, callOpenFairyGuiBackendTool as s, connectOpenFairyGuiMcpStdio as t, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS as u };
617
+ 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 };