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:
cliffhall 2026-08-23 23:03:55 -04:00 • committed by GitHub
commit ec40cf5531
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,6 +26,7 @@ const MUTATED_ENV_KEYS = [
"MCP_STORAGE_DIR",
"ALLOWED_ORIGINS",
"MCP_SANDBOX_PORT",
"MCP_APP_ORIGIN_PORT",
"SERVER_PORT",
"MCP_LOG_FILE",
"MCP_AUTO_OPEN_ENABLED",
@ -43,6 +44,7 @@ const baseConfig = (): WebServerConfig => ({
storageDir: undefined,
allowedOrigins: ["http://localhost:6274"],
sandboxPort: 0,
appOriginPort: 0,
sandboxHost: "127.0.0.1",
logger: undefined,
autoOpen: false,
@ -301,6 +303,17 @@ describe("buildWebServerConfigFromEnv", () => {
expect(buildWebServerConfigFromEnv().sandboxPort).toBe(9100);
});
it("resolves appOriginPort from MCP_APP_ORIGIN_PORT, defaulting to 6276", () => {
// The dedicated app origin (#2056). Deliberately NOT sharing the sandbox's
// SERVER_PORT fallback: it is a different listener and must be pinnable
// (and forwardable) on its own.
delete process.env.MCP_APP_ORIGIN_PORT;
process.env.SERVER_PORT = "9100";
expect(buildWebServerConfigFromEnv().appOriginPort).toBe(6276);
process.env.MCP_APP_ORIGIN_PORT = "9300";
expect(buildWebServerConfigFromEnv().appOriginPort).toBe(9300);
});
it("sets MCP_STORAGE_DIR when present", () => {
process.env.MCP_STORAGE_DIR = "/tmp/storage";
expect(buildWebServerConfigFromEnv().storageDir).toBe("/tmp/storage");