@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.
- package/README.md +4 -2
- package/dist/build-mcp.d.ts +84 -1
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +306 -3
- package/dist/console-config-routes.js +1 -1
- package/dist/console-mcp-tools-routes.d.ts +1 -1
- package/dist/console-mcp-tools-routes.d.ts.map +1 -1
- package/dist/console-mcp-tools-routes.js +1 -1
- package/dist/control-service.d.ts +1 -1
- package/dist/control-service.d.ts.map +1 -1
- package/dist/control-service.js +1 -1
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +24 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/llm-backed-negotiator.d.ts +10 -48
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +5 -135
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +7 -2
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +5 -2
- package/dist/oauth-as-routes.d.ts +3 -0
- package/dist/oauth-as-routes.d.ts.map +1 -1
- package/dist/oauth-as-routes.js +20 -13
- package/dist/oauth.d.ts +24 -18
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +42 -40
- package/dist/request-context.d.ts +19 -0
- package/dist/request-context.d.ts.map +1 -1
- package/dist/request-context.js +17 -1
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +23 -2
- package/dist/storage.d.ts +11 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -3
- 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-
|
|
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
|
|
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
|
|
package/dist/build-mcp.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
package/dist/build-mcp.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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;
|
|
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
|
|
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 {
|
|
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;
|
|
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"}
|
package/dist/control-service.js
CHANGED
|
@@ -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
|
|
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":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EAEb,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;
|
|
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 {
|