@ggui-ai/mcp-server 0.28.0 → 0.29.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 (39) hide show
  1. package/README.md +4 -2
  2. package/dist/build-mcp.d.ts +84 -1
  3. package/dist/build-mcp.d.ts.map +1 -1
  4. package/dist/build-mcp.js +306 -3
  5. package/dist/console-config-routes.js +1 -1
  6. package/dist/console-mcp-tools-routes.d.ts +1 -1
  7. package/dist/console-mcp-tools-routes.d.ts.map +1 -1
  8. package/dist/console-mcp-tools-routes.js +1 -1
  9. package/dist/control-service.d.ts +1 -1
  10. package/dist/control-service.d.ts.map +1 -1
  11. package/dist/control-service.js +1 -1
  12. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  13. package/dist/ggui-session-channel/subscribe.js +24 -0
  14. package/dist/index.d.ts +7 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +10 -1
  17. package/dist/llm-backed-negotiator.d.ts +10 -48
  18. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  19. package/dist/llm-backed-negotiator.js +5 -135
  20. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  21. package/dist/mcp-apps-outbound.js +7 -2
  22. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  23. package/dist/mcp-endpoint-routes.js +5 -2
  24. package/dist/oauth-as-routes.d.ts +3 -0
  25. package/dist/oauth-as-routes.d.ts.map +1 -1
  26. package/dist/oauth-as-routes.js +20 -13
  27. package/dist/oauth.d.ts +24 -18
  28. package/dist/oauth.d.ts.map +1 -1
  29. package/dist/oauth.js +42 -40
  30. package/dist/request-context.d.ts +19 -0
  31. package/dist/request-context.d.ts.map +1 -1
  32. package/dist/request-context.js +17 -1
  33. package/dist/server.d.ts +12 -0
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +23 -2
  36. package/dist/storage.d.ts +11 -1
  37. package/dist/storage.d.ts.map +1 -1
  38. package/dist/storage.js +4 -3
  39. package/package.json +15 -15
package/README.md CHANGED
@@ -47,9 +47,11 @@ separate packages and protocol flows.
47
47
 
48
48
  ### `tsc` under `moduleResolution: NodeNext` reports errors inside `node_modules/@modelcontextprotocol/ext-apps`
49
49
 
50
- Your own code is fine; the errors sit in a dependency's declarations. With `moduleResolution: "NodeNext"` (or `Node16`), `tsc` reports TS2834 and TS2339 inside `node_modules/@modelcontextprotocol/ext-apps/dist/src/app.d.ts`, and TS2305 / TS2460 on `@ggui-ai/protocol`'s `host-context.d.ts` for the three names it re-exports from there (`McpUiDisplayMode`, `McpUiHostContext`, `McpUiHostCapabilities`).
50
+ Your own code is fine; the errors sit in a dependency's declarations. With `moduleResolution: "NodeNext"` (or `Node16`), `tsc` reports TS2834 and TS2339 inside `node_modules/@modelcontextprotocol/ext-apps/dist/src/app.d.ts`, and, before #847, TS2305 / TS2460 on `@ggui-ai/protocol`'s `host-context.d.ts` for the three names it then re-exported from there.
51
51
 
52
- **Cause.** `@modelcontextprotocol/ext-apps` 1.7.5 ships extensionless relative specifiers in its own `.d.ts` files — a defect in the published package, tracked upstream at [modelcontextprotocol/ext-apps#704](https://github.com/modelcontextprotocol/ext-apps/issues/704). `@ggui-ai/*` re-exports those names rather than forking a frozen spec surface, so the defect surfaces on our line. Every `@ggui-ai/*` declaration itself carries explicit `.js` extensions since 0.15.0.
52
+ **Cause.** `@modelcontextprotocol/ext-apps` 1.7.5 ships extensionless relative specifiers in its own `.d.ts` files — a defect in the published package, tracked upstream at [modelcontextprotocol/ext-apps#704](https://github.com/modelcontextprotocol/ext-apps/issues/704). `@ggui-ai/mcp-server` and `@ggui-ai/iframe-runtime` depend on ext-apps, so the defect surfaces in a program that includes them (`@ggui-ai/protocol` alone no longer references ext-apps, #847). Every `@ggui-ai/*` declaration itself carries explicit `.js` extensions since 0.15.0.
53
+
54
+ **Scope.** The errors come from ext-apps' own declarations, which this package (and `@ggui-ai/iframe-runtime`) depends on. `@ggui-ai/protocol` alone does not: its declarations reference nothing from ext-apps, so a NodeNext consumer of the protocol package typechecks clean without the switch.
53
55
 
54
56
  **Fix.** Until ext-apps ships the upstream fix, set the TypeScript switch for third-party declaration defects in your `tsconfig.json`:
55
57
 
@@ -9,17 +9,38 @@
9
9
  * tool return advertises its shape — wire consumers can trust the output.
10
10
  */
11
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
- import { type ZodRawShape } from 'zod';
12
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
13
+ import * as z from 'zod';
14
+ import type { ZodRawShape } from 'zod';
13
15
  import { type HandlerContext, type SharedHandler } from '@ggui-ai/mcp-server-handlers';
14
16
  import type { Logger } from './logger.js';
15
17
  import { type LoadingIndicatorOption, type GguiRenderResourceTemplateOptions } from './mcp-apps-outbound.js';
16
18
  import { type ViewProofGate } from './view-proof-gate.js';
19
+ /**
20
+ * The output object a tool ADVERTISES in `tools/list` (ggui#1333): the
21
+ * handler's shape with every object node open. Throws at server build if
22
+ * the derivation left a closed object anywhere in the projection the SDK
23
+ * will publish — the MUST is enforced where the document is made, not
24
+ * only where a test happens to look.
25
+ */
26
+ export declare function openOutputObject(toolName: string, shape: ZodRawShape): z.ZodObject;
17
27
  export interface ServerInfo {
18
28
  readonly name: string;
19
29
  readonly version: string;
20
30
  readonly description?: string;
21
31
  }
22
32
  export interface BuildMcpServerOptions {
33
+ /**
34
+ * How many array elements and object members one `tools/call`'s
35
+ * `arguments` may carry before the SDK refuses the call ahead of any
36
+ * handler (ggui#1993). Default {@link MAX_TOOL_INPUT_ELEMENTS}; `Infinity`
37
+ * is the SDK's "no limit", leaving the request body limit as the only
38
+ * bound. A value below 1 or NaN is refused by the SDK at construction
39
+ * (`RangeError`); `createGguiServer` refuses it earlier, at composition,
40
+ * so a misconfigured deployment fails at boot and not at its first call
41
+ * (ggui#1996).
42
+ */
43
+ readonly maxToolInputElements?: number;
23
44
  /**
24
45
  * The view-origin proof's measuring gate (ggui#1415), built once per
25
46
  * server with the secret its ws envelopes are signed with. For a tool
@@ -183,5 +204,67 @@ export interface BuildMcpServerOptions {
183
204
  */
184
205
  readonly withholdResultMeta?: boolean;
185
206
  }
207
+ /**
208
+ * Largest combined number of array elements and object members one
209
+ * `tools/call` may carry in its `arguments` (ggui#1993). The MCP SDK
210
+ * counts them before any schema runs and before the handler is reached;
211
+ * a call over the ceiling takes the invalid-input door it already has —
212
+ * `isError: true` with one text block naming the tool and the ceiling —
213
+ * and the handler never sees it. A call at or under it is unaffected.
214
+ *
215
+ * The number: the largest legitimate tool input in this repository's
216
+ * corpora is about a thousand elements (a benchmark render of a
217
+ * 41-element contract with 945 elements of props; conformance-kit cases
218
+ * stay under 120; a theme document is ~325), so ten thousand is an order
219
+ * of magnitude of headroom over anything the surface is asked to carry
220
+ * today. Strings do not count toward it, however long. It bounds the
221
+ * work a request can ask of schema validation; the request body limit
222
+ * bounds everything else, and lives with the transport.
223
+ */
224
+ export declare const MAX_TOOL_INPUT_ELEMENTS = 10000;
186
225
  export declare function buildMcpServer(info: ServerInfo, handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>, getContext: () => HandlerContext, logger: Logger, opts?: BuildMcpServerOptions): McpServer;
226
+ /**
227
+ * Why an SDK-level `tools/call` refusal was refused, as far as the server
228
+ * can tell without the SDK exposing it (ggui#1995):
229
+ *
230
+ * - `elements_over_ceiling` — the SDK's element-count door
231
+ * (`maxToolInputElements`, {@link MAX_TOOL_INPUT_ELEMENTS}). Classified
232
+ * by the SDK's own sentence, which `build-mcp.test.ts` pins as the
233
+ * refusal text — an SDK wording change reds that pin before it could
234
+ * silently reclassify every over-ceiling refusal here.
235
+ * - `unknown_tool` — a name this server never registered.
236
+ * - `input_rejected` — every other `isError` answer the handler never saw:
237
+ * the SDK's input-schema validation, a disabled tool.
238
+ * - `rpc_error` — answered as a JSON-RPC `error` member rather than a
239
+ * tool result (in SDK 1.32.1 only `UrlElicitationRequired` leaves this
240
+ * way).
241
+ */
242
+ export type ToolCallRefusalReason = 'elements_over_ceiling' | 'unknown_tool' | 'input_rejected' | 'rpc_error';
243
+ /**
244
+ * ggui#1995 — every answered `tools/call` produces exactly one `tool_invoked`
245
+ * line. The SDK refuses an over-ceiling or schema-failing call inside its
246
+ * own handler, before the callback {@link buildMcpServer} registers, so the
247
+ * refusal is observable on the wire (`isError`) yet, without this, invisible
248
+ * in ops logs. There is no pre-dispatch seam in the SDK (`registerTool`
249
+ * takes zod only; the element counter is module-private), so the rule is
250
+ * structural and read at the transport: **a `tools/call` answered — by
251
+ * `result.isError` or by a JSON-RPC `error` — whose request id the callback
252
+ * never saw is an SDK-level refusal**, logged as `tool_invoked` with
253
+ * `outcome: 'refused'` and a {@link ToolCallRefusalReason}. The same event
254
+ * name as every other call, so an operator's per-tool counts include the
255
+ * refusals; the fields are the tool name, the request id and the app —
256
+ * never the arguments or the SDK's message (a schema message can carry
257
+ * received values).
258
+ *
259
+ * A decorator, not a patch: it implements `Transport` around the real one
260
+ * and is what `McpServer.connect` receives, so nothing on the SDK's object
261
+ * is replaced and the order of `connect` does not matter. A `tools/call`
262
+ * whose transport closes before an answer is a non-event here — the
263
+ * obligation is on answered calls.
264
+ *
265
+ * Parties: this server ↔ its operator's log consumer. Failure mode: a
266
+ * refusal with no line. Observable violation: `build-mcp.test.ts`
267
+ * (ggui#1995) and the route test over HTTP.
268
+ */
269
+ export declare function observeToolCallRefusals(inner: Transport, server: McpServer, logger: Logger, getContext: () => HandlerContext): Transport;
187
270
  //# sourceMappingURL=build-mcp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EACL,KAAK,sBAAsB,EAE3B,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE5E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IACnD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC;IAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC;AAqPD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EAChE,UAAU,EAAE,MAAM,cAAc,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,qBAA0B,GAC/B,SAAS,CAkSX"}
1
+ {"version":3,"file":"build-mcp.d.ts","sourceRoot":"","sources":["../src/build-mcp.ts"],"names":[],"mappings":"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"}
package/dist/build-mcp.js CHANGED
@@ -9,13 +9,152 @@
9
9
  * tool return advertises its shape — wire consumers can trust the output.
10
10
  */
11
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResponse, } from '@modelcontextprotocol/sdk/types.js';
12
13
  import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
13
14
  import { isRecord } from '@ggui-ai/protocol';
14
- import { z } from 'zod';
15
+ import * as z from 'zod';
15
16
  import { isHandlerFailure, } from '@ggui-ai/mcp-server-handlers';
16
17
  import { GGUI_RENDER_RESOURCE_URI } from '@ggui-ai/protocol/integrations/mcp-apps';
17
18
  import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
18
19
  import { runViewProofGate } from './view-proof-gate.js';
20
+ // =============================================================================
21
+ // Advertised output objects are OPEN (ggui#1333)
22
+ //
23
+ // The SDK projects a tool's registered output object into `tools/list` in
24
+ // zod's OUTPUT mode, which closes every object (`additionalProperties:
25
+ // false`) whether or not the zod object is strict. A host that caches that
26
+ // document and validates results — the TS SDK client does — then refuses a
27
+ // result carrying a member it does not name, so an optional output field
28
+ // added in release N failed every host that listed tools against N−1, and
29
+ // an additive field could only ship by declaring it one release before
30
+ // emitting it (ggui#1336). The MUST this section enforces: an advertised
31
+ // output object MUST NOT be closed, at any depth.
32
+ //
33
+ // The ADVERTISED document is therefore decoupled from the PARSE: what the
34
+ // server registers is an open derivation of the handler's shape (every
35
+ // object node loose, the top level included), while
36
+ // {@link validateOutputPayload} keeps validating with the handler's own
37
+ // shape, so an undeclared member is still stripped before the wire. The SDK's
38
+ // own output validation runs the loose object over the already-stripped
39
+ // payload and passes. `assertAdvertisedOpen` makes the obligation
40
+ // self-enforcing at server build — a node kind the derivation does not open
41
+ // fails loudly here, never silently as a closed object on the wire.
42
+ // =============================================================================
43
+ /**
44
+ * A node reached through a schema's definition is typed as zod's CORE
45
+ * `$ZodType`; every schema a handler builds with `z.*` is a classic
46
+ * `ZodType` instance, so this is a real check at the one place a core node
47
+ * is handed back, and a non-classic node is a definition-site bug.
48
+ */
49
+ function asClassic(node) {
50
+ if (node instanceof z.ZodType)
51
+ return node;
52
+ throw new Error('a tool output shape holds a schema node that is not a zod classic ZodType');
53
+ }
54
+ /**
55
+ * One open derivation per source node (ggui#1333): the memo is what keeps a
56
+ * recursive schema (`z.lazy`) from looping, and it keeps a node shared by two
57
+ * fields shared in the derivation too.
58
+ */
59
+ const OPENED = new WeakMap();
60
+ /** The open derivation of one node: every object under it becomes loose. */
61
+ function openNode(schema) {
62
+ const seen = OPENED.get(schema);
63
+ if (seen !== undefined)
64
+ return seen;
65
+ const meta = z.globalRegistry.get(schema);
66
+ const unmetered = openNodeUnmetered(schema);
67
+ const opened = unmetered === schema || meta === undefined ? unmetered : unmetered.meta(meta);
68
+ OPENED.set(schema, opened);
69
+ return opened;
70
+ }
71
+ function openNodes(nodes) {
72
+ return nodes.map((node) => openNode(asClassic(node)));
73
+ }
74
+ /** The inner node of a wrapper, as the classic type the derivation rebuilds with. */
75
+ function inner(node) {
76
+ return openNode(asClassic(node));
77
+ }
78
+ function openNodeUnmetered(schema) {
79
+ if (schema instanceof z.ZodObject)
80
+ return z.looseObject(openShape(schema.shape));
81
+ if (schema instanceof z.ZodArray)
82
+ return z.array(inner(schema.element));
83
+ // A discriminated union is a union in the projection (`anyOf`); the
84
+ // discriminator only speeds the parse, which stays on the original.
85
+ if (schema instanceof z.ZodUnion) {
86
+ const [first, ...rest] = openNodes(schema.options);
87
+ if (first === undefined)
88
+ return schema;
89
+ return z.union([first, ...rest]);
90
+ }
91
+ if (schema instanceof z.ZodIntersection) {
92
+ const { left, right } = schema._zod.def;
93
+ return z.intersection(inner(left), inner(right));
94
+ }
95
+ if (schema instanceof z.ZodTuple) {
96
+ const { items, rest } = schema._zod.def;
97
+ const [first, ...others] = openNodes(items);
98
+ if (first === undefined)
99
+ return rest === null ? z.tuple([]) : schema;
100
+ return rest === null ? z.tuple([first, ...others]) : z.tuple([first, ...others], inner(rest));
101
+ }
102
+ if (schema instanceof z.ZodRecord)
103
+ return z.record(schema.keyType, inner(schema.valueType));
104
+ if (schema instanceof z.ZodOptional)
105
+ return inner(schema.unwrap()).optional();
106
+ if (schema instanceof z.ZodNullable)
107
+ return inner(schema.unwrap()).nullable();
108
+ if (schema instanceof z.ZodNonOptional)
109
+ return inner(schema.unwrap()).nonoptional();
110
+ if (schema instanceof z.ZodReadonly)
111
+ return inner(schema.unwrap()).readonly();
112
+ if (schema instanceof z.ZodDefault) {
113
+ return inner(schema.unwrap()).default(schema._zod.def.defaultValue);
114
+ }
115
+ if (schema instanceof z.ZodCatch) {
116
+ return inner(schema.unwrap()).catch(schema._zod.def.catchValue);
117
+ }
118
+ if (schema instanceof z.ZodLazy)
119
+ return z.lazy(() => inner(schema.unwrap()));
120
+ if (schema instanceof z.ZodPipe)
121
+ return z.pipe(schema.in, inner(schema.out));
122
+ return schema;
123
+ }
124
+ function openShape(shape) {
125
+ return Object.fromEntries(Object.entries(shape).map(([key, member]) => [key, inner(member)]));
126
+ }
127
+ /** Every `additionalProperties: false` under a JSON Schema document, by path. */
128
+ function closedObjectsIn(node, path) {
129
+ if (Array.isArray(node)) {
130
+ return node.flatMap((item, i) => closedObjectsIn(item, `${path}[${i}]`));
131
+ }
132
+ if (!isRecord(node))
133
+ return [];
134
+ const closed = [];
135
+ for (const [key, value] of Object.entries(node)) {
136
+ if (key === 'additionalProperties' && value === false)
137
+ closed.push(path);
138
+ closed.push(...closedObjectsIn(value, `${path}.${key}`));
139
+ }
140
+ return closed;
141
+ }
142
+ /**
143
+ * The output object a tool ADVERTISES in `tools/list` (ggui#1333): the
144
+ * handler's shape with every object node open. Throws at server build if
145
+ * the derivation left a closed object anywhere in the projection the SDK
146
+ * will publish — the MUST is enforced where the document is made, not
147
+ * only where a test happens to look.
148
+ */
149
+ export function openOutputObject(toolName, shape) {
150
+ const advertised = z.looseObject(openShape(shape));
151
+ const closed = closedObjectsIn(z.toJSONSchema(advertised, { io: 'output' }), '$');
152
+ if (closed.length > 0) {
153
+ throw new Error(`Tool ${toolName} would advertise a closed output object at ${closed.join(', ')} — ` +
154
+ `an advertised output object MUST NOT be closed (ggui#1333); extend openNode for this node kind.`);
155
+ }
156
+ return advertised;
157
+ }
19
158
  /**
20
159
  * Build a fresh MCP server with every handler registered.
21
160
  *
@@ -88,6 +227,12 @@ function validateOutputPayload(handler, payload) {
88
227
  * The thrown path is untouched (`outcome: 'error'` + `errorClass`),
89
228
  * which is what makes "a gate that throws instead of returning a
90
229
  * refusal is a conformance failure" observable in ops.
230
+ *
231
+ * `outcome: 'refused'` has a second door since ggui#1995: a `tools/call`
232
+ * the SDK refused before any handler ran (element ceiling, input schema,
233
+ * unknown tool) logs the same outcome from {@link observeToolCallRefusals},
234
+ * carrying a `reason` instead of a registry `code`. One outcome, two
235
+ * fields: `code` names a handler's refusal, `reason` an SDK-level one.
91
236
  */
92
237
  function classifyFailurePayload(payload) {
93
238
  if (!declaresRefusedOutcome(payload) || !isRecord(payload)) {
@@ -231,15 +376,46 @@ function claimedSessionField(tool, input) {
231
376
  claimedSessionIdTruncated: true,
232
377
  };
233
378
  }
379
+ /**
380
+ * Largest combined number of array elements and object members one
381
+ * `tools/call` may carry in its `arguments` (ggui#1993). The MCP SDK
382
+ * counts them before any schema runs and before the handler is reached;
383
+ * a call over the ceiling takes the invalid-input door it already has —
384
+ * `isError: true` with one text block naming the tool and the ceiling —
385
+ * and the handler never sees it. A call at or under it is unaffected.
386
+ *
387
+ * The number: the largest legitimate tool input in this repository's
388
+ * corpora is about a thousand elements (a benchmark render of a
389
+ * 41-element contract with 945 elements of props; conformance-kit cases
390
+ * stay under 120; a theme document is ~325), so ten thousand is an order
391
+ * of magnitude of headroom over anything the surface is asked to carry
392
+ * today. Strings do not count toward it, however long. It bounds the
393
+ * work a request can ask of schema validation; the request body limit
394
+ * bounds everything else, and lives with the transport.
395
+ */
396
+ export const MAX_TOOL_INPUT_ELEMENTS = 10_000;
234
397
  export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
235
398
  // `instructions` is a ServerOptions field (the SECOND argument): the
236
399
  // SDK sends it in the `initialize` result. In the first argument (the
237
400
  // Implementation) it was dropped, and no host ever received it (ggui#1579).
401
+ // The element ceiling rides the same options object: every server this
402
+ // function builds carries it, on every mount (ggui#1993).
238
403
  const server = new McpServer({
239
404
  name: info.name,
240
405
  version: info.version,
241
406
  ...(info.description ? { description: info.description } : {}),
242
- }, opts.instructions ? { instructions: opts.instructions } : undefined);
407
+ }, {
408
+ maxToolInputElements: opts.maxToolInputElements ?? MAX_TOOL_INPUT_ELEMENTS,
409
+ ...(opts.instructions ? { instructions: opts.instructions } : {}),
410
+ });
411
+ // ggui#1995 — what `observeToolCallRefusals` pairs a `tools/call` answer
412
+ // against: the request ids whose registered callback ran, and the names
413
+ // this server registered (an unknown name is refused by the SDK too).
414
+ const accounting = {
415
+ invoked: new Set(),
416
+ tools: new Set(handlers.map((h) => h.name)),
417
+ };
418
+ TOOL_CALL_ACCOUNTING.set(server, accounting);
243
419
  // Content-addressed shell URI (stale-shell bust) — when MCP Apps
244
420
  // outbound wiring registers the shell, declarations advertising the
245
421
  // BARE `ui://ggui/render` are rewritten to the versioned twin below
@@ -295,6 +471,10 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
295
471
  continue;
296
472
  }
297
473
  const cb = async (input, extra) => {
474
+ // ggui#1995 — the callback ran for this request: whatever answer
475
+ // leaves for this id is the handler's, not an SDK-level refusal.
476
+ if (extra?.requestId !== undefined)
477
+ accounting.invoked.add(extra.requestId);
298
478
  // Thread per-request `_meta` AND the cancellation `signal` onto the
299
479
  // canonical context. The MCP SDK already parses `params._meta` for
300
480
  // us and exposes it on `RequestHandlerExtra._meta`; handlers that
@@ -454,7 +634,9 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
454
634
  ...(handler.title ? { title: handler.title } : {}),
455
635
  description: handler.description,
456
636
  inputSchema: handler.inputSchema,
457
- outputSchema: handler.outputSchema,
637
+ // Advertised OPEN (ggui#1333); the parse stays the handler's own shape
638
+ // in `validateOutputPayload`.
639
+ outputSchema: openOutputObject(handler.name, handler.outputSchema),
458
640
  };
459
641
  // Dispatch on declaration-level UI meta presence. `registerAppTool`
460
642
  // (from `@modelcontextprotocol/ext-apps/server`) normalizes the
@@ -527,3 +709,124 @@ function errorClassName(err) {
527
709
  }
528
710
  return 'Unknown';
529
711
  }
712
+ const TOOL_CALL_ACCOUNTING = new WeakMap();
713
+ const CEILING_SENTENCE = 'arguments contain more than the maximum of';
714
+ /**
715
+ * ggui#1995 — every answered `tools/call` produces exactly one `tool_invoked`
716
+ * line. The SDK refuses an over-ceiling or schema-failing call inside its
717
+ * own handler, before the callback {@link buildMcpServer} registers, so the
718
+ * refusal is observable on the wire (`isError`) yet, without this, invisible
719
+ * in ops logs. There is no pre-dispatch seam in the SDK (`registerTool`
720
+ * takes zod only; the element counter is module-private), so the rule is
721
+ * structural and read at the transport: **a `tools/call` answered — by
722
+ * `result.isError` or by a JSON-RPC `error` — whose request id the callback
723
+ * never saw is an SDK-level refusal**, logged as `tool_invoked` with
724
+ * `outcome: 'refused'` and a {@link ToolCallRefusalReason}. The same event
725
+ * name as every other call, so an operator's per-tool counts include the
726
+ * refusals; the fields are the tool name, the request id and the app —
727
+ * never the arguments or the SDK's message (a schema message can carry
728
+ * received values).
729
+ *
730
+ * A decorator, not a patch: it implements `Transport` around the real one
731
+ * and is what `McpServer.connect` receives, so nothing on the SDK's object
732
+ * is replaced and the order of `connect` does not matter. A `tools/call`
733
+ * whose transport closes before an answer is a non-event here — the
734
+ * obligation is on answered calls.
735
+ *
736
+ * Parties: this server ↔ its operator's log consumer. Failure mode: a
737
+ * refusal with no line. Observable violation: `build-mcp.test.ts`
738
+ * (ggui#1995) and the route test over HTTP.
739
+ */
740
+ export function observeToolCallRefusals(inner, server, logger, getContext) {
741
+ const accounting = TOOL_CALL_ACCOUNTING.get(server);
742
+ if (!accounting) {
743
+ throw new Error('observeToolCallRefusals: the server was not built by buildMcpServer');
744
+ }
745
+ return new ToolCallRefusalObserver(inner, accounting, logger, getContext);
746
+ }
747
+ class ToolCallRefusalObserver {
748
+ inner;
749
+ accounting;
750
+ logger;
751
+ getContext;
752
+ /** `tools/call` requests seen inbound and not yet answered: id → tool name. */
753
+ pending = new Map();
754
+ onclose;
755
+ onerror;
756
+ onmessage;
757
+ constructor(inner, accounting, logger, getContext) {
758
+ this.inner = inner;
759
+ this.accounting = accounting;
760
+ this.logger = logger;
761
+ this.getContext = getContext;
762
+ inner.onmessage = (message, extra) => {
763
+ if (isJSONRPCRequest(message) && message.method === 'tools/call') {
764
+ const name = message.params?.['name'];
765
+ this.pending.set(message.id, typeof name === 'string' ? name : '');
766
+ }
767
+ this.onmessage?.(message, extra);
768
+ };
769
+ inner.onclose = () => {
770
+ this.pending.clear();
771
+ this.onclose?.();
772
+ };
773
+ inner.onerror = (error) => this.onerror?.(error);
774
+ }
775
+ get sessionId() {
776
+ return this.inner.sessionId;
777
+ }
778
+ setProtocolVersion(version) {
779
+ this.inner.setProtocolVersion?.(version);
780
+ }
781
+ start() {
782
+ return this.inner.start();
783
+ }
784
+ close() {
785
+ return this.inner.close();
786
+ }
787
+ send(message, options) {
788
+ this.observe(message);
789
+ return this.inner.send(message, options);
790
+ }
791
+ observe(message) {
792
+ let id;
793
+ let reason;
794
+ if (isJSONRPCResponse(message)) {
795
+ id = message.id;
796
+ const result = message.result;
797
+ if (result['isError'] !== true) {
798
+ this.pending.delete(id);
799
+ return;
800
+ }
801
+ reason = 'input_rejected';
802
+ const content = result['content'];
803
+ const first = Array.isArray(content) ? content[0] : undefined;
804
+ const text = isRecord(first) && typeof first['text'] === 'string' ? first['text'] : '';
805
+ if (text.includes(CEILING_SENTENCE))
806
+ reason = 'elements_over_ceiling';
807
+ }
808
+ else if (isJSONRPCErrorResponse(message)) {
809
+ // An error response may carry no id (a parse error); nothing to pair.
810
+ if (message.id === undefined)
811
+ return;
812
+ id = message.id;
813
+ reason = 'rpc_error';
814
+ }
815
+ else {
816
+ return;
817
+ }
818
+ const tool = this.pending.get(id);
819
+ this.pending.delete(id);
820
+ if (tool === undefined || this.accounting.invoked.has(id))
821
+ return;
822
+ if (reason !== 'rpc_error' && !this.accounting.tools.has(tool))
823
+ reason = 'unknown_tool';
824
+ this.logger.warn('tool_invoked', {
825
+ tool,
826
+ appId: this.getContext().appId,
827
+ outcome: 'refused',
828
+ reason,
829
+ requestId: String(id),
830
+ });
831
+ }
832
+ }
@@ -23,7 +23,7 @@
23
23
  import { GguiJsonV1 } from "@ggui-ai/project-config";
24
24
  import { findGguiJson, safeLoadGguiJson } from "@ggui-ai/project-config/node";
25
25
  import { readFileSync } from "node:fs";
26
- import { z } from "zod";
26
+ import * as z from "zod";
27
27
  import { applyDevtoolSecurityHeaders } from "./console-headers.js";
28
28
  /**
29
29
  * Mount `GET /ggui/console/config` onto the express app. Returns
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { SharedHandler } from "@ggui-ai/mcp-server-handlers";
24
24
  import type { Express } from "express";
25
- import { type ZodRawShape } from "zod";
25
+ import type { ZodRawShape } from "zod";
26
26
  import type { Logger } from "./logger.js";
27
27
  interface MountOptions {
28
28
  /** Express app to mount onto. */
@@ -1 +1 @@
1
- {"version":3,"file":"console-mcp-tools-routes.d.ts","sourceRoot":"","sources":["../src/console-mcp-tools-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAE1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwCnE"}
1
+ {"version":3,"file":"console-mcp-tools-routes.d.ts","sourceRoot":"","sources":["../src/console-mcp-tools-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwCnE"}
@@ -20,7 +20,7 @@
20
20
  * and warn-logs — operators still see name + description, just
21
21
  * without typed input/output detail.
22
22
  */
23
- import { z } from "zod";
23
+ import * as z from "zod";
24
24
  import { applyDevtoolSecurityHeaders } from "./console-headers.js";
25
25
  /**
26
26
  * Mount `GET /ggui/console/mcp/tools` onto the express app. Returns
@@ -43,7 +43,7 @@
43
43
  * preview that leaks which operations exist against which account.
44
44
  */
45
45
  import { type SharedHandler, type SharedHandlerOutputBound } from "@ggui-ai/mcp-server-handlers";
46
- import { type ZodRawShape } from "zod";
46
+ import type { ZodRawShape } from "zod";
47
47
  import type { McpService } from "./mcp-mounts.js";
48
48
  type AnyHandler = SharedHandler<ZodRawShape, ZodRawShape>;
49
49
  /** Every audience tag a handler may declare. */
@@ -1 +1 @@
1
- {"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAK,KAAK,WAAW,EAAmB,MAAM,KAAK,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAc9C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC/G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAGxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC9G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAYxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EACjH,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAkCzC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,cAAc,CAwBjF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C"}
1
+ {"version":3,"file":"control-service.d.ts","sourceRoot":"","sources":["../src/control-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,wBAAwB,EAC9B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,KAAK,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,UAAU,GAAG,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAE1D,gDAAgD;AAChD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,CAAC;AAEnE,6CAA6C;AAC7C,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAE9C,sDAAsD;AACtD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,WAAW,CAAwB,CAAC;AAErF,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,WAAW,CAAuB,CAAC;AAEvF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,CAAC,UAAU,CAAC,EAC9B,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,GAClC,aAAa,CAAC,UAAU,CAAC,CAK3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAc9C,CAAC;AAEH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC/G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAGxB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EAC9G,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAYxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,wBAAwB,CAAC,CAAC,CAAC,EACjH,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACxB,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAkCzC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,cAAc,CAwBjF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C"}
@@ -43,7 +43,7 @@
43
43
  * preview that leaks which operations exist against which account.
44
44
  */
45
45
  import { AuthRequiredError, } from "@ggui-ai/mcp-server-handlers";
46
- import { z } from "zod";
46
+ import * as z from "zod";
47
47
  /** HTTP path the control plane mounts at. */
48
48
  export const CONTROL_PATH = "/control";
49
49
  /** Service name the control plane reports in validation errors + telemetry. */
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@
7
7
  * dev-mode render provisioning, replay, registration, and the ack.
8
8
  */
9
9
  import { PROTOCOL_SCHEMA_VERSION, UPGRADE_REQUIRED } from "@ggui-ai/protocol";
10
+ import { renderReadAllowed } from "@ggui-ai/mcp-server-handlers/renders";
10
11
  import { defaultAppIdFromIdentity, resolveIdentityFromHeaders, UnauthenticatedError, } from "../auth.js";
11
12
  import { createWsSink } from "./outbound.js";
12
13
  /**
@@ -344,6 +345,29 @@ export function createSubscribeHandlers(deps) {
344
345
  deps.sendError(ws, "APP_MISMATCH", `GguiSession ${payload.sessionId} belongs to a different app`, message.requestId);
345
346
  return;
346
347
  }
348
+ // A bearer that carries an end-user identity attaches only to its
349
+ // own sessions (ggui#1483, SPEC §12.2): the render read door's
350
+ // predicate, the app and then the subject, on the stored row —
351
+ // before anything registers, acks or replays, since an accepted
352
+ // subscribe both reads the session and carries `action` frames
353
+ // into it. The token path is exempt, not forgotten: a `wsToken` is a
354
+ // bearer capability bound to one session (SPEC §5.5.1.1), holding it
355
+ // is the authorization, and its synthesized identity names the
356
+ // session, not a person (ggui#446). A refusal answers
357
+ // SESSION_NOT_FOUND (§7.9.1's one code for a session this caller
358
+ // cannot use) with a fixed message naming no one. Because an unknown
359
+ // id is provisioned here rather than refused, the refusal tells the
360
+ // caller the id exists and is not theirs (ggui#1554).
361
+ const endUserId = effectiveIdentity.identity.kind === "user" ? effectiveIdentity.identity.userId : undefined;
362
+ if (!viaWsToken && !renderReadAllowed(stored, { appId: effectiveAppId, userId: endUserId })) {
363
+ deps.logger.warn("render_channel_subject_refused", {
364
+ sessionId: payload.sessionId,
365
+ appId: effectiveAppId,
366
+ source: effectiveIdentity.source,
367
+ });
368
+ deps.sendError(ws, "SESSION_NOT_FOUND", "No GguiSession with this id is usable by this credential", message.requestId);
369
+ return;
370
+ }
347
371
  }
348
372
  else {
349
373
  try {