MCP OAuth: CLI does not try RFC 8414 path-insertion for authorization-server metadata; also ignores registration_endpoint

Status Open
Reported on v2.1.227
Maintainer reply None cached
Activity 0 comments · opened Aug 11, 2026

Summary

Claude Code's MCP OAuth discovery only probes the root /.well-known/oauth-authorization-server. When an MCP server is hosted under a path prefix and publishes its authorization-server metadata via the RFC 8414 path-insertion form, discovery never finds it and the connection times out.

Reproduced against a real, spec-compliant public server: Meta's MCP endpoint at https://mcp.facebook.com/devtools.

Measured with Claude Code 2.1.227 on 2026-08-11.

What is measured

| probe | result |
|---|---|
| GET https://mcp.facebook.com/.well-known/oauth-authorization-server (root) | 404 |
| GET https://mcp.facebook.com/.well-known/oauth-authorization-server/devtools (path-insertion, RFC 8414 §3.1) | 200 — full metadata, issuer=www.facebook.com, PKCE S256 |
| GET https://mcp.facebook.com/.well-known/oauth-protected-resource/devtools | 200 |

The CLI only issues the first request, gets 404, and stalls until a 30s timeout.

Three distinct problems

  1. Path-insertion not attempted. RFC 8414 §3.1 defines the path-insertion form for issuers with a path component. The server publishes it correctly; the CLI never requests it.
  2. registration_endpoint ignored. Even once metadata is available, the CLI uses a hardcoded /register instead of the registration_endpoint from the discovered document (Meta's is /.well-known/register/<path>). This is the same class as #36743, which was closed — the CLI path still exhibits it.
  3. Identity fields must align (secondary). The protected-resource metadata resource / authorization_servers values and the resource_metadata pointer in the 401 WWW-Authenticate header must all resolve consistently. Rewriting only some of them produces Failed to connect: Protected resource ... does not match expected .... Related surface: #85563.

Reproduction

A minimal stdlib-only reverse proxy (195 LOC, binds 127.0.0.1 only) that (a) serves root AS-metadata from the path-insertion form, (b) rewrites registration_endpoint, the protected-resource resource / authorization_servers, and the WWW-Authenticate pointer to the proxy origin.

With the proxy in front, the CLI advances from Failed to connect to Needs authentication — i.e. OAuth discovery completes. That isolates discovery as the failing step.

<details>
<summary>meta_mcp_localhost_sim.py (195 LOC, stdlib only, no credentials)</summary>

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
META-MCP LOCALHOST KESIF-SIMI  (is-emri master 1e8b12, 2026-08-11)

KOK (master + claude-code-guide olculdu): Meta MCP (mcp.facebook.com/devtools) SPEC-UYUMLU
(RFC-8414 PATH-INSERTION metadata 200-TAM) ama CLI 2.1.227 o formu denemiyor + bilinen bug
#36743 registration_endpoint'i yoksayip /register'i KOK-pathte hardcode'lar (Meta'ninki
non-standard .well-known/register/devtools). Sim bu iki-uyumsuzlugu localhost'ta koprular.

DORT-PARCA (is-emri):
  1. /devtools*  -> https://mcp.facebook.com/devtools*  AYNEN gecir (header dahil).
  2. /.well-known/oauth-authorization-server (KOK) -> Meta'nin PATH-INSERTION metadata'sini
     (/.well-known/oauth-authorization-server/devtools) don AMA registration_endpoint'i
     kendi simindeki /register'a cevir.
  3. /register -> https://mcp.facebook.com/.well-known/register/devtools'a gecir (#36743 hardcode).
  4. (kayit-degisimi ayri adim: profillerdeki .claude.json url -> http://127.0.0.1:<port>/devtools)

SINIRLAR (is-emri, MUTLAK):
  · YALNIZ 127.0.0.1 dinler (asla 0.0.0.0 — LAN-maruziyeti yok).
  · HICBIR header/token/deger LOGLANMAZ — yalniz "METOT YOL -> durum-kodu" satiri.
  · Tek-dosya, koddan-okunur.

Calistirma: py meta_mcp_localhost_sim.py [--port 3477]
Saglik:     GET /healthz -> 200 {"ok":true}
"""
import argparse
import json
import sys
import urllib.request
import urllib.error
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

UPSTREAM = "https://mcp.facebook.com"
# Meta'nin metadata'yi verdigi PATH-INSERTION formu (kok-path DEGIL):
META_AS_METADATA = "/.well-known/oauth-authorization-server/devtools"
META_REGISTER = "/.well-known/register/devtools"
# Istemciye/upstream'e tasinmayacak hop-by-hop basliklar (RFC-7230):
HOP = {"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
       "te", "trailers", "transfer-encoding", "upgrade", "host", "content-length"}


def _log(metot, yol, kod):
    # SIR-GUVENLIGI: yalniz metot+yol+durum. Header/token/govde ASLA.
    sys.stderr.write("%s %s -> %s\n" % (metot, yol, kod))
    sys.stderr.flush()


class Handler(BaseHTTPRequestHandler):
    server_version = "MetaMcpSim/1"
    protocol_version = "HTTP/1.1"

    def log_message(self, *a):  # stdlib'in kendi log'unu SUSTUR (URL/UA sizdirabilir)
        pass

    def _kendi_port(self):
        return self.server.server_address[1]

    def _govde_oku(self):
        try:
            n = int(self.headers.get("Content-Length") or 0)
        except ValueError:
            n = 0
        return self.rfile.read(n) if n > 0 else None

    def _yanit_yaz(self, kod, govde: bytes, basliklar):
        self.send_response(kod)
        for k, v in basliklar:
            if k.lower() in HOP or k.lower() == "content-length":
                continue
            self.send_header(k, v)
        self.send_header("Content-Length", str(len(govde)))
        self.end_headers()
        if govde:
            self.wfile.write(govde)

    def _passthrough(self, ust_yol):
        """Istegi UPSTREAM'e AYNEN ilet (header+govde+metot), yaniti AYNEN dondur."""
        gonderilecek = {k: v for k, v in self.headers.items() if k.lower() not in HOP}
        gonderilecek["Host"] = "mcp.facebook.com"
        govde = self._govde_oku()
        req = urllib.request.Request(UPSTREAM + ust_yol, data=govde, method=self.command,
                                     headers=gonderilecek)
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                cevap, kod, basliklar = r.read(), r.getcode(), list(r.headers.items())
        except urllib.error.HTTPError as e:
            cevap, kod, basliklar = e.read(), e.code, list(e.headers.items())
        except Exception as e:
            cevap = json.dumps({"sim_error": "upstream unreachable: %s" % type(e).__name__}).encode()
            kod, basliklar = 502, [("Content-Type", "application/json")]
        # PARCA-1b: 401 WWW-Authenticate icindeki resource_metadata pointer'i mcp.facebook.com'u
        # gosteriyor -> CLI onu takip edince upstream'e doner (localhost-kimligiyle celisir).
        # Pointer'i KENDI protected-resource yoluma cevir (identity-rewrite butunlugu).
        onek = "http://127.0.0.1:%d" % self._kendi_port()
        yeni_basliklar = []
        for k, v in basliklar:
            if k.lower() == "www-authenticate" and "mcp.facebook.com" in v:
                v = v.replace("https://mcp.facebook.com", onek)
            yeni_basliklar.append((k, v))
        _log(self.command, self.path, kod)
        self._yanit_yaz(kod, cevap, yeni_basliklar)

    def _as_metadata(self):
        """PARCA-2: kok-metadata sorulunca Meta'nin path-insertion formunu getir,
        registration_endpoint'i KENDI /register'imiza cevir (bug #36743 koprusu)."""
        req = urllib.request.Request(UPSTREAM + META_AS_METADATA, method="GET",
                                     headers={"Host": "mcp.facebook.com", "Accept": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                meta = json.loads(r.read())
                kod = r.getcode()
        except urllib.error.HTTPError as e:
            _log("GET", self.path, e.code)
            self._yanit_yaz(e.code, e.read(), list(e.headers.items()))
            return
        except Exception as e:
            _log("GET", self.path, 502)
            self._yanit_yaz(502, json.dumps({"sim_error": type(e).__name__}).encode(),
                            [("Content-Type", "application/json")])
            return
        meta["registration_endpoint"] = "http://127.0.0.1:%d/register" % self._kendi_port()
        cevap = json.dumps(meta).encode("utf-8")
        _log("GET", self.path, "%s(metadata-koprulendi)" % kod)
        self._yanit_yaz(200, cevap, [("Content-Type", "application/json")])

    def _yonlendir(self):
        yol = self.path.split("?")[0]
        if yol == "/healthz":
            _log(self.command, yol, 200)
            self._yanit_yaz(200, b'{"ok":true}', [("Content-Type", "application/json")])
            return
        # PARCA-2: kok oauth-authorization-server (+openid-configuration ayni-koprü)
        if yol in ("/.well-known/oauth-authorization-server",
                   "/.well-known/openid-configuration"):
            self._as_metadata()
            return
        # PARCA-3: #36743 hardcode'u -> Meta'nin gercek non-standard register-path'ine
        if yol == "/register":
            self._passthrough(META_REGISTER)
            return
        # PARCA-1b (kabul-testi bulgusu, 08-11): protected-resource metadata'sinin `resource` ve
        # `authorization_servers` alanlari mcp.facebook.com'u gosteriyordu; CLI 'Protected resource
        # ... does not match expected http://127.0.0.1:.../devtools' diye REDDETTI. Bu metadata'yi
        # da KOPRULE: resource + authorization_servers -> kendi localhost URL'im.
        if yol.startswith("/.well-known/oauth-protected-resource"):
            self._protected_resource_kopru()
            return
        # PARCA-1: /devtools* + diger her sey AYNEN gecir
        self._passthrough(self.path)

    def _protected_resource_kopru(self):
        onek = "http://127.0.0.1:%d" % self._kendi_port()
        req = urllib.request.Request(UPSTREAM + self.path, method="GET",
                                     headers={"Host": "mcp.facebook.com", "Accept": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                meta = json.loads(r.read())
        except Exception as e:
            _log("GET", self.path, 502)
            self._yanit_yaz(502, json.dumps({"sim_error": type(e).__name__}).encode(),
                            [("Content-Type", "application/json")])
            return
        if meta.get("resource"):
            meta["resource"] = onek + "/devtools"
        if isinstance(meta.get("authorization_servers"), list):
            meta["authorization_servers"] = [onek]   # kok-metadata'yi ben servis ediyorum (PARCA-2)
        _log("GET", self.path, "200(protected-resource-koprulendi)")
        self._yanit_yaz(200, json.dumps(meta).encode("utf-8"), [("Content-Type", "application/json")])

    do_GET = _yonlendir
    do_POST = _yonlendir
    do_PUT = _yonlendir
    do_DELETE = _yonlendir
    do_PATCH = _yonlendir
    def do_OPTIONS(self):  # noqa: N802
        self._yonlendir()


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--port", type=int, default=3477)
    a = ap.parse_args()
    # 127.0.0.1 SABIT (is-emri: LAN-maruziyeti YOK).
    httpd = ThreadingHTTPServer(("127.0.0.1", a.port), Handler)
    sys.stderr.write("META-MCP-SIM 127.0.0.1:%d (upstream=%s; header/token LOGLANMAZ)\n" % (a.port, UPSTREAM))
    sys.stderr.flush()
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        httpd.shutdown()


if __name__ == "__main__":
    sys.exit(main())

</details>

Suggested fix

  • Add the RFC 8414 path-insertion form to authorization-server metadata discovery, as a fallback when root discovery 404s.
  • Honor registration_endpoint from the discovered metadata instead of hardcoding /register.

Related

  • #83681 — same root cause on a different surface (claude.ai org connector); this report is the CLI.
  • #36743 — registration_endpoint ignored (closed; CLI path still affected).
  • #85563 — WWW-Authenticate resource_metadata handling.

Environment

  • Claude Code 2.1.227, Windows 10
  • Target: https://mcp.facebook.com/devtools (public, spec-compliant)

View original on GitHub ↗