You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Opt-in OAuth HTTP exception for trusted local development hosts #1911
Thanks for the detailed writeup — the requirements you listed (off by default, exact hostname, no CIDRs, prominent warning, dev-only) are exactly the right ones to ask for. I dug into why the refusal happens, and the short answer is that the Inspector cannot implement this: the rejection is not ours, and the SDK offers no seam to relax it.
Where the refusal comes from
It's an unconditional assert inside @modelcontextprotocol/client:
/** Loopback hosts exempt from the in-transit `https:` requirement (RFC 8252 §7.3). */functionisLoopbackHost(hostname){returnhostname==="localhost"||hostname==="127.0.0.1"||hostname==="[::1]"||hostname==="::1";}/** SEP-2207: refuse to send credentials to a non-TLS, non-loopback token endpoint. */functionassertSecureTokenEndpoint(tokenEndpoint){consturl=newURL(String(tokenEndpoint));if(url.protocol!=="https:"&&!isLoopbackHost(url.hostname))thrownewInsecureTokenEndpointError(url.href);returnurl;}
host.docker.internal misses that four-value list — it isn't a loopback address, it resolves to a routable host-gateway address (typically 192.168.65.254 on Docker Desktop). The message you're seeing is:
Refusing to send credentials to non-https token endpoint '…'. OAuth token requests MUST use TLS (localhost / 127.0.0.1 / ::1 are exempt).
Three things about that assert matter here:
It takes no options. No flag, no env var, no provider hook. It runs inside executeTokenRequest — the single funnel for authorization-code exchange and refresh — before any fetch the Inspector could wrap. There is no configuration we could expose that would reach it.
The SDK goes out of its way to make it terminal.auth() special-cases this one error to rethrow rather than fall through to a fresh /authorize redirect. That's a deliberate hard stop, not an incidental validation.
The spec is behind it. MCP's authorization spec says "All authorization server endpoints MUST be served over HTTPS," incorporating OAuth 2.1 §1.5: "All the OAuth protocol URLs … MUST use the https scheme except for loopback interface redirect URIs." The single carve-out is the redirect URI, not the token endpoint.
To be precise about the last point: I'm reading intent from the implementation, not from a recorded decision — I'm not aware of an upstream proposal that was formally considered and declined. But as things stand the SDK provides no way to opt out, and the Inspector adding one would mean deliberately defeating that check from outside it, which we're not going to do.
Narrower than it may appear
Only the token endpoint is gated. The authorization endpoint has no scheme check at all, so http://host.docker.internal/authorize already works today. It's specifically the credential-carrying request that's refused.
What works right now
Make the endpoint genuinely loopback from the Inspector's point of view, rather than asking for an exception. Inside the container:
then point the token endpoint at http://127.0.0.1:8080/token. The SDK's loopback exemption then applies legitimately rather than by override, and the credential travels the same wire either way. This mirrors the approach the CLI/TUI OAuth callback docs already recommend for reaching a listener across a host boundary.
If you need to override the discovered endpoint to do that, v2 now has per-server Authorization URL and Token URL overrides in Server Settings → Authorization (#1906), which replace whatever the authorization server's metadata advertises.
Closing this
I'm closing this as not planned, because the change would have to land in the SDK before the Inspector could expose anything — there's no version of it we can build on our side.
If you'd still like to push for it, the place to file is modelcontextprotocol/typescript-sdk. The most likely-to-succeed framing, for what it's worth, is asking for assertSecureTokenEndpoint to become caller-relaxable — e.g. an allowInsecureTokenEndpointHosts?: string[] on AuthOptions — rather than asking to broaden the loopback list, since the former keeps the decision opt-in and per-caller while the latter would loosen the default for every client. Please do link back to this issue; if such an option ever lands, wiring it to a per-server field in Server Settings is a small change on our side and I'd be glad to pick it up.
Activity
cliffhall commentedon Aug 17, 2026
Thanks for the detailed writeup — the requirements you listed (off by default, exact hostname, no CIDRs, prominent warning, dev-only) are exactly the right ones to ask for. I dug into why the refusal happens, and the short answer is that the Inspector cannot implement this: the rejection is not ours, and the SDK offers no seam to relax it.
Where the refusal comes from
It's an unconditional assert inside
@modelcontextprotocol/client:host.docker.internalmisses that four-value list — it isn't a loopback address, it resolves to a routable host-gateway address (typically192.168.65.254on Docker Desktop). The message you're seeing is:Three things about that assert matter here:
executeTokenRequest— the single funnel for authorization-code exchange and refresh — before anyfetchthe Inspector could wrap. There is no configuration we could expose that would reach it.auth()special-cases this one error to rethrow rather than fall through to a fresh/authorizeredirect. That's a deliberate hard stop, not an incidental validation.httpsscheme except for loopback interface redirect URIs." The single carve-out is the redirect URI, not the token endpoint.To be precise about the last point: I'm reading intent from the implementation, not from a recorded decision — I'm not aware of an upstream proposal that was formally considered and declined. But as things stand the SDK provides no way to opt out, and the Inspector adding one would mean deliberately defeating that check from outside it, which we're not going to do.
Narrower than it may appear
Only the token endpoint is gated. The authorization endpoint has no scheme check at all, so
http://host.docker.internal/authorizealready works today. It's specifically the credential-carrying request that's refused.What works right now
Make the endpoint genuinely loopback from the Inspector's point of view, rather than asking for an exception. Inside the container:
then point the token endpoint at
http://127.0.0.1:8080/token. The SDK's loopback exemption then applies legitimately rather than by override, and the credential travels the same wire either way. This mirrors the approach the CLI/TUI OAuth callback docs already recommend for reaching a listener across a host boundary.If you need to override the discovered endpoint to do that, v2 now has per-server Authorization URL and Token URL overrides in Server Settings → Authorization (#1906), which replace whatever the authorization server's metadata advertises.
Closing this
I'm closing this as not planned, because the change would have to land in the SDK before the Inspector could expose anything — there's no version of it we can build on our side.
If you'd still like to push for it, the place to file is
modelcontextprotocol/typescript-sdk. The most likely-to-succeed framing, for what it's worth, is asking forassertSecureTokenEndpointto become caller-relaxable — e.g. anallowInsecureTokenEndpointHosts?: string[]onAuthOptions— rather than asking to broaden the loopback list, since the former keeps the decision opt-in and per-caller while the latter would loosen the default for every client. Please do link back to this issue; if such an option ever lands, wiring it to a per-server field in Server Settings is a small change on our side and I'd be glad to pick it up.fix: report a terminal OAuth token-endpoint refusal instead of a dead…