@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.
- package/README.md +5 -1
- package/dist/index.cjs +26 -2
- package/dist/index.d.cts +45 -231
- package/dist/index.d.mts +46 -232
- package/dist/index.mjs +2 -2
- package/dist/{stdio-48jCJzS3.mjs → stdio-9ka7bvOr.mjs} +259 -238
- package/dist/{stdio-9ewjcCag.cjs → stdio-B0OU-oZC.cjs} +285 -241
- package/dist/stdio.cjs +1 -1
- package/dist/stdio.mjs +1 -1
- package/package.json +7 -4
- package/src/contract-schema.ts +29 -0
- package/src/index.ts +6 -1
- package/src/prompt-definitions.ts +5 -0
- package/src/resource-definitions.ts +59 -0
- package/src/server.ts +27 -8
- package/src/stdio.ts +6 -1
- package/src/tool-definitions.ts +49 -243
- package/src/tool-handler.ts +53 -128
- package/src/tool-metadata.ts +157 -0
|
@@ -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-
|
|
148
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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 {
|
|
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 };
|