diff --git a/clients/web/README.md b/clients/web/README.md index 350009950..02d97a926 100644 --- a/clients/web/README.md +++ b/clients/web/README.md @@ -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, because a same-origin `GET` carries no `Origin` header and so never reaches the guard, while **every state-changing request is rejected with a 403** — browsers attach `Origin` to anything that is not a `GET`/`HEAD`. Adding and connecting a server (`POST`) are the most visible, but saving settings, reordering and deleting a server (`PUT`/`DELETE`) fail the same way. The result 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: