feat(web): configurable public addresses for the MCP Apps listeners (#1862)
Add MCP_SANDBOX_FULL_ADDRESS and MCP_APP_ORIGIN_FULL_ADDRESS so a reverse proxy or ingress deployment can advertise the sandbox proxy URL and the _meta.ui.domain app origin the browser actually reaches, instead of the bind-derived http://localhost:<port> ones. The process still listens on MCP_SANDBOX_PORT / MCP_APP_ORIGIN_PORT. Both values are validated in public-address.ts and refused (warned, bind- derived address kept) when malformed or when their origin collapses onto the Inspector UI's (or, for the app origin, the sandbox's) — the MCP Apps spec requires host != sandbox origin, and the single-hostname proxy layout is exactly the one that breaks it. Plain http under an https UI is warned. The sandbox proxy page's referrer check was a hardcoded http://localhost|127.0.0.1 pattern no https embedder could match. It now compares the referrer's parsed origin against an exact list injected by the controller, derived from the same filtered allow-list as its frame-ancestors header, with the old loopback check kept only as the no-list fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzuzRPJv7M8wGe3wyw5sSg Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
3e728a3b8c
commit
12544d251c
6 changed files with 737 additions and 16 deletions
|
|
@ -162,6 +162,14 @@ export interface AppOriginControllerOptions {
|
|||
* loopback, exactly as the sandbox proxy's own `frame-ancestors` does.
|
||||
*/
|
||||
embedderOrigins?: string[];
|
||||
/**
|
||||
* The origin to serve published documents under instead of the bind-derived
|
||||
* one — the operator's `MCP_APP_ORIGIN_FULL_ADDRESS`, already validated by
|
||||
* `public-address.ts` (#1862). Only adopted once the listener is up, so a
|
||||
* failed bind still makes {@link AppOriginController.publish} return null
|
||||
* and the renderer keeps its srcdoc fallback.
|
||||
*/
|
||||
publicOrigin?: string;
|
||||
}
|
||||
|
||||
/** A document handed to {@link AppOriginController.publish}. */
|
||||
|
|
@ -237,9 +245,17 @@ export function createAppOriginController(
|
|||
// Same defaulting rationale as the sandbox controller: never the *name*
|
||||
// `localhost`, which resolves to a single address family and would put this
|
||||
// listener on a different family than the web server (#1951).
|
||||
const { port, host = DEFAULT_BIND_HOST, embedderOrigins } = options;
|
||||
const {
|
||||
port,
|
||||
host = DEFAULT_BIND_HOST,
|
||||
embedderOrigins,
|
||||
publicOrigin,
|
||||
} = options;
|
||||
let server: Server | null = null;
|
||||
let origin: string | null = null;
|
||||
// Tracked separately from `origin`: a public origin carries the public port
|
||||
// (or none), not the one this listener bound.
|
||||
let boundPort = 0;
|
||||
|
||||
// Insertion-ordered, so the first key is the oldest entry.
|
||||
const documents = new Map<string, StoredDocument>();
|
||||
|
|
@ -268,7 +284,7 @@ export function createAppOriginController(
|
|||
return {
|
||||
async start(): Promise<{ port: number; url: string }> {
|
||||
if (server && origin) {
|
||||
return { port: parseInt(new URL(origin).port, 10), url: origin };
|
||||
return { port: boundPort, url: origin };
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
|
|
@ -364,7 +380,10 @@ export function createAppOriginController(
|
|||
const urlHost = isAllInterfacesHost(canonicalHost)
|
||||
? "localhost"
|
||||
: canonicalHost;
|
||||
origin = `http://${urlHost}:${actualPort}`;
|
||||
// An operator-supplied public origin (#1862) replaces the derived one
|
||||
// only now that the listener it routes to is actually up.
|
||||
origin = publicOrigin ?? `http://${urlHost}:${actualPort}`;
|
||||
boundPort = actualPort;
|
||||
settle({ port: actualPort, url: origin });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
201
clients/web/server/public-address.ts
Normal file
201
clients/web/server/public-address.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Public addresses for the MCP Apps listeners (#1862): the URL the browser is
|
||||
* told to load the sandbox proxy from, and the origin a `_meta.ui.domain` app
|
||||
* document is served from, when either differs from the address the process
|
||||
* binds.
|
||||
*
|
||||
* ## Why an override exists
|
||||
*
|
||||
* Both listeners advertise a URL built from their own bind — `http://localhost:
|
||||
* 6275/sandbox` under a wildcard bind. That is right whenever the browser shares
|
||||
* the process's view of the network (a desktop, a container published on the
|
||||
* same port numbers, an SSH tunnel), and wrong behind a reverse proxy or an
|
||||
* ingress, where the browser reaches the listener at a public hostname and
|
||||
* frequently over HTTPS. The port knobs (`MCP_SANDBOX_PORT`,
|
||||
* `MCP_APP_ORIGIN_PORT`) cannot express a scheme or a hostname, so:
|
||||
*
|
||||
* - `MCP_SANDBOX_FULL_ADDRESS` — the sandbox proxy URL, e.g.
|
||||
* `https://inspector-sandbox.example.com/sandbox`. A bare origin gets
|
||||
* `/sandbox` appended, since that is the only path the listener serves.
|
||||
* - `MCP_APP_ORIGIN_FULL_ADDRESS` — the app-origin listener's origin, e.g.
|
||||
* `https://inspector-apps.example.com`. An origin only: documents are served
|
||||
* at `<origin>/app-document/<id>`, so a path would name nothing.
|
||||
*
|
||||
* Neither changes what the process listens on; the operator routes the public
|
||||
* address to the listener.
|
||||
*
|
||||
* ## Why the values are validated rather than passed through
|
||||
*
|
||||
* The sandbox origin is an isolation boundary, not merely an address. The MCP
|
||||
* Apps spec requires host ≠ sandbox origin, and today that holds structurally —
|
||||
* the listener is on its own port of the same process, so it cannot be
|
||||
* misconfigured. An operator-supplied address makes it configurable, and the
|
||||
* most natural reverse-proxy layout (`https://inspector.example.com/sandbox`
|
||||
* routed to `:6275`) is exactly the one that collapses it. The proxy page's own
|
||||
* self-test and its #2056 `src` check both fail closed, but their symptom is a
|
||||
* blank frame and a console throw; refusing here names the cause at boot.
|
||||
*
|
||||
* So a value is **refused** — warned and ignored, falling back to the
|
||||
* bind-derived address, matching the warn-and-drop precedent `ALLOWED_ORIGINS`
|
||||
* and `MCP_SANDBOX_PORT` set — when it is not an absolute `http(s)` URL, carries
|
||||
* credentials, a query, a fragment, a wildcard, or a bracketed IPv6 literal (not
|
||||
* a valid CSP host-source, and the sandbox origin is emitted into published
|
||||
* documents' `frame-ancestors`), or when its origin equals an Inspector UI
|
||||
* origin (or, for the app origin, the sandbox's). A plain-`http` address under
|
||||
* an `https` UI is only **warned**: the browser blocks it as mixed content, but
|
||||
* `ALLOWED_ORIGINS` can legitimately list both schemes at once, so it is not
|
||||
* provably wrong.
|
||||
*
|
||||
* Not `DANGEROUSLY_`-prefixed: it widens no bind and disables no check, and the
|
||||
* configurations that would be dangerous are rejected rather than renamed.
|
||||
*/
|
||||
|
||||
export const SANDBOX_FULL_ADDRESS_ENV = "MCP_SANDBOX_FULL_ADDRESS";
|
||||
export const APP_ORIGIN_FULL_ADDRESS_ENV = "MCP_APP_ORIGIN_FULL_ADDRESS";
|
||||
|
||||
/** The only path the sandbox listener serves (see `sandbox-controller.ts`). */
|
||||
const SANDBOX_PATH = "/sandbox";
|
||||
|
||||
/** A parsed address, or the reason it was refused. */
|
||||
type Parsed = { url: URL } | { reason: string };
|
||||
|
||||
/**
|
||||
* Shape checks shared by both addresses. Returns the reason for the first
|
||||
* failure, so the warning names one concrete thing to fix.
|
||||
*/
|
||||
function parsePublicUrl(raw: string): Parsed {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
return { reason: "it is not an absolute URL" };
|
||||
}
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return { reason: "it needs an http:// or https:// scheme" };
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
return { reason: "it must not carry credentials" };
|
||||
}
|
||||
// `new URL` accepts `*` in a hostname; it is never a reachable address.
|
||||
if (url.hostname.includes("*")) {
|
||||
return { reason: "it must not contain a wildcard" };
|
||||
}
|
||||
if (url.hostname.startsWith("[")) {
|
||||
return {
|
||||
reason:
|
||||
"a bracketed IPv6 literal is not a valid CSP host-source — use a hostname or an IPv4 address",
|
||||
};
|
||||
}
|
||||
if (url.search || url.hash) {
|
||||
return { reason: "it must not carry a query string or fragment" };
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
|
||||
function warnIgnored(env: string, raw: string, reason: string): void {
|
||||
console.warn(
|
||||
`Ignoring ${env}="${raw}": ${reason}. Advertising the bind-derived address instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn when a plain-`http` address will be loaded by an `https` UI. Not a
|
||||
* refusal — see the header comment.
|
||||
*/
|
||||
function warnMixedContent(
|
||||
env: string,
|
||||
url: URL,
|
||||
allowedOrigins: readonly string[],
|
||||
): void {
|
||||
if (
|
||||
url.protocol === "http:" &&
|
||||
allowedOrigins.some((o) => o.startsWith("https:"))
|
||||
) {
|
||||
console.warn(
|
||||
`${env} is plain http (${url.origin}) but ALLOWED_ORIGINS includes an https origin: ` +
|
||||
`a browser on the https UI blocks it as mixed content and MCP Apps will not render there.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public sandbox proxy URL from {@link SANDBOX_FULL_ADDRESS_ENV}, or
|
||||
* `undefined` when it is unset, blank, or refused (warned).
|
||||
*
|
||||
* @param allowedOrigins The resolved Inspector UI origins — the sandbox must
|
||||
* not share an origin with any of them.
|
||||
*/
|
||||
export function resolveSandboxPublicUrl(
|
||||
raw: string | undefined,
|
||||
allowedOrigins: readonly string[],
|
||||
): string | undefined {
|
||||
const value = raw?.trim();
|
||||
if (!value) return undefined;
|
||||
const parsed = parsePublicUrl(value);
|
||||
if ("reason" in parsed) {
|
||||
warnIgnored(SANDBOX_FULL_ADDRESS_ENV, value, parsed.reason);
|
||||
return undefined;
|
||||
}
|
||||
const { url } = parsed;
|
||||
// A bare origin names the listener, not the page; the page is `/sandbox`.
|
||||
if (url.pathname === "/") url.pathname = SANDBOX_PATH;
|
||||
if (allowedOrigins.includes(url.origin)) {
|
||||
warnIgnored(
|
||||
SANDBOX_FULL_ADDRESS_ENV,
|
||||
value,
|
||||
`its origin ${url.origin} is also an Inspector UI origin (ALLOWED_ORIGINS), and the MCP Apps ` +
|
||||
`sandbox must be on a different origin from its host — route it to its own hostname or port`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
warnMixedContent(SANDBOX_FULL_ADDRESS_ENV, url, allowedOrigins);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public app-origin from {@link APP_ORIGIN_FULL_ADDRESS_ENV}, or
|
||||
* `undefined` when it is unset, blank, or refused (warned).
|
||||
*
|
||||
* @param allowedOrigins The resolved Inspector UI origins.
|
||||
* @param sandboxUrl The advertised sandbox URL when it is known at config time
|
||||
* (i.e. overridden). A bind-derived one is on its own port and cannot match.
|
||||
*/
|
||||
export function resolveAppOriginPublicOrigin(
|
||||
raw: string | undefined,
|
||||
allowedOrigins: readonly string[],
|
||||
sandboxUrl: string | undefined,
|
||||
): string | undefined {
|
||||
const value = raw?.trim();
|
||||
if (!value) return undefined;
|
||||
const parsed = parsePublicUrl(value);
|
||||
if ("reason" in parsed) {
|
||||
warnIgnored(APP_ORIGIN_FULL_ADDRESS_ENV, value, parsed.reason);
|
||||
return undefined;
|
||||
}
|
||||
const { url } = parsed;
|
||||
if (url.pathname !== "/") {
|
||||
warnIgnored(
|
||||
APP_ORIGIN_FULL_ADDRESS_ENV,
|
||||
value,
|
||||
"it must be an origin only, with no path — app documents are served at <origin>/app-document/<id>",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
// Same-origin with the host or the proxy is precisely what the dedicated
|
||||
// origin must never be: the inner frame is granted `allow-same-origin` on
|
||||
// this path, so sharing either origin hands the app that realm.
|
||||
const trusted = sandboxUrl
|
||||
? [...allowedOrigins, new URL(sandboxUrl).origin]
|
||||
: allowedOrigins;
|
||||
if (trusted.includes(url.origin)) {
|
||||
warnIgnored(
|
||||
APP_ORIGIN_FULL_ADDRESS_ENV,
|
||||
value,
|
||||
`its origin ${url.origin} is also the Inspector UI's or the sandbox's, and a dedicated app ` +
|
||||
`origin must differ from both — route it to its own hostname or port`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
warnMixedContent(APP_ORIGIN_FULL_ADDRESS_ENV, url, allowedOrigins);
|
||||
return url.origin;
|
||||
}
|
||||
|
|
@ -29,6 +29,14 @@ export interface SandboxControllerOptions {
|
|||
* list (only a hand-constructed caller or a test) falls back to loopback-only.
|
||||
*/
|
||||
allowedOrigins?: string[];
|
||||
/**
|
||||
* The URL to advertise instead of the bind-derived one — the operator's
|
||||
* `MCP_SANDBOX_FULL_ADDRESS`, already validated by `public-address.ts`
|
||||
* (#1862). It changes what {@link SandboxController.getUrl} returns, never
|
||||
* what is bound; and it is only advertised once the listener is actually up,
|
||||
* so a failed bind still reports no sandbox rather than a dead URL.
|
||||
*/
|
||||
publicUrl?: string;
|
||||
}
|
||||
|
||||
// Loopback fallback. NB: no `http://[::1]:*` — a **bracketed IPv6 literal is not
|
||||
|
|
@ -86,11 +94,59 @@ export function sandboxFrameAncestors(allowedOrigins?: string[]): string {
|
|||
* derive it here rather than each rolling one.
|
||||
*/
|
||||
export function frameAncestorsDirective(origins?: string[]): string {
|
||||
const valid = (origins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
|
||||
const valid = validHostSources(origins);
|
||||
const sources = valid.length > 0 ? valid : LOOPBACK_FRAME_ANCESTORS;
|
||||
return `frame-ancestors ${sources.join(" ")}`;
|
||||
}
|
||||
|
||||
/** The entries of `origins` that are well-formed CSP host-sources. */
|
||||
function validHostSources(origins?: string[]): string[] {
|
||||
return (origins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
|
||||
}
|
||||
|
||||
/**
|
||||
* The marker in `static/sandbox_proxy.html` that {@link renderSandboxProxyHtml}
|
||||
* replaces, together with the `null` it stands in front of. Matching the pair
|
||||
* means an unrendered page is still valid JavaScript, and it falls back to the
|
||||
* loopback check rather than to accepting anything.
|
||||
*/
|
||||
export const EMBEDDER_ORIGINS_PLACEHOLDER =
|
||||
"/*INSPECTOR_EMBEDDER_ORIGINS*/ null";
|
||||
|
||||
/**
|
||||
* The JavaScript literal the proxy page's referrer check reads as its exact
|
||||
* embedder allow-list (#1862).
|
||||
*
|
||||
* It is derived from the SAME filtered list as {@link sandboxFrameAncestors},
|
||||
* so the referrer check and the `frame-ancestors` header admit one set. Before
|
||||
* this the page hardcoded `http://localhost|127.0.0.1`, which no `https://`
|
||||
* referrer can match — so the proxy threw on every HTTPS deployment regardless
|
||||
* of `ALLOWED_ORIGINS`. When nothing valid remains the literal is `null`, and
|
||||
* the page falls back to that loopback check, mirroring the header's own
|
||||
* loopback fallback.
|
||||
*
|
||||
* The value lands inside a `<script>`, so `<` is escaped: `CSP_HOST_SOURCE`
|
||||
* does not exclude it, and a `</script>` inside a string literal would close
|
||||
* the element regardless of the quotes around it.
|
||||
*/
|
||||
export function embedderOriginsLiteral(origins?: string[]): string {
|
||||
const valid = validHostSources(origins);
|
||||
if (valid.length === 0) return "null";
|
||||
return JSON.stringify(valid).replace(/</g, "\\u003c");
|
||||
}
|
||||
|
||||
/** Substitute the embedder allow-list into the proxy page's source. */
|
||||
export function renderSandboxProxyHtml(
|
||||
html: string,
|
||||
origins?: string[],
|
||||
): string {
|
||||
// Function-form replacement: a string replacement would interpret `$&` and
|
||||
// friends, and the literal is operator-derived.
|
||||
return html.replace(EMBEDDER_ORIGINS_PLACEHOLDER, () =>
|
||||
embedderOriginsLiteral(origins),
|
||||
);
|
||||
}
|
||||
|
||||
export interface SandboxController {
|
||||
start(): Promise<{ port: number; url: string }>;
|
||||
close(): Promise<void>;
|
||||
|
|
@ -156,9 +212,12 @@ export function createSandboxController(
|
|||
// `localhost` — a name resolves to one address family and would reintroduce
|
||||
// the #1951 split (web on IPv4, sandbox on IPv6) for any future call site
|
||||
// that omits `host`. Both call sites pass `config.sandboxHost` today.
|
||||
const { port, host = DEFAULT_BIND_HOST, allowedOrigins } = options;
|
||||
const { port, host = DEFAULT_BIND_HOST, allowedOrigins, publicUrl } = options;
|
||||
let server: Server | null = null;
|
||||
let sandboxUrl: string | null = null;
|
||||
// Tracked separately from the URL: an advertised public URL carries the
|
||||
// public port (or none at all), not the one this listener bound.
|
||||
let boundPort = 0;
|
||||
|
||||
// Defense-in-depth for the proxy page itself. Only `frame-ancestors` is set
|
||||
// here — fetch directives (`default-src`, `connect-src`, etc.) are
|
||||
|
|
@ -175,7 +234,10 @@ export function createSandboxController(
|
|||
let sandboxHtml: string;
|
||||
try {
|
||||
const sandboxHtmlPath = join(__dirname, "../static/sandbox_proxy.html");
|
||||
sandboxHtml = readFileSync(sandboxHtmlPath, "utf-8");
|
||||
sandboxHtml = renderSandboxProxyHtml(
|
||||
readFileSync(sandboxHtmlPath, "utf-8"),
|
||||
allowedOrigins,
|
||||
);
|
||||
} catch (e) {
|
||||
sandboxHtml =
|
||||
"<!DOCTYPE html><html><body>Sandbox not loaded: " +
|
||||
|
|
@ -186,8 +248,7 @@ export function createSandboxController(
|
|||
return {
|
||||
async start(): Promise<{ port: number; url: string }> {
|
||||
if (server && sandboxUrl) {
|
||||
const p = parseInt(new URL(sandboxUrl).port, 10);
|
||||
return { port: p, url: sandboxUrl };
|
||||
return { port: boundPort, url: sandboxUrl };
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
// Guard so a `listen` error followed by a (theoretically possible)
|
||||
|
|
@ -277,7 +338,10 @@ export function createSandboxController(
|
|||
const urlHost = isAllInterfacesHost(canonicalHost)
|
||||
? "localhost"
|
||||
: canonicalHost;
|
||||
sandboxUrl = `http://${urlHost}:${actualPort}/sandbox`;
|
||||
// An operator-supplied public URL (#1862) replaces the derived one
|
||||
// only now that the listener it routes to is actually up.
|
||||
sandboxUrl = publicUrl ?? `http://${urlHost}:${actualPort}/sandbox`;
|
||||
boundPort = actualPort;
|
||||
settle({ port: actualPort, url: sandboxUrl });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export async function startHonoServer(
|
|||
port: config.sandboxPort,
|
||||
host: config.sandboxHost,
|
||||
allowedOrigins: config.allowedOrigins,
|
||||
publicUrl: config.sandboxPublicUrl,
|
||||
});
|
||||
await sandboxController.start();
|
||||
// The dedicated origin apps declaring `_meta.ui.domain` are served from
|
||||
|
|
@ -54,6 +55,7 @@ export async function startHonoServer(
|
|||
const appOriginController = createAppOriginController({
|
||||
port: config.appOriginPort,
|
||||
host: config.sandboxHost,
|
||||
publicOrigin: config.appOriginPublicOrigin,
|
||||
embedderOrigins: appDocumentEmbedders(
|
||||
sandboxController.getUrl(),
|
||||
config.allowedOrigins,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
|
|||
port: config.sandboxPort,
|
||||
host: config.sandboxHost,
|
||||
allowedOrigins: config.allowedOrigins,
|
||||
publicUrl: config.sandboxPublicUrl,
|
||||
});
|
||||
await sandboxController.start();
|
||||
// The dedicated origin apps declaring `_meta.ui.domain` are served from
|
||||
|
|
@ -77,6 +78,7 @@ export function honoMiddlewarePlugin(config: WebServerConfig): Plugin {
|
|||
const appOriginController = createAppOriginController({
|
||||
port: config.appOriginPort,
|
||||
host: config.sandboxHost,
|
||||
publicOrigin: config.appOriginPublicOrigin,
|
||||
embedderOrigins: appDocumentEmbedders(
|
||||
sandboxController.getUrl(),
|
||||
config.allowedOrigins,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import { readInspectorVersionSafe } from "../../../core/node/version.ts";
|
|||
import { resolveSandboxPort } from "./sandbox-controller.js";
|
||||
import { resolveAppOriginPort } from "./app-origin-controller.js";
|
||||
import { resolveBindHostname } from "./resolve-bind-host.js";
|
||||
import {
|
||||
APP_ORIGIN_FULL_ADDRESS_ENV,
|
||||
resolveAppOriginPublicOrigin,
|
||||
resolveSandboxPublicUrl,
|
||||
SANDBOX_FULL_ADDRESS_ENV,
|
||||
} from "./public-address.js";
|
||||
import {
|
||||
canonicalUrlHost,
|
||||
isAllInterfacesHost,
|
||||
|
|
@ -74,6 +80,17 @@ export interface WebServerConfig {
|
|||
* address the sandbox uses.
|
||||
*/
|
||||
appOriginPort: number;
|
||||
/**
|
||||
* Public sandbox proxy URL advertised in place of the bind-derived one
|
||||
* (`MCP_SANDBOX_FULL_ADDRESS`, #1862), or undefined when unset or refused.
|
||||
* See `public-address.ts` for what is refused and why.
|
||||
*/
|
||||
sandboxPublicUrl?: string;
|
||||
/**
|
||||
* Public origin published app documents are served under
|
||||
* (`MCP_APP_ORIGIN_FULL_ADDRESS`, #1862), or undefined when unset or refused.
|
||||
*/
|
||||
appOriginPublicOrigin?: string;
|
||||
logger: Logger | undefined;
|
||||
/** When true, open browser after server starts. */
|
||||
autoOpen: boolean;
|
||||
|
|
@ -460,6 +477,22 @@ export function buildWebServerConfig(
|
|||
}
|
||||
})
|
||||
.filter((o): o is string => o !== null);
|
||||
const allowedOrigins = configuredOrigins?.length
|
||||
? configuredOrigins
|
||||
: defaultAllowedOrigins(hostname, port);
|
||||
|
||||
// Resolved after the allow-list because both are checked against it: neither
|
||||
// listener may share an origin with the Inspector page (#1862). The sandbox
|
||||
// goes first so the app origin can be checked against it too.
|
||||
const sandboxPublicUrl = resolveSandboxPublicUrl(
|
||||
process.env[SANDBOX_FULL_ADDRESS_ENV],
|
||||
allowedOrigins,
|
||||
);
|
||||
const appOriginPublicOrigin = resolveAppOriginPublicOrigin(
|
||||
process.env[APP_ORIGIN_FULL_ADDRESS_ENV],
|
||||
allowedOrigins,
|
||||
sandboxPublicUrl,
|
||||
);
|
||||
|
||||
return {
|
||||
port,
|
||||
|
|
@ -471,12 +504,12 @@ export function buildWebServerConfig(
|
|||
writable,
|
||||
initialServers,
|
||||
storageDir: process.env.MCP_STORAGE_DIR,
|
||||
allowedOrigins: configuredOrigins?.length
|
||||
? configuredOrigins
|
||||
: defaultAllowedOrigins(hostname, port),
|
||||
allowedOrigins,
|
||||
sandboxPort,
|
||||
sandboxHost: hostname,
|
||||
appOriginPort,
|
||||
sandboxPublicUrl,
|
||||
appOriginPublicOrigin,
|
||||
logger,
|
||||
autoOpen: resolveAutoOpen(),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue