@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 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 advertises the fixed 20-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`.
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-CB98zdOf.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
@@ -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
- 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>;
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
- 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>;
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-BNobuRxx.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;
@@ -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 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);
560
571
  const startedAt = Date.now();
561
572
  try {
562
- const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
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) response = jsonResult({
565
- ...unhandledBackendFailure(startedAt),
566
- error: {
567
- code: "mcp_response_budget_exceeded",
568
- message: "The complete MCP tool response exceeds its byte limit.",
569
- maxBytes: definition.maxResponseBytes
570
- }
571
- }, true);
572
- definition.outputSchema.parse(response.structuredContent);
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.1";
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: definition.outputSchema
618
- }, async (args) => callOpenFairyGuiBackendTool(runtime, definition.name, args));
619
- tools.push(ToolSchema.parse({
620
- ...metadata,
621
- inputSchema: z.toJSONSchema(definition.inputSchema, {
622
- target: "draft-07",
623
- io: "input",
624
- reused: "ref"
625
- }),
626
- outputSchema: z.toJSONSchema(definition.outputSchema, {
627
- target: "draft-07",
628
- io: "output",
629
- reused: "ref"
630
- })
631
- }));
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 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);
583
594
  const startedAt = Date.now();
584
595
  try {
585
- const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === "getCapabilities" ? [] : [decoded]);
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) response = jsonResult({
588
- ...unhandledBackendFailure(startedAt),
589
- error: {
590
- code: "mcp_response_budget_exceeded",
591
- message: "The complete MCP tool response exceeds its byte limit.",
592
- maxBytes: definition.maxResponseBytes
593
- }
594
- }, true);
595
- definition.outputSchema.parse(response.structuredContent);
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.1";
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: definition.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
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_stdio = require("./stdio-CB98zdOf.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-BNobuRxx.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.5.0-alpha.1",
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.1"
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.1"
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,
@@ -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
- const result = await Reflect.apply(runtime[definition.backendMethod], runtime, definition.backendMethod === 'getCapabilities' ? [] : [decoded]);
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);