@ggui-ai/mcp-server 0.2.0-alpha.3 → 0.3.0-rc.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/dist/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +1 -1
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +39 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +182 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +25 -25
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -248
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +204 -183
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- package/dist/render-channel.js +0 -1775
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-blueprints-transport.d.ts","sourceRoot":"","sources":["../src/admin-blueprints-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-blueprints-transport.d.ts","sourceRoot":"","sources":["../src/admin-blueprints-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE/E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,sBAAsB,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,QAAQ,EAAE;QACpB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI,CAAC;CACV;AAED,0EAA0E;AAC1E,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,iBAAiB,GAC1B,QAAQ,IAAI,iBAAiB,GAAG,mBAAmB,CAIrD;AAED,MAAM,WAAW,+BAA+B;IAC9C,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,+BAA+B,GACpC,IAAI,CA8GN"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
1
2
|
import { resolveIdentity, UnauthenticatedError } from './auth.js';
|
|
2
3
|
/**
|
|
3
4
|
* Default URL path the admin-blueprints route is mounted at. Sits
|
|
@@ -56,7 +57,7 @@ export function mountAdminBlueprintsTransport(app, opts) {
|
|
|
56
57
|
// 2. Body validation. Minimum shape: {id: string, name: string}.
|
|
57
58
|
// Optional fields: description, category, tags (string[]),
|
|
58
59
|
// updatedAt (ISO string). Anything else is ignored.
|
|
59
|
-
const body = (req.body
|
|
60
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
60
61
|
const id = typeof body['id'] === 'string' ? body['id'] : undefined;
|
|
61
62
|
const name = typeof body['name'] === 'string' ? body['name'] : undefined;
|
|
62
63
|
if (!id || !name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-oauth-providers-transport.d.ts","sourceRoot":"","sources":["../src/admin-oauth-providers-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"admin-oauth-providers-transport.d.ts","sourceRoot":"","sources":["../src/admin-oauth-providers-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAEV,SAAS,EACT,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,eAAO,MAAM,kCAAkC,gCAAgC,CAAC;AAEhF,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAqCD,wBAAgB,iCAAiC,CAC/C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,mCAAmC,GACxC,IAAI,CAiQN"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
1
2
|
import { resolveIdentity, UnauthenticatedError } from './auth.js';
|
|
3
|
+
import { singleParam } from './route-param.js';
|
|
2
4
|
export const DEFAULT_ADMIN_OAUTH_PROVIDERS_PATH = '/ggui/admin/oauth-providers';
|
|
3
5
|
function redact(record) {
|
|
4
6
|
return {
|
|
@@ -100,8 +102,8 @@ export function mountAdminOAuthProvidersTransport(app, opts) {
|
|
|
100
102
|
const reqLogger = opts.logger.child({ route: 'PUT ' + basePath + '/:providerId' });
|
|
101
103
|
if (!(await requireBuilder(req, res, reqLogger)))
|
|
102
104
|
return;
|
|
103
|
-
const providerId = req.params['providerId'] ?? '';
|
|
104
|
-
const body = (req.body
|
|
105
|
+
const providerId = singleParam(req.params['providerId']) ?? '';
|
|
106
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
105
107
|
const clientId = typeof body['clientId'] === 'string' ? body['clientId'] : undefined;
|
|
106
108
|
const clientSecret = typeof body['clientSecret'] === 'string' ? body['clientSecret'] : undefined;
|
|
107
109
|
const enabled = typeof body['enabled'] === 'boolean' ? body['enabled'] : undefined;
|
|
@@ -171,8 +173,8 @@ export function mountAdminOAuthProvidersTransport(app, opts) {
|
|
|
171
173
|
});
|
|
172
174
|
if (!(await requireBuilder(req, res, reqLogger)))
|
|
173
175
|
return;
|
|
174
|
-
const providerId = req.params['providerId'] ?? '';
|
|
175
|
-
const body = (req.body
|
|
176
|
+
const providerId = singleParam(req.params['providerId']) ?? '';
|
|
177
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
176
178
|
const enabled = typeof body['enabled'] === 'boolean' ? body['enabled'] : undefined;
|
|
177
179
|
if (enabled === undefined) {
|
|
178
180
|
res.status(400).json({
|
|
@@ -233,7 +235,7 @@ export function mountAdminOAuthProvidersTransport(app, opts) {
|
|
|
233
235
|
});
|
|
234
236
|
if (!(await requireBuilder(req, res, reqLogger)))
|
|
235
237
|
return;
|
|
236
|
-
const providerId = req.params['providerId'] ?? '';
|
|
238
|
+
const providerId = singleParam(req.params['providerId']) ?? '';
|
|
237
239
|
try {
|
|
238
240
|
await opts.store.remove(providerId);
|
|
239
241
|
reqLogger.info('admin_oauth_providers_remove', { providerId });
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wsToken-gated render read routes (R6 + R7).
|
|
3
|
+
*
|
|
4
|
+
* R6 — GET /api/sessions/:sessionId/state?wsToken=<token>
|
|
5
|
+
*
|
|
6
|
+
* Auth'd snapshot read of the current render state, returning the
|
|
7
|
+
* same slice envelope as the wire `_meta` (a single
|
|
8
|
+
* `{"ai.ggui/render": {...}}` slice — Phase B collapsed the prior
|
|
9
|
+
* session + stack-item pair). Polling clients call this on a fixed
|
|
10
|
+
* interval (registry-level polling — see R6 library refactor) to
|
|
11
|
+
* pick up changes when WS is blocked at the host's CSP layer.
|
|
12
|
+
*
|
|
13
|
+
* wsToken-gated: same credential as the live-channel WS upgrade
|
|
14
|
+
* (`?wsToken=<token>` on `/ws`). Drift-free with the WS surface —
|
|
15
|
+
* the iframe-runtime already has the token from the bootstrap
|
|
16
|
+
* envelope; no separate refresh path needed.
|
|
17
|
+
*
|
|
18
|
+
* Distinct from `/r/:shortCode` (JSON branch):
|
|
19
|
+
* - `/r/...` was shortCode-gated (bearer-by-obscurity; anyone with
|
|
20
|
+
* the URL could read). R5 deleted that surface entirely.
|
|
21
|
+
* - `/api/sessions/.../state` is wsToken-gated (HMAC-signed,
|
|
22
|
+
* short-TTL, scoped to sessionId+appId). Survives R5.
|
|
23
|
+
*
|
|
24
|
+
* R7 — GET /api/sessions/:sessionId/events?wsToken=&sinceSequence=N&limit=M
|
|
25
|
+
*
|
|
26
|
+
* Cursor-replay read from the GguiSessionEvent ledger. Returns events
|
|
27
|
+
* with `seq > sinceSequence`, up to `limit` (default 100, max 500).
|
|
28
|
+
*
|
|
29
|
+
* Unification: WS subscribe's `sinceSequence` cursor and this HTTP
|
|
30
|
+
* endpoint read from the SAME ledger via the same `listEventsSince`
|
|
31
|
+
* GguiSessionStore method. Different transports, same cursor model —
|
|
32
|
+
* that's R7's payoff.
|
|
33
|
+
*
|
|
34
|
+
* Auth: wsToken-gated, identical posture to /state.
|
|
35
|
+
*
|
|
36
|
+
* Responses (/events):
|
|
37
|
+
* - 200 — `{events, lastSequence, hasMore}` (matches
|
|
38
|
+
* `EventsResponse` from @ggui-ai/protocol/integrations/mcp-apps).
|
|
39
|
+
* - 401 — wsToken missing / invalid / wrong-scope.
|
|
40
|
+
* - 404 — sessionId does not resolve.
|
|
41
|
+
* - 410 — `{reason: 'REPLAY_HORIZON_PASSED', currentSequence}` when
|
|
42
|
+
* `sinceSequence` is below the server's replay horizon OR strictly
|
|
43
|
+
* greater than `lastSequence` (cursor is from a stale deployment
|
|
44
|
+
* or the render was reset). Clients re-mount from /state.
|
|
45
|
+
*/
|
|
46
|
+
import type { AppMetadataStore, CodeStore, GguiSessionStore } from "@ggui-ai/mcp-server-core";
|
|
47
|
+
import { type McpAppAiGguiRenderMeta } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
48
|
+
import type { Express } from "express";
|
|
49
|
+
import type { Logger } from "./logger.js";
|
|
50
|
+
interface MountOptions {
|
|
51
|
+
/** Express app to mount onto. */
|
|
52
|
+
readonly app: Express;
|
|
53
|
+
/** GguiSession store both routes read from. */
|
|
54
|
+
readonly renderStore: GguiSessionStore;
|
|
55
|
+
/** Shared HMAC secret the wsToken query credential verifies against. */
|
|
56
|
+
readonly secret: string;
|
|
57
|
+
/** Per-app metadata store for the publicEnv projection (optional). */
|
|
58
|
+
readonly appMetadataStore?: AppMetadataStore;
|
|
59
|
+
/** Operator-picked theme preset id stamped on /state reads. */
|
|
60
|
+
readonly themeId?: string;
|
|
61
|
+
/** Operator-picked theme mode stamped on /state reads. */
|
|
62
|
+
readonly themeMode?: McpAppAiGguiRenderMeta["themeMode"];
|
|
63
|
+
/** Content-addressable store for codeUrl / validatorsUrl emission. */
|
|
64
|
+
readonly codeStore?: CodeStore;
|
|
65
|
+
/** Operator-configured public origin for absolute URL composition. */
|
|
66
|
+
readonly publicBaseUrl?: string;
|
|
67
|
+
/** Live-mode credential minter — fresh trio on every /state read. */
|
|
68
|
+
readonly mintBootstrap?: (sessionId: string, appId: string) => {
|
|
69
|
+
wsUrl: string;
|
|
70
|
+
token: string;
|
|
71
|
+
expiresAt: string;
|
|
72
|
+
};
|
|
73
|
+
/** Per-request runtime-bundle URL resolver (tunnel/proxy aware). */
|
|
74
|
+
readonly resolveRuntimeUrl: () => string;
|
|
75
|
+
/** Structured logger for store-read failures. */
|
|
76
|
+
readonly logger: Logger;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Mount `GET /api/sessions/:sessionId/state` +
|
|
80
|
+
* `GET /api/sessions/:sessionId/events` onto the express app.
|
|
81
|
+
* Returns nothing — the routes self-register.
|
|
82
|
+
*/
|
|
83
|
+
export declare function mountApiRendersRoutes(opts: MountOptions): void;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=api-renders-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-renders-routes.d.ts","sourceRoot":"","sources":["../src/api-renders-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAO9F,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IACzD,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,iBAAiB,EAAE,MAAM,MAAM,CAAC;IACzC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwU9D"}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wsToken-gated render read routes (R6 + R7).
|
|
3
|
+
*
|
|
4
|
+
* R6 — GET /api/sessions/:sessionId/state?wsToken=<token>
|
|
5
|
+
*
|
|
6
|
+
* Auth'd snapshot read of the current render state, returning the
|
|
7
|
+
* same slice envelope as the wire `_meta` (a single
|
|
8
|
+
* `{"ai.ggui/render": {...}}` slice — Phase B collapsed the prior
|
|
9
|
+
* session + stack-item pair). Polling clients call this on a fixed
|
|
10
|
+
* interval (registry-level polling — see R6 library refactor) to
|
|
11
|
+
* pick up changes when WS is blocked at the host's CSP layer.
|
|
12
|
+
*
|
|
13
|
+
* wsToken-gated: same credential as the live-channel WS upgrade
|
|
14
|
+
* (`?wsToken=<token>` on `/ws`). Drift-free with the WS surface —
|
|
15
|
+
* the iframe-runtime already has the token from the bootstrap
|
|
16
|
+
* envelope; no separate refresh path needed.
|
|
17
|
+
*
|
|
18
|
+
* Distinct from `/r/:shortCode` (JSON branch):
|
|
19
|
+
* - `/r/...` was shortCode-gated (bearer-by-obscurity; anyone with
|
|
20
|
+
* the URL could read). R5 deleted that surface entirely.
|
|
21
|
+
* - `/api/sessions/.../state` is wsToken-gated (HMAC-signed,
|
|
22
|
+
* short-TTL, scoped to sessionId+appId). Survives R5.
|
|
23
|
+
*
|
|
24
|
+
* R7 — GET /api/sessions/:sessionId/events?wsToken=&sinceSequence=N&limit=M
|
|
25
|
+
*
|
|
26
|
+
* Cursor-replay read from the GguiSessionEvent ledger. Returns events
|
|
27
|
+
* with `seq > sinceSequence`, up to `limit` (default 100, max 500).
|
|
28
|
+
*
|
|
29
|
+
* Unification: WS subscribe's `sinceSequence` cursor and this HTTP
|
|
30
|
+
* endpoint read from the SAME ledger via the same `listEventsSince`
|
|
31
|
+
* GguiSessionStore method. Different transports, same cursor model —
|
|
32
|
+
* that's R7's payoff.
|
|
33
|
+
*
|
|
34
|
+
* Auth: wsToken-gated, identical posture to /state.
|
|
35
|
+
*
|
|
36
|
+
* Responses (/events):
|
|
37
|
+
* - 200 — `{events, lastSequence, hasMore}` (matches
|
|
38
|
+
* `EventsResponse` from @ggui-ai/protocol/integrations/mcp-apps).
|
|
39
|
+
* - 401 — wsToken missing / invalid / wrong-scope.
|
|
40
|
+
* - 404 — sessionId does not resolve.
|
|
41
|
+
* - 410 — `{reason: 'REPLAY_HORIZON_PASSED', currentSequence}` when
|
|
42
|
+
* `sinceSequence` is below the server's replay horizon OR strictly
|
|
43
|
+
* greater than `lastSequence` (cursor is from a stale deployment
|
|
44
|
+
* or the render was reset). Clients re-mount from /state.
|
|
45
|
+
*/
|
|
46
|
+
import { verifyToken } from "@ggui-ai/mcp-server-core";
|
|
47
|
+
import { deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, } from "@ggui-ai/mcp-server-handlers/renders";
|
|
48
|
+
import { MCP_APP_AI_GGUI_RENDER_META_KEY, } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
49
|
+
/**
|
|
50
|
+
* Mount `GET /api/sessions/:sessionId/state` +
|
|
51
|
+
* `GET /api/sessions/:sessionId/events` onto the express app.
|
|
52
|
+
* Returns nothing — the routes self-register.
|
|
53
|
+
*/
|
|
54
|
+
export function mountApiRendersRoutes(opts) {
|
|
55
|
+
const { app, renderStore, secret, appMetadataStore, themeId, themeMode, codeStore, publicBaseUrl, mintBootstrap, resolveRuntimeUrl, logger, } = opts;
|
|
56
|
+
app.get("/api/sessions/:sessionId/state", async (req, res) => {
|
|
57
|
+
const sessionId = req.params["sessionId"];
|
|
58
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
59
|
+
res.status(400).type("text/plain").send("sessionId required");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const wsTokenRaw = req.query["wsToken"];
|
|
63
|
+
const wsToken = typeof wsTokenRaw === "string" ? wsTokenRaw : "";
|
|
64
|
+
if (wsToken.length === 0) {
|
|
65
|
+
res.status(401).type("text/plain").send("wsToken query required");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const verify = verifyToken(wsToken, secret, "ws");
|
|
69
|
+
if (!verify.ok) {
|
|
70
|
+
// 410 Gone for expired (matches the `BOOTSTRAP_EXPIRED`
|
|
71
|
+
// semantics on the WS upgrade): the envelope was once valid
|
|
72
|
+
// but has aged out; the iframe-runtime branches on this to
|
|
73
|
+
// surface a refresh-vs-rehandshake prompt instead of treating
|
|
74
|
+
// it as a hostile request.
|
|
75
|
+
if (verify.reason === "expired") {
|
|
76
|
+
res.status(410).type("text/plain").send("wsToken expired");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// 401 for tamper / wrong-kind / malformed / invalid-format —
|
|
80
|
+
// the caller is broken or hostile; no info leak.
|
|
81
|
+
res.status(401).type("text/plain").send("wsToken invalid");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Tenancy gate: the wsToken's claimed sessionId MUST match the
|
|
85
|
+
// URL's sessionId. A wsToken minted for render A MUST NOT read
|
|
86
|
+
// render B's state.
|
|
87
|
+
if (verify.claims.sessionId !== sessionId) {
|
|
88
|
+
res.status(401).type("text/plain").send("wsToken scope mismatch");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let stored;
|
|
92
|
+
try {
|
|
93
|
+
stored = await renderStore.get(sessionId);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.warn("state_read_failed", {
|
|
97
|
+
sessionId,
|
|
98
|
+
error: String(err),
|
|
99
|
+
});
|
|
100
|
+
res.status(500).type("text/plain").send("internal error");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!stored) {
|
|
104
|
+
// 404: render evicted / never existed. Polling clients fold
|
|
105
|
+
// this into "stop polling" — distinct from 410 which signals
|
|
106
|
+
// "credential aged out, refresh".
|
|
107
|
+
res.status(404).type("text/plain").send("render not found");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Tenancy gate (round 2): the wsToken's appId MUST match the
|
|
111
|
+
// render's appId. Closes the case where a render is created
|
|
112
|
+
// under a different appId than the token was minted for.
|
|
113
|
+
if (verify.claims.appId !== stored.appId) {
|
|
114
|
+
res.status(401).type("text/plain").send("wsToken scope mismatch");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Phase B: a render IS the addressable unit. Pick directly from
|
|
118
|
+
// the resolved GguiSession (no stack walk).
|
|
119
|
+
const render = stored.render;
|
|
120
|
+
const isMcpApps = render.type === "mcpApps";
|
|
121
|
+
const isSystem = !isMcpApps && render.type === "system";
|
|
122
|
+
const renderKind = isSystem && typeof render.kind === "string"
|
|
123
|
+
? render.kind
|
|
124
|
+
: undefined;
|
|
125
|
+
// Build render-slice meta. Mirror render.resultMeta's shape so
|
|
126
|
+
// the iframe-runtime parser admits identical envelopes regardless
|
|
127
|
+
// of which surface served them. `lastSequence` is the load-bearing
|
|
128
|
+
// R6 addition: polling clients use it to initialize the R7 /events
|
|
129
|
+
// cursor (`?sinceSequence=N`) aligned with the WS stream.
|
|
130
|
+
const view = !isMcpApps ? deriveRenderMeta(render) : undefined;
|
|
131
|
+
let statePublicEnv;
|
|
132
|
+
if (appMetadataStore && !isMcpApps) {
|
|
133
|
+
try {
|
|
134
|
+
const appRecord = await appMetadataStore.get(stored.appId);
|
|
135
|
+
statePublicEnv = derivePublicEnvProjection(render, appRecord?.publicEnv);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Silent — wrappers calling getPublicEnv throw clearly.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Live-mode credential trio. Always minted fresh on /state reads
|
|
142
|
+
// so the iframe-runtime gets a long-TTL token + the host learns
|
|
143
|
+
// `wsUrl` (the CSP-permitted WebSocket origin). Without this,
|
|
144
|
+
// any caller of /state — including the restore-bootstrap path in
|
|
145
|
+
// every host (claude.ai, ChatGPT, our sample-agent) — receives a
|
|
146
|
+
// render slice missing `wsUrl`. CSP `connect-src` then omits the
|
|
147
|
+
// `ws://` scheme, the browser blocks the upgrade, and props_update
|
|
148
|
+
// never reaches the iframe. The render-commit handler already
|
|
149
|
+
// stamps this trio on its resultMeta; mirroring here closes the
|
|
150
|
+
// drift.
|
|
151
|
+
const liveTrio = mintBootstrap ? mintBootstrap(stored.id, stored.appId) : undefined;
|
|
152
|
+
// Polling URL — the iframe-runtime's R6 polling-fallback path
|
|
153
|
+
// composes its fetch URL from `render.pollingUrl`. The /state
|
|
154
|
+
// endpoint IS that URL, so we stamp it here; without it the
|
|
155
|
+
// iframe-runtime can't fall back when the WS upgrade fails.
|
|
156
|
+
const requestHostForPolling = req.get("host") ?? "";
|
|
157
|
+
const pollingBase = publicBaseUrl
|
|
158
|
+
? publicBaseUrl.replace(/\/$/, "")
|
|
159
|
+
: `${req.protocol}://${requestHostForPolling}`;
|
|
160
|
+
const pollingUrl = `${pollingBase}/api/sessions/${encodeURIComponent(stored.id)}/state`;
|
|
161
|
+
// Static-component delivery via codeUrl (the same content-addressable
|
|
162
|
+
// channel the code routes serve). Polling clients are render-capable
|
|
163
|
+
// and need the URL to mount/refresh the static-component variant.
|
|
164
|
+
let renderCodeUrl;
|
|
165
|
+
let renderCodeHash;
|
|
166
|
+
let renderContractHash;
|
|
167
|
+
let renderValidatorsUrl;
|
|
168
|
+
if (!isSystem && !isMcpApps && codeStore) {
|
|
169
|
+
const code = render.componentCode;
|
|
170
|
+
if (typeof code === "string" && code.length > 0) {
|
|
171
|
+
try {
|
|
172
|
+
const hash = codeStore.hashOf(code);
|
|
173
|
+
await codeStore.put(hash, code);
|
|
174
|
+
renderCodeHash = hash;
|
|
175
|
+
const requestHost = req.get("host") ?? "";
|
|
176
|
+
const base = publicBaseUrl
|
|
177
|
+
? publicBaseUrl.replace(/\/$/, "")
|
|
178
|
+
: `${req.protocol}://${requestHost}`;
|
|
179
|
+
renderCodeUrl = `${base}/code/${hash}.js`;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Silent — caller falls back to live-mode delivery.
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const bundle = await deriveContractBundle(render);
|
|
186
|
+
if (bundle) {
|
|
187
|
+
await codeStore.put(bundle.contractHash, bundle.bundleSource);
|
|
188
|
+
renderContractHash = bundle.contractHash;
|
|
189
|
+
const requestHost = req.get("host") ?? "";
|
|
190
|
+
const base = publicBaseUrl
|
|
191
|
+
? publicBaseUrl.replace(/\/$/, "")
|
|
192
|
+
: `${req.protocol}://${requestHost}`;
|
|
193
|
+
renderValidatorsUrl = `${base}/contract/${bundle.contractHash}.js`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Silent — server-side gate remains authoritative.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Phase B: single flat ai.ggui/render slice — render + visible-
|
|
202
|
+
// bits surface fields merged into one shape.
|
|
203
|
+
const renderMeta = {
|
|
204
|
+
sessionId: stored.id,
|
|
205
|
+
appId: stored.appId,
|
|
206
|
+
runtimeUrl: resolveRuntimeUrl(),
|
|
207
|
+
...(liveTrio !== undefined
|
|
208
|
+
? {
|
|
209
|
+
wsUrl: liveTrio.wsUrl,
|
|
210
|
+
wsToken: liveTrio.token,
|
|
211
|
+
expiresAt: liveTrio.expiresAt,
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
pollingUrl,
|
|
215
|
+
...(themeId !== undefined ? { themeId } : {}),
|
|
216
|
+
...(themeMode !== undefined ? { themeMode } : {}),
|
|
217
|
+
// Per-app theme overlay projected by `deriveRenderMeta` from the
|
|
218
|
+
// render's `theme` sidecar — same field the render result-meta
|
|
219
|
+
// carries, so a /state read returns the identical overlay.
|
|
220
|
+
...(view?.theme !== undefined ? { theme: view.theme } : {}),
|
|
221
|
+
...(view?.gadgets !== undefined && view.gadgets.length > 0 ? { gadgets: view.gadgets } : {}),
|
|
222
|
+
...(statePublicEnv !== undefined && Object.keys(statePublicEnv).length > 0
|
|
223
|
+
? { publicEnv: statePublicEnv }
|
|
224
|
+
: {}),
|
|
225
|
+
...(view?.permissionsPolicy !== undefined && view.permissionsPolicy.length > 0
|
|
226
|
+
? { permissionsPolicy: view.permissionsPolicy }
|
|
227
|
+
: {}),
|
|
228
|
+
// R6 — load-bearing ledger cursor. Always stamped on /state
|
|
229
|
+
// reads so polling clients can position the R7 /events cursor.
|
|
230
|
+
lastSequence: stored.eventSequence,
|
|
231
|
+
// Visible-bits surface merged onto the single render slice.
|
|
232
|
+
...(renderKind !== undefined ? { kind: renderKind } : {}),
|
|
233
|
+
...(renderCodeUrl !== undefined
|
|
234
|
+
? {
|
|
235
|
+
codeUrl: renderCodeUrl,
|
|
236
|
+
...(renderCodeHash !== undefined ? { codeHash: renderCodeHash } : {}),
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
...(view?.propsJson !== undefined ? { propsJson: view.propsJson } : {}),
|
|
240
|
+
...(view?.contextSlots !== undefined ? { contextSlots: view.contextSlots } : {}),
|
|
241
|
+
...(renderContractHash !== undefined && renderValidatorsUrl !== undefined
|
|
242
|
+
? {
|
|
243
|
+
contractHash: renderContractHash,
|
|
244
|
+
validatorsUrl: renderValidatorsUrl,
|
|
245
|
+
}
|
|
246
|
+
: {}),
|
|
247
|
+
};
|
|
248
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
249
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
250
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
251
|
+
res.status(200).json({
|
|
252
|
+
[MCP_APP_AI_GGUI_RENDER_META_KEY]: renderMeta,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
app.get("/api/sessions/:sessionId/events", async (req, res) => {
|
|
256
|
+
const sessionId = req.params["sessionId"];
|
|
257
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
258
|
+
res.status(400).type("text/plain").send("sessionId required");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const wsTokenRaw = req.query["wsToken"];
|
|
262
|
+
const wsToken = typeof wsTokenRaw === "string" ? wsTokenRaw : "";
|
|
263
|
+
if (wsToken.length === 0) {
|
|
264
|
+
res.status(401).type("text/plain").send("wsToken query required");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const verify = verifyToken(wsToken, secret, "ws");
|
|
268
|
+
if (!verify.ok) {
|
|
269
|
+
if (verify.reason === "expired") {
|
|
270
|
+
res.status(410).type("text/plain").send("wsToken expired");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
res.status(401).type("text/plain").send("wsToken invalid");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (verify.claims.sessionId !== sessionId) {
|
|
277
|
+
res.status(401).type("text/plain").send("wsToken scope mismatch");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Parse + validate sinceSequence (required, non-negative integer)
|
|
281
|
+
// and limit (optional, 1..500, default 100).
|
|
282
|
+
//
|
|
283
|
+
// sinceSequence is REQUIRED — explicit cursor reads only. A missing
|
|
284
|
+
// query is a caller bug (no sensible default; `0` means "replay
|
|
285
|
+
// everything" which we don't want to fire unintentionally).
|
|
286
|
+
const sinceSequenceRaw = req.query["sinceSequence"];
|
|
287
|
+
if (typeof sinceSequenceRaw !== "string" || sinceSequenceRaw.length === 0) {
|
|
288
|
+
res
|
|
289
|
+
.status(400)
|
|
290
|
+
.type("text/plain")
|
|
291
|
+
.send("sinceSequence query required (non-negative integer)");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const sinceSequence = Number(sinceSequenceRaw);
|
|
295
|
+
if (!Number.isInteger(sinceSequence) || sinceSequence < 0) {
|
|
296
|
+
res.status(400).type("text/plain").send("sinceSequence must be a non-negative integer");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const limitRaw = req.query["limit"];
|
|
300
|
+
let limit = 100;
|
|
301
|
+
if (typeof limitRaw === "string" && limitRaw.length > 0) {
|
|
302
|
+
const parsed = Number(limitRaw);
|
|
303
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) {
|
|
304
|
+
res.status(400).type("text/plain").send("limit must be an integer in [1, 500]");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
limit = parsed;
|
|
308
|
+
}
|
|
309
|
+
let result;
|
|
310
|
+
try {
|
|
311
|
+
result = await renderStore.listEventsSince(sessionId, sinceSequence, limit);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
logger.warn("events_read_failed", {
|
|
315
|
+
sessionId,
|
|
316
|
+
error: String(err),
|
|
317
|
+
});
|
|
318
|
+
res.status(500).type("text/plain").send("internal error");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (result === null) {
|
|
322
|
+
// 404 — render not found. Distinct from 410 (cursor stale on
|
|
323
|
+
// a live render) so polling clients can branch.
|
|
324
|
+
res.status(404).type("text/plain").send("render not found");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Tenancy gate (round 2): the wsToken's appId MUST match the
|
|
328
|
+
// render's appId. We need the render record to check — fetch
|
|
329
|
+
// it. listEventsSince validated the render exists.
|
|
330
|
+
const stored = await renderStore.get(sessionId);
|
|
331
|
+
if (!stored) {
|
|
332
|
+
res.status(404).type("text/plain").send("render not found");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (verify.claims.appId !== stored.appId) {
|
|
336
|
+
res.status(401).type("text/plain").send("wsToken scope mismatch");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Replay-horizon gate. Two cases collapse to REPLAY_HORIZON_PASSED:
|
|
340
|
+
// (a) sinceSequence < horizonSeq — events evicted (cloud TTL,
|
|
341
|
+
// in-mem never).
|
|
342
|
+
// (b) sinceSequence > lastSequence — cursor from a different
|
|
343
|
+
// deployment / reset render; the server has no events
|
|
344
|
+
// beyond what it knows, and we can't safely advance.
|
|
345
|
+
if (sinceSequence > result.lastSequence) {
|
|
346
|
+
res.status(410).type("application/json").json({
|
|
347
|
+
reason: "REPLAY_HORIZON_PASSED",
|
|
348
|
+
currentSequence: result.lastSequence,
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (sinceSequence < result.horizonSeq) {
|
|
353
|
+
res.status(410).type("application/json").json({
|
|
354
|
+
reason: "REPLAY_HORIZON_PASSED",
|
|
355
|
+
currentSequence: result.lastSequence,
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// GguiSessionEvent is now the unified wire-shape ledger primitive
|
|
360
|
+
// (Wave 7 of flatten-render-identity, 2026-05-28). The store
|
|
361
|
+
// returns events in protocol-canonical shape (seq + type +
|
|
362
|
+
// timestamp[ISO] + data); no projection needed.
|
|
363
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
364
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
365
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
366
|
+
res.status(200).json({
|
|
367
|
+
events: result.events,
|
|
368
|
+
lastSequence: result.lastSequence,
|
|
369
|
+
hasMore: result.hasMore,
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
package/dist/build-mcp.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface BuildMcpServerOptions {
|
|
|
40
40
|
/**
|
|
41
41
|
* Per-render self-contained shell options. When supplied,
|
|
42
42
|
* `installMcpAppsOutbound` ALSO registers
|
|
43
|
-
* `ui://ggui/render/{
|
|
43
|
+
* `ui://ggui/render/{sessionId}` as a resource template — the URI
|
|
44
44
|
* `ggui_render.resultMeta` stamps on per-call `_meta.ui.resourceUri`
|
|
45
45
|
* for third-party MCP Apps hosts (Claude Desktop, claude.ai web)
|
|
46
46
|
* that don't speak ggui's custom postMessage protocol.
|
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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAK,KAAK,WAAW,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAEL,KAAK,iCAAiC,EACvC,MAAM,wBAAwB,CAAC;AAEhC,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;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,iCAAiC,CAAC;IAC3D;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;;;;;;;;;;;;;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;CACtE;AAED;;;;;;GAMG;AACH,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,CAyJX"}
|
package/dist/build-mcp.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
|
13
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
13
14
|
import { z } from 'zod';
|
|
14
15
|
import { installMcpAppsOutbound, } from './mcp-apps-outbound.js';
|
|
15
16
|
/**
|
|
@@ -74,11 +75,13 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
74
75
|
// deadline. Both ride the canonical context without leaking the
|
|
75
76
|
// SDK type into the handlers package.
|
|
76
77
|
const baseCtx = getContext();
|
|
78
|
+
// `_meta` is `unknown` at the SDK seam; per JSON-RPC it MUST be
|
|
79
|
+
// an object, so narrow with the validating predicate and DROP
|
|
80
|
+
// anything else rather than asserting.
|
|
81
|
+
const requestMeta = extra?._meta;
|
|
77
82
|
const ctx = {
|
|
78
83
|
...baseCtx,
|
|
79
|
-
...(
|
|
80
|
-
? { requestMeta: extra._meta }
|
|
81
|
-
: {}),
|
|
84
|
+
...(isRecord(requestMeta) ? { requestMeta } : {}),
|
|
82
85
|
...(extra?.signal !== undefined ? { signal: extra.signal } : {}),
|
|
83
86
|
};
|
|
84
87
|
const start = Date.now();
|
|
@@ -128,11 +131,18 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
128
131
|
// single canonical key only; the helper owns the back-compat
|
|
129
132
|
// shape. Handlers without `_meta.ui` fall through to plain
|
|
130
133
|
// `registerTool` — the ext-apps helper requires `_meta.ui` to be
|
|
131
|
-
// typed.
|
|
134
|
+
// typed. A declared-but-malformed `_meta.ui` is a programming
|
|
135
|
+
// error on the handler author's side — fail loud rather than
|
|
136
|
+
// silently registering the tool without its UI surface.
|
|
132
137
|
if (handler._meta && 'ui' in handler._meta) {
|
|
138
|
+
const ui = handler._meta['ui'];
|
|
139
|
+
if (!isMcpUiToolMeta(ui)) {
|
|
140
|
+
throw new Error(`Tool ${handler.name} declares _meta.ui with an invalid shape — ` +
|
|
141
|
+
`expected { resourceUri?: string; visibility?: ('model' | 'app')[] }.`);
|
|
142
|
+
}
|
|
133
143
|
registerAppTool(server, handler.name, {
|
|
134
144
|
...baseConfig,
|
|
135
|
-
_meta: handler._meta,
|
|
145
|
+
_meta: { ...handler._meta, ui },
|
|
136
146
|
}, cb);
|
|
137
147
|
}
|
|
138
148
|
else {
|
|
@@ -144,6 +154,30 @@ export function buildMcpServer(info, handlers, getContext, logger, opts = {}) {
|
|
|
144
154
|
}
|
|
145
155
|
return server;
|
|
146
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Validating narrower for declaration-level MCP-Apps UI meta. The
|
|
159
|
+
* `SharedHandler` seam types `_meta` as `Record<string, unknown>`;
|
|
160
|
+
* `registerAppTool` requires `_meta.ui` typed as `McpUiToolMeta`.
|
|
161
|
+
* Validates the two fields the ext-apps helper actually reads —
|
|
162
|
+
* `resourceUri` (string when present) and `visibility`
|
|
163
|
+
* (`"model"`/`"app"` array when present) — instead of asserting
|
|
164
|
+
* blindly across the SDK seam.
|
|
165
|
+
*/
|
|
166
|
+
function isMcpUiToolMeta(value) {
|
|
167
|
+
if (!isRecord(value))
|
|
168
|
+
return false;
|
|
169
|
+
if (value.resourceUri !== undefined && typeof value.resourceUri !== 'string') {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (value.visibility !== undefined) {
|
|
173
|
+
if (!Array.isArray(value.visibility))
|
|
174
|
+
return false;
|
|
175
|
+
if (!value.visibility.every((v) => v === 'model' || v === 'app')) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
147
181
|
function errorClassName(err) {
|
|
148
182
|
if (err instanceof Error) {
|
|
149
183
|
if (err.name && err.name !== 'Error')
|