(fix): serve legacy OAuth discovery endpoints for 2025-03-26 MCP clients#77
Conversation
Raycast (and other clients on the 2025-03-26 MCP authorization spec) ignore the resource_metadata hint in the 401 challenge and instead fetch /.well-known/oauth-authorization-server from the MCP origin, which 404'd and surfaced as "Invalid response: not found". Serve the Appwrite authorization server's discovery document there, and also serve the protected-resource metadata at its root form for clients that don't apply RFC 9728 path insertion.
Greptile SummaryThis PR adds two backwards-compatibility discovery shims to fix OAuth flow failures for MCP clients (e.g. Raycast) that implement the 2025-03-26 spec and fetch authorization-server metadata directly from the MCP origin rather than following the
Confidence Score: 4/5Safe to merge — the changes are additive (new routes only), well-tested, and do not touch the existing auth or MCP handler paths. Both new routes are unauthenticated, read-only, and additive. The main open question is whether the Both changed files are straightforward; Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/mcp_server_appwrite/http_app.py:302-312
**RFC 8414 issuer mismatch may cause strict clients to reject the document**
RFC 8414 §3 requires that the `issuer` value in an `/.well-known/oauth-authorization-server` response equals the URL prefix used to construct the discovery request (i.e. `https://mcp.appwrite.io`). This endpoint returns the raw Appwrite upstream document, whose `issuer` is `https://cloud.appwrite.io/v1/oauth2/console`. Strictly conformant clients (and any client that adds RFC 8414 validation in a future update) will verify this and reject the document. The current fix relies on Raycast not enforcing this check. Worth noting explicitly in the docstring that this is a deliberate deviation, and consider whether an `issuer`-patched copy could work (though it would break any client that validates token issuers against this document).
### Issue 2 of 2
src/mcp_server_appwrite/http_app.py:302-312
**Unhandled discovery failure returns 500 with no client-facing error**
`authorization_server_metadata()` raises when the upstream discovery fetch fails and there is no stale cached document (e.g. first request after a cold start during an Appwrite outage). The exception propagates unhandled and Starlette returns a 500 response. The existing `protected_resource_metadata_endpoint` has the same gap, but this new endpoint is on the unauthenticated discovery path — a 500 here prevents any client from completing OAuth at all. A `try/except` that returns a `503 Service Unavailable` with a `Retry-After` header would give clients a clearer signal.
Reviews (1): Last reviewed commit: "(fix): serve legacy OAuth discovery endp..." | Re-trigger Greptile |
| async def authorization_server_metadata_endpoint(request: Request) -> JSONResponse: | ||
| """Legacy discovery shim (MCP authorization spec 2025-03-26). | ||
|
|
||
| Older clients fetch authorization-server metadata from the MCP server's own | ||
| origin rather than the ``authorization_servers`` entry in the protected | ||
| resource metadata. Serve the Appwrite authorization server's discovery | ||
| document verbatim so those clients find the real authorize/token/register | ||
| endpoints (the ``issuer`` inside points at the Appwrite endpoint, not here). | ||
| """ | ||
| metadata = await authorization_server_metadata() | ||
| return JSONResponse(metadata, headers=dict(CORS_HEADERS)) |
There was a problem hiding this comment.
RFC 8414 issuer mismatch may cause strict clients to reject the document
RFC 8414 §3 requires that the issuer value in an /.well-known/oauth-authorization-server response equals the URL prefix used to construct the discovery request (i.e. https://mcp.appwrite.io). This endpoint returns the raw Appwrite upstream document, whose issuer is https://cloud.appwrite.io/v1/oauth2/console. Strictly conformant clients (and any client that adds RFC 8414 validation in a future update) will verify this and reject the document. The current fix relies on Raycast not enforcing this check. Worth noting explicitly in the docstring that this is a deliberate deviation, and consider whether an issuer-patched copy could work (though it would break any client that validates token issuers against this document).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcp_server_appwrite/http_app.py
Line: 302-312
Comment:
**RFC 8414 issuer mismatch may cause strict clients to reject the document**
RFC 8414 §3 requires that the `issuer` value in an `/.well-known/oauth-authorization-server` response equals the URL prefix used to construct the discovery request (i.e. `https://mcp.appwrite.io`). This endpoint returns the raw Appwrite upstream document, whose `issuer` is `https://cloud.appwrite.io/v1/oauth2/console`. Strictly conformant clients (and any client that adds RFC 8414 validation in a future update) will verify this and reject the document. The current fix relies on Raycast not enforcing this check. Worth noting explicitly in the docstring that this is a deliberate deviation, and consider whether an `issuer`-patched copy could work (though it would break any client that validates token issuers against this document).
How can I resolve this? If you propose a fix, please make it concise.| async def authorization_server_metadata_endpoint(request: Request) -> JSONResponse: | ||
| """Legacy discovery shim (MCP authorization spec 2025-03-26). | ||
|
|
||
| Older clients fetch authorization-server metadata from the MCP server's own | ||
| origin rather than the ``authorization_servers`` entry in the protected | ||
| resource metadata. Serve the Appwrite authorization server's discovery | ||
| document verbatim so those clients find the real authorize/token/register | ||
| endpoints (the ``issuer`` inside points at the Appwrite endpoint, not here). | ||
| """ | ||
| metadata = await authorization_server_metadata() | ||
| return JSONResponse(metadata, headers=dict(CORS_HEADERS)) |
There was a problem hiding this comment.
Unhandled discovery failure returns 500 with no client-facing error
authorization_server_metadata() raises when the upstream discovery fetch fails and there is no stale cached document (e.g. first request after a cold start during an Appwrite outage). The exception propagates unhandled and Starlette returns a 500 response. The existing protected_resource_metadata_endpoint has the same gap, but this new endpoint is on the unauthenticated discovery path — a 500 here prevents any client from completing OAuth at all. A try/except that returns a 503 Service Unavailable with a Retry-After header would give clients a clearer signal.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcp_server_appwrite/http_app.py
Line: 302-312
Comment:
**Unhandled discovery failure returns 500 with no client-facing error**
`authorization_server_metadata()` raises when the upstream discovery fetch fails and there is no stale cached document (e.g. first request after a cold start during an Appwrite outage). The exception propagates unhandled and Starlette returns a 500 response. The existing `protected_resource_metadata_endpoint` has the same gap, but this new endpoint is on the unauthenticated discovery path — a 500 here prevents any client from completing OAuth at all. A `try/except` that returns a `503 Service Unavailable` with a `Retry-After` header would give clients a clearer signal.
How can I resolve this? If you propose a fix, please make it concise.
Problem
Connecting Raycast to
https://mcp.appwrite.io/mcpfails with:A logging proxy showed Raycast (1.104.21) implements the 2025-03-26 MCP authorization spec: after the 401 it ignores the
resource_metadataURL inWWW-Authenticateand instead fetches authorization-server metadata from the MCP origin itself:Fix
Add the backwards-compatibility shim the current MCP spec recommends for exactly this case:
/.well-known/oauth-authorization-server— mirrors the Appwrite authorization server's discovery document verbatim (already fetched and cached byauth.authorization_server_metadata()), so legacy clients find the real authorize/token/register endpoints./.well-known/oauth-protected-resource(root form) — same handler as the existing/mcp-suffixed route, for clients that don't apply RFC 9728 path insertion.Verification
_UPLOAD_TRANSPORTmodule global thatbuild_app()flips, so the stdio-path tests keep passing).ruff,black --check,pyright, and the full unit suite (165 tests) pass.Note
This gets legacy clients through discovery and the OAuth flow. If Raycast then fails post-login with a 401, the next (separate) issue would be RFC 8707 audience binding — 2025-03-26 clients don't send the
resourceparameter, andAppwriteTokenVerifierhard-rejects tokens not audience-bound tohttps://mcp.appwrite.io/mcp.