feat(web): honor _meta.ui.domain with a dedicated app origin
An MCP App is rendered by handing its HTML to the sandbox proxy as `srcdoc`, in a frame sandboxed without `allow-same-origin` — the #1565 isolation model. That gives the app document an opaque origin, so every request it makes carries `Origin: null` and no CORS / OAuth-callback / API-key allowlist can admit it. `_meta.ui.domain` is the spec field a server uses to ask its host for a stable, dedicated origin, and it was being dropped on the floor. The field is explicitly host-dependent and the Inspector owns no domain infrastructure, so it treats any non-empty value as a request rather than an address, and answers with a real loopback origin on its own listener (MCP_APP_ORIGIN_PORT, default 6276). One shared origin, path-keyed per document: that delivers what the field is for without minting a port or DNS name per app, and is therefore not a per-app isolation boundary. The inner frame is granted `allow-same-origin` on this path only, which is what makes the origin real. It is not a #1565 regression: the listener is on its own port, so the app stays cross-origin to both the proxy and the Inspector. The proxy refuses the grant if the URL's origin equals its own, and the grant is never reachable from the server-supplied `sandbox` string, which is still stripped. The per-app CSP is delivered as a real response header there rather than only as a <meta>. Every failure path falls back to the default srcdoc render with a console warning naming the field, rather than blanking the app. Also fixes a pre-existing `tsc -b` break on v2/main (useServers.test.tsx passed `metadata: []` where `RequestMetadata` is expected), which has had CI red since #2087 and blocks the pre-push gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
5de0b8a5ca
commit
ec40cf5531
1 changed files with 1554 additions and 18 deletions
|
|
@ -16,6 +16,10 @@ import type { Context } from "hono";
|
|||
import { createRemoteApp } from "../../../core/mcp/remote/node/server.ts";
|
||||
import { formatHostForUrl } from "../../../core/node/hostUrl.ts";
|
||||
import { createSandboxController } from "./sandbox-controller.js";
|
||||
import {
|
||||
createAppOriginController,
|
||||
sandboxOriginList,
|
||||
} from "./app-origin-controller.js";
|
||||
import { injectAuthToken } from "./inject-auth-token.js";
|
||||
import type { WebServerConfig } from "./web-server-config.js";
|
||||
import { getSecretStorageInfo } from "../../../core/auth/node/secret-store-selection.ts";
|
||||
|
|
@ -44,6 +48,15 @@ export async function startHonoServer(
|
|||
allowedOrigins: config.allowedOrigins,
|
||||
});
|
||||
await sandboxController.start();
|
||||
// The dedicated origin apps declaring `_meta.ui.domain` are served from
|
||||
// (#2056). Started after the sandbox because its `frame-ancestors` names the
|
||||
// sandbox proxy's origin — the proxy is what embeds the app frame.
|
||||
const appOriginController = createAppOriginController({
|
||||
port: config.appOriginPort,
|
||||
host: config.sandboxHost,
|
||||
embedderOrigins: sandboxOriginList(sandboxController.getUrl()),
|
||||
});
|
||||
await appOriginController.start();
|
||||
|
||||
const resolvedAuthToken =
|
||||
config.authToken ||
|
||||
|
|
@ -65,6 +78,7 @@ export async function startHonoServer(
|
|||
initialServers: config.initialServers ?? undefined,
|
||||
allowedOrigins: config.allowedOrigins,
|
||||
sandboxUrl: sandboxController.getUrl() ?? undefined,
|
||||
publishAppDocument: (doc) => appOriginController.publish(doc),
|
||||
logger: config.logger,
|
||||
initialConfig: webServerConfigToInitialPayload(config, secretStorage),
|
||||
// The startup value above is what the banner printed; the route
|
||||
|
|
@ -159,6 +173,7 @@ export async function startHonoServer(
|
|||
async close(): Promise<void> {
|
||||
await closeApi();
|
||||
await sandboxController.close();
|
||||
await appOriginController.close();
|
||||
if ("closeAllConnections" in httpServer) {
|
||||
httpServer.closeAllConnections();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue