@ggui-ai/mcp-server 0.6.3 → 0.8.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/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +9 -1
- package/dist/browser-cors.d.ts +29 -0
- package/dist/browser-cors.d.ts.map +1 -0
- package/dist/browser-cors.js +64 -0
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +5 -1
- package/dist/code-store-fs.d.ts +3 -0
- package/dist/code-store-fs.d.ts.map +1 -1
- package/dist/code-store-fs.js +27 -3
- package/dist/console-session-routes.d.ts +10 -5
- package/dist/console-session-routes.d.ts.map +1 -1
- package/dist/console-session-routes.js +10 -5
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +12 -0
- package/dist/ggui-session-channel/socket-router.d.ts +33 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
- package/dist/ggui-session-channel/socket-router.js +155 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +43 -2
- package/dist/ggui-session-channel.d.ts +114 -0
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +77 -2
- package/dist/health-routes.d.ts +3 -0
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +11 -1
- package/dist/mcp-apps-outbound.d.ts +236 -31
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +940 -261
- package/dist/origin-validation.d.ts +120 -0
- package/dist/origin-validation.d.ts.map +1 -0
- package/dist/origin-validation.js +199 -0
- package/dist/render-read-gate.d.ts +50 -0
- package/dist/render-read-gate.d.ts.map +1 -0
- package/dist/render-read-gate.js +36 -0
- package/dist/runtime-bundle-route.d.ts +15 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -1
- package/dist/runtime-bundle-route.js +20 -4
- package/dist/server.d.ts +227 -9
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +292 -28
- package/package.json +12 -12
package/dist/server.js
CHANGED
|
@@ -51,7 +51,8 @@ import { setLlmTraceSink } from "@ggui-ai/ui-gen/harness/llm-trace-sink";
|
|
|
51
51
|
import { setValidatorTraceSink } from "@ggui-ai/ui-gen/harness/validator-trace-sink";
|
|
52
52
|
import express from "express";
|
|
53
53
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
54
|
-
import { randomBytes } from "node:crypto";
|
|
54
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
55
|
+
import fs from "node:fs";
|
|
55
56
|
import path from "node:path";
|
|
56
57
|
import { BoundedCacheTraceSink, mountConsoleCacheRoutes } from "./console-cache.js";
|
|
57
58
|
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
@@ -59,6 +60,7 @@ import { BoundedLlmTraceSink, mountConsoleLlmTraceRoutes } from "./console-llm-t
|
|
|
59
60
|
import { BoundedPayloadTraceSink, mountConsolePayloadsRoutes } from "./console-payloads.js";
|
|
60
61
|
import { mountDevtoolThemeRoutes, } from "./console-theme-routes.js";
|
|
61
62
|
import { mountConsoleTimelineRoutes } from "./console-timeline.js";
|
|
63
|
+
import { buildInlineRenderShellHtml } from "./mcp-apps-outbound.js";
|
|
62
64
|
import { BoundedValidatorTraceSink, mountConsoleValidatorRoutes } from "./console-validator.js";
|
|
63
65
|
// Operator-class MCP handlers — twelve `ggui_ops_*` handlers across
|
|
64
66
|
// four domains (apps / orgs / connector-keys / coupon). Every factory
|
|
@@ -96,7 +98,9 @@ import { mountEmailLoginRoutes } from "./email-login.js";
|
|
|
96
98
|
import { resolveMcpInstructions } from "./instructions-presets.js";
|
|
97
99
|
import { buildLlmCaller, createLlmBackedHandshakeNegotiator } from "./llm-backed-negotiator.js";
|
|
98
100
|
import { createConsoleLogger } from "./logger.js";
|
|
99
|
-
import { buildControlService } from "./control-service.js";
|
|
101
|
+
import { buildControlService, CONTROL_PATH } from "./control-service.js";
|
|
102
|
+
import { createBrowserCorsMiddleware } from "./browser-cors.js";
|
|
103
|
+
import { buildOriginHostPolicy, createOriginHostValidationMiddleware, validateOriginHost, } from "./origin-validation.js";
|
|
100
104
|
import { composeHandlersWithMounts, validateMcpServices, validateServiceHandlers, } from "./mcp-mounts.js";
|
|
101
105
|
import { mountOAuthLoginRoutes } from "./oauth-login.js";
|
|
102
106
|
import { createOAuthProvidersStore } from "./oauth-providers-store.js";
|
|
@@ -202,6 +206,7 @@ export function defaultHandlers(deps) {
|
|
|
202
206
|
embedding: deps.embedding,
|
|
203
207
|
vectors: deps.vectors,
|
|
204
208
|
...(deps.blueprints ? { blueprints: deps.blueprints } : {}),
|
|
209
|
+
...(deps.registrySearch ? { registry: deps.registrySearch } : {}),
|
|
205
210
|
}),
|
|
206
211
|
createListFeaturedBlueprintsHandler(deps.blueprints ? { blueprints: deps.blueprints } : {}),
|
|
207
212
|
// Spec / discovery handlers — zero-deps. These may be tagged
|
|
@@ -272,6 +277,12 @@ export function defaultHandlers(deps) {
|
|
|
272
277
|
if (deps.render) {
|
|
273
278
|
handlers.push(createGguiSyncContextHandler({
|
|
274
279
|
renderStore: deps.render.renderStore,
|
|
280
|
+
// Same store `ggui_render` writes identity records to — a
|
|
281
|
+
// snapshot sync re-commits the row, so the record's view of it
|
|
282
|
+
// is refreshed off the same seam.
|
|
283
|
+
...(deps.render.renderIdentityStore
|
|
284
|
+
? { renderIdentityStore: deps.render.renderIdentityStore }
|
|
285
|
+
: {}),
|
|
275
286
|
}));
|
|
276
287
|
// `ggui_runtime_refresh_ws_token` — G14 (2026-05-23) signed-
|
|
277
288
|
// envelope refresh tool. Registered only when a refresh seam is
|
|
@@ -345,6 +356,9 @@ export function defaultHandlers(deps) {
|
|
|
345
356
|
if (deps.update) {
|
|
346
357
|
handlers.push(createGguiUpdateHandler({
|
|
347
358
|
renderStore: deps.update.renderStore,
|
|
359
|
+
...(deps.update.renderIdentityStore
|
|
360
|
+
? { renderIdentityStore: deps.update.renderIdentityStore }
|
|
361
|
+
: {}),
|
|
348
362
|
...(deps.update.propsUpdateNotifier
|
|
349
363
|
? { propsUpdateNotifier: deps.update.propsUpdateNotifier }
|
|
350
364
|
: {}),
|
|
@@ -486,6 +500,9 @@ export function defaultHandlers(deps) {
|
|
|
486
500
|
if (deps.render) {
|
|
487
501
|
handlers.push(createGguiRenderHandler({
|
|
488
502
|
renderStore: deps.render.renderStore,
|
|
503
|
+
...(deps.render.renderTtlMs !== undefined
|
|
504
|
+
? { renderTtlMs: deps.render.renderTtlMs }
|
|
505
|
+
: {}),
|
|
489
506
|
// Plugin slice Commit 3 — render reads App.gadgets to
|
|
490
507
|
// gate `clientCapabilities.gadgets[*].hook` references via
|
|
491
508
|
// `assertGadgetsRegistered`. Same instance the
|
|
@@ -505,6 +522,9 @@ export function defaultHandlers(deps) {
|
|
|
505
522
|
: {}),
|
|
506
523
|
...(deps.render.rateLimiter ? { rateLimiter: deps.render.rateLimiter } : {}),
|
|
507
524
|
...(deps.render.shortCodeIndex ? { shortCodeIndex: deps.render.shortCodeIndex } : {}),
|
|
525
|
+
...(deps.render.renderIdentityStore
|
|
526
|
+
? { renderIdentityStore: deps.render.renderIdentityStore }
|
|
527
|
+
: {}),
|
|
508
528
|
...(deps.render.provisionalPreview
|
|
509
529
|
? { provisionalPreview: deps.render.provisionalPreview }
|
|
510
530
|
: {}),
|
|
@@ -524,9 +544,11 @@ export function defaultHandlers(deps) {
|
|
|
524
544
|
? { checkRenderContracts: deps.render.checkRenderContracts }
|
|
525
545
|
: {}),
|
|
526
546
|
// Content-addressable code store. Both fields are forwarded
|
|
527
|
-
// together; the render handler
|
|
528
|
-
//
|
|
529
|
-
//
|
|
547
|
+
// together; the render handler requires both to emit
|
|
548
|
+
// `codeUrl`. Absent or partial = no cache-addressable static
|
|
549
|
+
// channel; the bootstrap still carries the inline `codeB64`
|
|
550
|
+
// twin for under-cap renders and mounts through the live trio
|
|
551
|
+
// where minted (see render.ts handler body).
|
|
530
552
|
...(deps.render.codeStore && deps.render.codeBaseUrl
|
|
531
553
|
? {
|
|
532
554
|
codeStore: deps.render.codeStore,
|
|
@@ -613,7 +635,7 @@ export function buildOpsBundleHandlers(deps) {
|
|
|
613
635
|
const handlers = [];
|
|
614
636
|
if (deps.opsApps) {
|
|
615
637
|
const { apps, userDefaultApp } = deps.opsApps;
|
|
616
|
-
handlers.push(createListAppsHandler({ apps }), createCreateAppHandler({ apps }), createUpdateAppHandler({ apps }), createSetAppThemeHandler({ apps }), createDeleteAppHandler({ apps }), createSetDefaultAppHandler({ apps, userDefaultApp }));
|
|
638
|
+
handlers.push(createListAppsHandler({ apps }), createCreateAppHandler({ apps }), createUpdateAppHandler({ apps }), createSetAppThemeHandler({ apps }), createDeleteAppHandler({ apps, userDefaultApp }), createSetDefaultAppHandler({ apps, userDefaultApp }));
|
|
617
639
|
}
|
|
618
640
|
if (deps.opsOrgs) {
|
|
619
641
|
const { orgs, invites } = deps.opsOrgs;
|
|
@@ -752,7 +774,47 @@ export function createGguiServer(opts = {}) {
|
|
|
752
774
|
const runtimeBundleFile = runtimeConfig.distDir !== undefined
|
|
753
775
|
? path.join(runtimeConfig.distDir, "iframe-runtime.js")
|
|
754
776
|
: RUNTIME_BUNDLE_FILE;
|
|
755
|
-
|
|
777
|
+
// Bundle bytes, captured ONCE at composition. Feed three consumers:
|
|
778
|
+
// the content hash below, the immutable hashed route (which must
|
|
779
|
+
// serve exactly the bytes its name hashes — see runtime-bundle-
|
|
780
|
+
// route.ts), and the opt-in inline-runtime shell. Unreadable file ⇒
|
|
781
|
+
// undefined ⇒ every consumer falls back to its pre-#472 behavior.
|
|
782
|
+
let runtimeBundleBytes;
|
|
783
|
+
if (runtimeEnabled) {
|
|
784
|
+
try {
|
|
785
|
+
runtimeBundleBytes = fs.readFileSync(runtimeBundleFile);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
// The plain route's own existsSync handles the missing-bundle
|
|
789
|
+
// warning; nothing extra to say here.
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Content-hashed runtime URL (#472). The stamped `runtimeUrl` (and
|
|
793
|
+
// the hashed route it points at) embeds sha256[0..12) of the bytes:
|
|
794
|
+
// clients cache one download per content version with zero
|
|
795
|
+
// revalidations, and a deploy rolls over by changing the URL. The
|
|
796
|
+
// rewrite applies to a configured absolute `runtime.url` ONLY when
|
|
797
|
+
// its filename matches the served bundle's route (a CDN fronting
|
|
798
|
+
// THIS server still resolves the hashed name at its origin); a URL
|
|
799
|
+
// pointing at a foreign copy is left untouched, and
|
|
800
|
+
// `runtime.hashedUrl: false` opts out entirely.
|
|
801
|
+
const runtimeBundleHash = runtimeBundleBytes !== undefined && runtimeConfig.hashedUrl !== false
|
|
802
|
+
? createHash("sha256").update(runtimeBundleBytes).digest("hex").slice(0, 12)
|
|
803
|
+
: undefined;
|
|
804
|
+
const insertHash = (urlOrPath) => {
|
|
805
|
+
if (runtimeBundleHash === undefined)
|
|
806
|
+
return urlOrPath;
|
|
807
|
+
const plainName = runtimePath.slice(runtimePath.lastIndexOf("/") + 1);
|
|
808
|
+
const dot = plainName.lastIndexOf(".");
|
|
809
|
+
const hashedName = dot === -1
|
|
810
|
+
? `${plainName}.${runtimeBundleHash}`
|
|
811
|
+
: `${plainName.slice(0, dot)}.${runtimeBundleHash}${plainName.slice(dot)}`;
|
|
812
|
+
if (!urlOrPath.endsWith(`/${plainName}`) && urlOrPath !== plainName)
|
|
813
|
+
return urlOrPath;
|
|
814
|
+
return `${urlOrPath.slice(0, urlOrPath.length - plainName.length)}${hashedName}`;
|
|
815
|
+
};
|
|
816
|
+
const hashedRuntimePath = runtimeBundleHash !== undefined ? insertHash(runtimePath) : undefined;
|
|
817
|
+
const runtimeBootstrapUrl = insertHash(runtimeConfig.url ?? runtimePath);
|
|
756
818
|
// Lazy resolver: each render/update handler invocation looks up the
|
|
757
819
|
// request-context-derived absolute base inside the request scope
|
|
758
820
|
// (via AsyncLocalStorage). Static `publicBaseUrl` wins when set;
|
|
@@ -899,7 +961,19 @@ export function createGguiServer(opts = {}) {
|
|
|
899
961
|
const generationWithCache = opts.generation
|
|
900
962
|
? {
|
|
901
963
|
...opts.generation,
|
|
902
|
-
cache: opts.generation.cache ??
|
|
964
|
+
cache: opts.generation.cache ??
|
|
965
|
+
{
|
|
966
|
+
embedding,
|
|
967
|
+
vectorStore: vectors,
|
|
968
|
+
index,
|
|
969
|
+
// Durable write-through rides the cache bundle because
|
|
970
|
+
// registration is where it fires. Omitted entirely when
|
|
971
|
+
// unwired so the bundle stays byte-identical to before on
|
|
972
|
+
// deployments that bound no durable store.
|
|
973
|
+
...(opts.durableBlueprints !== undefined
|
|
974
|
+
? { durability: opts.durableBlueprints }
|
|
975
|
+
: {}),
|
|
976
|
+
},
|
|
903
977
|
// Thread the shared/seed pools into the generation deps so the
|
|
904
978
|
// render handler's §6 reuse point-read can fall back to them on a
|
|
905
979
|
// per-app miss. Same `opts.seedPools` fed to the negotiator below;
|
|
@@ -1155,6 +1229,7 @@ export function createGguiServer(opts = {}) {
|
|
|
1155
1229
|
// Registers `ggui_runtime_declare_tool_catalog`.
|
|
1156
1230
|
toolIdentityCatalogStore,
|
|
1157
1231
|
...(opts.blueprintProvider ? { blueprints: opts.blueprintProvider } : {}),
|
|
1232
|
+
...(opts.registrySearch ? { registrySearch: opts.registrySearch } : {}),
|
|
1158
1233
|
// UI registry for `ggui_render_blueprint`. Absent = render tool
|
|
1159
1234
|
// is NOT registered on this server (defaultHandlers' own opt-in
|
|
1160
1235
|
// rule). OSS CLI wires a `LocalUiRegistry` here so manifest
|
|
@@ -1219,6 +1294,7 @@ export function createGguiServer(opts = {}) {
|
|
|
1219
1294
|
? {
|
|
1220
1295
|
render: {
|
|
1221
1296
|
renderStore,
|
|
1297
|
+
...(opts.renderTtlMs !== undefined ? { renderTtlMs: opts.renderTtlMs } : {}),
|
|
1222
1298
|
...(mintBootstrap ? { mintBootstrap } : {}),
|
|
1223
1299
|
// G14 (2026-05-23) refresh seam. Same `channelBootstrap`
|
|
1224
1300
|
// the WS upgrade path uses — sharing it means one HMAC
|
|
@@ -1267,6 +1343,12 @@ export function createGguiServer(opts = {}) {
|
|
|
1267
1343
|
// index. Absent = hosted cloud (DDB side-table) / no
|
|
1268
1344
|
// console consumer.
|
|
1269
1345
|
...(opts.shortCodeIndex ? { shortCodeIndex: opts.shortCodeIndex } : {}),
|
|
1346
|
+
// Durable render-identity side record. Same opt-in
|
|
1347
|
+
// posture: absent = no records written, every read path
|
|
1348
|
+
// unchanged.
|
|
1349
|
+
...(opts.renderIdentityStore
|
|
1350
|
+
? { renderIdentityStore: opts.renderIdentityStore }
|
|
1351
|
+
: {}),
|
|
1270
1352
|
// Provisional A2UI preview. When `opts.provisionalPreview.enabled`
|
|
1271
1353
|
// flipped on above, the deps object owns
|
|
1272
1354
|
// `sendEnvelope` (late-bound to the live channel) +
|
|
@@ -1339,9 +1421,11 @@ export function createGguiServer(opts = {}) {
|
|
|
1339
1421
|
// handler along with the base
|
|
1340
1422
|
// URL the code-blob route resolves to. We prefer the
|
|
1341
1423
|
// explicit `--public-base-url` (so the URL is reachable
|
|
1342
|
-
// from
|
|
1343
|
-
// back to "no codeUrl emission" — the
|
|
1344
|
-
//
|
|
1424
|
+
// from a remote host's iframe sandbox); when absent we
|
|
1425
|
+
// fall back to "no codeUrl emission" — the iframe then
|
|
1426
|
+
// mounts through the live trio and the WS subscribe
|
|
1427
|
+
// carries the render body, which is the delivery path a
|
|
1428
|
+
// render-channel deployment already has.
|
|
1345
1429
|
...(opts.codeStore && opts.publicBaseUrl
|
|
1346
1430
|
? {
|
|
1347
1431
|
codeStore: opts.codeStore,
|
|
@@ -1413,6 +1497,11 @@ export function createGguiServer(opts = {}) {
|
|
|
1413
1497
|
? {
|
|
1414
1498
|
update: {
|
|
1415
1499
|
renderStore,
|
|
1500
|
+
// Same instance the render deps got — one record per
|
|
1501
|
+
// render, written by render and refreshed by update.
|
|
1502
|
+
...(opts.renderIdentityStore
|
|
1503
|
+
? { renderIdentityStore: opts.renderIdentityStore }
|
|
1504
|
+
: {}),
|
|
1416
1505
|
propsUpdateNotifier: {
|
|
1417
1506
|
sendPropsUpdate: async (sessionId, props) => {
|
|
1418
1507
|
if (!channelForHealth)
|
|
@@ -1552,6 +1641,45 @@ export function createGguiServer(opts = {}) {
|
|
|
1552
1641
|
// (render/update resultMeta.runtimeUrl) can adapt to the tunnel host.
|
|
1553
1642
|
// Trust gate lives inside the middleware — see request-context.ts.
|
|
1554
1643
|
app.use(buildRequestContextMiddleware());
|
|
1644
|
+
// DNS-rebinding defense (ggui#438a; Streamable HTTP spec §Security
|
|
1645
|
+
// Warning). Mounted BEFORE the body parsers so a rejected request
|
|
1646
|
+
// never allocates a parsed body, and before auth so the verdict does
|
|
1647
|
+
// not depend on credentials. Reads RAW headers — see the module
|
|
1648
|
+
// docstring for why the request-context-derived host is unsafe here.
|
|
1649
|
+
//
|
|
1650
|
+
// Origin enforcement is scoped to the MCP wire; the Host check is
|
|
1651
|
+
// global. NOTE: per-app routing WITHOUT a pathPrefix mounts at
|
|
1652
|
+
// `/:appId` — no distinguishing prefix exists, so those requests get
|
|
1653
|
+
// the Host check only (which is the actual rebinding defense);
|
|
1654
|
+
// enforcing Origin app-wide would 403 the public cross-origin read
|
|
1655
|
+
// surfaces (runtime-bundle, /code, /api/renders).
|
|
1656
|
+
const mcpOriginEnforcedPrefixes = [
|
|
1657
|
+
opts.universalMcpPath ?? "/mcp",
|
|
1658
|
+
CONTROL_PATH,
|
|
1659
|
+
...mcpServices.map((svc) => svc.path),
|
|
1660
|
+
...(opts.perAppRouting?.pathPrefix !== undefined ? [opts.perAppRouting.pathPrefix] : []),
|
|
1661
|
+
];
|
|
1662
|
+
const originHostPolicy = buildOriginHostPolicy({
|
|
1663
|
+
bindHost: opts.host ?? "127.0.0.1",
|
|
1664
|
+
...(opts.publicBaseUrl !== undefined ? { publicBaseUrl: opts.publicBaseUrl } : {}),
|
|
1665
|
+
...(opts.browserOrigins !== undefined ? { browserOrigins: opts.browserOrigins } : {}),
|
|
1666
|
+
});
|
|
1667
|
+
app.use(createOriginHostValidationMiddleware({
|
|
1668
|
+
policy: originHostPolicy,
|
|
1669
|
+
enforceOriginPathPrefixes: mcpOriginEnforcedPrefixes,
|
|
1670
|
+
logger: logger.child({ middleware: "origin-validation" }),
|
|
1671
|
+
}));
|
|
1672
|
+
// Browser CORS (ggui#438b). Mounted app-wide and BEFORE auth:
|
|
1673
|
+
// preflights carry no Authorization by spec, so an auth-gated OPTIONS
|
|
1674
|
+
// would kill every authenticated browser session. One policy object
|
|
1675
|
+
// covers every MCP surface — universal /mcp, the per-app route,
|
|
1676
|
+
// /control, isolated services — and the OAuth discovery + token +
|
|
1677
|
+
// registration routes a browser client must fetch to authenticate.
|
|
1678
|
+
// Error responses (401/403/405) inherit the headers because the layer
|
|
1679
|
+
// runs before the handlers that produce them. Routes that set their
|
|
1680
|
+
// own ACAO (runtime-bundle, /code, /api/renders — all `*`) win: they
|
|
1681
|
+
// setHeader later, which replaces this layer's value.
|
|
1682
|
+
app.use(createBrowserCorsMiddleware({ policy: originHostPolicy }));
|
|
1555
1683
|
app.use(express.json({ limit: bodyLimit }));
|
|
1556
1684
|
// Form-urlencoded body parser for /oauth/token (RFC 6749 §4.1.3
|
|
1557
1685
|
// requires application/x-www-form-urlencoded). JSON bodies still
|
|
@@ -1697,16 +1825,42 @@ export function createGguiServer(opts = {}) {
|
|
|
1697
1825
|
oauthEnabled,
|
|
1698
1826
|
...(oauthConfig.issuerUrl !== undefined ? { oauthIssuerUrl: oauthConfig.issuerUrl } : {}),
|
|
1699
1827
|
logger,
|
|
1828
|
+
corsOrigins: originHostPolicy.allowedOrigins,
|
|
1700
1829
|
});
|
|
1830
|
+
// Inline-runtime shell (opt-in): embed the runtime bundle's source
|
|
1831
|
+
// into the static-resource shell so fetch-blocked hosts (iframe CSP
|
|
1832
|
+
// without external script-src) can still boot. Resolved ONCE at
|
|
1833
|
+
// composition time from the same bundle file the runtime mount
|
|
1834
|
+
// serves. An explicit `shellHtml` wins; a read failure warns and
|
|
1835
|
+
// falls back to the thin shell (the server still works everywhere
|
|
1836
|
+
// the thin shell worked).
|
|
1837
|
+
let inlineShellHtml;
|
|
1838
|
+
if (mcpAppsEnabled && mcpAppsConfig.inlineRuntimeShell === true) {
|
|
1839
|
+
if (runtimeBundleBytes !== undefined) {
|
|
1840
|
+
inlineShellHtml = buildInlineRenderShellHtml(runtimeBundleBytes.toString("utf8"));
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
// inlineRuntimeShell is on but the runtime bundle could not be
|
|
1844
|
+
// read at composition — serve the thin postMessage shell instead.
|
|
1845
|
+
logger.warn("mcp_apps_inline_shell_bundle_unreadable", {
|
|
1846
|
+
runtimeBundleFile,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1701
1850
|
// Per-boot `buildMcpServer` options — every input below is fixed at
|
|
1702
1851
|
// composition time, so the bundle is assembled once and the MCP
|
|
1703
1852
|
// endpoint family spreads a fresh copy per request.
|
|
1704
1853
|
const buildMcpOptions = {
|
|
1705
1854
|
mcpAppsOutbound: mcpAppsEnabled,
|
|
1706
|
-
// Caller-provided `shellHtml` overrides the default
|
|
1707
|
-
// `installMcpAppsOutbound` falls back to
|
|
1708
|
-
// `GGUI_RENDER_SHELL_HTML` constant when
|
|
1709
|
-
|
|
1855
|
+
// Caller-provided `shellHtml` overrides the default (and the
|
|
1856
|
+
// inline-runtime shell); `installMcpAppsOutbound` falls back to
|
|
1857
|
+
// its baked `GGUI_RENDER_SHELL_HTML` constant when both are
|
|
1858
|
+
// absent.
|
|
1859
|
+
...(mcpAppsConfig.shellHtml !== undefined
|
|
1860
|
+
? { shellHtml: mcpAppsConfig.shellHtml }
|
|
1861
|
+
: inlineShellHtml !== undefined
|
|
1862
|
+
? { shellHtml: inlineShellHtml }
|
|
1863
|
+
: {}),
|
|
1710
1864
|
// Forward the operator-supplied public origin so the static
|
|
1711
1865
|
// `ui://ggui/render` resource declares `_meta.ui.csp` for
|
|
1712
1866
|
// spec-compliant hosts (Claude Desktop / claude.ai Connector /
|
|
@@ -1740,14 +1894,21 @@ export function createGguiServer(opts = {}) {
|
|
|
1740
1894
|
: {}),
|
|
1741
1895
|
// Resume contract — registry-only fallback. Wired
|
|
1742
1896
|
// when the blueprint vector store is available so the
|
|
1743
|
-
// resource handler can
|
|
1744
|
-
//
|
|
1745
|
-
// the
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
// undefined
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1897
|
+
// resource handler can rehydrate a render-evicted
|
|
1898
|
+
// locator from the registered blueprint instead of
|
|
1899
|
+
// failing the read. `defaultAppIdFallback` bounds the
|
|
1900
|
+
// registry lookup to the OSS single-tenant identity.
|
|
1901
|
+
//
|
|
1902
|
+
// Leaving it undefined does not soften the response —
|
|
1903
|
+
// there is no placeholder shell to fall back to any more.
|
|
1904
|
+
// It removes the fallback, and reads it would have served
|
|
1905
|
+
// fail typed instead. The reason to leave it undefined is
|
|
1906
|
+
// that the lookup answers "a blueprint with this key
|
|
1907
|
+
// exists under this scope" to whoever asks, which a
|
|
1908
|
+
// deployment serving several tenants from one scope may
|
|
1909
|
+
// not want, and which it could not scope correctly anyway
|
|
1910
|
+
// (a missing render carries no way to derive whose it
|
|
1911
|
+
// was).
|
|
1751
1912
|
...(vectors
|
|
1752
1913
|
? {
|
|
1753
1914
|
vectorStore: vectors,
|
|
@@ -1760,9 +1921,15 @@ export function createGguiServer(opts = {}) {
|
|
|
1760
1921
|
}
|
|
1761
1922
|
: {}),
|
|
1762
1923
|
// T3-1 (2026-05-13) — content-addressable code delivery
|
|
1763
|
-
// for the MCP-resource shell.
|
|
1764
|
-
//
|
|
1765
|
-
//
|
|
1924
|
+
// for the MCP-resource shell. One of the handler's two
|
|
1925
|
+
// delivery channels: with these it inlines a `codeUrl` the
|
|
1926
|
+
// iframe-runtime fetches, and `mintWsToken` below is the
|
|
1927
|
+
// other. A compiled component needs at least one, and a
|
|
1928
|
+
// read that resolves a render this server can deliver by
|
|
1929
|
+
// neither fails NOT_MOUNTABLE rather than returning a
|
|
1930
|
+
// shell that would never paint. Wiring both (the shape
|
|
1931
|
+
// this factory produces) also means a fault on one
|
|
1932
|
+
// degrades to the other instead of failing the read.
|
|
1766
1933
|
...(opts.codeStore && opts.publicBaseUrl
|
|
1767
1934
|
? {
|
|
1768
1935
|
codeStore: opts.codeStore,
|
|
@@ -1786,6 +1953,37 @@ export function createGguiServer(opts = {}) {
|
|
|
1786
1953
|
// diagnosed live on 2026-05-18 against the cloudflared
|
|
1787
1954
|
// tunnel.
|
|
1788
1955
|
...(opts.publicBaseUrl !== undefined ? { publicBaseUrl: opts.publicBaseUrl } : {}),
|
|
1956
|
+
// #430 slice 3 — the durable substrate, forwarded to the
|
|
1957
|
+
// READ side. Both stores are single top-level options with
|
|
1958
|
+
// two consumers: the write paths above spend them keeping
|
|
1959
|
+
// a record, and this is where the record is spent. A
|
|
1960
|
+
// factory that accepts them and forwards neither here
|
|
1961
|
+
// boots, compiles, and answers every evicted locator as
|
|
1962
|
+
// though the operator had wired nothing — which is what a
|
|
1963
|
+
// record-less server is supposed to do, so the regression
|
|
1964
|
+
// is invisible in the response.
|
|
1965
|
+
//
|
|
1966
|
+
// Forwarded independently, never as a pair: the template's
|
|
1967
|
+
// own accessor requires all three stores before it will
|
|
1968
|
+
// consult any of them, so a half-wired deployment is
|
|
1969
|
+
// already handled there, in one place.
|
|
1970
|
+
...(opts.renderIdentityStore
|
|
1971
|
+
? { renderIdentityStore: opts.renderIdentityStore }
|
|
1972
|
+
: {}),
|
|
1973
|
+
...(opts.durableBlueprints !== undefined
|
|
1974
|
+
? { durableBlueprints: opts.durableBlueprints }
|
|
1975
|
+
: {}),
|
|
1976
|
+
// Same retention knob `ggui_render` stamps new renders
|
|
1977
|
+
// with. The read path spends it on the row a re-mint
|
|
1978
|
+
// commits and on an expired-but-readable row it puts back
|
|
1979
|
+
// into service; unforwarded, both silently fall back to an
|
|
1980
|
+
// hour on a deployment whose renders live far longer.
|
|
1981
|
+
...(opts.renderTtlMs !== undefined ? { renderTtlMs: opts.renderTtlMs } : {}),
|
|
1982
|
+
// #457 — the resurrection cap rides with the retention
|
|
1983
|
+
// knob; both surfaces it bounds live in the read path.
|
|
1984
|
+
...(opts.maxRenderLifetimeMs !== undefined
|
|
1985
|
+
? { maxRenderLifetimeMs: opts.maxRenderLifetimeMs }
|
|
1986
|
+
: {}),
|
|
1789
1987
|
// Live-channel wsToken minter — when wired, every
|
|
1790
1988
|
// per-render resource shell embeds `{wsUrl, wsToken}`
|
|
1791
1989
|
// so the iframe-runtime opens a WebSocket on mount and
|
|
@@ -1836,7 +2034,15 @@ export function createGguiServer(opts = {}) {
|
|
|
1836
2034
|
// `runtime.url`. The bootstrap still carries a `runtimeUrl` so
|
|
1837
2035
|
// the shell knows where to look — just not our HTTP listener.
|
|
1838
2036
|
if (runtimeEnabled) {
|
|
1839
|
-
mountRuntimeBundleRoute({
|
|
2037
|
+
mountRuntimeBundleRoute({
|
|
2038
|
+
app,
|
|
2039
|
+
runtimePath,
|
|
2040
|
+
runtimeBundleFile,
|
|
2041
|
+
logger,
|
|
2042
|
+
...(hashedRuntimePath !== undefined && runtimeBundleBytes !== undefined
|
|
2043
|
+
? { hashed: { path: hashedRuntimePath, source: runtimeBundleBytes } }
|
|
2044
|
+
: {}),
|
|
2045
|
+
});
|
|
1840
2046
|
}
|
|
1841
2047
|
// R6 /state snapshot + R7 /events cursor-replay reads — see
|
|
1842
2048
|
// `./api-renders-routes.ts` for the wsToken auth posture, tenancy
|
|
@@ -2567,6 +2773,18 @@ export function createGguiServer(opts = {}) {
|
|
|
2567
2773
|
? { extraReservedValidators: composedReservedValidators }
|
|
2568
2774
|
: {}),
|
|
2569
2775
|
...(opts.versionPolicy !== undefined ? { versionPolicy: opts.versionPolicy } : {}),
|
|
2776
|
+
// Pre-subscribe caps (ggui#444). Forwarded only when set — the
|
|
2777
|
+
// channel resolves its own generous defaults otherwise.
|
|
2778
|
+
...(opts.wsMaxPayloadBytes !== undefined ? { maxPayloadBytes: opts.wsMaxPayloadBytes } : {}),
|
|
2779
|
+
...(opts.wsMaxPreSubscribePayloadBytes !== undefined
|
|
2780
|
+
? { maxPreSubscribePayloadBytes: opts.wsMaxPreSubscribePayloadBytes }
|
|
2781
|
+
: {}),
|
|
2782
|
+
...(opts.wsPreSubscribeIdleMs !== undefined
|
|
2783
|
+
? { preSubscribeIdleMs: opts.wsPreSubscribeIdleMs }
|
|
2784
|
+
: {}),
|
|
2785
|
+
...(opts.wsMaxPreSubscribeConnections !== undefined
|
|
2786
|
+
? { maxPreSubscribeConnections: opts.wsMaxPreSubscribeConnections }
|
|
2787
|
+
: {}),
|
|
2570
2788
|
...(opts.onFirstSubscriber ? { onFirstSubscriber: opts.onFirstSubscriber } : {}),
|
|
2571
2789
|
...(opts.onLastSubscriberGone ? { onLastSubscriberGone: opts.onLastSubscriberGone } : {}),
|
|
2572
2790
|
// Shared TelemetrySink — bound once at composition so future
|
|
@@ -2658,7 +2876,24 @@ export function createGguiServer(opts = {}) {
|
|
|
2658
2876
|
blueprintStore,
|
|
2659
2877
|
blueprintSelector,
|
|
2660
2878
|
blueprintSearch,
|
|
2661
|
-
async listen(port = 0, host = "127.0.0.1") {
|
|
2879
|
+
async listen(port = 0, host = opts.host ?? "127.0.0.1") {
|
|
2880
|
+
// The Origin/Host validation policy (ggui#438a) was built above
|
|
2881
|
+
// from `opts.host ?? "127.0.0.1"` — the declared bind. A caller
|
|
2882
|
+
// that passes an explicit, DIFFERENT host here binds somewhere
|
|
2883
|
+
// the policy doesn't know about (e.g. a loopback-believing
|
|
2884
|
+
// policy fronting a wide-open listener), silently reintroducing
|
|
2885
|
+
// the rebinding gap the policy exists to close. Not thrown —
|
|
2886
|
+
// some embedders call `listen()` directly without threading
|
|
2887
|
+
// `CreateGguiServerOptions.host` — but always logged loudly so
|
|
2888
|
+
// the divergence is visible in ops.
|
|
2889
|
+
const declaredHost = opts.host ?? "127.0.0.1";
|
|
2890
|
+
if (host !== declaredHost) {
|
|
2891
|
+
logger.error("listen_host_policy_mismatch", {
|
|
2892
|
+
declaredHost,
|
|
2893
|
+
listenHost: host,
|
|
2894
|
+
hint: "Pass the same host via CreateGguiServerOptions.host — the Origin/Host validation policy was built from it and now describes a different bind.",
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2662
2897
|
return new Promise((resolve, reject) => {
|
|
2663
2898
|
const server = app.listen(port, host, () => {
|
|
2664
2899
|
const addr = server.address();
|
|
@@ -2683,7 +2918,36 @@ export function createGguiServer(opts = {}) {
|
|
|
2683
2918
|
// other paths (or a future second WS endpoint) are rejected.
|
|
2684
2919
|
if (channel) {
|
|
2685
2920
|
server.on("upgrade", (req, socket, head) => {
|
|
2921
|
+
// Second ingress (ggui#438a): upgrade requests never
|
|
2922
|
+
// traverse Express middleware, so the same validator runs
|
|
2923
|
+
// here explicitly. The channel is MCP-plane — Origin is
|
|
2924
|
+
// always enforced. WebSocket handshakes are exempt from
|
|
2925
|
+
// the same-origin policy (any page can open a socket), so
|
|
2926
|
+
// an unchecked upgrade is a cross-site WS hijack surface.
|
|
2927
|
+
//
|
|
2928
|
+
// Opaque-origin ("null") admission is narrowed to sockets
|
|
2929
|
+
// that declare bootstrap intent via `?wsToken=` on the
|
|
2930
|
+
// upgrade URL — the srcdoc-bootstrap topology it exists to
|
|
2931
|
+
// serve. A bare opaque-origin socket with no bootstrap
|
|
2932
|
+
// intent is validated as the ordinary "http" ingress and
|
|
2933
|
+
// gets the standard rejection. Admission here is NOT
|
|
2934
|
+
// authentication: the subscribe-time handler
|
|
2935
|
+
// (`ggui-session-channel/subscribe.ts`) still rejects an
|
|
2936
|
+
// admitted socket outright unless it goes on to present a
|
|
2937
|
+
// verifying credential on the `subscribe` message itself.
|
|
2686
2938
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2939
|
+
const bootstrapIntentDeclared = url.searchParams.has("wsToken");
|
|
2940
|
+
const wsRejection = validateOriginHost(req.headers.host, typeof req.headers.origin === "string" ? req.headers.origin : undefined, originHostPolicy, bootstrapIntentDeclared ? "ws-upgrade" : "http");
|
|
2941
|
+
if (wsRejection !== null) {
|
|
2942
|
+
logger.warn("origin_host_rejected", {
|
|
2943
|
+
header: wsRejection.header,
|
|
2944
|
+
value: wsRejection.value,
|
|
2945
|
+
transport: "websocket",
|
|
2946
|
+
});
|
|
2947
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n" + "Connection: close\r\n\r\n");
|
|
2948
|
+
socket.destroy();
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2687
2951
|
if (url.pathname !== channel.path) {
|
|
2688
2952
|
socket.write("HTTP/1.1 404 Not Found\r\n" + "Connection: close\r\n\r\n");
|
|
2689
2953
|
socket.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Self-hosted MCP server for the ggui protocol. An HTTP/MCP binding layer over @ggui-ai/mcp-server-handlers and the reference adapters from @ggui-ai/mcp-server-core — run it via `ggui serve`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
"resend": "^6.12.2",
|
|
33
33
|
"ws": "^8.20.1",
|
|
34
34
|
"zod": "^4.3.6",
|
|
35
|
-
"@ggui-ai/console": "0.
|
|
36
|
-
"@ggui-ai/mcp-server-
|
|
37
|
-
"@ggui-ai/negotiator": "0.
|
|
38
|
-
"@ggui-ai/
|
|
39
|
-
"@ggui-ai/
|
|
40
|
-
"@ggui-ai/preview-a2ui": "0.
|
|
41
|
-
"@ggui-ai/
|
|
42
|
-
"@ggui-ai/
|
|
43
|
-
"@ggui-ai/ui-
|
|
44
|
-
"@ggui-ai/
|
|
35
|
+
"@ggui-ai/console": "0.8.0",
|
|
36
|
+
"@ggui-ai/mcp-server-handlers": "0.8.0",
|
|
37
|
+
"@ggui-ai/negotiator": "0.8.0",
|
|
38
|
+
"@ggui-ai/mcp-server-core": "0.8.0",
|
|
39
|
+
"@ggui-ai/iframe-runtime": "0.8.0",
|
|
40
|
+
"@ggui-ai/preview-a2ui": "0.8.0",
|
|
41
|
+
"@ggui-ai/protocol": "0.8.0",
|
|
42
|
+
"@ggui-ai/project-config": "0.8.0",
|
|
43
|
+
"@ggui-ai/ui-gen": "0.8.0",
|
|
44
|
+
"@ggui-ai/ui-registry": "0.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/express": "^5.0.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/ws": "^8.5.10",
|
|
51
51
|
"typescript": "^5.0.0",
|
|
52
52
|
"vitest": "^3.2.6",
|
|
53
|
-
"@ggui-ai/protocol-conformance": "0.
|
|
53
|
+
"@ggui-ai/protocol-conformance": "0.8.0"
|
|
54
54
|
},
|
|
55
55
|
"repository": {
|
|
56
56
|
"type": "git",
|