fix: MCP OAuth discovery for ChatGPT (RFC 9728 /mcp PRM, WWW-Authenticate)
- scopes_supported on protected-resource metadata - /.well-known/oauth-protected-resource/mcp (path-prefix match) - 401 on /mcp points resource_metadata at PRM URL; advertise scopes - Prefer client_secret_basic in AS metadata (ChatGPT connector quirk) - README: does not implement OAuth / 502 / 404 troubleshooting
This commit is contained in:
+38
-13
@@ -102,6 +102,27 @@ def intersect_oauth_scopes(client: dict, requested: str) -> str:
|
||||
return " ".join(sorted(granted, key=lambda x: order.get(x, 9)))
|
||||
|
||||
|
||||
def oauth_protected_resource_document(base: str, *, resource_path: str = "/mcp") -> dict:
|
||||
"""RFC 9728 / MCP authorization discovery document."""
|
||||
resource = f"{base.rstrip('/')}{resource_path}"
|
||||
return {
|
||||
"resource": resource,
|
||||
"authorization_servers": [base.rstrip("/")],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"scopes_supported": ["ctxd.read", "ctxd.write"],
|
||||
"resource_documentation": f"{base.rstrip('/')}/",
|
||||
}
|
||||
|
||||
|
||||
def oauth_protected_resource_metadata_url(base: str, *, mcp_path: str = "/mcp") -> str:
|
||||
"""Well-known URL for the MCP resource (RFC 9728 path insertion)."""
|
||||
base = base.rstrip("/")
|
||||
if mcp_path in ("", "/"):
|
||||
return f"{base}/.well-known/oauth-protected-resource"
|
||||
suffix = mcp_path.lstrip("/")
|
||||
return f"{base}/.well-known/oauth-protected-resource/{suffix}"
|
||||
|
||||
|
||||
def _is_public_oauth_as_route(path: str, method: str) -> bool:
|
||||
"""OAuth authorization-server endpoints (public). Admin /oauth/clients stays on HTTPServer."""
|
||||
if path.startswith("/.well-known/oauth-protected-resource"):
|
||||
@@ -1513,12 +1534,7 @@ class CombinedApp:
|
||||
qs = urllib.parse.parse_qs(scope.get("query_string", b"").decode())
|
||||
|
||||
if method == "GET" and path.startswith("/.well-known/oauth-protected-resource"):
|
||||
payload = {
|
||||
"resource": f"{base}/mcp",
|
||||
"authorization_servers": [issuer],
|
||||
"bearer_methods_supported": ["header"],
|
||||
"resource_documentation": f"{base}/",
|
||||
}
|
||||
payload = oauth_protected_resource_document(base)
|
||||
await self._send_json(send, 200, payload)
|
||||
return
|
||||
|
||||
@@ -1531,7 +1547,11 @@ class CombinedApp:
|
||||
"response_types_supported": ["code"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic", "none"],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"none",
|
||||
],
|
||||
"scopes_supported": ["ctxd.read", "ctxd.write"],
|
||||
}
|
||||
await self._send_json(send, 200, payload)
|
||||
@@ -1695,14 +1715,19 @@ class CombinedApp:
|
||||
break
|
||||
return token
|
||||
|
||||
async def _auth_error(status: int = 401, message: str = "unauthorized"):
|
||||
async def _auth_error(status: int = 401, message: str = "unauthorized", *, mcp_resource: bool = False):
|
||||
headers = [(b"content-type", b"application/json")]
|
||||
if self.cfg.oauth_enabled:
|
||||
base = self._base_url(scope)
|
||||
headers.append((
|
||||
b"www-authenticate",
|
||||
f'Bearer resource_metadata="{base}/.well-known/oauth-protected-resource"'.encode(),
|
||||
))
|
||||
if mcp_resource:
|
||||
meta_url = oauth_protected_resource_metadata_url(base, mcp_path="/mcp")
|
||||
else:
|
||||
meta_url = oauth_protected_resource_metadata_url(base)
|
||||
www = (
|
||||
f'Bearer resource_metadata="{meta_url}", '
|
||||
f'scope="ctxd.read ctxd.write"'
|
||||
)
|
||||
headers.append((b"www-authenticate", www.encode()))
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
@@ -1734,7 +1759,7 @@ class CombinedApp:
|
||||
if ok:
|
||||
await self._serve_oauth_mcp_streamable(scope, receive, send, ctx)
|
||||
return
|
||||
await _auth_error()
|
||||
await _auth_error(mcp_resource=True)
|
||||
return
|
||||
|
||||
# Legacy SSE write path (prefer /mcp Streamable HTTP).
|
||||
|
||||
Reference in New Issue
Block a user