@ggui-ai/mcp-server 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +20 -0
  2. package/dist/api-renders-routes.d.ts +0 -7
  3. package/dist/api-renders-routes.d.ts.map +1 -1
  4. package/dist/api-renders-routes.js +5 -5
  5. package/dist/api-renders-stream-route.js +4 -4
  6. package/dist/auth.d.ts +3 -3
  7. package/dist/auth.js +6 -6
  8. package/dist/build-mcp.d.ts +1 -1
  9. package/dist/console-blueprint-routes.js +2 -2
  10. package/dist/console-chat-routes.d.ts +1 -1
  11. package/dist/console-chat-routes.js +1 -1
  12. package/dist/console-keys-routes.js +2 -2
  13. package/dist/console-llm-keys-routes.d.ts +1 -1
  14. package/dist/console-llm-keys-routes.js +5 -5
  15. package/dist/console-registry-routes.d.ts +1 -1
  16. package/dist/console-registry-routes.js +1 -1
  17. package/dist/console-theme-routes.d.ts +1 -1
  18. package/dist/console-theme-routes.js +1 -1
  19. package/dist/control-service.d.ts +3 -3
  20. package/dist/control-service.js +3 -3
  21. package/dist/ggui-session-channel/socket-router.js +2 -2
  22. package/dist/ggui-session-channel/subscribe.d.ts +1 -1
  23. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  24. package/dist/ggui-session-channel/subscribe.js +2 -2
  25. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +1 -1
  26. package/dist/ggui-session-channel/subscriber-lifecycle.js +1 -1
  27. package/dist/ggui-session-channel.d.ts +4 -4
  28. package/dist/ggui-session-channel.js +4 -4
  29. package/dist/health-routes.d.ts +7 -6
  30. package/dist/health-routes.d.ts.map +1 -1
  31. package/dist/health-routes.js +8 -7
  32. package/dist/index.js +1 -1
  33. package/dist/instructions-presets.js +5 -5
  34. package/dist/mcp-apps-outbound.d.ts +17 -11
  35. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  36. package/dist/mcp-apps-outbound.js +53 -25
  37. package/dist/mcp-endpoint-routes.d.ts +12 -4
  38. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  39. package/dist/mcp-endpoint-routes.js +89 -30
  40. package/dist/oauth-as-routes.d.ts +1 -1
  41. package/dist/oauth.d.ts +3 -3
  42. package/dist/oauth.js +3 -3
  43. package/dist/render-read-gate.d.ts +3 -3
  44. package/dist/render-read-gate.js +2 -2
  45. package/dist/runtime-bundle-hash.d.ts +1 -1
  46. package/dist/runtime-bundle-hash.js +1 -1
  47. package/dist/schema-compat.d.ts +2 -1
  48. package/dist/schema-compat.d.ts.map +1 -1
  49. package/dist/schema-compat.js +5 -5
  50. package/dist/server.d.ts +48 -76
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +29 -41
  53. package/dist/thread-transport.js +1 -1
  54. package/package.json +14 -14
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * POST <universalMcpPath> — DATA PLANE: agent+runtime tools
5
5
  * (default `/mcp`)
6
- * POST <pathPrefix>/:appId — data plane, per-tenant variant
6
+ * POST <pathPrefix>/:appId — data plane, per-app variant
7
7
  * (opt-in via `perAppRouting`)
8
8
  * POST /control — CONTROL PLANE: design-time
9
9
  * spec/discovery (anonymous) +
@@ -31,13 +31,50 @@
31
31
  * (`agent` / `runtime` / `protocol` / `ops`) and the wire-name prefix
32
32
  * rules.
33
33
  */
34
- import { isRecord } from "@ggui-ai/protocol";
34
+ import { MCP_ERROR_CODES, isRecord } from "@ggui-ai/protocol";
35
35
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
36
36
  import { randomUUID } from "node:crypto";
37
37
  import { resolveIdentity, UnauthenticatedError } from "./auth.js";
38
38
  import { buildMcpServer } from "./build-mcp.js";
39
39
  import { CONTROL_PATH, DATA_PLANE_AUDIENCES, filterHandlersByAudience, } from "./control-service.js";
40
40
  import { buildWwwAuthenticate, resolveIssuerUrl } from "./oauth.js";
41
+ /** The JSON-RPC error object a mapped result becomes on the wire. */
42
+ function jsonRpcError(mapped) {
43
+ return {
44
+ code: mapped.code,
45
+ message: mapped.message,
46
+ ...(mapped.data !== undefined ? { data: mapped.data } : {}),
47
+ };
48
+ }
49
+ /** The two statuses an authorization refusal may carry — a mapper is bounded to them. */
50
+ const AUTHORIZATION_REFUSAL_STATUSES = new Set([401, 403]);
51
+ /**
52
+ * A deployment's error mapper may attach JSON-RPC `data` (and its own
53
+ * `code` / `message` / headers) to a per-app authorization refusal, so a
54
+ * client can read a structured reason instead of parsing prose. The
55
+ * refusal stays a 401 or a 403: a mapper answering any other status, or
56
+ * throwing, is ignored and logged, and the default-deny 403 stands
57
+ * byte-identical to a deployment with no mapper at all.
58
+ */
59
+ function mapAuthorizationRefusal(err, errorMapper, log) {
60
+ if (!errorMapper)
61
+ return undefined;
62
+ let mapped;
63
+ try {
64
+ mapped = errorMapper(err);
65
+ }
66
+ catch (mapperErr) {
67
+ log.warn("error_mapper_failed", { error: String(mapperErr) });
68
+ return undefined;
69
+ }
70
+ if (mapped === undefined)
71
+ return undefined;
72
+ if (!AUTHORIZATION_REFUSAL_STATUSES.has(mapped.status)) {
73
+ log.warn("per_app_authorize_mapper_out_of_bounds", { status: mapped.status });
74
+ return undefined;
75
+ }
76
+ return mapped;
77
+ }
41
78
  /**
42
79
  * Resolve the resource path that `WWW-Authenticate` should point at
43
80
  * for the current request. Per-app `/mcp` requests
@@ -113,7 +150,7 @@ export function mountMcpEndpoints(opts) {
113
150
  identity = { identity: { kind: "builder" }, source: "anonymous" };
114
151
  }
115
152
  else {
116
- // Diagnostic: did a Bearer credential actually reach the pod on
153
+ // Diagnostic: did a Bearer credential actually reach the server on
117
154
  // this request? This splits "the client/transport never sent one"
118
155
  // from "we received it but the adapter rejected it" — otherwise
119
156
  // indistinguishable in `auth_failed`. Only a short, non-secret
@@ -145,7 +182,7 @@ export function mountMcpEndpoints(opts) {
145
182
  }
146
183
  res.status(401).json({
147
184
  jsonrpc: "2.0",
148
- error: { code: -32000, message: err.message },
185
+ error: { code: MCP_ERROR_CODES.UNAUTHORIZED, message: err.message },
149
186
  id: null,
150
187
  });
151
188
  return;
@@ -171,7 +208,10 @@ export function mountMcpEndpoints(opts) {
171
208
  reqLogger.warn("federated_identity_rejected", { route: req.path });
172
209
  res.status(403).json({
173
210
  jsonrpc: "2.0",
174
- error: { code: -32000, message: "federated identities are not permitted on this route" },
211
+ error: {
212
+ code: MCP_ERROR_CODES.UNAUTHORIZED,
213
+ message: "federated identities are not permitted on this route",
214
+ },
175
215
  id: null,
176
216
  });
177
217
  return;
@@ -196,7 +236,7 @@ export function mountMcpEndpoints(opts) {
196
236
  res.status(401).json({
197
237
  jsonrpc: "2.0",
198
238
  error: {
199
- code: -32000,
239
+ code: MCP_ERROR_CODES.UNAUTHORIZED,
200
240
  message: `${opsToolName} is an operator tool and needs an authenticated caller. ` +
201
241
  `Present a bearer token this deployment accepts, or complete the OAuth flow ` +
202
242
  `advertised in WWW-Authenticate (universal connector keys come from the ` +
@@ -207,10 +247,10 @@ export function mountMcpEndpoints(opts) {
207
247
  return;
208
248
  }
209
249
  }
210
- // Per-tenant URL routing. When `perAppRouting`
250
+ // Per-app URL routing. When `perAppRouting`
211
251
  // is configured AND the request matched the per-app path
212
252
  // `/:${paramName}/mcp`, Express populates `req.params[paramName]`
213
- // with the validated tenant id. Use it as `ctx.appId` for this
253
+ // with the validated app id. Use it as `ctx.appId` for this
214
254
  // request, overriding `appIdFromIdentity`. The universal `/mcp`
215
255
  // route doesn't have the param so it falls through to the
216
256
  // identity-based resolution.
@@ -218,11 +258,14 @@ export function mountMcpEndpoints(opts) {
218
258
  const hasUrlAppId = typeof urlAppId === "string" && urlAppId.length > 0;
219
259
  // Per-app authorize hook — when the deployment configured
220
260
  // `perAppRouting.authorize` AND the request matched the per-app
221
- // path, invoke the callback. Throwing collapses to a 403 before
222
- // the MCP handler ever sees the request, which is the boundary
223
- // that prevents cross-user blueprint reads when pod tools bypass
261
+ // path, invoke the callback. Throwing refuses before the MCP
262
+ // handler ever sees the request, which is the boundary that
263
+ // prevents cross-user blueprint reads when a deployment's own tools bypass
224
264
  // AppSync owner-auth via raw DDB. Universal-endpoint requests
225
- // skip this entirely (no urlAppId).
265
+ // skip this entirely (no urlAppId). The deployment's `errorMapper`
266
+ // may give the refusal a structured JSON-RPC `data` (bounded to
267
+ // 401 / 403, see `mapAuthorizationRefusal`); otherwise — and for
268
+ // every mapping outside those bounds — the default-deny 403 stands.
226
269
  if (hasUrlAppId && perAppRouting?.authorize) {
227
270
  try {
228
271
  await perAppRouting.authorize(urlAppId, identity);
@@ -232,9 +275,17 @@ export function mountMcpEndpoints(opts) {
232
275
  urlAppId,
233
276
  reason: err instanceof Error ? err.message : String(err),
234
277
  });
235
- res.status(403).json({
278
+ const mapped = mapAuthorizationRefusal(err, errorMapper, reqLogger);
279
+ if (mapped?.headers)
280
+ res.set(mapped.headers);
281
+ res.status(mapped?.status ?? 403).json({
236
282
  jsonrpc: "2.0",
237
- error: { code: -32000, message: "Forbidden" },
283
+ // #836: a first-party server never answers -32000 — that is the SDK
284
+ // client's own ConnectionClosed number, so a refusal and a dropped
285
+ // socket would read the same. An authorization refusal is UNAUTHORIZED.
286
+ error: mapped
287
+ ? jsonRpcError(mapped)
288
+ : { code: MCP_ERROR_CODES.UNAUTHORIZED, message: "Unauthorized" },
238
289
  id: null,
239
290
  });
240
291
  return;
@@ -248,11 +299,11 @@ export function mountMcpEndpoints(opts) {
248
299
  // handler should branch on the specific mechanism.
249
300
  authSource: identity.source,
250
301
  // Identity is the canonical source of two mutually-exclusive
251
- // hosted fields: `apiKeyHash` for kind=app, `userId` for kind=user.
252
- // Threading them onto HandlerContext here means hosted handlers
253
- // (the K8s ggui-protocol pod's billing gate + per-user blueprint
254
- // scoping) can read identity directly without a parallel pod-only
255
- // context shape; OSS handlers continue to ignore both fields.
302
+ // fields: `apiKeyHash` for kind=app, `userId` for kind=user.
303
+ // Threading them onto HandlerContext here means a deployment's own
304
+ // handlers (per-key metering, per-user scoping) can read identity
305
+ // directly without a parallel context shape; the handlers this
306
+ // package ships ignore both fields.
256
307
  ...(identity.identity.kind === "app" ? { apiKeyHash: identity.identity.apiKeyHash } : {}),
257
308
  ...(identity.identity.kind === "user" ? { userId: identity.identity.userId } : {}),
258
309
  // What the credential itself may act on, when the adapter
@@ -301,7 +352,7 @@ export function mountMcpEndpoints(opts) {
301
352
  res.set(mapped.headers);
302
353
  res.status(mapped.status).json({
303
354
  jsonrpc: "2.0",
304
- error: { code: mapped.code, message: mapped.message },
355
+ error: jsonRpcError(mapped),
305
356
  id: null,
306
357
  });
307
358
  }
@@ -316,7 +367,9 @@ export function mountMcpEndpoints(opts) {
316
367
  }
317
368
  };
318
369
  const methodNotAllowed = (_req, res) => {
319
- res.status(405).json({
370
+ // A 405 names what IS allowed (RFC 9110 §15.5.6); every mount here is
371
+ // POST-only.
372
+ res.set("Allow", "POST").status(405).json({
320
373
  jsonrpc: "2.0",
321
374
  error: {
322
375
  code: -32000,
@@ -341,24 +394,24 @@ export function mountMcpEndpoints(opts) {
341
394
  anonymousOpsChallenge: controlOpsToolNames,
342
395
  });
343
396
  // Universal endpoint — `appId` resolved from the auth identity via
344
- // `appIdFromIdentity`. Cloud `mcp.ggui.ai` deployments resolve this
345
- // to `User.defaultAppId` via the auth-adapter; OSS deployments fall
346
- // through to userId / DEFAULT_BUILDER_APP_ID.
397
+ // `appIdFromIdentity`. A deployment's auth adapter may resolve it
398
+ // (e.g. to the user's default app); without one it falls through to
399
+ // userId / DEFAULT_BUILDER_APP_ID.
347
400
  //
348
- // Path defaults to `/mcp` (Streamable HTTP convention). Cloud
349
- // `mcp.ggui.ai` overrides to `/` so the bare-root URL is the
350
- // universal endpoint — domain already says "mcp", no path repeat.
401
+ // Path defaults to `/mcp` (Streamable HTTP convention). A deployment
402
+ // whose hostname already says "mcp" may serve it at `/` so the
403
+ // bare-root URL is the universal endpoint — no path repeat.
351
404
  // Exposes audience tags ['agent', 'runtime'] — runtime tools stay
352
405
  // routable on the same endpoint but invisible to the agent's
353
406
  // `tools/list` via the `_meta.ui.visibility: ['app']` filter.
354
407
  app.post(universalMcpPath, agentMcpHandler);
355
- // Per-tenant endpoint — only mounted when the deployment opts in
408
+ // Per-app endpoint — only mounted when the deployment opts in
356
409
  // via `perAppRouting`. The same handler reads `req.params[paramName]`
357
410
  // and uses it as `ctx.appId` for the request.
358
411
  //
359
412
  // When `pathPrefix` is set, the route mounts at
360
- // `${pathPrefix}/:${paramName}` — cloud uses `/apps` so URLs are
361
- // `mcp.ggui.ai/apps/<appId>`. The prefix segments per-tenant traffic
413
+ // `${pathPrefix}/:${paramName}` — e.g. `/apps`, so URLs read
414
+ // `<host>/apps/<appId>`. The prefix segments per-app traffic
362
415
  // from system routes (`/health`, `/oauth/*`, `/.well-known/*`,
363
416
  // `/r/*`) so an opaque appId can never shadow a future static path.
364
417
  //
@@ -386,6 +439,12 @@ export function mountMcpEndpoints(opts) {
386
439
  });
387
440
  const route = pathPrefix !== undefined ? `${pathPrefix}/:${paramName}` : `/:${paramName}`;
388
441
  app.post(route, agentMcpHandler);
442
+ // The per-app endpoint is POST-only like every other mount: GET (a
443
+ // client's optional SSE listener) and DELETE (session terminate) read
444
+ // the transport's 405, never Express's text/html 404 — a 404 says "no
445
+ // such resource" and a client retries it every turn (ggui#850).
446
+ app.get(route, methodNotAllowed);
447
+ app.delete(route, methodNotAllowed);
389
448
  }
390
449
  // /control — the control plane. Hosts every `audience: ['protocol']`
391
450
  // tool (design-time spec/discovery, anonymous) and every
@@ -29,7 +29,7 @@ interface MountOptions {
29
29
  /** Universal MCP endpoint path — the default RFC 9728 resource. */
30
30
  readonly universalMcpPath: string;
31
31
  /**
32
- * Per-tenant URL routing shape. When configured, a second
32
+ * Per-app URL routing shape. When configured, a second
33
33
  * well-known endpoint mounts under the same path prefix the
34
34
  * per-app `/mcp` handler lives on.
35
35
  */
package/dist/oauth.d.ts CHANGED
@@ -51,13 +51,13 @@
51
51
  * Auth codes + DCR clients live in-memory ({@link InMemoryOAuthStorage}).
52
52
  * For multi-replica deployments (2+ processes behind one load
53
53
  * balancer), either:
54
- * - Use nginx-ingress sticky sessions so the same pod handles both
55
- * `/oauth/authorize` and `/oauth/token` (current sandbox posture).
54
+ * - Use sticky sessions at your load balancer / reverse proxy so the
55
+ * same replica handles both `/oauth/authorize` and `/oauth/token`.
56
56
  * - Plug a Redis-backed {@link OAuthStorage} via the
57
57
  * `oauth.storage` config option (production posture).
58
58
  *
59
59
  * DCR clients are short-lived in practice — Claude Desktop registers
60
- * once per install + caches the `client_id`. A pod restart drops all
60
+ * once per install + caches the `client_id`. A server restart drops all
61
61
  * registrations; clients re-register transparently on next failure.
62
62
  */
63
63
  import type { Request, Response } from 'express';
package/dist/oauth.js CHANGED
@@ -51,13 +51,13 @@
51
51
  * Auth codes + DCR clients live in-memory ({@link InMemoryOAuthStorage}).
52
52
  * For multi-replica deployments (2+ processes behind one load
53
53
  * balancer), either:
54
- * - Use nginx-ingress sticky sessions so the same pod handles both
55
- * `/oauth/authorize` and `/oauth/token` (current sandbox posture).
54
+ * - Use sticky sessions at your load balancer / reverse proxy so the
55
+ * same replica handles both `/oauth/authorize` and `/oauth/token`.
56
56
  * - Plug a Redis-backed {@link OAuthStorage} via the
57
57
  * `oauth.storage` config option (production posture).
58
58
  *
59
59
  * DCR clients are short-lived in practice — Claude Desktop registers
60
- * once per install + caches the `client_id`. A pod restart drops all
60
+ * once per install + caches the `client_id`. A server restart drops all
61
61
  * registrations; clients re-register transparently on next failure.
62
62
  */
63
63
  import { createHash, randomBytes } from 'node:crypto';
@@ -15,7 +15,7 @@ export interface RenderReadRowView {
15
15
  * first time.
16
16
  *
17
17
  * Absent stays a legitimate state: builder and anonymous
18
- * single-tenant flows mint rows with no subject, and those still
18
+ * single-app flows mint rows with no subject, and those still
19
19
  * pass rung 4.
20
20
  */
21
21
  readonly userId?: string;
@@ -31,9 +31,9 @@ export interface RenderReadRowView {
31
31
  * that subject. The row's subject is its `userId`, written at
32
32
  * commit; see {@link RenderReadRowView.userId} for why this rung
33
33
  * only starts binding now.
34
- * 4. Everything else same-app passes: app credentials (tenant trust —
34
+ * 4. Everything else same-app passes: app credentials (app trust —
35
35
  * the app is obligated to enforce its own user-ownership before
36
- * fetching on a user's behalf), builder/anonymous single-tenant
36
+ * fetching on a user's behalf), builder/anonymous single-app
37
37
  * flows, and rows with no subject.
38
38
  *
39
39
  * Deny is surfaced by the CALLER byte-identically to a missing row —
@@ -9,9 +9,9 @@
9
9
  * that subject. The row's subject is its `userId`, written at
10
10
  * commit; see {@link RenderReadRowView.userId} for why this rung
11
11
  * only starts binding now.
12
- * 4. Everything else same-app passes: app credentials (tenant trust —
12
+ * 4. Everything else same-app passes: app credentials (app trust —
13
13
  * the app is obligated to enforce its own user-ownership before
14
- * fetching on a user's behalf), builder/anonymous single-tenant
14
+ * fetching on a user's behalf), builder/anonymous single-app
15
15
  * flows, and rows with no subject.
16
16
  *
17
17
  * Deny is surfaced by the CALLER byte-identically to a missing row —
@@ -45,7 +45,7 @@ export declare function resolveHashedRuntimeBundleUrl(plainUrl: string, bundleFi
45
45
  * `bundleFile` (default: the workspace's built bundle — the same file
46
46
  * `createGguiServer` hashes, so both derive the same value), or
47
47
  * `undefined` when the bundle is unreadable. Deployments that compose
48
- * their own render handler (the cloud pod's `handlers: tools` shape)
48
+ * their own render handler (a production deployment's `handlers:` shape)
49
49
  * use this to build a `createCodeModuleUrlMinter` that stamps the SAME
50
50
  * `<rt>` the co-resident factory's variant route serves (ggui#522
51
51
  * slice 2) — re-deriving the scheme by hand is how the codeUrl binding
@@ -69,7 +69,7 @@ export function resolveHashedRuntimeBundleUrl(plainUrl, bundleFile = RUNTIME_BUN
69
69
  * `bundleFile` (default: the workspace's built bundle — the same file
70
70
  * `createGguiServer` hashes, so both derive the same value), or
71
71
  * `undefined` when the bundle is unreadable. Deployments that compose
72
- * their own render handler (the cloud pod's `handlers: tools` shape)
72
+ * their own render handler (a production deployment's `handlers:` shape)
73
73
  * use this to build a `createCodeModuleUrlMinter` that stamps the SAME
74
74
  * `<rt>` the co-resident factory's variant route serves (ggui#522
75
75
  * slice 2) — re-deriving the scheme by hand is how the codeUrl binding
@@ -33,6 +33,7 @@
33
33
  * loudly at registration time.
34
34
  */
35
35
  import type { ZodRawShape } from 'zod';
36
+ import { DomainError } from '@ggui-ai/protocol';
36
37
  import { type SubsetViolation, type ActionSpec } from '@ggui-ai/protocol';
37
38
  /**
38
39
  * Stance the host takes when a check surfaces violations.
@@ -136,7 +137,7 @@ export interface GguiSessionContractShape {
136
137
  * finding. Carries the full {@link SchemaCompatReport} so callers
137
138
  * can log / surface the detail beyond the `message` string.
138
139
  */
139
- export declare class SchemaCompatError extends Error {
140
+ export declare class SchemaCompatError extends DomainError<'schema_mismatch_error'> {
140
141
  readonly report: SchemaCompatReport;
141
142
  constructor(report: SchemaCompatReport, context: string);
142
143
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,gBAA2B,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACrC;uDACmD;IACnD,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACnD;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACpD,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;gBACxB,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAKxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,wBAAwB,EAChC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC9B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,GACd,kBAAkB,CAuFpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAEnE"}
1
+ {"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,EAAE,gBAA2B,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mCAAmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACrC;uDACmD;IACnD,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACnD;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;KACpD,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,WAAW,CAAC,uBAAuB,CAAC;IACzE,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;gBACxB,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAIxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,wBAAwB,EAChC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC9B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,GACd,kBAAkB,CAuFpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAEnE"}
@@ -1,3 +1,4 @@
1
+ import { DomainError } from '@ggui-ai/protocol';
1
2
  import { isSchemaSubset, zodToJsonSchema, } from '@ggui-ai/protocol';
2
3
  /**
3
4
  * Default mode applied when the host does not override. Matches the
@@ -9,11 +10,10 @@ export const DEFAULT_SCHEMA_COMPAT_MODE = 'reject';
9
10
  * finding. Carries the full {@link SchemaCompatReport} so callers
10
11
  * can log / surface the detail beyond the `message` string.
11
12
  */
12
- export class SchemaCompatError extends Error {
13
+ export class SchemaCompatError extends DomainError {
13
14
  report;
14
15
  constructor(report, context) {
15
- super(formatReport(report, context));
16
- this.name = 'SchemaCompatError';
16
+ super('schema_mismatch_error', formatReport(report, context));
17
17
  this.report = report;
18
18
  }
19
19
  }
@@ -136,7 +136,7 @@ export function hasErrorFinding(report) {
136
136
  */
137
137
  function formatReport(report, context) {
138
138
  if (report.compatible) {
139
- return `${context}: SCHEMA_MISMATCH_ERROR (no findings — internal error)`;
139
+ return `${context} — no findings (internal error)`;
140
140
  }
141
141
  const sorted = [...report.findings].sort((a, b) => a.specName < b.specName ? -1 : a.specName > b.specName ? 1 : 0);
142
142
  const lines = sorted.map((f) => {
@@ -153,7 +153,7 @@ function formatReport(report, context) {
153
153
  return `- ${f.kind} "${f.specName}" (tool "${f.toolName}") — schema mismatch${suffix}`;
154
154
  });
155
155
  return [
156
- `${context}: SCHEMA_MISMATCH_ERROR — ${report.findings.length} finding${report.findings.length > 1 ? 's' : ''}`,
156
+ `${context} — ${report.findings.length} finding${report.findings.length > 1 ? 's' : ''}`,
157
157
  ...lines,
158
158
  ].join('\n');
159
159
  }