@openfairygui/mcp 0.5.0-alpha.1 → 0.5.0-alpha.2
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 +42 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.mts +20 -3
- package/dist/index.mjs +1 -1
- package/dist/{stdio-BNobuRxx.mjs → stdio--4YykL4s.mjs} +43 -34
- package/dist/{stdio-CB98zdOf.cjs → stdio-D33c37mD.cjs} +43 -34
- package/dist/stdio.cjs +1 -1
- package/dist/stdio.mjs +1 -1
- package/package.json +3 -3
- package/src/contract-schema.ts +10 -0
- package/src/index.ts +1 -0
- package/src/server.ts +20 -16
- package/src/tool-handler.ts +19 -2
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Each tool exposes a method-specific output schema for `structuredContent.backend
|
|
|
36
36
|
- `error?`
|
|
37
37
|
- `meta?`
|
|
38
38
|
|
|
39
|
-
The factory
|
|
39
|
+
The factory registers the 20-method Backend catalog. The SDK owns dynamic discovery and dispatch, including Host tools added through `server.registerTool()`. Input/output schemas come from the canonical installed contract; discovery uses self-contained draft-07 `definitions`/`$ref` to reuse repeated structures without loosening the 41-operation union, call validators or input budgets. Installed operation documentation remains available through `openfairygui://contracts/operations` and `openfairygui://docs/index`.
|
|
40
40
|
|
|
41
41
|
P1 also registers MCP-native ergonomics around the same backend surface:
|
|
42
42
|
|
|
@@ -71,6 +71,47 @@ For stdio clients, use the package binary:
|
|
|
71
71
|
ofgui-mcp
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Host composition
|
|
75
|
+
|
|
76
|
+
Pass `instructions` to the factory to publish Host guidance in the SDK initialize handshake.
|
|
77
|
+
|
|
78
|
+
Use `toolPolicies` to gate selected tools before Backend executes. Each policy declares a synchronous Zod `failureSchema` and a `beforeCall` callback. The callback receives a detached copy of the validated wire input and may be async. Returning `undefined` invokes the original Backend method once with the original input; returning a declared `ok: false` envelope stops the call and produces matching text/`structuredContent.backendResult` with `isError: true`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createOpenFairyGuiMcpServer, type OpenFairyGuiMcpToolPolicy } from '@openfairygui/mcp';
|
|
82
|
+
import { z } from 'zod';
|
|
83
|
+
|
|
84
|
+
const policy: OpenFairyGuiMcpToolPolicy = {
|
|
85
|
+
failureSchema: z.strictObject({
|
|
86
|
+
ok: z.literal(false),
|
|
87
|
+
error: z.strictObject({
|
|
88
|
+
code: z.literal('save_approval_required'),
|
|
89
|
+
approvalRequestId: z.string(),
|
|
90
|
+
approvalPath: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
beforeCall(input) {
|
|
94
|
+
// Host-owned grant store: bind the grant to session, revision, operation and all options.
|
|
95
|
+
if (hostGrants.consume('saveSession', input)) return undefined;
|
|
96
|
+
return { ok: false, error: {
|
|
97
|
+
code: 'save_approval_required',
|
|
98
|
+
approvalRequestId: hostGrants.request('saveSession', input),
|
|
99
|
+
approvalPath: '/#save-approvals',
|
|
100
|
+
} };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const server = createOpenFairyGuiMcpServer({ runtime, instructions: 'Host writes require owner approval.', toolPolicies: {
|
|
104
|
+
openfairygui_backend_save_session: policy,
|
|
105
|
+
} });
|
|
106
|
+
server.registerTool('host_probe', { inputSchema: z.object({}) }, async () => ({
|
|
107
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
108
|
+
}));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`runtime` and `hostGrants` belong to the embedding Host. Configure `materialize_session` separately when it also requires approval; grants must distinguish the two operations. The policy must consume approval before allowing the call and must not call the Backend write itself. Backend still checks revision, paths and disk state after approval; failures are not retried automatically. Read tools without policies do not require grants.
|
|
112
|
+
|
|
113
|
+
The selected tool's advertised output includes its declared Host failure branch. `_meta['openfairygui/hostPolicy']` marks that extension; the contract digest and installed documentation describe the unchanged Backend branch. Host failures do not become Backend error codes. Unknown tool-policy names fail server construction. Invalid inputs never reach the policy; throwing policies or invalid policy results stop before Backend and return `backend_unhandled_error`. Backend results always pass their canonical schema, even if a Host schema would accept them. Method response budgets also apply to policy failures. The direct `callOpenFairyGuiBackendTool(runtime, name, input, policy)` entry accepts the same policy as its optional fourth argument.
|
|
114
|
+
|
|
74
115
|
Example local MCP client configuration:
|
|
75
116
|
|
|
76
117
|
```json
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_stdio = require("./stdio-
|
|
2
|
+
const require_stdio = require("./stdio-D33c37mD.cjs");
|
|
3
3
|
let _openfairygui_backend_docs = require("@openfairygui/backend/docs");
|
|
4
4
|
exports.OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = require_stdio.OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI;
|
|
5
5
|
exports.OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS = require_stdio.OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { connectOpenFairyGuiMcpStdio } from "./stdio.cjs";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { BackendMethodName, BackendMethodName as BackendMethodName$1, BackendRuntime } from "@openfairygui/backend";
|
|
5
4
|
import { z } from "zod";
|
|
5
|
+
import { BackendMethodName, BackendMethodName as BackendMethodName$1, BackendRuntime } from "@openfairygui/backend";
|
|
6
6
|
import { OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema } from "@openfairygui/backend/docs";
|
|
7
7
|
|
|
8
8
|
//#region src/tool-metadata.d.ts
|
|
@@ -240,7 +240,20 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS: readonly OpenFairyGuiBacken
|
|
|
240
240
|
//#endregion
|
|
241
241
|
//#region src/tool-handler.d.ts
|
|
242
242
|
type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName$1>;
|
|
243
|
-
|
|
243
|
+
/** Host policy runs after input validation, before the single Backend invocation. */
|
|
244
|
+
interface OpenFairyGuiMcpToolPolicy {
|
|
245
|
+
/** Explicit Host-owned failure envelope, carried in structuredContent.backendResult. */
|
|
246
|
+
failureSchema: z.ZodType<{
|
|
247
|
+
ok: false;
|
|
248
|
+
}>;
|
|
249
|
+
/** Return a declared failure to stop, or undefined to call Backend with the original input. */
|
|
250
|
+
beforeCall(input: Readonly<Record<string, unknown>>): {
|
|
251
|
+
ok: false;
|
|
252
|
+
} | undefined | Promise<{
|
|
253
|
+
ok: false;
|
|
254
|
+
} | undefined>;
|
|
255
|
+
}
|
|
256
|
+
declare function callOpenFairyGuiBackendTool(runtime: OpenFairyGuiBackendRuntime, name: OpenFairyGuiBackendToolName, input: Record<string, unknown>, policy?: OpenFairyGuiMcpToolPolicy): Promise<CallToolResult>;
|
|
244
257
|
//#endregion
|
|
245
258
|
//#region src/server.d.ts
|
|
246
259
|
interface CreateOpenFairyGuiMcpServerOptions {
|
|
@@ -249,6 +262,10 @@ interface CreateOpenFairyGuiMcpServerOptions {
|
|
|
249
262
|
allowedProjectRoots?: readonly string[];
|
|
250
263
|
name?: string;
|
|
251
264
|
version?: string;
|
|
265
|
+
/** Host guidance returned by the SDK initialize handshake. */
|
|
266
|
+
instructions?: string;
|
|
267
|
+
/** Per-tool Host failures do not change the canonical Backend contracts. */
|
|
268
|
+
toolPolicies?: Partial<Record<OpenFairyGuiBackendToolName, OpenFairyGuiMcpToolPolicy>>;
|
|
252
269
|
}
|
|
253
270
|
declare function createOpenFairyGuiMcpServer(options?: CreateOpenFairyGuiMcpServerOptions): McpServer;
|
|
254
271
|
//#endregion
|
|
@@ -291,4 +308,4 @@ declare const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS: readonly [{
|
|
|
291
308
|
declare const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
|
|
292
309
|
declare const OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES: readonly ["openfairygui://docs/methods/{method}", "openfairygui://docs/cli/{command}", "openfairygui://docs/diagnostics/{code}", "openfairygui://contracts/operations/{kind}", "openfairygui://backend/session/{sessionId}", "openfairygui://backend/session/{sessionId}/outline", "openfairygui://backend/cache/{sessionId}", "openfairygui://backend/job/{sessionId}/{jobId}"];
|
|
293
310
|
//#endregion
|
|
294
|
-
export { type BackendMethodName, type CreateOpenFairyGuiMcpServerOptions, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, OPENFAIRYGUI_BACKEND_PROMPT_NAMES, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, OPENFAIRYGUI_BACKEND_TOOL_NAMES, OPENFAIRYGUI_BACKEND_TOOL_PREFIX, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, type OpenFairyGuiBackendPromptName, type OpenFairyGuiBackendRuntime, type OpenFairyGuiBackendToolDefinition, type OpenFairyGuiBackendToolName, callOpenFairyGuiBackendTool, connectOpenFairyGuiMcpStdio, createOpenFairyGuiMcpServer, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema };
|
|
311
|
+
export { type BackendMethodName, type CreateOpenFairyGuiMcpServerOptions, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, OPENFAIRYGUI_BACKEND_PROMPT_NAMES, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, OPENFAIRYGUI_BACKEND_TOOL_NAMES, OPENFAIRYGUI_BACKEND_TOOL_PREFIX, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, type OpenFairyGuiBackendPromptName, type OpenFairyGuiBackendRuntime, type OpenFairyGuiBackendToolDefinition, type OpenFairyGuiBackendToolName, type OpenFairyGuiMcpToolPolicy, callOpenFairyGuiBackendTool, connectOpenFairyGuiMcpStdio, createOpenFairyGuiMcpServer, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { connectOpenFairyGuiMcpStdio } from "./stdio.mjs";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { BackendMethodName, BackendMethodName as BackendMethodName$1, BackendRuntime } from "@openfairygui/backend";
|
|
6
5
|
import { OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema } from "@openfairygui/backend/docs";
|
|
6
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
|
|
8
8
|
//#region src/tool-metadata.d.ts
|
|
9
9
|
interface BackendToolMetadata {
|
|
@@ -240,7 +240,20 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS: readonly OpenFairyGuiBacken
|
|
|
240
240
|
//#endregion
|
|
241
241
|
//#region src/tool-handler.d.ts
|
|
242
242
|
type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName$1>;
|
|
243
|
-
|
|
243
|
+
/** Host policy runs after input validation, before the single Backend invocation. */
|
|
244
|
+
interface OpenFairyGuiMcpToolPolicy {
|
|
245
|
+
/** Explicit Host-owned failure envelope, carried in structuredContent.backendResult. */
|
|
246
|
+
failureSchema: z.ZodType<{
|
|
247
|
+
ok: false;
|
|
248
|
+
}>;
|
|
249
|
+
/** Return a declared failure to stop, or undefined to call Backend with the original input. */
|
|
250
|
+
beforeCall(input: Readonly<Record<string, unknown>>): {
|
|
251
|
+
ok: false;
|
|
252
|
+
} | undefined | Promise<{
|
|
253
|
+
ok: false;
|
|
254
|
+
} | undefined>;
|
|
255
|
+
}
|
|
256
|
+
declare function callOpenFairyGuiBackendTool(runtime: OpenFairyGuiBackendRuntime, name: OpenFairyGuiBackendToolName, input: Record<string, unknown>, policy?: OpenFairyGuiMcpToolPolicy): Promise<CallToolResult>;
|
|
244
257
|
//#endregion
|
|
245
258
|
//#region src/server.d.ts
|
|
246
259
|
interface CreateOpenFairyGuiMcpServerOptions {
|
|
@@ -249,6 +262,10 @@ interface CreateOpenFairyGuiMcpServerOptions {
|
|
|
249
262
|
allowedProjectRoots?: readonly string[];
|
|
250
263
|
name?: string;
|
|
251
264
|
version?: string;
|
|
265
|
+
/** Host guidance returned by the SDK initialize handshake. */
|
|
266
|
+
instructions?: string;
|
|
267
|
+
/** Per-tool Host failures do not change the canonical Backend contracts. */
|
|
268
|
+
toolPolicies?: Partial<Record<OpenFairyGuiBackendToolName, OpenFairyGuiMcpToolPolicy>>;
|
|
252
269
|
}
|
|
253
270
|
declare function createOpenFairyGuiMcpServer(options?: CreateOpenFairyGuiMcpServerOptions): McpServer;
|
|
254
271
|
//#endregion
|
|
@@ -291,4 +308,4 @@ declare const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS: readonly [{
|
|
|
291
308
|
declare const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
|
|
292
309
|
declare const OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES: readonly ["openfairygui://docs/methods/{method}", "openfairygui://docs/cli/{command}", "openfairygui://docs/diagnostics/{code}", "openfairygui://contracts/operations/{kind}", "openfairygui://backend/session/{sessionId}", "openfairygui://backend/session/{sessionId}/outline", "openfairygui://backend/cache/{sessionId}", "openfairygui://backend/job/{sessionId}/{jobId}"];
|
|
293
310
|
//#endregion
|
|
294
|
-
export { type BackendMethodName, type CreateOpenFairyGuiMcpServerOptions, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, OPENFAIRYGUI_BACKEND_PROMPT_NAMES, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, OPENFAIRYGUI_BACKEND_TOOL_NAMES, OPENFAIRYGUI_BACKEND_TOOL_PREFIX, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, type OpenFairyGuiBackendPromptName, type OpenFairyGuiBackendRuntime, type OpenFairyGuiBackendToolDefinition, type OpenFairyGuiBackendToolName, callOpenFairyGuiBackendTool, connectOpenFairyGuiMcpStdio, createOpenFairyGuiMcpServer, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema };
|
|
311
|
+
export { type BackendMethodName, type CreateOpenFairyGuiMcpServerOptions, OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, OPENFAIRYGUI_BACKEND_PROMPT_NAMES, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, OPENFAIRYGUI_BACKEND_TOOL_NAMES, OPENFAIRYGUI_BACKEND_TOOL_PREFIX, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, type OpenFairyGuiBackendPromptName, type OpenFairyGuiBackendRuntime, type OpenFairyGuiBackendToolDefinition, type OpenFairyGuiBackendToolName, type OpenFairyGuiMcpToolPolicy, callOpenFairyGuiBackendTool, connectOpenFairyGuiMcpStdio, createOpenFairyGuiMcpServer, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as OPENFAIRYGUI_BACKEND_TOOL_NAMES, c as OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, d as getOpenFairyGuiOperationCatalog, f as getOpenFairyGuiOperationSchema, i as OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, l as OPENFAIRYGUI_OPERATION_CATALOG_URI, m as OPENFAIRYGUI_BACKEND_PROMPT_NAMES, n as createOpenFairyGuiMcpServer, o as OPENFAIRYGUI_BACKEND_TOOL_PREFIX, p as OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, r as callOpenFairyGuiBackendTool, s as OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, t as connectOpenFairyGuiMcpStdio, u as OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE } from "./stdio
|
|
1
|
+
import { a as OPENFAIRYGUI_BACKEND_TOOL_NAMES, c as OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, d as getOpenFairyGuiOperationCatalog, f as getOpenFairyGuiOperationSchema, i as OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, l as OPENFAIRYGUI_OPERATION_CATALOG_URI, m as OPENFAIRYGUI_BACKEND_PROMPT_NAMES, n as createOpenFairyGuiMcpServer, o as OPENFAIRYGUI_BACKEND_TOOL_PREFIX, p as OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, r as callOpenFairyGuiBackendTool, s as OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, t as connectOpenFairyGuiMcpStdio, u as OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE } from "./stdio--4YykL4s.mjs";
|
|
2
2
|
export { OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI, OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS, OPENFAIRYGUI_BACKEND_PROMPT_NAMES, OPENFAIRYGUI_BACKEND_RESOURCE_TEMPLATES, OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS, OPENFAIRYGUI_BACKEND_TOOL_NAMES, OPENFAIRYGUI_BACKEND_TOOL_PREFIX, OPENFAIRYGUI_OPERATION_CATALOG_URI, OPENFAIRYGUI_OPERATION_SCHEMA_TEMPLATE, callOpenFairyGuiBackendTool, connectOpenFairyGuiMcpStdio, createOpenFairyGuiMcpServer, getOpenFairyGuiOperationCatalog, getOpenFairyGuiOperationSchema };
|
|
@@ -1,6 +1,5 @@
|
|
|
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";
|
|
4
3
|
import { createNodeBackendRuntime } from "@openfairygui/backend/node";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
import { BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_CONTRACT_VERSION, BACKEND_DIAGNOSTICS_URI, BACKEND_DIAGNOSTIC_TEMPLATE, getBackendDiagnosticCatalog, getBackendDiagnosticGuide } from "@openfairygui/backend";
|
|
@@ -113,6 +112,17 @@ function contractObjectSchema(schema) {
|
|
|
113
112
|
if (!(result instanceof z.ZodObject)) throw new TypeError("Tool contract must be an object");
|
|
114
113
|
return result;
|
|
115
114
|
}
|
|
115
|
+
/** Keep SDK discovery dynamic without expanding shared contract definitions. */
|
|
116
|
+
function compactToolSchema(schema, io) {
|
|
117
|
+
return z.looseObject({}).superRefine((value, context) => {
|
|
118
|
+
const parsed = schema.safeParse(value);
|
|
119
|
+
if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue({ ...issue });
|
|
120
|
+
}).meta(z.toJSONSchema(schema, {
|
|
121
|
+
target: "draft-07",
|
|
122
|
+
io,
|
|
123
|
+
reused: "ref"
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
116
126
|
/** Decode only generated Uint8Array locations; arbitrary JSON metadata is not rewritten. */
|
|
117
127
|
function decodeToolBytes(input, paths) {
|
|
118
128
|
if (!paths.length) return input;
|
|
@@ -552,24 +562,34 @@ function unhandledBackendFailure(startedAt) {
|
|
|
552
562
|
}
|
|
553
563
|
};
|
|
554
564
|
}
|
|
555
|
-
async function callOpenFairyGuiBackendTool(runtime, name, input) {
|
|
565
|
+
async function callOpenFairyGuiBackendTool(runtime, name, input, policy) {
|
|
556
566
|
if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
|
|
557
567
|
const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
|
|
558
568
|
if (!definition) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
|
|
559
|
-
const
|
|
569
|
+
const parsed = definition.inputSchema.parse(input);
|
|
570
|
+
const decoded = decodeToolBytes(parsed, CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
|
|
560
571
|
const startedAt = Date.now();
|
|
561
572
|
try {
|
|
562
|
-
|
|
573
|
+
let hostFailure = await policy?.beforeCall(structuredClone(parsed));
|
|
574
|
+
if (hostFailure !== void 0) {
|
|
575
|
+
hostFailure = policy.failureSchema.parse(hostFailure);
|
|
576
|
+
if (!isBackendFailure(hostFailure)) throw new TypeError("Host policy must return a failure or undefined.");
|
|
577
|
+
}
|
|
578
|
+
const result = hostFailure ?? await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
|
|
563
579
|
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)
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
580
|
+
if (definition.maxResponseBytes !== void 0 && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes) {
|
|
581
|
+
response = jsonResult({
|
|
582
|
+
...unhandledBackendFailure(startedAt),
|
|
583
|
+
error: {
|
|
584
|
+
code: "mcp_response_budget_exceeded",
|
|
585
|
+
message: "The complete MCP tool response exceeds its byte limit.",
|
|
586
|
+
maxBytes: definition.maxResponseBytes
|
|
587
|
+
}
|
|
588
|
+
}, true);
|
|
589
|
+
hostFailure = void 0;
|
|
590
|
+
}
|
|
591
|
+
if (hostFailure === void 0) definition.outputSchema.parse(response.structuredContent);
|
|
592
|
+
else policy.failureSchema.parse(response.structuredContent?.backendResult);
|
|
573
593
|
return response;
|
|
574
594
|
} catch {
|
|
575
595
|
return jsonResult(unhandledBackendFailure(startedAt), true);
|
|
@@ -579,7 +599,7 @@ async function callOpenFairyGuiBackendTool(runtime, name, input) {
|
|
|
579
599
|
//#region src/server.ts
|
|
580
600
|
const require = createRequire(import.meta.url);
|
|
581
601
|
function getInjectedPackageVersion() {
|
|
582
|
-
const version = "0.5.0-alpha.
|
|
602
|
+
const version = "0.5.0-alpha.2";
|
|
583
603
|
return typeof version === "string" && true ? version : null;
|
|
584
604
|
}
|
|
585
605
|
function readPackageVersion() {
|
|
@@ -593,13 +613,15 @@ function readPackageVersion() {
|
|
|
593
613
|
}
|
|
594
614
|
const PACKAGE_VERSION = readPackageVersion();
|
|
595
615
|
function createOpenFairyGuiMcpServer(options = {}) {
|
|
616
|
+
for (const name of Object.keys(options.toolPolicies ?? {})) if (!OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.some((definition) => definition.name === name)) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool policy: ${name}`);
|
|
596
617
|
const runtime = options.runtime ?? createNodeBackendRuntime({ allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()] });
|
|
597
618
|
const server = new McpServer({
|
|
598
619
|
name: options.name ?? "openfairygui-mcp",
|
|
599
620
|
version: options.version ?? PACKAGE_VERSION
|
|
600
|
-
});
|
|
601
|
-
const tools = [];
|
|
621
|
+
}, { instructions: options.instructions });
|
|
602
622
|
for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) {
|
|
623
|
+
const policy = options.toolPolicies?.[definition.name];
|
|
624
|
+
const outputSchema = policy ? definition.outputSchema.extend({ backendResult: z.union([definition.outputSchema.shape.backendResult, policy.failureSchema]) }) : definition.outputSchema;
|
|
603
625
|
const metadata = {
|
|
604
626
|
name: definition.name,
|
|
605
627
|
title: definition.title,
|
|
@@ -608,29 +630,16 @@ function createOpenFairyGuiMcpServer(options = {}) {
|
|
|
608
630
|
_meta: {
|
|
609
631
|
"openfairygui/backendMethod": definition.backendMethod,
|
|
610
632
|
"openfairygui/adapter": "thin-backend-p2",
|
|
611
|
-
"openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
|
|
633
|
+
"openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest,
|
|
634
|
+
...policy ? { "openfairygui/hostPolicy": true } : {}
|
|
612
635
|
}
|
|
613
636
|
};
|
|
614
637
|
server.registerTool(definition.name, {
|
|
615
638
|
...metadata,
|
|
616
|
-
inputSchema: definition.inputSchema,
|
|
617
|
-
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
|
-
}));
|
|
639
|
+
inputSchema: compactToolSchema(definition.inputSchema, "input"),
|
|
640
|
+
outputSchema: compactToolSchema(outputSchema, "output")
|
|
641
|
+
}, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args, policy));
|
|
632
642
|
}
|
|
633
|
-
server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
|
|
634
643
|
registerOpenFairyGuiBackendResources(server, runtime);
|
|
635
644
|
registerOpenFairyGuiBackendPrompts(server);
|
|
636
645
|
return server;
|
|
@@ -21,7 +21,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
//#endregion
|
|
23
23
|
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
24
|
-
let _modelcontextprotocol_sdk_types_js = require("@modelcontextprotocol/sdk/types.js");
|
|
25
24
|
let _openfairygui_backend_node = require("@openfairygui/backend/node");
|
|
26
25
|
let node_module = require("node:module");
|
|
27
26
|
let zod = require("zod");
|
|
@@ -136,6 +135,17 @@ function contractObjectSchema(schema) {
|
|
|
136
135
|
if (!(result instanceof zod.z.ZodObject)) throw new TypeError("Tool contract must be an object");
|
|
137
136
|
return result;
|
|
138
137
|
}
|
|
138
|
+
/** Keep SDK discovery dynamic without expanding shared contract definitions. */
|
|
139
|
+
function compactToolSchema(schema, io) {
|
|
140
|
+
return zod.z.looseObject({}).superRefine((value, context) => {
|
|
141
|
+
const parsed = schema.safeParse(value);
|
|
142
|
+
if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue({ ...issue });
|
|
143
|
+
}).meta(zod.z.toJSONSchema(schema, {
|
|
144
|
+
target: "draft-07",
|
|
145
|
+
io,
|
|
146
|
+
reused: "ref"
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
139
149
|
/** Decode only generated Uint8Array locations; arbitrary JSON metadata is not rewritten. */
|
|
140
150
|
function decodeToolBytes(input, paths) {
|
|
141
151
|
if (!paths.length) return input;
|
|
@@ -575,24 +585,34 @@ function unhandledBackendFailure(startedAt) {
|
|
|
575
585
|
}
|
|
576
586
|
};
|
|
577
587
|
}
|
|
578
|
-
async function callOpenFairyGuiBackendTool(runtime, name, input) {
|
|
588
|
+
async function callOpenFairyGuiBackendTool(runtime, name, input, policy) {
|
|
579
589
|
if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
|
|
580
590
|
const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
|
|
581
591
|
if (!definition) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
|
|
582
|
-
const
|
|
592
|
+
const parsed = definition.inputSchema.parse(input);
|
|
593
|
+
const decoded = decodeToolBytes(parsed, CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
|
|
583
594
|
const startedAt = Date.now();
|
|
584
595
|
try {
|
|
585
|
-
|
|
596
|
+
let hostFailure = await policy?.beforeCall(structuredClone(parsed));
|
|
597
|
+
if (hostFailure !== void 0) {
|
|
598
|
+
hostFailure = policy.failureSchema.parse(hostFailure);
|
|
599
|
+
if (!isBackendFailure(hostFailure)) throw new TypeError("Host policy must return a failure or undefined.");
|
|
600
|
+
}
|
|
601
|
+
const result = hostFailure ?? await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
|
|
586
602
|
let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== void 0);
|
|
587
|
-
if (definition.maxResponseBytes !== void 0 && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
603
|
+
if (definition.maxResponseBytes !== void 0 && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes) {
|
|
604
|
+
response = jsonResult({
|
|
605
|
+
...unhandledBackendFailure(startedAt),
|
|
606
|
+
error: {
|
|
607
|
+
code: "mcp_response_budget_exceeded",
|
|
608
|
+
message: "The complete MCP tool response exceeds its byte limit.",
|
|
609
|
+
maxBytes: definition.maxResponseBytes
|
|
610
|
+
}
|
|
611
|
+
}, true);
|
|
612
|
+
hostFailure = void 0;
|
|
613
|
+
}
|
|
614
|
+
if (hostFailure === void 0) definition.outputSchema.parse(response.structuredContent);
|
|
615
|
+
else policy.failureSchema.parse(response.structuredContent?.backendResult);
|
|
596
616
|
return response;
|
|
597
617
|
} catch {
|
|
598
618
|
return jsonResult(unhandledBackendFailure(startedAt), true);
|
|
@@ -602,7 +622,7 @@ async function callOpenFairyGuiBackendTool(runtime, name, input) {
|
|
|
602
622
|
//#region src/server.ts
|
|
603
623
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
604
624
|
function getInjectedPackageVersion() {
|
|
605
|
-
const version = "0.5.0-alpha.
|
|
625
|
+
const version = "0.5.0-alpha.2";
|
|
606
626
|
return typeof version === "string" && true ? version : null;
|
|
607
627
|
}
|
|
608
628
|
function readPackageVersion() {
|
|
@@ -616,13 +636,15 @@ function readPackageVersion() {
|
|
|
616
636
|
}
|
|
617
637
|
const PACKAGE_VERSION = readPackageVersion();
|
|
618
638
|
function createOpenFairyGuiMcpServer(options = {}) {
|
|
639
|
+
for (const name of Object.keys(options.toolPolicies ?? {})) if (!OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.some((definition) => definition.name === name)) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool policy: ${name}`);
|
|
619
640
|
const runtime = options.runtime ?? (0, _openfairygui_backend_node.createNodeBackendRuntime)({ allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()] });
|
|
620
641
|
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
621
642
|
name: options.name ?? "openfairygui-mcp",
|
|
622
643
|
version: options.version ?? PACKAGE_VERSION
|
|
623
|
-
});
|
|
624
|
-
const tools = [];
|
|
644
|
+
}, { instructions: options.instructions });
|
|
625
645
|
for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) {
|
|
646
|
+
const policy = options.toolPolicies?.[definition.name];
|
|
647
|
+
const outputSchema = policy ? definition.outputSchema.extend({ backendResult: zod.z.union([definition.outputSchema.shape.backendResult, policy.failureSchema]) }) : definition.outputSchema;
|
|
626
648
|
const metadata = {
|
|
627
649
|
name: definition.name,
|
|
628
650
|
title: definition.title,
|
|
@@ -631,29 +653,16 @@ function createOpenFairyGuiMcpServer(options = {}) {
|
|
|
631
653
|
_meta: {
|
|
632
654
|
"openfairygui/backendMethod": definition.backendMethod,
|
|
633
655
|
"openfairygui/adapter": "thin-backend-p2",
|
|
634
|
-
"openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
|
|
656
|
+
"openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest,
|
|
657
|
+
...policy ? { "openfairygui/hostPolicy": true } : {}
|
|
635
658
|
}
|
|
636
659
|
};
|
|
637
660
|
server.registerTool(definition.name, {
|
|
638
661
|
...metadata,
|
|
639
|
-
inputSchema: definition.inputSchema,
|
|
640
|
-
outputSchema:
|
|
641
|
-
}, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
|
|
642
|
-
tools.push(_modelcontextprotocol_sdk_types_js.ToolSchema.parse({
|
|
643
|
-
...metadata,
|
|
644
|
-
inputSchema: zod.z.toJSONSchema(definition.inputSchema, {
|
|
645
|
-
target: "draft-07",
|
|
646
|
-
io: "input",
|
|
647
|
-
reused: "ref"
|
|
648
|
-
}),
|
|
649
|
-
outputSchema: zod.z.toJSONSchema(definition.outputSchema, {
|
|
650
|
-
target: "draft-07",
|
|
651
|
-
io: "output",
|
|
652
|
-
reused: "ref"
|
|
653
|
-
})
|
|
654
|
-
}));
|
|
662
|
+
inputSchema: compactToolSchema(definition.inputSchema, "input"),
|
|
663
|
+
outputSchema: compactToolSchema(outputSchema, "output")
|
|
664
|
+
}, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args, policy));
|
|
655
665
|
}
|
|
656
|
-
server.server.setRequestHandler(_modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
|
|
657
666
|
registerOpenFairyGuiBackendResources(server, runtime);
|
|
658
667
|
registerOpenFairyGuiBackendPrompts(server);
|
|
659
668
|
return server;
|
package/dist/stdio.cjs
CHANGED
package/dist/stdio.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as connectOpenFairyGuiMcpStdio } from "./stdio
|
|
1
|
+
import { t as connectOpenFairyGuiMcpStdio } from "./stdio--4YykL4s.mjs";
|
|
2
2
|
export { connectOpenFairyGuiMcpStdio };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/mcp",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.2",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK - MCP server adapter for the backend runtime.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
63
63
|
"zod": "^4.3.6",
|
|
64
|
-
"@openfairygui/backend": "0.5.0-alpha.
|
|
64
|
+
"@openfairygui/backend": "0.5.0-alpha.2"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"ava": "^7.0.0",
|
|
68
68
|
"tsx": "^4.0.0",
|
|
69
69
|
"@openfairygui/test-utils": "0.3.0",
|
|
70
|
-
"@openfairygui/core": "0.5.0-alpha.
|
|
70
|
+
"@openfairygui/core": "0.5.0-alpha.2"
|
|
71
71
|
},
|
|
72
72
|
"ava": {
|
|
73
73
|
"extensions": {
|
package/src/contract-schema.ts
CHANGED
|
@@ -10,6 +10,16 @@ export function contractObjectSchema(schema: ContractSchema): z.ZodObject {
|
|
|
10
10
|
return result;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/** Keep SDK discovery dynamic without expanding shared contract definitions. */
|
|
14
|
+
export function compactToolSchema(schema: z.ZodObject, io: 'input' | 'output'): z.ZodObject {
|
|
15
|
+
// Zod metadata supplies the wire schema; validation still delegates to the original schema.
|
|
16
|
+
// A separate object avoids Zod's cycle extraction overwriting the metadata's definitions.
|
|
17
|
+
return z.looseObject({}).superRefine((value, context) => {
|
|
18
|
+
const parsed = schema.safeParse(value);
|
|
19
|
+
if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue({ ...issue });
|
|
20
|
+
}).meta(z.toJSONSchema(schema, { target: 'draft-07', io, reused: 'ref' }));
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
/** Decode only generated Uint8Array locations; arbitrary JSON metadata is not rewritten. */
|
|
14
24
|
export function decodeToolBytes(input: Record<string, unknown>, paths: string[][]): Record<string, unknown> {
|
|
15
25
|
if (!paths.length) return input;
|
package/src/index.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { ListToolsRequestSchema, ToolSchema, type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
3
2
|
import { createNodeBackendRuntime } from '@openfairygui/backend/node';
|
|
4
3
|
import { createRequire } from 'node:module';
|
|
5
4
|
import { z } from 'zod';
|
|
6
5
|
import { registerOpenFairyGuiBackendPrompts } from './prompt-definitions.js';
|
|
7
6
|
import { registerOpenFairyGuiBackendResources } from './resource-definitions.js';
|
|
8
|
-
import { CONTRACT_SNAPSHOT } from './contract-schema.js';
|
|
9
|
-
import { callOpenFairyGuiBackendTool, type OpenFairyGuiBackendRuntime } from './tool-handler.js';
|
|
7
|
+
import { compactToolSchema, CONTRACT_SNAPSHOT } from './contract-schema.js';
|
|
8
|
+
import { callOpenFairyGuiBackendTool, type OpenFairyGuiBackendRuntime, type OpenFairyGuiMcpToolPolicy } from './tool-handler.js';
|
|
10
9
|
import {
|
|
11
10
|
OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS,
|
|
12
11
|
type OpenFairyGuiBackendToolName,
|
|
@@ -42,19 +41,31 @@ export interface CreateOpenFairyGuiMcpServerOptions {
|
|
|
42
41
|
allowedProjectRoots?: readonly string[];
|
|
43
42
|
name?: string;
|
|
44
43
|
version?: string;
|
|
44
|
+
/** Host guidance returned by the SDK initialize handshake. */
|
|
45
|
+
instructions?: string;
|
|
46
|
+
/** Per-tool Host failures do not change the canonical Backend contracts. */
|
|
47
|
+
toolPolicies?: Partial<Record<OpenFairyGuiBackendToolName, OpenFairyGuiMcpToolPolicy>>;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export function createOpenFairyGuiMcpServer(options: CreateOpenFairyGuiMcpServerOptions = {}): McpServer {
|
|
51
|
+
for (const name of Object.keys(options.toolPolicies ?? {})) {
|
|
52
|
+
if (!OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.some((definition) => definition.name === name)) {
|
|
53
|
+
throw new RangeError(`Unknown OpenFairyGUI backend MCP tool policy: ${name}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
const runtime = options.runtime ?? createNodeBackendRuntime({
|
|
49
57
|
allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()],
|
|
50
58
|
});
|
|
51
59
|
const server = new McpServer({
|
|
52
60
|
name: options.name ?? 'openfairygui-mcp',
|
|
53
61
|
version: options.version ?? PACKAGE_VERSION,
|
|
54
|
-
});
|
|
62
|
+
}, { instructions: options.instructions });
|
|
55
63
|
|
|
56
|
-
const tools: Tool[] = [];
|
|
57
64
|
for (const definition of OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS) {
|
|
65
|
+
const policy = options.toolPolicies?.[definition.name];
|
|
66
|
+
const outputSchema = policy ? definition.outputSchema.extend({
|
|
67
|
+
backendResult: z.union([definition.outputSchema.shape.backendResult, policy.failureSchema]),
|
|
68
|
+
}) : definition.outputSchema;
|
|
58
69
|
const metadata = {
|
|
59
70
|
name: definition.name, title: definition.title, description: definition.description,
|
|
60
71
|
annotations: definition.annotations,
|
|
@@ -62,26 +73,19 @@ export function createOpenFairyGuiMcpServer(options: CreateOpenFairyGuiMcpServer
|
|
|
62
73
|
'openfairygui/backendMethod': definition.backendMethod,
|
|
63
74
|
'openfairygui/adapter': 'thin-backend-p2',
|
|
64
75
|
'openfairygui/contractDigest': CONTRACT_SNAPSHOT.digest,
|
|
76
|
+
...(policy ? { 'openfairygui/hostPolicy': true } : {}),
|
|
65
77
|
},
|
|
66
78
|
};
|
|
67
79
|
server.registerTool(
|
|
68
80
|
definition.name,
|
|
69
81
|
{
|
|
70
82
|
...metadata,
|
|
71
|
-
inputSchema: definition.inputSchema,
|
|
72
|
-
outputSchema:
|
|
83
|
+
inputSchema: compactToolSchema(definition.inputSchema, 'input'),
|
|
84
|
+
outputSchema: compactToolSchema(outputSchema, 'output'),
|
|
73
85
|
},
|
|
74
|
-
async (args: Record<string, unknown>) => callOpenFairyGuiBackendTool(runtime, definition.name
|
|
86
|
+
async (args: Record<string, unknown>) => callOpenFairyGuiBackendTool(runtime, definition.name, args, policy),
|
|
75
87
|
);
|
|
76
|
-
tools.push(ToolSchema.parse({
|
|
77
|
-
...metadata,
|
|
78
|
-
inputSchema: z.toJSONSchema(definition.inputSchema, { target: 'draft-07', io: 'input', reused: 'ref' }),
|
|
79
|
-
outputSchema: z.toJSONSchema(definition.outputSchema, { target: 'draft-07', io: 'output', reused: 'ref' }),
|
|
80
|
-
}));
|
|
81
88
|
}
|
|
82
|
-
// The installed Backend catalog is fixed. Reuse local definitions in discovery only;
|
|
83
|
-
// registered Zod schemas and the handler's structural/budget validation remain unchanged.
|
|
84
|
-
server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
|
|
85
89
|
|
|
86
90
|
registerOpenFairyGuiBackendResources(server, runtime);
|
|
87
91
|
registerOpenFairyGuiBackendPrompts(server);
|
package/src/tool-handler.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { z } from 'zod';
|
|
2
3
|
import {
|
|
3
4
|
BACKEND_CAPABILITY_SCHEMA_VERSION,
|
|
4
5
|
BACKEND_CONTRACT_VERSION,
|
|
@@ -16,6 +17,14 @@ import type { McpUnhandledFailure, McpResponseBudgetFailure } from './tool-metad
|
|
|
16
17
|
|
|
17
18
|
export type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName>;
|
|
18
19
|
|
|
20
|
+
/** Host policy runs after input validation, before the single Backend invocation. */
|
|
21
|
+
export interface OpenFairyGuiMcpToolPolicy {
|
|
22
|
+
/** Explicit Host-owned failure envelope, carried in structuredContent.backendResult. */
|
|
23
|
+
failureSchema: z.ZodType<{ ok: false }>;
|
|
24
|
+
/** Return a declared failure to stop, or undefined to call Backend with the original input. */
|
|
25
|
+
beforeCall(input: Readonly<Record<string, unknown>>): { ok: false } | undefined | Promise<{ ok: false } | undefined>;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function jsonResult(payload: unknown, isError = false, compact = false): CallToolResult {
|
|
20
29
|
const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, compact ? undefined : 2);
|
|
21
30
|
const wirePayload = JSON.parse(text) as unknown;
|
|
@@ -63,6 +72,7 @@ export async function callOpenFairyGuiBackendTool(
|
|
|
63
72
|
runtime: OpenFairyGuiBackendRuntime,
|
|
64
73
|
name: OpenFairyGuiBackendToolName,
|
|
65
74
|
input: Record<string, unknown>,
|
|
75
|
+
policy?: OpenFairyGuiMcpToolPolicy,
|
|
66
76
|
): Promise<CallToolResult> {
|
|
67
77
|
if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) {
|
|
68
78
|
throw new RangeError('MCP input exceeds the depth, node, key, string, or byte budget.');
|
|
@@ -73,15 +83,22 @@ export async function callOpenFairyGuiBackendTool(
|
|
|
73
83
|
const decoded = decodeToolBytes(parsed, CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
|
|
74
84
|
const startedAt = Date.now();
|
|
75
85
|
try {
|
|
76
|
-
|
|
86
|
+
let hostFailure = await policy?.beforeCall(structuredClone(parsed));
|
|
87
|
+
if (hostFailure !== undefined) {
|
|
88
|
+
hostFailure = policy!.failureSchema.parse(hostFailure);
|
|
89
|
+
if (!isBackendFailure(hostFailure)) throw new TypeError('Host policy must return a failure or undefined.');
|
|
90
|
+
}
|
|
91
|
+
const result = hostFailure ?? await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === 'getCapabilities' ? [] : [decoded]);
|
|
77
92
|
let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== undefined);
|
|
78
93
|
if (definition.maxResponseBytes !== undefined && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes) {
|
|
79
94
|
response = jsonResult({
|
|
80
95
|
...unhandledBackendFailure(startedAt),
|
|
81
96
|
error: { code: 'mcp_response_budget_exceeded', message: 'The complete MCP tool response exceeds its byte limit.', maxBytes: definition.maxResponseBytes },
|
|
82
97
|
} satisfies McpResponseBudgetFailure, true);
|
|
98
|
+
hostFailure = undefined;
|
|
83
99
|
}
|
|
84
|
-
definition.outputSchema.parse(response.structuredContent);
|
|
100
|
+
if (hostFailure === undefined) definition.outputSchema.parse(response.structuredContent);
|
|
101
|
+
else policy!.failureSchema.parse(response.structuredContent?.backendResult);
|
|
85
102
|
return response;
|
|
86
103
|
} catch {
|
|
87
104
|
return jsonResult(unhandledBackendFailure(startedAt), true);
|