docs: ALLOWED_ORIGINS recipe for a *.localhost proxy
Closes #2289 Running the Inspector behind a reverse proxy on a *.localhost name works today, but only with ALLOWED_ORIGINS set, and nothing said so or showed the shape. Raised by the reporter of #1944, whose whole service cluster uses those names. Three things the recipe has to carry, all learned the expensive way: - ALLOWED_ORIGINS REPLACES the default list rather than merging, so an entry of only the proxy origin silently breaks browsing at localhost:PORT. The loopback trio has to be listed too. - The failure is confusing rather than obvious. A same-origin GET carries no Origin header, so the page loads and only the POSTs that add or connect a server are rejected — which reads as a connection problem rather than a configuration one. - Only the browser resolves *.localhost for free. Chrome and Firefox map those names to loopback per RFC 6761; the OS resolver on macOS does not and Safari does not at all, so an MCP *server* on such a host still needs /etc/hosts or dnsmasq — the Node backend is what dials it — and with OAuth is refused by the SDK regardless (typescript-sdk#2591). Documented rather than changing the default: see #2282 for why ~1,100 lines of origin and CSP code was a bad trade for removing one env var. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018MvNbmAtQuWhDvzi4yXDju Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
98911df74d
commit
6650c0b525
1 changed files with 13 additions and 0 deletions
|
|
@ -394,6 +394,19 @@ Both the prod backend (`server/web-server-config.ts`) and the dev Vite server (`
|
|||
|
||||
The backend's `/api/*` routes also enforce an **origin allow-list** (`allowedOrigins`) as DNS-rebinding protection. When left to default on a loopback host, it expands to all three interchangeable loopback origin forms for the port — `http://localhost:PORT`, `http://127.0.0.1:PORT`, and `http://[::1]:PORT` — because `localhost` resolves to either IPv4 or IPv6 loopback and Node/Vite may bind the IPv6 form, so the browser can legitimately arrive at `http://[::1]:PORT`. Set `ALLOWED_ORIGINS` (comma-separated) to override; entries are canonicalized (`new URL(o).origin`), so a trailing slash / uppercase host / explicit `:80` still match. **Each entry must include the scheme** — `http://localhost:6274`, not `localhost:6274` (a scheme-less value is dropped with a warning). `ALLOWED_ORIGINS` **replaces** the default list (it does not merge), so **list every origin you'll browse from, including the loopback forms** you still want (`http://localhost:PORT`, `http://127.0.0.1:PORT`, `http://[::1]:PORT`) — otherwise local access stops working. A blank `ALLOWED_ORIGINS` does **not** disable the check — it falls back to the default (fail closed); there is no env knob to turn origin validation off.
|
||||
|
||||
**Running the Inspector behind a `*.localhost` proxy.** A common local-dev shape gives each service a friendly name — `my-api.localhost`, `my-app.localhost`, `inspector.localhost` — instead of a set of ports. The Inspector works there, but the origin allow-list does not include those names by default, so set it explicitly:
|
||||
|
||||
```sh
|
||||
ALLOWED_ORIGINS=http://inspector.localhost,http://localhost:6274,http://127.0.0.1:6274,http://[::1]:6274 \
|
||||
mcp-inspector --web
|
||||
```
|
||||
|
||||
⚠️ **List the loopback forms too, not just your proxy origin.** `ALLOWED_ORIGINS` **replaces** the default list rather than merging with it, so an entry of only `http://inspector.localhost` silently breaks browsing at `http://localhost:6274`. Add the port to the proxy origin if it is not on `:80`. The MCP Apps sandbox `frame-ancestors` is derived from the same list, so one entry covers the Apps tab as well.
|
||||
|
||||
Without this you get a confusing failure rather than an obvious one: the page loads (a same-origin `GET` carries no `Origin` header, so it never reaches the guard) and only the **POSTs** that add or connect a server are rejected with a 403 — which reads as a connection problem rather than a configuration one.
|
||||
|
||||
⚠️ **Only the browser resolves `*.localhost` for free.** Chrome and Firefox map those names to loopback internally per [RFC 6761 §6.3](https://www.rfc-editor.org/info/rfc6761/); the OS resolver on macOS does **not**, and Safari does not resolve them at all. That is fine for reaching the Inspector, but an **MCP server** URL on such a host is dialled by the Inspector's Node backend, so it still needs an `/etc/hosts` entry or dnsmasq. Note also that an MCP server on a `*.localhost` host **using OAuth** is currently refused by the SDK, which exempts only `localhost`, `127.0.0.1` and `::1` from its TLS requirement — see [typescript-sdk#2591](https://github.com/modelcontextprotocol/typescript-sdk/issues/2591).
|
||||
|
||||
### Hosting on a network
|
||||
|
||||
The guard blocks only the **wildcard** all-interfaces addresses. Binding a **specific** IP or hostname is allowed with no opt-in — that's a single, deliberate exposure, unlike the wildcard which binds every interface at once (the pattern DNS-rebinding exploits). To serve the Inspector on a LAN or the internet:
|
||||
|
|
|
|||
Loading…
Reference in a new issue