@ggui-ai/mcp-server 0.9.0 → 0.10.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 (37) hide show
  1. package/dist/api-renders-routes.d.ts +21 -0
  2. package/dist/api-renders-routes.d.ts.map +1 -1
  3. package/dist/api-renders-routes.js +22 -5
  4. package/dist/build-mcp.d.ts +38 -7
  5. package/dist/build-mcp.d.ts.map +1 -1
  6. package/dist/build-mcp.js +23 -2
  7. package/dist/code-module-variant.d.ts +150 -0
  8. package/dist/code-module-variant.d.ts.map +1 -0
  9. package/dist/code-module-variant.js +243 -0
  10. package/dist/code-routes.d.ts +12 -2
  11. package/dist/code-routes.d.ts.map +1 -1
  12. package/dist/code-routes.js +12 -2
  13. package/dist/control-service.d.ts +29 -3
  14. package/dist/control-service.d.ts.map +1 -1
  15. package/dist/control-service.js +26 -2
  16. package/dist/health-routes.d.ts +19 -3
  17. package/dist/health-routes.d.ts.map +1 -1
  18. package/dist/health-routes.js +26 -18
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +7 -1
  22. package/dist/mcp-apps-outbound.d.ts +40 -8
  23. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  24. package/dist/mcp-apps-outbound.js +127 -30
  25. package/dist/mcp-endpoint-routes.d.ts +6 -0
  26. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  27. package/dist/mcp-endpoint-routes.js +67 -1
  28. package/dist/oauth-as-routes.d.ts +11 -0
  29. package/dist/oauth-as-routes.d.ts.map +1 -1
  30. package/dist/oauth-as-routes.js +9 -1
  31. package/dist/runtime-bundle-hash.d.ts +12 -0
  32. package/dist/runtime-bundle-hash.d.ts.map +1 -1
  33. package/dist/runtime-bundle-hash.js +19 -0
  34. package/dist/server.d.ts +223 -56
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +263 -125
  37. package/package.json +13 -12
package/dist/server.js CHANGED
@@ -38,7 +38,7 @@
38
38
  * once at boot so operators see the shape they're running.
39
39
  */
40
40
  import { CONSOLE_DIST_DIR } from "@ggui-ai/console/server";
41
- import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH } from "@ggui-ai/iframe-runtime/server";
41
+ import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH, RUNTIME_SHIMS_DIR, RUNTIME_SHIMS_URL_PREFIX, } from "@ggui-ai/iframe-runtime/server";
42
42
  import { createDeterministicBlueprintSelector, isTokenRegisteringAuthAdapter, mintSessionToken, mintWsToken, refreshWsToken, verifyToken, } from "@ggui-ai/mcp-server-core";
43
43
  import { createInMemoryBlueprintSearch, createInMemoryGeneratorRegistry, FixedWindowRateLimiter, InMemoryActiveConsumerRegistry, InMemoryAppMetadataStore, InMemoryAuthAdapter, InMemoryBlueprintIndex, InMemoryBlueprintStore, InMemoryKeyValueStore, InMemoryPairingService, InMemoryPendingEventConsumer, InMemoryQuotaStore, InMemoryGguiSessionStore, InMemoryGguiSessionStreamBuffer, InMemoryVectorStore, MockEmbeddingProvider, NoopAuditSink, NoopRateLimiter, NoopTelemetrySink, } from "@ggui-ai/mcp-server-core/in-memory";
44
44
  import { createGguiListGadgetsHandler, createGguiListThemesHandler, } from "@ggui-ai/mcp-server-handlers/app-discovery";
@@ -71,7 +71,7 @@ import { createCreateAppHandler, createDeleteAppHandler, createListAppsHandler,
71
71
  import { createIssueConnectorKeyHandler, createListConnectorKeysHandler, createRevokeConnectorKeyHandler, } from "@ggui-ai/mcp-server-handlers/ops-connector-keys";
72
72
  import { createRedeemCouponHandler, } from "@ggui-ai/mcp-server-handlers/ops-coupon";
73
73
  import { createCreateOrgHandler, createGetOrgBalanceHandler, createInviteToOrgHandler, createListOrgsHandler, createRemoveOrgMemberHandler, createRenameOrgHandler, createRevokeInviteHandler, } from "@ggui-ai/mcp-server-handlers/ops-orgs";
74
- import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiRuntimePullHandler, createGguiRuntimeTelemetryHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, createGguiAmendHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
74
+ import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetRenderSourceHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiRuntimePullHandler, createGguiRuntimeTelemetryHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, createGguiAmendHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
75
75
  import { DEFAULT_ADMIN_BLUEPRINTS_PATH, mountAdminBlueprintsTransport, } from "./admin-blueprints-transport.js";
76
76
  import { mountAdminOAuthProvidersTransport } from "./admin-oauth-providers-transport.js";
77
77
  import { DEFAULT_BUILDER_APP_ID, defaultAppIdFromIdentity } from "./auth.js";
@@ -90,6 +90,7 @@ import { mountConsoleSessionRoutes } from "./console-session-routes.js";
90
90
  import { mountConsoleStaticRoutes } from "./console-static-routes.js";
91
91
  import { mountConsoleSessionsRoutes } from "./console-sessions-routes.js";
92
92
  import { mountCodeRoutes } from "./code-routes.js";
93
+ import { captureShimSources, createCodeModuleUrlMinter, mountShimRoutes, } from "./code-module-variant.js";
93
94
  import { mountHealthRoutes } from "./health-routes.js";
94
95
  import { mountOAuthAuthorizationServerRoutes } from "./oauth-as-routes.js";
95
96
  import { mountOAuthClientsRoutes } from "./oauth-clients-routes.js";
@@ -126,30 +127,6 @@ const DEFAULT_INFO = {
126
127
  version: "0.0.1",
127
128
  description: "Open self-hosted MCP server for the ggui protocol. Powered by @ggui-ai/mcp-server-handlers.",
128
129
  };
129
- /**
130
- * Canonical default handler set. Every `@ggui-ai/mcp-server-handlers`
131
- * family the OSS server ships with lands here, bound to the caller-
132
- * supplied deps. Use this when you want to EXTEND the defaults rather
133
- * than replace them wholesale:
134
- *
135
- * ```ts
136
- * const server = createGguiServer({
137
- * vectors, embedding,
138
- * handlers: [
139
- * ...defaultHandlers({ vectors, embedding }),
140
- * myCustomHandler,
141
- * ],
142
- * });
143
- * ```
144
- *
145
- * Without this helper, `handlers:` replaces the full list — callers
146
- * lose the defaults unless they copy-paste them. Keeping `defaultHandlers`
147
- * named means the default set stays discoverable + testable in one place.
148
- *
149
- * `render` is opt-in via `deps.render` — it's only useful when the server
150
- * was booted with `mcpApps: true` (so `ui://ggui/render` is served)
151
- * and pairs a real GguiSessionStore. Callers get the choice explicitly.
152
- */
153
130
  /**
154
131
  * Assemble the `opsBlueprint` dep bundle for `defaultHandlers`.
155
132
  *
@@ -179,9 +156,82 @@ function buildOpsBlueprintDeps(input) {
179
156
  ...(input.resolveLlm ? { resolveLlm: input.resolveLlm } : {}),
180
157
  ...(input.blueprints ? { blueprints: input.blueprints } : {}),
181
158
  ...(input.cacheRegistry ? { cacheRegistry: input.cacheRegistry } : {}),
159
+ authorizeAppAccess: input.authorizeAppAccess ?? (async () => ({ allowed: true })),
182
160
  },
183
161
  };
184
162
  }
163
+ /**
164
+ * Materialize the operator-class blueprint family from a bound
165
+ * {@link OpsBlueprintBundle}. Registered on the control plane via
166
+ * `audience: ['ops']`.
167
+ *
168
+ * Four of the five land whenever the bundle is present; the `generate`
169
+ * tool additionally requires `resolveLlm` + `blueprints` (the same deps
170
+ * the render generation path reads) because without them it has no
171
+ * dispatch target.
172
+ *
173
+ * Lives here — not inline in {@link defaultHandlers} — so BOTH
174
+ * composition paths build the family from ONE body:
175
+ * `defaultHandlers` for the default handler set, and
176
+ * {@link buildOpsBundleHandlers} for deployments that supply their own
177
+ * `CreateGguiServerOptions.handlers` array. The construction used to sit
178
+ * only inside `defaultHandlers`, so an explicit handler list silently
179
+ * dropped all five tools even with the bundle bound.
180
+ */
181
+ export function buildOpsBlueprintHandlers(input) {
182
+ const { bundle } = input;
183
+ const handlers = [];
184
+ // Bundle-explicit `appMetadataStore` wins; falls back to the
185
+ // server-level one — same precedence
186
+ // `deps.handshake.appMetadataStore ?? deps.appMetadataStore` uses.
187
+ // Gates `assertGadgetsRegistered` inside both generate + register
188
+ // (see each handler's own no-op-when-unbound posture).
189
+ const opsBlueprintAppMetadataStore = bundle.appMetadataStore ?? input.appMetadataStore;
190
+ if (bundle.resolveLlm && bundle.blueprints) {
191
+ handlers.push(createGguiOpsGenerateBlueprintHandler({
192
+ registry: bundle.registry,
193
+ blueprintStore: bundle.blueprintStore,
194
+ resolveLlm: bundle.resolveLlm,
195
+ blueprints: bundle.blueprints,
196
+ ...(bundle.putCode ? { putCode: bundle.putCode } : {}),
197
+ ...(bundle.listAllForApp ? { listAllForApp: bundle.listAllForApp } : {}),
198
+ ...(opsBlueprintAppMetadataStore
199
+ ? { appMetadataStore: opsBlueprintAppMetadataStore }
200
+ : {}),
201
+ ...(bundle.cacheRegistry ? { cacheRegistry: bundle.cacheRegistry } : {}),
202
+ ...(input.telemetry ? { telemetry: input.telemetry } : {}),
203
+ authorizeAppAccess: bundle.authorizeAppAccess,
204
+ }));
205
+ }
206
+ // `ggui_ops_register_blueprint` — sibling of `_generate_*` that
207
+ // accepts pre-built componentCode bytes. No LLM dispatch, so it
208
+ // registers whenever the ops dep bundle is bound (no resolveLlm
209
+ // / blueprints gate). Operator UX entry point for fixture
210
+ // seeding + export/reimport round-trips.
211
+ handlers.push(createGguiOpsRegisterBlueprintHandler({
212
+ blueprintStore: bundle.blueprintStore,
213
+ ...(bundle.putCode ? { putCode: bundle.putCode } : {}),
214
+ ...(bundle.listAllForApp ? { listAllForApp: bundle.listAllForApp } : {}),
215
+ ...(opsBlueprintAppMetadataStore ? { appMetadataStore: opsBlueprintAppMetadataStore } : {}),
216
+ ...(bundle.cacheRegistry ? { cacheRegistry: bundle.cacheRegistry } : {}),
217
+ ...(input.telemetry ? { telemetry: input.telemetry } : {}),
218
+ authorizeAppAccess: bundle.authorizeAppAccess,
219
+ }));
220
+ handlers.push(createGguiOpsListBlueprintsHandler({
221
+ blueprintStore: bundle.blueprintStore,
222
+ blueprintSearch: bundle.blueprintSearch,
223
+ authorizeAppAccess: bundle.authorizeAppAccess,
224
+ }));
225
+ handlers.push(createGguiOpsUpdateBlueprintHandler({
226
+ blueprintStore: bundle.blueprintStore,
227
+ authorizeAppAccess: bundle.authorizeAppAccess,
228
+ }));
229
+ handlers.push(createGguiOpsDeleteBlueprintHandler({
230
+ blueprintStore: bundle.blueprintStore,
231
+ authorizeAppAccess: bundle.authorizeAppAccess,
232
+ }));
233
+ return handlers;
234
+ }
185
235
  export function defaultHandlers(deps) {
186
236
  // Single shared pending-events pipe (Model C, sessionId-keyed).
187
237
  // render opens (`markCreated`), submit_action appends, consume drains,
@@ -377,6 +427,11 @@ export function defaultHandlers(deps) {
377
427
  ? { serverCapabilities: deps.handshake.serverCapabilities }
378
428
  : {}),
379
429
  ...(lifecycleEmitter ? { lifecycleEmitter } : {}),
430
+ // Server-level TelemetrySink → the handler's `handshake.decided`
431
+ // emission. Without this thread the composed emission never
432
+ // fired (the handler dep predates the wiring — P2 of
433
+ // docs/plans/2026-08-19-schema-precise-render.md pinned it).
434
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
380
435
  }));
381
436
  }
382
437
  if (deps.update) {
@@ -401,6 +456,10 @@ export function defaultHandlers(deps) {
401
456
  ...(deps.update.themeProvider !== undefined
402
457
  ? { themeProvider: deps.update.themeProvider }
403
458
  : {}),
459
+ // Mutation-time `render.contract_violation` events (P2
460
+ // measurement — update-time violations are baselined with
461
+ // render-time ones, not hidden).
462
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
404
463
  }));
405
464
  // ggui_amend rides the SAME deps slot (#483 tool split): one
406
465
  // mutation core, one wiring. It reads only the mutation-flow deps
@@ -415,6 +474,8 @@ export function defaultHandlers(deps) {
415
474
  ...(deps.update.propsUpdateNotifier
416
475
  ? { propsUpdateNotifier: deps.update.propsUpdateNotifier }
417
476
  : {}),
477
+ // Same mutation core as ggui_update — same violation events.
478
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
418
479
  }));
419
480
  }
420
481
  // ggui_consume registers whenever render is bound (it shares the
@@ -480,6 +541,12 @@ export function defaultHandlers(deps) {
480
541
  handlers.push(createGguiGetSessionHandler({
481
542
  renderStore: deps.render.renderStore,
482
543
  }));
544
+ // ggui_get_render_source (#282 data-plane rider) — the calling
545
+ // app reads its OWN render's generated source. No heartbeat: a
546
+ // one-shot source read is not an activity signal.
547
+ handlers.push(createGguiGetRenderSourceHandler({
548
+ renderStore: deps.render.renderStore,
549
+ }));
483
550
  // ggui_list_sessions — host-scoped render enumeration for resume.
484
551
  // Folds the ws-token mint into the same call so the host doesn't
485
552
  // round-trip twice (list, then mint-per-render). Reuses the
@@ -567,6 +634,11 @@ export function defaultHandlers(deps) {
567
634
  ? { themeProvider: deps.render.themeProvider }
568
635
  : {}),
569
636
  ...(deps.render.rateLimiter ? { rateLimiter: deps.render.rateLimiter } : {}),
637
+ // Render measurement events (`render.attempted` /
638
+ // `render.contract_violation` / `render.committed`) — P2 of
639
+ // docs/plans/2026-08-19-schema-precise-render.md. One
640
+ // server-level sink, every consumer.
641
+ ...(deps.telemetry ? { telemetrySink: deps.telemetry } : {}),
570
642
  ...(deps.render.shortCodeIndex ? { shortCodeIndex: deps.render.shortCodeIndex } : {}),
571
643
  ...(deps.render.renderIdentityStore
572
644
  ? { renderIdentityStore: deps.render.renderIdentityStore }
@@ -599,6 +671,9 @@ export function defaultHandlers(deps) {
599
671
  ? {
600
672
  codeStore: deps.render.codeStore,
601
673
  codeBaseUrl: deps.render.codeBaseUrl,
674
+ ...(deps.render.mintCodeModuleUrl
675
+ ? { mintCodeModuleUrl: deps.render.mintCodeModuleUrl }
676
+ : {}),
602
677
  }
603
678
  : {}),
604
679
  // Share the handshake KV store between the two handlers so
@@ -611,61 +686,24 @@ export function defaultHandlers(deps) {
611
686
  ...(lifecycleEmitter ? { lifecycleEmitter } : {}),
612
687
  }));
613
688
  }
614
- // Operator-class blueprint tools. Registered
615
- // on /ops via `audience: ['ops']`. Three read-mutating tools land
616
- // whenever the blueprint store + search seam is bound; the
617
- // `generate` tool additionally requires `resolveLlm` +
618
- // `blueprints` (same deps the render generation path reads). Cloud
619
- // pods wire all four through their own composition layer.
689
+ // Operator-class blueprint tools — see `buildOpsBlueprintHandlers`
690
+ // for the family's registration rules. The server-level
691
+ // `deps.appMetadataStore` + `deps.telemetry` are offered as the
692
+ // bundle's fallbacks; render already gets the server-level store at
693
+ // :1398. Cloud pods that pass their own handler list pick the same
694
+ // family up through `buildOpsBundleHandlers`.
620
695
  if (deps.opsBlueprint) {
621
- if (deps.opsBlueprint.resolveLlm && deps.opsBlueprint.blueprints) {
622
- handlers.push(createGguiOpsGenerateBlueprintHandler({
623
- registry: deps.opsBlueprint.registry,
624
- blueprintStore: deps.opsBlueprint.blueprintStore,
625
- resolveLlm: deps.opsBlueprint.resolveLlm,
626
- blueprints: deps.opsBlueprint.blueprints,
627
- ...(deps.opsBlueprint.putCode ? { putCode: deps.opsBlueprint.putCode } : {}),
628
- ...(deps.opsBlueprint.listAllForApp
629
- ? { listAllForApp: deps.opsBlueprint.listAllForApp }
630
- : {}),
631
- ...(deps.opsBlueprint.cacheRegistry
632
- ? { cacheRegistry: deps.opsBlueprint.cacheRegistry }
633
- : {}),
634
- ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
635
- }));
636
- }
637
- // `ggui_ops_register_blueprint` — sibling of `_generate_*` that
638
- // accepts pre-built componentCode bytes. No LLM dispatch, so it
639
- // registers whenever the ops dep bundle is bound (no resolveLlm
640
- // / blueprints gate). Operator UX entry point for fixture
641
- // seeding + export/reimport round-trips.
642
- handlers.push(createGguiOpsRegisterBlueprintHandler({
643
- blueprintStore: deps.opsBlueprint.blueprintStore,
644
- ...(deps.opsBlueprint.putCode ? { putCode: deps.opsBlueprint.putCode } : {}),
645
- ...(deps.opsBlueprint.listAllForApp
646
- ? { listAllForApp: deps.opsBlueprint.listAllForApp }
647
- : {}),
648
- ...(deps.opsBlueprint.cacheRegistry
649
- ? { cacheRegistry: deps.opsBlueprint.cacheRegistry }
650
- : {}),
696
+ handlers.push(...buildOpsBlueprintHandlers({
697
+ bundle: deps.opsBlueprint,
698
+ ...(deps.appMetadataStore ? { appMetadataStore: deps.appMetadataStore } : {}),
651
699
  ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
652
700
  }));
653
- handlers.push(createGguiOpsListBlueprintsHandler({
654
- blueprintStore: deps.opsBlueprint.blueprintStore,
655
- blueprintSearch: deps.opsBlueprint.blueprintSearch,
656
- }));
657
- handlers.push(createGguiOpsUpdateBlueprintHandler({
658
- blueprintStore: deps.opsBlueprint.blueprintStore,
659
- }));
660
- handlers.push(createGguiOpsDeleteBlueprintHandler({
661
- blueprintStore: deps.opsBlueprint.blueprintStore,
662
- }));
663
701
  }
664
702
  return handlers;
665
703
  }
666
704
  /**
667
705
  * Build the operator-class handlers for the apps / orgs /
668
- * connector-keys / coupon domains. Each domain materializes
706
+ * connector-keys / coupon / blueprint domains. Each domain materializes
669
707
  * independently when its deps seam is bound; deployments that wired
670
708
  * none get an empty list.
671
709
  *
@@ -696,6 +734,15 @@ export function buildOpsBundleHandlers(deps) {
696
734
  coupons: deps.opsCoupon.coupons,
697
735
  }));
698
736
  }
737
+ if (deps.opsBlueprint) {
738
+ // No server-level `appMetadataStore` fallback on this path — the
739
+ // bundle's own field governs, since a deployment that binds its own
740
+ // bundle here binds the store with it.
741
+ handlers.push(...buildOpsBlueprintHandlers({
742
+ bundle: deps.opsBlueprint,
743
+ ...(deps.telemetry ? { telemetry: deps.telemetry } : {}),
744
+ }));
745
+ }
699
746
  return handlers;
700
747
  }
701
748
  /**
@@ -707,6 +754,9 @@ export function createGguiServer(opts = {}) {
707
754
  const info = { ...DEFAULT_INFO, ...opts.info };
708
755
  const logger = opts.logger ?? createConsoleLogger({ server: info.name });
709
756
  const bodyLimit = opts.bodyLimit ?? "4mb";
757
+ // Where the content-addressable routes are reached from the iframe:
758
+ // an explicit asset host, else the public origin (see `codeBaseUrl`).
759
+ const codeBaseUrl = opts.codeBaseUrl ?? opts.publicBaseUrl;
710
760
  // Operator mode: gates the `/devtools/*` namespace. Explicit option
711
761
  // wins; otherwise read GGUI_MODE env (`'dev'` opts in, anything else
712
762
  // including unset is `'prod'`). Surfaced via `/info` so the SPA
@@ -852,6 +902,33 @@ export function createGguiServer(opts = {}) {
852
902
  : insertRuntimeBundleHash(urlOrPath, runtimeBundleHash, runtimePath.slice(runtimePath.lastIndexOf("/") + 1));
853
903
  const hashedRuntimePath = runtimeBundleHash !== undefined ? insertHash(runtimePath) : undefined;
854
904
  const runtimeBootstrapUrl = insertHash(runtimeConfig.url ?? runtimePath);
905
+ // Strict-CSP module-variant family (ggui#522 slice 2). Every
906
+ // ingredient must exist for any of it to mount: the shim files the
907
+ // runtime build emitted next to the bundle (the fetchable twins of
908
+ // the data-url shims), the bundle content hash (the variant KEY —
909
+ // shims ship in the bundle's dist, so one hash versions both), the
910
+ // code store (the variant is a `/code` twin), and an absolute code
911
+ // base URL (the rewrite embeds absolute shim URLs — a srcdoc frame
912
+ // has no base to resolve relative ones). Missing any ⇒ no variant
913
+ // URLs are minted and no variant routes mount; renders keep the raw
914
+ // codeUrl/codeB64 carriers and the renderer's blob ladder.
915
+ const shimSources = runtimeEnabled && runtimeBundleHash !== undefined
916
+ ? captureShimSources(runtimeConfig.distDir !== undefined
917
+ ? path.join(runtimeConfig.distDir, "shims")
918
+ : RUNTIME_SHIMS_DIR)
919
+ : undefined;
920
+ const codeModuleVariant = shimSources !== undefined &&
921
+ runtimeBundleHash !== undefined &&
922
+ opts.codeStore !== undefined &&
923
+ codeBaseUrl !== undefined
924
+ ? {
925
+ runtimeHash: runtimeBundleHash,
926
+ shimBaseUrl: `${codeBaseUrl.replace(/\/$/, "")}${RUNTIME_SHIMS_URL_PREFIX}/${runtimeBundleHash}`,
927
+ }
928
+ : undefined;
929
+ const mintCodeModuleUrl = codeModuleVariant !== undefined
930
+ ? createCodeModuleUrlMinter({ runtimeHash: codeModuleVariant.runtimeHash })
931
+ : undefined;
855
932
  // Lazy resolver: each render/update handler invocation looks up the
856
933
  // request-context-derived absolute base inside the request scope
857
934
  // (via AsyncLocalStorage). Static `publicBaseUrl` wins when set;
@@ -1463,18 +1540,22 @@ export function createGguiServer(opts = {}) {
1463
1540
  //
1464
1541
  // Content-addressable code delivery. When the operator
1465
1542
  // wired `opts.codeStore`, forward it to the render
1466
- // handler along with the base
1467
- // URL the code-blob route resolves to. We prefer the
1468
- // explicit `--public-base-url` (so the URL is reachable
1469
- // from a remote host's iframe sandbox); when absent we
1470
- // fall back to "no codeUrl emission" — the iframe then
1543
+ // handler along with the base URL the code-blob route
1544
+ // resolves to: the explicit `codeBaseUrl` (an edge-cached
1545
+ // asset host, ggui#522) or, absent that, the
1546
+ // `--public-base-url` (so the URL is reachable from a
1547
+ // remote host's iframe sandbox); with neither we fall
1548
+ // back to "no codeUrl emission" — the iframe then
1471
1549
  // mounts through the live trio and the WS subscribe
1472
1550
  // carries the render body, which is the delivery path a
1473
1551
  // render-channel deployment already has.
1474
- ...(opts.codeStore && opts.publicBaseUrl
1552
+ ...(opts.codeStore && codeBaseUrl !== undefined
1475
1553
  ? {
1476
1554
  codeStore: opts.codeStore,
1477
- codeBaseUrl: opts.publicBaseUrl,
1555
+ codeBaseUrl,
1556
+ ...(mintCodeModuleUrl !== undefined
1557
+ ? { mintCodeModuleUrl }
1558
+ : {}),
1478
1559
  }
1479
1560
  : {}),
1480
1561
  // Bootstrap-side mirror of the handshake's
@@ -1597,42 +1678,48 @@ export function createGguiServer(opts = {}) {
1597
1678
  // `appMetadataStore` to register `ggui_list_themes`; absent ⇒
1598
1679
  // tool omitted from `tools/list`.
1599
1680
  ...(opts.themes ? { themes: opts.themes } : {}),
1600
- // Operator-class blueprint tool wiring. Threads the
1601
- // resolved blueprint store + search + generator registry into
1602
- // defaultHandlers; the four `ggui_ops_*` tools land on /ops
1603
- // via their `audience: ['ops']` tag. The `resolveLlm` +
1604
- // `blueprints` deps come from the same source render reads, so
1605
- // generate dispatches through the same credential + catalog
1606
- // path as live agent traffic. listAllForApp wires only when
1607
- // the resolved store is the in-memory adapter (which exposes
1608
- // it); cloud adapters bind their own listAllForApp via the
1609
- // search seam.
1610
- // Wire only when we have a resolved generator
1611
- // registry. Without `generators`, the ops `generate` path has
1612
- // no dispatch target; the list/update/delete trio could
1613
- // technically run without it but the operator UX expects all
1614
- // four together, so we gate the whole block on the registry.
1615
- ...(generators
1616
- ? buildOpsBlueprintDeps({
1617
- registry: generators,
1618
- blueprintStore,
1619
- blueprintSearch,
1620
- ...(generationWithCache?.resolveLlm
1621
- ? { resolveLlm: generationWithCache.resolveLlm }
1622
- : {}),
1623
- ...(generationWithCache?.blueprints
1624
- ? { blueprints: generationWithCache.blueprints }
1625
- : opts.blueprintProvider
1626
- ? { blueprints: opts.blueprintProvider }
1681
+ // Operator-class blueprint tool wiring. Explicit-wins: a
1682
+ // deployment that supplies `opts.opsBlueprint` gets that bundle
1683
+ // threaded through AS-IS (its own store/search/registry/
1684
+ // authorizer — the factory does not assemble or override it).
1685
+ // Otherwise falls back to the OSS default assembly below, which
1686
+ // threads the resolved blueprint store + search + generator
1687
+ // registry into defaultHandlers; the five `ggui_ops_*` tools
1688
+ // land on the control plane via their `audience: ['ops']` tag.
1689
+ // The `resolveLlm` + `blueprints` deps come from the same
1690
+ // source render reads, so generate dispatches through the same
1691
+ // credential + catalog path as live agent traffic.
1692
+ // listAllForApp wires only when the resolved store is the
1693
+ // in-memory adapter (which exposes it); cloud adapters bind
1694
+ // their own listAllForApp via the search seam.
1695
+ // The fallback is gated on `generators`: without it, the ops
1696
+ // `generate` path has no dispatch target; the
1697
+ // register/list/update/delete quartet could technically run
1698
+ // without it but the operator UX expects all five together, so
1699
+ // we gate the whole block on the registry.
1700
+ ...(opts.opsBlueprint
1701
+ ? { opsBlueprint: opts.opsBlueprint }
1702
+ : generators
1703
+ ? buildOpsBlueprintDeps({
1704
+ registry: generators,
1705
+ blueprintStore,
1706
+ blueprintSearch,
1707
+ ...(generationWithCache?.resolveLlm
1708
+ ? { resolveLlm: generationWithCache.resolveLlm }
1627
1709
  : {}),
1628
- // Mirror operator-authored blueprints into the cache
1629
- // vectorStore so the agent-facing matchBlueprint exact-
1630
- // key probe (handshake + render) finds them. Same bundle
1631
- // the render handler + handshake negotiator already
1632
- // consume.
1633
- ...(generationWithCache?.cache ? { cacheRegistry: generationWithCache.cache } : {}),
1634
- })
1635
- : {}),
1710
+ ...(generationWithCache?.blueprints
1711
+ ? { blueprints: generationWithCache.blueprints }
1712
+ : opts.blueprintProvider
1713
+ ? { blueprints: opts.blueprintProvider }
1714
+ : {}),
1715
+ // Mirror operator-authored blueprints into the cache
1716
+ // vectorStore so the agent-facing matchBlueprint exact-
1717
+ // key probe (handshake + render) finds them. Same bundle
1718
+ // the render handler + handshake negotiator already
1719
+ // consume.
1720
+ ...(generationWithCache?.cache ? { cacheRegistry: generationWithCache.cache } : {}),
1721
+ })
1722
+ : {}),
1636
1723
  // ggui_emit resolves the channel via
1637
1724
  // a lazy getter so the handler captures whatever
1638
1725
  // `channelForHealth` ends up pointing at after
@@ -1648,11 +1735,13 @@ export function createGguiServer(opts = {}) {
1648
1735
  logger,
1649
1736
  });
1650
1737
  // Operator-class domain handlers (apps / orgs / connector-keys /
1651
- // coupon). Built on EVERY path — they hang off their own explicit
1652
- // options, so a deployment that supplies a custom base handler list
1653
- // still gets the domains it wired. A name already claimed by the base
1654
- // list wins: that's how the cloud pod ships its own
1655
- // `ggui_ops_create_app` while still picking up the rest of the family.
1738
+ // coupon / blueprints). Built on EVERY path — they hang off their own
1739
+ // explicit options, so a deployment that supplies a custom base
1740
+ // handler list still gets the domains it wired. A name already claimed
1741
+ // by the base list wins: that's how the cloud pod ships its own
1742
+ // `ggui_ops_create_app` while still picking up the rest of the family,
1743
+ // and it is what keeps the blueprint family single-registered when the
1744
+ // default handler set already built it from the same bundle.
1656
1745
  const baseHandlerNames = new Set(baseHandlers.map((h) => h.name));
1657
1746
  const opsBundleHandlers = buildOpsBundleHandlers(opts).filter((h) => !baseHandlerNames.has(h.name));
1658
1747
  const handlers = composeHandlersWithMounts(opsBundleHandlers.length > 0 ? [...baseHandlers, ...opsBundleHandlers] : baseHandlers, opts.mcpMounts);
@@ -1820,6 +1909,7 @@ export function createGguiServer(opts = {}) {
1820
1909
  buildResourceValidator({
1821
1910
  universalMcpPath: opts.universalMcpPath ?? "/mcp",
1822
1911
  perAppRouting: opts.perAppRouting,
1912
+ controlPath: CONTROL_PATH,
1823
1913
  }),
1824
1914
  };
1825
1915
  const oauthStorage = oauthConfig.storage ?? new InMemoryOAuthStorage();
@@ -1833,6 +1923,7 @@ export function createGguiServer(opts = {}) {
1833
1923
  oauthConfig,
1834
1924
  oauthStorage,
1835
1925
  universalMcpPath: opts.universalMcpPath ?? "/mcp",
1926
+ controlPath: CONTROL_PATH,
1836
1927
  ...(opts.perAppRouting !== undefined
1837
1928
  ? {
1838
1929
  perAppRouting: {
@@ -1859,6 +1950,7 @@ export function createGguiServer(opts = {}) {
1859
1950
  info,
1860
1951
  toolCount: handlers.length,
1861
1952
  readinessChecks: opts.readinessChecks ?? [],
1953
+ advisoryChecks: opts.advisoryChecks ?? [],
1862
1954
  getChannel: () => channelForHealth,
1863
1955
  ...(opts.threads !== undefined
1864
1956
  ? {
@@ -1952,6 +2044,13 @@ export function createGguiServer(opts = {}) {
1952
2044
  ...(opts.theme !== undefined && opts.theme.source !== "default"
1953
2045
  ? { themeMode: opts.theme.mode }
1954
2046
  : {}),
2047
+ // The live pick reaches the read door too (ggui#539) — same
2048
+ // getter the render handler's deps carry, so the FIRST
2049
+ // resolution layer is identical across the tool-result
2050
+ // slice and the served shell.
2051
+ ...(opts.themeProvider !== undefined
2052
+ ? { themeProvider: opts.themeProvider }
2053
+ : {}),
1955
2054
  // Resume contract — registry-only fallback. Wired
1956
2055
  // when the blueprint vector store is available so the
1957
2056
  // resource handler can rehydrate a render-evicted
@@ -1990,10 +2089,13 @@ export function createGguiServer(opts = {}) {
1990
2089
  // shell that would never paint. Wiring both (the shape
1991
2090
  // this factory produces) also means a fault on one
1992
2091
  // degrades to the other instead of failing the read.
1993
- ...(opts.codeStore && opts.publicBaseUrl
2092
+ ...(opts.codeStore && codeBaseUrl !== undefined
1994
2093
  ? {
1995
2094
  codeStore: opts.codeStore,
1996
- codeBaseUrl: opts.publicBaseUrl,
2095
+ codeBaseUrl,
2096
+ ...(mintCodeModuleUrl !== undefined
2097
+ ? { mintCodeModuleUrl }
2098
+ : {}),
1997
2099
  }
1998
2100
  : {}),
1999
2101
  // Bind the app-metadata store so the resource
@@ -2060,6 +2162,7 @@ export function createGguiServer(opts = {}) {
2060
2162
  ...(opts.allowedKinds !== undefined ? { allowedKinds: opts.allowedKinds } : {}),
2061
2163
  ...(resolvedInstructions !== undefined ? { instructions: resolvedInstructions } : {}),
2062
2164
  ...(opts.extraResources !== undefined ? { extraResources: opts.extraResources } : {}),
2165
+ ...(opts.withholdResultMeta === true ? { withholdResultMeta: true } : {}),
2063
2166
  };
2064
2167
  // MCP wire endpoints (data plane: universal / per-app; control
2065
2168
  // plane: /control; plus any isolated services) — see
@@ -2074,6 +2177,7 @@ export function createGguiServer(opts = {}) {
2074
2177
  info,
2075
2178
  handlers,
2076
2179
  controlHandlers: controlService.handlers,
2180
+ controlOpsToolNames: controlService.opsToolNames,
2077
2181
  mcpServices,
2078
2182
  als,
2079
2183
  appIdFromIdentity,
@@ -2103,6 +2207,20 @@ export function createGguiServer(opts = {}) {
2103
2207
  ? { hashed: { path: hashedRuntimePath, source: runtimeBundleBytes } }
2104
2208
  : {}),
2105
2209
  });
2210
+ // Static shim assets (ggui#522 slice 2) — the fetchable twins of
2211
+ // the data-url import shims, served immutable under the runtime
2212
+ // bundle's content hash. Mounted whenever the build shipped them
2213
+ // (independent of the code store: the `/code` variant route is the
2214
+ // usual consumer, but a foreign composition may rewrite against
2215
+ // these shims itself).
2216
+ if (shimSources !== undefined && runtimeBundleHash !== undefined) {
2217
+ mountShimRoutes({
2218
+ app,
2219
+ urlPrefix: RUNTIME_SHIMS_URL_PREFIX,
2220
+ runtimeHash: runtimeBundleHash,
2221
+ shims: shimSources,
2222
+ });
2223
+ }
2106
2224
  }
2107
2225
  // R6 /state snapshot + R7 /events cursor-replay reads — see
2108
2226
  // `./api-renders-routes.ts` for the wsToken auth posture, tenancy
@@ -2123,6 +2241,10 @@ export function createGguiServer(opts = {}) {
2123
2241
  : {}),
2124
2242
  ...(opts.codeStore ? { codeStore: opts.codeStore } : {}),
2125
2243
  ...(opts.publicBaseUrl !== undefined ? { publicBaseUrl: opts.publicBaseUrl } : {}),
2244
+ // Asset host for the content-addressable URLs the /state read
2245
+ // composes (ggui#522) — session-API URLs keep the public origin.
2246
+ ...(opts.codeBaseUrl !== undefined ? { codeBaseUrl: opts.codeBaseUrl } : {}),
2247
+ ...(mintCodeModuleUrl !== undefined ? { mintCodeModuleUrl } : {}),
2126
2248
  ...(mintBootstrap ? { mintBootstrap } : {}),
2127
2249
  resolveRuntimeUrl: resolveRuntimeUrlForResultMeta,
2128
2250
  logger,
@@ -2146,7 +2268,14 @@ export function createGguiServer(opts = {}) {
2146
2268
  // `./code-routes.ts` for the route contract (cache posture, CORS,
2147
2269
  // hash validation).
2148
2270
  if (opts.codeStore) {
2149
- mountCodeRoutes({ app, codeStore: opts.codeStore, logger });
2271
+ mountCodeRoutes({
2272
+ app,
2273
+ codeStore: opts.codeStore,
2274
+ logger,
2275
+ ...(codeModuleVariant !== undefined
2276
+ ? { moduleVariant: codeModuleVariant }
2277
+ : {}),
2278
+ });
2150
2279
  }
2151
2280
  // Pairing transport + auth bridge. Opt-in via `opts.pairing`. When
2152
2281
  // enabled with defaults, we mint an `InMemoryPairingService` and wire
@@ -3049,7 +3178,7 @@ export function createGguiServer(opts = {}) {
3049
3178
  * Build the {@link OAuthConfig.validateResource} callback from the
3050
3179
  * deployment shape (RFC 8707).
3051
3180
  *
3052
- * Two valid resource shapes are recognized:
3181
+ * Three valid resource shapes are recognized:
3053
3182
  * - **Universal** — exactly `${issuer}` when `universalMcpPath` is
3054
3183
  * `/`, otherwise `${issuer}${universalMcpPath}`. Cloud
3055
3184
  * `mcp.ggui.ai` collapses the bare-root case (the domain already
@@ -3057,6 +3186,11 @@ export function createGguiServer(opts = {}) {
3057
3186
  * - **Per-app** — `${issuer}${perAppRouting.pathPrefix}/<appId>`
3058
3187
  * where `<appId>` matches `perAppRouting.paramPattern`. Cloud
3059
3188
  * uses `/apps` prefix + `[A-Za-z0-9]{8}`.
3189
+ * - **Control plane** — `${issuer}${controlPath}` (ggui#505). A
3190
+ * host naming `/control` consents through the same ceremony and
3191
+ * receives a UNIVERSAL key: the consent page extracts an appId
3192
+ * only from the per-app shape, and control-plane ops are
3193
+ * account-level by design.
3060
3194
  *
3061
3195
  * Anything else returns `false` → /authorize emits `invalid_target`
3062
3196
  * per RFC 8707 §2 before showing consent. Defense-in-depth — the
@@ -3064,7 +3198,7 @@ export function createGguiServer(opts = {}) {
3064
3198
  * can trust the value because it's already been validated.
3065
3199
  */
3066
3200
  function buildResourceValidator(opts) {
3067
- const { universalMcpPath, perAppRouting } = opts;
3201
+ const { universalMcpPath, perAppRouting, controlPath } = opts;
3068
3202
  // Normalize a single trailing slash on the path-only-root form. RFC
3069
3203
  // 3986 §6.2.3 says `https://host` and `https://host/` are equivalent
3070
3204
  // when no other path segments follow. Some clients (claude.ai 2026-05)
@@ -3077,6 +3211,10 @@ function buildResourceValidator(opts) {
3077
3211
  const universalResource = universalMcpPath === "/" ? issuer : `${issuer}${universalMcpPath}`;
3078
3212
  if (stripTrailingSlash(resource) === stripTrailingSlash(universalResource))
3079
3213
  return true;
3214
+ if (controlPath !== undefined &&
3215
+ stripTrailingSlash(resource) === stripTrailingSlash(`${issuer}${controlPath}`)) {
3216
+ return true;
3217
+ }
3080
3218
  if (!perAppRouting)
3081
3219
  return false;
3082
3220
  const { paramPattern, pathPrefix = "" } = perAppRouting;