@openfairygui/mcp 0.4.0 → 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 CHANGED
@@ -14,6 +14,8 @@ It maps the backend P2 runtime surface into MCP tools:
14
14
  - `getSession`
15
15
  - `getProjectOutline`
16
16
  - `queryEntity`
17
+ - `readSessionState`
18
+ - `readResourceBytes`
17
19
  - `validateSession`
18
20
  - `preflightTransaction`
19
21
  - `applyTransaction`
@@ -34,7 +36,7 @@ Each tool exposes a method-specific output schema for `structuredContent.backend
34
36
  - `error?`
35
37
  - `meta?`
36
38
 
37
- The factory advertises the fixed 18-method Backend catalog. 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`.
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`.
38
40
 
39
41
  P1 also registers MCP-native ergonomics around the same backend surface:
40
42
 
@@ -49,6 +51,8 @@ P1 also registers MCP-native ergonomics around the same backend surface:
49
51
  Resources return `application/json` text containing the unchanged backend result envelope. Parameterized polling remains tool-based: `getEvents` and `listJobs` are not exposed as resource URI query grammars.
50
52
  The project outline is revision-bound and exposes package, resource, folder, display-node, controller-page, and transition identities for transaction planning. It intentionally omits source bytes and full property payloads. `validateSession` returns the backend-owned read-only project validation report; the MCP adapter does not reinterpret its diagnostics.
51
53
 
54
+ `readSessionState` returns a detached public UAM model without primary asset bytes, plus the current edit revision and source-read diagnostics. `readResourceBytes` reads one already-loaded primary asset with a required matching revision. Neither reads storage or changes the session. Both tools enforce their native response limits and a separate 16 MiB complete MCP response limit; see the installed method schemas and [workflow](../backend/docs/workflow.md).
55
+
52
56
  It does **not** redefine transaction selectors, transaction operations, path policy, session semantics, job semantics, cache semantics, or backend error envelopes. Those remain owned by `@openfairygui/backend`, `@openfairygui/functions`, and `@openfairygui/core`.
53
57
 
54
58
  It also does **not** activate artifact publish/restore jobs, subscriptions, persistent jobs, or cache-as-source-of-truth behavior. MCP roots may be useful client context, but this package does not enforce roots or duplicate backend path canonicalization; backend path policy remains authoritative.
@@ -67,6 +71,47 @@ For stdio clients, use the package binary:
67
71
  ofgui-mcp
68
72
  ```
69
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
+
70
115
  Example local MCP client configuration:
71
116
 
72
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-B0OU-oZC.cjs");
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
@@ -11,6 +11,8 @@ interface BackendToolMetadata {
11
11
  backendMethod: BackendMethodName$1;
12
12
  title: string;
13
13
  description: string;
14
+ /** Bound the complete CallToolResult JSON; bounded reads use compact text JSON. */
15
+ maxResponseBytes?: number;
14
16
  annotations: {
15
17
  readOnlyHint?: boolean;
16
18
  destructiveHint?: boolean;
@@ -78,6 +80,28 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_METADATA: readonly [{
78
80
  readonly idempotentHint: true;
79
81
  readonly openWorldHint: false;
80
82
  };
83
+ }, {
84
+ readonly name: "openfairygui_backend_read_session_state";
85
+ readonly backendMethod: "readSessionState";
86
+ readonly title: "Read Session State";
87
+ readonly description: "Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.";
88
+ readonly maxResponseBytes: 16777216;
89
+ readonly annotations: {
90
+ readonly readOnlyHint: true;
91
+ readonly idempotentHint: true;
92
+ readonly openWorldHint: false;
93
+ };
94
+ }, {
95
+ readonly name: "openfairygui_backend_read_resource_bytes";
96
+ readonly backendMethod: "readResourceBytes";
97
+ readonly title: "Read Resource Bytes";
98
+ readonly description: "Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.";
99
+ readonly maxResponseBytes: 16777216;
100
+ readonly annotations: {
101
+ readonly readOnlyHint: true;
102
+ readonly idempotentHint: true;
103
+ readonly openWorldHint: false;
104
+ };
81
105
  }, {
82
106
  readonly name: "openfairygui_backend_validate_session";
83
107
  readonly backendMethod: "validateSession";
@@ -206,7 +230,7 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_METADATA: readonly [{
206
230
  //#region src/tool-definitions.d.ts
207
231
  declare const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
208
232
  type OpenFairyGuiBackendToolName = typeof OPENFAIRYGUI_BACKEND_TOOL_METADATA[number]['name'];
209
- declare const OPENFAIRYGUI_BACKEND_TOOL_NAMES: ("openfairygui_backend_get_capabilities" | "openfairygui_backend_open_session" | "openfairygui_backend_open_project_session" | "openfairygui_backend_get_session" | "openfairygui_backend_get_project_outline" | "openfairygui_backend_query_entity" | "openfairygui_backend_validate_session" | "openfairygui_backend_preflight_transaction" | "openfairygui_backend_apply_transaction" | "openfairygui_backend_save_session" | "openfairygui_backend_materialize_session" | "openfairygui_backend_close_session" | "openfairygui_backend_get_events" | "openfairygui_backend_get_job" | "openfairygui_backend_list_jobs" | "openfairygui_backend_cancel_job" | "openfairygui_backend_get_cache_snapshot" | "openfairygui_backend_refresh_cache")[];
233
+ declare const OPENFAIRYGUI_BACKEND_TOOL_NAMES: ("openfairygui_backend_get_capabilities" | "openfairygui_backend_open_session" | "openfairygui_backend_open_project_session" | "openfairygui_backend_get_session" | "openfairygui_backend_get_project_outline" | "openfairygui_backend_query_entity" | "openfairygui_backend_read_session_state" | "openfairygui_backend_read_resource_bytes" | "openfairygui_backend_validate_session" | "openfairygui_backend_preflight_transaction" | "openfairygui_backend_apply_transaction" | "openfairygui_backend_save_session" | "openfairygui_backend_materialize_session" | "openfairygui_backend_close_session" | "openfairygui_backend_get_events" | "openfairygui_backend_get_job" | "openfairygui_backend_list_jobs" | "openfairygui_backend_cancel_job" | "openfairygui_backend_get_cache_snapshot" | "openfairygui_backend_refresh_cache")[];
210
234
  interface OpenFairyGuiBackendToolDefinition extends BackendToolMetadata {
211
235
  name: OpenFairyGuiBackendToolName;
212
236
  inputSchema: z.ZodObject;
@@ -216,7 +240,20 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS: readonly OpenFairyGuiBacken
216
240
  //#endregion
217
241
  //#region src/tool-handler.d.ts
218
242
  type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName$1>;
219
- declare function callOpenFairyGuiBackendTool(runtime: OpenFairyGuiBackendRuntime, name: OpenFairyGuiBackendToolName, input: Record<string, unknown>): Promise<CallToolResult>;
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>;
220
257
  //#endregion
221
258
  //#region src/server.d.ts
222
259
  interface CreateOpenFairyGuiMcpServerOptions {
@@ -225,6 +262,10 @@ interface CreateOpenFairyGuiMcpServerOptions {
225
262
  allowedProjectRoots?: readonly string[];
226
263
  name?: string;
227
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>>;
228
269
  }
229
270
  declare function createOpenFairyGuiMcpServer(options?: CreateOpenFairyGuiMcpServerOptions): McpServer;
230
271
  //#endregion
@@ -267,4 +308,4 @@ declare const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS: readonly [{
267
308
  declare const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
268
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}"];
269
310
  //#endregion
270
- 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 {
@@ -11,6 +11,8 @@ interface BackendToolMetadata {
11
11
  backendMethod: BackendMethodName$1;
12
12
  title: string;
13
13
  description: string;
14
+ /** Bound the complete CallToolResult JSON; bounded reads use compact text JSON. */
15
+ maxResponseBytes?: number;
14
16
  annotations: {
15
17
  readOnlyHint?: boolean;
16
18
  destructiveHint?: boolean;
@@ -78,6 +80,28 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_METADATA: readonly [{
78
80
  readonly idempotentHint: true;
79
81
  readonly openWorldHint: false;
80
82
  };
83
+ }, {
84
+ readonly name: "openfairygui_backend_read_session_state";
85
+ readonly backendMethod: "readSessionState";
86
+ readonly title: "Read Session State";
87
+ readonly description: "Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.";
88
+ readonly maxResponseBytes: 16777216;
89
+ readonly annotations: {
90
+ readonly readOnlyHint: true;
91
+ readonly idempotentHint: true;
92
+ readonly openWorldHint: false;
93
+ };
94
+ }, {
95
+ readonly name: "openfairygui_backend_read_resource_bytes";
96
+ readonly backendMethod: "readResourceBytes";
97
+ readonly title: "Read Resource Bytes";
98
+ readonly description: "Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.";
99
+ readonly maxResponseBytes: 16777216;
100
+ readonly annotations: {
101
+ readonly readOnlyHint: true;
102
+ readonly idempotentHint: true;
103
+ readonly openWorldHint: false;
104
+ };
81
105
  }, {
82
106
  readonly name: "openfairygui_backend_validate_session";
83
107
  readonly backendMethod: "validateSession";
@@ -206,7 +230,7 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_METADATA: readonly [{
206
230
  //#region src/tool-definitions.d.ts
207
231
  declare const OPENFAIRYGUI_BACKEND_TOOL_PREFIX = "openfairygui_backend_";
208
232
  type OpenFairyGuiBackendToolName = typeof OPENFAIRYGUI_BACKEND_TOOL_METADATA[number]['name'];
209
- declare const OPENFAIRYGUI_BACKEND_TOOL_NAMES: ("openfairygui_backend_get_capabilities" | "openfairygui_backend_open_session" | "openfairygui_backend_open_project_session" | "openfairygui_backend_get_session" | "openfairygui_backend_get_project_outline" | "openfairygui_backend_query_entity" | "openfairygui_backend_validate_session" | "openfairygui_backend_preflight_transaction" | "openfairygui_backend_apply_transaction" | "openfairygui_backend_save_session" | "openfairygui_backend_materialize_session" | "openfairygui_backend_close_session" | "openfairygui_backend_get_events" | "openfairygui_backend_get_job" | "openfairygui_backend_list_jobs" | "openfairygui_backend_cancel_job" | "openfairygui_backend_get_cache_snapshot" | "openfairygui_backend_refresh_cache")[];
233
+ declare const OPENFAIRYGUI_BACKEND_TOOL_NAMES: ("openfairygui_backend_get_capabilities" | "openfairygui_backend_open_session" | "openfairygui_backend_open_project_session" | "openfairygui_backend_get_session" | "openfairygui_backend_get_project_outline" | "openfairygui_backend_query_entity" | "openfairygui_backend_read_session_state" | "openfairygui_backend_read_resource_bytes" | "openfairygui_backend_validate_session" | "openfairygui_backend_preflight_transaction" | "openfairygui_backend_apply_transaction" | "openfairygui_backend_save_session" | "openfairygui_backend_materialize_session" | "openfairygui_backend_close_session" | "openfairygui_backend_get_events" | "openfairygui_backend_get_job" | "openfairygui_backend_list_jobs" | "openfairygui_backend_cancel_job" | "openfairygui_backend_get_cache_snapshot" | "openfairygui_backend_refresh_cache")[];
210
234
  interface OpenFairyGuiBackendToolDefinition extends BackendToolMetadata {
211
235
  name: OpenFairyGuiBackendToolName;
212
236
  inputSchema: z.ZodObject;
@@ -216,7 +240,20 @@ declare const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS: readonly OpenFairyGuiBacken
216
240
  //#endregion
217
241
  //#region src/tool-handler.d.ts
218
242
  type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName$1>;
219
- declare function callOpenFairyGuiBackendTool(runtime: OpenFairyGuiBackendRuntime, name: OpenFairyGuiBackendToolName, input: Record<string, unknown>): Promise<CallToolResult>;
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>;
220
257
  //#endregion
221
258
  //#region src/server.d.ts
222
259
  interface CreateOpenFairyGuiMcpServerOptions {
@@ -225,6 +262,10 @@ interface CreateOpenFairyGuiMcpServerOptions {
225
262
  allowedProjectRoots?: readonly string[];
226
263
  name?: string;
227
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>>;
228
269
  }
229
270
  declare function createOpenFairyGuiMcpServer(options?: CreateOpenFairyGuiMcpServerOptions): McpServer;
230
271
  //#endregion
@@ -267,4 +308,4 @@ declare const OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS: readonly [{
267
308
  declare const OPENFAIRYGUI_BACKEND_CAPABILITIES_RESOURCE_URI = "openfairygui://backend/capabilities";
268
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}"];
269
310
  //#endregion
270
- 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-9ka7bvOr.mjs";
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;
@@ -304,6 +314,30 @@ const OPENFAIRYGUI_BACKEND_TOOL_METADATA = [
304
314
  openWorldHint: false
305
315
  }
306
316
  },
317
+ {
318
+ name: "openfairygui_backend_read_session_state",
319
+ backendMethod: "readSessionState",
320
+ title: "Read Session State",
321
+ description: "Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.",
322
+ maxResponseBytes: 16777216,
323
+ annotations: {
324
+ readOnlyHint: true,
325
+ idempotentHint: true,
326
+ openWorldHint: false
327
+ }
328
+ },
329
+ {
330
+ name: "openfairygui_backend_read_resource_bytes",
331
+ backendMethod: "readResourceBytes",
332
+ title: "Read Resource Bytes",
333
+ description: "Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.",
334
+ maxResponseBytes: 16777216,
335
+ annotations: {
336
+ readOnlyHint: true,
337
+ idempotentHint: true,
338
+ openWorldHint: false
339
+ }
340
+ },
307
341
  {
308
342
  name: "openfairygui_backend_validate_session",
309
343
  backendMethod: "validateSession",
@@ -495,8 +529,8 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = OPENFAIRYGUI_BACKEND_TOOL_METADATA
495
529
  });
496
530
  //#endregion
497
531
  //#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);
532
+ function jsonResult(payload, isError = false, compact = false) {
533
+ const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, compact ? void 0 : 2);
500
534
  const wirePayload = JSON.parse(text);
501
535
  return {
502
536
  content: [{
@@ -528,16 +562,34 @@ function unhandledBackendFailure(startedAt) {
528
562
  }
529
563
  };
530
564
  }
531
- async function callOpenFairyGuiBackendTool(runtime, name, input) {
565
+ async function callOpenFairyGuiBackendTool(runtime, name, input, policy) {
532
566
  if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
533
567
  const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
534
568
  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);
569
+ const parsed = definition.inputSchema.parse(input);
570
+ const decoded = decodeToolBytes(parsed, CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
536
571
  const startedAt = Date.now();
537
572
  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);
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]);
579
+ let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== void 0);
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);
541
593
  return response;
542
594
  } catch {
543
595
  return jsonResult(unhandledBackendFailure(startedAt), true);
@@ -547,7 +599,7 @@ async function callOpenFairyGuiBackendTool(runtime, name, input) {
547
599
  //#region src/server.ts
548
600
  const require = createRequire(import.meta.url);
549
601
  function getInjectedPackageVersion() {
550
- const version = "0.4.0";
602
+ const version = "0.5.0-alpha.2";
551
603
  return typeof version === "string" && true ? version : null;
552
604
  }
553
605
  function readPackageVersion() {
@@ -561,13 +613,15 @@ function readPackageVersion() {
561
613
  }
562
614
  const PACKAGE_VERSION = readPackageVersion();
563
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}`);
564
617
  const runtime = options.runtime ?? createNodeBackendRuntime({ allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()] });
565
618
  const server = new McpServer({
566
619
  name: options.name ?? "openfairygui-mcp",
567
620
  version: options.version ?? PACKAGE_VERSION
568
- });
569
- const tools = [];
621
+ }, { instructions: options.instructions });
570
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;
571
625
  const metadata = {
572
626
  name: definition.name,
573
627
  title: definition.title,
@@ -576,29 +630,16 @@ function createOpenFairyGuiMcpServer(options = {}) {
576
630
  _meta: {
577
631
  "openfairygui/backendMethod": definition.backendMethod,
578
632
  "openfairygui/adapter": "thin-backend-p2",
579
- "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
633
+ "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest,
634
+ ...policy ? { "openfairygui/hostPolicy": true } : {}
580
635
  }
581
636
  };
582
637
  server.registerTool(definition.name, {
583
638
  ...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
- }));
639
+ inputSchema: compactToolSchema(definition.inputSchema, "input"),
640
+ outputSchema: compactToolSchema(outputSchema, "output")
641
+ }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args, policy));
600
642
  }
601
- server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
602
643
  registerOpenFairyGuiBackendResources(server, runtime);
603
644
  registerOpenFairyGuiBackendPrompts(server);
604
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;
@@ -327,6 +337,30 @@ const OPENFAIRYGUI_BACKEND_TOOL_METADATA = [
327
337
  openWorldHint: false
328
338
  }
329
339
  },
340
+ {
341
+ name: "openfairygui_backend_read_session_state",
342
+ backendMethod: "readSessionState",
343
+ title: "Read Session State",
344
+ description: "Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.",
345
+ maxResponseBytes: 16777216,
346
+ annotations: {
347
+ readOnlyHint: true,
348
+ idempotentHint: true,
349
+ openWorldHint: false
350
+ }
351
+ },
352
+ {
353
+ name: "openfairygui_backend_read_resource_bytes",
354
+ backendMethod: "readResourceBytes",
355
+ title: "Read Resource Bytes",
356
+ description: "Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.",
357
+ maxResponseBytes: 16777216,
358
+ annotations: {
359
+ readOnlyHint: true,
360
+ idempotentHint: true,
361
+ openWorldHint: false
362
+ }
363
+ },
330
364
  {
331
365
  name: "openfairygui_backend_validate_session",
332
366
  backendMethod: "validateSession",
@@ -518,8 +552,8 @@ const OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS = OPENFAIRYGUI_BACKEND_TOOL_METADATA
518
552
  });
519
553
  //#endregion
520
554
  //#region src/tool-handler.ts
521
- function jsonResult(payload, isError = false) {
522
- const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, 2);
555
+ function jsonResult(payload, isError = false, compact = false) {
556
+ const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, compact ? void 0 : 2);
523
557
  const wirePayload = JSON.parse(text);
524
558
  return {
525
559
  content: [{
@@ -551,16 +585,34 @@ function unhandledBackendFailure(startedAt) {
551
585
  }
552
586
  };
553
587
  }
554
- async function callOpenFairyGuiBackendTool(runtime, name, input) {
588
+ async function callOpenFairyGuiBackendTool(runtime, name, input, policy) {
555
589
  if (!isOpenFairyGuiMcpPayloadWithinBudget(input)) throw new RangeError("MCP input exceeds the depth, node, key, string, or byte budget.");
556
590
  const definition = OPENFAIRYGUI_BACKEND_TOOL_DEFINITIONS.find((entry) => entry.name === name);
557
591
  if (!definition) throw new RangeError(`Unknown OpenFairyGUI backend MCP tool: ${name}`);
558
- const decoded = decodeToolBytes(definition.inputSchema.parse(input), CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
592
+ const parsed = definition.inputSchema.parse(input);
593
+ const decoded = decodeToolBytes(parsed, CONTRACT_SNAPSHOT.tools[definition.backendMethod].bytePaths);
559
594
  const startedAt = Date.now();
560
595
  try {
561
- const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
562
- const response = jsonResult(result, isBackendFailure(result));
563
- definition.outputSchema.parse(response.structuredContent);
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]);
602
+ let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== void 0);
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);
564
616
  return response;
565
617
  } catch {
566
618
  return jsonResult(unhandledBackendFailure(startedAt), true);
@@ -570,7 +622,7 @@ async function callOpenFairyGuiBackendTool(runtime, name, input) {
570
622
  //#region src/server.ts
571
623
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
572
624
  function getInjectedPackageVersion() {
573
- const version = "0.4.0";
625
+ const version = "0.5.0-alpha.2";
574
626
  return typeof version === "string" && true ? version : null;
575
627
  }
576
628
  function readPackageVersion() {
@@ -584,13 +636,15 @@ function readPackageVersion() {
584
636
  }
585
637
  const PACKAGE_VERSION = readPackageVersion();
586
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}`);
587
640
  const runtime = options.runtime ?? (0, _openfairygui_backend_node.createNodeBackendRuntime)({ allowedProjectRoots: options.allowedProjectRoots ?? [process.cwd()] });
588
641
  const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
589
642
  name: options.name ?? "openfairygui-mcp",
590
643
  version: options.version ?? PACKAGE_VERSION
591
- });
592
- const tools = [];
644
+ }, { instructions: options.instructions });
593
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;
594
648
  const metadata = {
595
649
  name: definition.name,
596
650
  title: definition.title,
@@ -599,29 +653,16 @@ function createOpenFairyGuiMcpServer(options = {}) {
599
653
  _meta: {
600
654
  "openfairygui/backendMethod": definition.backendMethod,
601
655
  "openfairygui/adapter": "thin-backend-p2",
602
- "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest
656
+ "openfairygui/contractDigest": CONTRACT_SNAPSHOT.digest,
657
+ ...policy ? { "openfairygui/hostPolicy": true } : {}
603
658
  }
604
659
  };
605
660
  server.registerTool(definition.name, {
606
661
  ...metadata,
607
- inputSchema: definition.inputSchema,
608
- outputSchema: definition.outputSchema
609
- }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
610
- tools.push(_modelcontextprotocol_sdk_types_js.ToolSchema.parse({
611
- ...metadata,
612
- inputSchema: zod.z.toJSONSchema(definition.inputSchema, {
613
- target: "draft-07",
614
- io: "input",
615
- reused: "ref"
616
- }),
617
- outputSchema: zod.z.toJSONSchema(definition.outputSchema, {
618
- target: "draft-07",
619
- io: "output",
620
- reused: "ref"
621
- })
622
- }));
662
+ inputSchema: compactToolSchema(definition.inputSchema, "input"),
663
+ outputSchema: compactToolSchema(outputSchema, "output")
664
+ }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args, policy));
623
665
  }
624
- server.server.setRequestHandler(_modelcontextprotocol_sdk_types_js.ListToolsRequestSchema, () => ({ tools: structuredClone(tools) }));
625
666
  registerOpenFairyGuiBackendResources(server, runtime);
626
667
  registerOpenFairyGuiBackendPrompts(server);
627
668
  return server;
package/dist/stdio.cjs CHANGED
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_stdio = require("./stdio-B0OU-oZC.cjs");
2
+ const require_stdio = require("./stdio-D33c37mD.cjs");
3
3
  exports.connectOpenFairyGuiMcpStdio = require_stdio.connectOpenFairyGuiMcpStdio;
package/dist/stdio.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as connectOpenFairyGuiMcpStdio } from "./stdio-9ka7bvOr.mjs";
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.4.0",
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.4.0"
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
- "@openfairygui/core": "0.4.0",
70
- "@openfairygui/test-utils": "0.3.0"
69
+ "@openfairygui/test-utils": "0.3.0",
70
+ "@openfairygui/core": "0.5.0-alpha.2"
71
71
  },
72
72
  "ava": {
73
73
  "extensions": {
@@ -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
@@ -8,6 +8,7 @@ export {
8
8
  export {
9
9
  callOpenFairyGuiBackendTool,
10
10
  type OpenFairyGuiBackendRuntime,
11
+ type OpenFairyGuiMcpToolPolicy,
11
12
  } from './tool-handler.js';
12
13
  export {
13
14
  OPENFAIRYGUI_BACKEND_PROMPT_DEFINITIONS,
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: definition.outputSchema,
83
+ inputSchema: compactToolSchema(definition.inputSchema, 'input'),
84
+ outputSchema: compactToolSchema(outputSchema, 'output'),
73
85
  },
74
- async (args: Record<string, unknown>) => callOpenFairyGuiBackendTool(runtime, definition.name as OpenFairyGuiBackendToolName, args),
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);
@@ -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,
@@ -12,12 +13,20 @@ import {
12
13
  } from './tool-definitions.js';
13
14
 
14
15
  import { decodeToolBytes, CONTRACT_SNAPSHOT } from './contract-schema.js';
15
- import type { McpUnhandledFailure } from './tool-metadata.js';
16
+ import type { McpUnhandledFailure, McpResponseBudgetFailure } from './tool-metadata.js';
16
17
 
17
18
  export type OpenFairyGuiBackendRuntime = Pick<BackendRuntime, BackendMethodName>;
18
19
 
19
- function jsonResult(payload: unknown, isError = false): CallToolResult {
20
- const text = JSON.stringify(payload, (_key, value) => value instanceof Uint8Array ? [...value] : value, 2);
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
+
28
+ function jsonResult(payload: unknown, isError = false, compact = false): CallToolResult {
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;
22
31
  return {
23
32
  content: [
@@ -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,9 +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
- const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === 'getCapabilities' ? [] : [decoded]);
77
- const response = jsonResult(result, isBackendFailure(result));
78
- definition.outputSchema.parse(response.structuredContent);
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]);
92
+ let response = jsonResult(result, isBackendFailure(result), definition.maxResponseBytes !== undefined);
93
+ if (definition.maxResponseBytes !== undefined && new TextEncoder().encode(JSON.stringify(response)).byteLength > definition.maxResponseBytes) {
94
+ response = jsonResult({
95
+ ...unhandledBackendFailure(startedAt),
96
+ error: { code: 'mcp_response_budget_exceeded', message: 'The complete MCP tool response exceeds its byte limit.', maxBytes: definition.maxResponseBytes },
97
+ } satisfies McpResponseBudgetFailure, true);
98
+ hostFailure = undefined;
99
+ }
100
+ if (hostFailure === undefined) definition.outputSchema.parse(response.structuredContent);
101
+ else policy!.failureSchema.parse(response.structuredContent?.backendResult);
79
102
  return response;
80
103
  } catch {
81
104
  return jsonResult(unhandledBackendFailure(startedAt), true);
@@ -5,6 +5,8 @@ export interface BackendToolMetadata {
5
5
  backendMethod: BackendMethodName;
6
6
  title: string;
7
7
  description: string;
8
+ /** Bound the complete CallToolResult JSON; bounded reads use compact text JSON. */
9
+ maxResponseBytes?: number;
8
10
  annotations: {
9
11
  readOnlyHint?: boolean;
10
12
  destructiveHint?: boolean;
@@ -20,6 +22,12 @@ export interface McpUnhandledFailure {
20
22
  error: { code: 'backend_unhandled_error'; message: string };
21
23
  }
22
24
 
25
+ export interface McpResponseBudgetFailure {
26
+ ok: false;
27
+ meta: BackendResponseMeta;
28
+ error: { code: 'mcp_response_budget_exceeded'; message: string; maxBytes: number };
29
+ }
30
+
23
31
  /** Host objects cannot cross JSON; materialize keeps its existing MCP target boundary. */
24
32
  export const MCP_OMITTED_INPUT_FIELDS = {
25
33
  openProjectSession: ['storage'],
@@ -70,6 +78,22 @@ export const OPENFAIRYGUI_BACKEND_TOOL_METADATA = [
70
78
  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.',
71
79
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
72
80
  },
81
+ {
82
+ name: 'openfairygui_backend_read_session_state',
83
+ backendMethod: 'readSessionState',
84
+ title: 'Read Session State',
85
+ description: 'Read a detached copy of the currently committed public UAM model without primary asset sourceBytes, with revision, dirty state and source-read diagnostics. Optional expectedRevision rejects stale reads. Does not hydrate, write, reserve history or guarantee downstream usability. Complete tool response is limited to 16 MiB.',
86
+ maxResponseBytes: 16777216,
87
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
88
+ },
89
+ {
90
+ name: 'openfairygui_backend_read_resource_bytes',
91
+ backendMethod: 'readResourceBytes',
92
+ title: 'Read Resource Bytes',
93
+ description: 'Read a detached copy of one asset resource primary sourceBytes already held in the session, using exact packageId/resourceId and the required model edit revision. No filesystem hydration or auxiliary-file discovery. Stale reads require restarting the model/bytes read. Complete tool response is limited to 16 MiB.',
94
+ maxResponseBytes: 16777216,
95
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
96
+ },
73
97
  {
74
98
  name: 'openfairygui_backend_validate_session',
75
99
  backendMethod: 'validateSession',