anthropics / anthropics/claude-ai-mcp
Custom connector: DCR never attempted against self-hosted authorization server (ofid_fbe4f942d550e0ab)
- 主要言語
- 言語のデータがありません
- スター
- 471
- フォーク
- 76
- PR マージ指標
- 30日以内にマージされた PR はありません
説明
### What happened?
## Summary
A custom remote MCP connector consistently fails to complete OAuth setup
against a spec-compliant server with a cross-host-turned-same-host
authorization server. Two distinct failure modes were observed across
attempts, both apparently caused by client-side (claude.ai) behavior rather
than anything discoverable as wrong on the server side — verified by
independent server-side logging, live `curl` checks, and the third-party
IdP's own audit logs.
- Reference ID (attempt 1, cross-host AS): `ofid_fb8b8d043a595f1f`
- Reference ID (attempt 2, self-hosted AS, this report's focus): `ofid_fbe4f942d550e0ab`
## Server under test
- MCP endpoint: `https://win.tailb112a4.ts.net:10000/mcp` (Tailscale Funnel,
confirmed publicly routable — `tailscale funnel status` shows Funnel on
for this host/port)
- Framework: official `mcp` Python SDK, `FastMCP.streamable_http_app()`,
fronted by a hand-rolled Starlette middleware for bearer-token
verification and a set of `@mcp.custom_route(...)`-registered OAuth
endpoints (see "Attempt 2" below).
- Third-party IdP: Auth0 (`dev-im36ghmasvd8riht.us.auth0.com`), a standard
hosted tenant serving `/.well-known/openid-configuration` (confirmed
complete and RFC-compliant, see below).
## Attempt 1 — Auth0 listed directly as a cross-host authorization server
`protected-resource` metadata:
```json
{
"resource": "https://win.tailb112a4.ts.net:10000/mcp",
"authorization_servers": ["https://dev-im36ghmasvd8riht.us.auth0.com/"]
}
```
Auth0's `/.well-known/openid-configuration` was confirmed reachable and
complete from a public network, including:
```json
{
"issuer": "https://dev-im36ghmasvd8riht.us.auth0.com/",
"authorization_endpoint": "https://dev-im36ghmasvd8riht.us.auth0.com/authorize",
"token_endpoint": "https://dev-im36ghmasvd8riht.us.auth0.com/oauth/token",
"registration_endpoint": "https://dev-im36ghmasvd8riht.us.auth0.com/oidc/register",
"code_challenge_methods_supported": ["S256", "plain"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth", "none"],
"grant_types_supported": ["client_credentials", "authorization_code", "refresh_token", "..."]
}
```
`https://claude.ai/api/mcp/auth_callback` was registered in Auth0's Allowed
Callback URLs for the application before this attempt.
**Expected:** per the "Cross-host authorization servers" documentation
("Claude resolves it regardless of which host it points at"), the browser
should be redirected to `https://dev-im36ghmasvd8riht.us.auth0.com/authorize`.
**Actual:** the browser was redirected to:
```
https://win.tailb112a4.ts.net:10000/authorize?response_type=code&client_id=&redirect_uri=https%3A%2F%2Fclaude.ai%2Fapi%2Fmcp%2Fauth_callback&code_challenge=...&code_challenge_method=S256&state=...
```
i.e. the *resource server's own host*, not the authorization server's host
from `authorization_servers`. That route did not exist on the resource
server at the time, so it returned Starlette's default `Not Found`. The
`client_id` in that URL matches a client ID that had previously been
entered manually in the connector's Advanced Settings on an earlier setup
attempt, suggesting client identity resolution worked, but authorization
*endpoint* resolution did not use the authorization server's real
`authorization_endpoint`.
## Attempt 2 — self-hosted authorization server (OAuth Proxy pattern)
To rule out any cross-host resolution issue, the resource server was
changed to be its own authorization server from the MCP client's point of
view, delegating the actual Auth0 login server-to-server. Full source is
available on request. In short:
- `protected-resource` metadata now advertises
`"authorization_servers": ["https://win.tailb112a4.ts.net:10000"]`
(self, same origin as the MCP endpoint).
- `https://win.tailb112a4.ts.net:10000/.well-known/oauth-authorization-server`
(and an identical alias at `/.well-known/openid-configuration`) serves:
```json
{
"issuer": "https://win.tailb112a4.ts.net:10000",
"authorization_endpoint": "https://win.tailb112a4.ts.net:10000/authorize",
"token_endpoint": "https://win.tailb112a4.ts.net:10000/token",
"registration_endpoint": "https://win.tailb112a4.ts.net:10000/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["openid", "profile", "email", "offline_access"]
}
```
- `/register` implements RFC 7591 DCR (always succeeds, issues a fresh
`client_id`, no secret, public client). Verified locally with a
Starlette `TestClient` end-to-end (register → authorize → Auth0 callback
→ token exchange, including PKCE S256 verification, single-use code
rejection, and refresh-token exchange) before deploying.
- `/authorize`, `/oauth/callback`, `/token` implement the proxy: PKCE
between the MCP client and this server is enforced here; this server
authenticates to Auth0 as a confidential client server-to-server. Access
tokens handed back to the MCP client are Auth0's real RS256 JWTs,
unmodified.
Live checks immediately before this attempt, from a public network:
```
$ curl -s https://win.tailb112a4.ts.net:10000/.well-known/oauth-authorization-server
{"issuer":"https://win.tailb112a4.ts.net:10000", ...as above...}
$ curl -s https://win.tailb112a4.ts.net:10000/.well-known/oauth-protected-resource/mcp
{"resource":"https://win.tailb112a4.ts.net:10000/mcp","authorization_servers":["https://win.tailb112a4.ts.net:10000"]}
```
The custom connector ("axess-mcp" in our account) was deleted and re-added
from scratch pointing at `https://win.tailb112a4.ts.net:10000/mcp`, with
the Advanced Settings OAuth Client ID field left **empty** (to force fresh
DCR rather than reuse of a previously-entered Auth0 client ID).
**Expected:** Claude fetches protected-resource metadata → fetches this
server's own AS metadata (same origin) → POSTs to
`https://win.tailb112a4.ts.net:10000/register` to obtain a client_id via
DCR → redirects the browser to `.../authorize`.
**Actual:** the connection attempt failed immediately with no browser
redirect at all:
> axess-mcpへの接続を開始しましたが、完了しませんでした。
> axess-mcpのサインインサービスに登録できませんでした。もう一度お試しいただくか、コネクタ設定でOAuth Client IDを追加してください。
> (~ "Could not register with axess-mcp's sign-in service. Try again, or
> add an OAuth Client ID in the connector settings.")
>
> Reference: `ofid_fbe4f942d550e0ab`
**Server-side access log**, immediately before and during this attempt
(full `uvicorn` output, unedited apart from an IP address):
```
INFO: 100.65.130.34:0 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 200 OK
INFO: 100.65.130.34:0 - "GET /.well-known/oauth-protected-resource/mcp HTTP/1.1" 200 OK
```
**No `POST /register` request ever arrived at this server** — not before,
during, or after the failed attempt. (100.65.130.34 is this Windows host's
own Tailscale interface address, which Tailscale Funnel uses when
forwarding external requests to the local listener; these two lines are
the genuine external discovery requests from the connector attempt.)
To rule out a stale/cached discovery result causing Claude to instead
attempt DCR directly against Auth0 (from a *previous* attempt, before this
server pointed `authorization_servers` at itself), Auth0's own tenant
audit log (Monitoring → Logs) was checked for the several minutes
surrounding the failure timestamp. It shows no `mgmt_api` or `sapi` entries
for an anonymous/external DCR call (`POST /oidc/register`) at all — only
our own authenticated dashboard session activity (`Get client by ID`,
`Update a client`) from earlier debugging, unrelated to this attempt.
## Conclusion / what we're asking
Both the resource server's own logs and the third-party IdP's own audit
log independently show that **no dynamic client registration request was
ever sent anywhere** — neither to this server's `/register` (which
correctly advertises a `registration_endpoint` per RFC 8414 and implements
RFC 7591) nor to Auth0's. Yet the connector failed with an error message
whose text specifically describes a registration failure ("サインインサービス
に登録できませんでした" / "could not register with the sign-in service").
This suggests one of:
1. Something in the discovery → DCR decision path fails silently before
any registration HTTP request is issued (a bug), possibly related to
stale connector state from an earlier attempt against the same MCP
server URL (attempt 1, above) that isn't being fully invalidated by
deleting and re-adding the connector, or
2. Some other precondition for attempting DCR (documented or not) isn't
being met by our AS metadata document, and the failure mode for that
case is this generic "could not register" message rather than a more
specific error, or
3. Some client-side/network-side failure that never leaves our process
(e.g. a request that never leaves Anthropic's infrastructure).
We'd appreciate a look at what actually happened for `ofid_fbe4f942d550e0ab`
on your side — specifically, whether a registration request was attempted
at all, and if so, why it wouldn't have reached
`https://win.tailb112a4.ts.net:10000/register`, which was confirmed
reachable, correctly implemented (verified independently with an automated
test suite), and receiving *other* requests (the two discovery GETs above)
from the same connection attempt seconds earlier.
Happy to provide the full `oauth_proxy.py` source, the exact AS metadata
JSON, or reproduce with additional logging if useful.
### What did you expect to happen?
Given /.well-known/oauth-protected-resource/mcp correctly advertises
authorization_servers: ["https://win.tailb112a4.ts.net:10000"], and that
same host's /.well-known/oauth-authorization-server correctly advertises
a registration_endpoint (https://win.tailb112a4.ts.net:10000/register)
per RFC 8414, I expected claude.ai to POST to that registration_endpoint
to perform Dynamic Client Registration (RFC 7591), then redirect the
browser to the returned authorization_endpoint so the user could sign in
(proxied server-side to Auth0).
Instead, no request to /register was ever sent -- confirmed independently
via both this MCP server's own access log and the upstream IdP's (Auth0)
audit log for the same time window -- and the connector failed
immediately with a generic "could not register with the sign-in service"
error, without any browser redirect at all.
### Steps to reproduce
1. Deploy an MCP server at https://win.tailb112a4.ts.net:10000/mcp (Streamable HTTP, mcp Python SDK / FastMCP) acting as its own OAuth 2.1 authorization server: RFC 8414 metadata + RFC 7591 Dynamic Client Registration, delegating the actual login to Auth0 server-to-server (Auth0 is never contacted directly by the MCP client).
2. Confirm from a public network that discovery works:
GET https://win.tailb112a4.ts.net:10000/.well-known/oauth-protected-resource/mcp
-> 200 {"resource":"https://win.tailb112a4.ts.net:10000/mcp","authorization_servers":["https://win.tailb112a4.ts.net:10000"]}
GET https://win.tailb112a4.ts.net:10000/.well-known/oauth-authorization-server
-> 200, full AS metadata including "registration_endpoint":"https://win.tailb112a4.ts.net:10000/register"
3. In claude.ai: Customize > Connectors > Add custom connector.
4. Enter the MCP server URL (https://win.tailb112a4.ts.net:10000/mcp). Leave the Advanced Settings "OAuth Client ID" field empty (to force fresh DCR rather than reuse of a client ID entered in an earlier attempt). Click Add.
5. Click "Connect" on the newly added connector ("axess-mcp").
6. Observed: no browser redirect to any authorization/login page occurs. The UI immediately shows a failure toast: "Could not register with axess-mcp's sign-in service. Try again, or add an OAuth Client ID in the connector settings." Reference: ofid_fbe4f942d550e0ab
7. Simultaneously tailing the MCP server's access log shows only the two discovery GETs from step 2 being received again for this attempt. POST /register is never logged -- before, during, or after -- confirming the DCR request never reached the resource server.
8. Cross-checked Auth0's own tenant audit log (Monitoring > Logs) for the same time window: no anonymous/external DCR request (POST /oidc/register) appears there either, ruling out the client instead calling the upstream IdP's registration_endpoint directly.
### Area
OAuth / Authentication
### MCP Server (if applicable)
Custom self-hosted server (mcp Python SDK / FastMCP, Streamable HTTP over Tailscale Funnel, Auth0 as upstream IdP via a self-hosted OAuth proxy)
### Error messages or logs
```shell
axess-mcpへの接続を開始しましたが、完了しませんでした。
axess-mcpのサインインサービスに登録できませんでした。もう一度お試しいただくか、コネクタ設定でOAuth Client IDを追加してください。
Reference: ofid_fbe4f942d550e0ab
INFO: 100.65.130.34:0 - "GET /.well-known/oauth-authorization-server HTTP/1.1" 200 OK
INFO: 100.65.130.34:0 - "GET /.well-known/oauth-protected-resource/mcp HTTP/1.1" 200 OK
(POST /register was never logged, before, during, or after this attempt)
```
### Additional context
Possibly related: #879 shows a similar pattern (a step in the OAuth flow silently never reaching the server) at a different point in the flow (post-token /mcp POST vs. pre-token DCR POST here).
Related: #82 (closed as "not planned") documents the same root cause as
our first attempt (cross-host authorization_endpoint ignored) and
recommends exactly the workaround implemented here (self-hosted
/register, /authorize, /token proxying to the external IdP). This report
is that recommended workaround, fully implemented and independently
verified correct, still failing -- but via a different, seemingly new
failure mode (DCR never attempted at all).
Possibly related: #879 shows a similar pattern (a step in the OAuth flow silently never reaching the server) at a different point in the flow (post-token /mcp POST vs. pre-token DCR POST here).
Related: #82 (closed as "not planned") documents the same root cause as our first attempt (cross-host authorization_endpoint ignored) and recommends exactly the workaround implemented here (self-hosted /register, /authorize, /token proxying to the external IdP). This report is that recommended workaround, fully implemented and independently verified correct, still failing -- but via a different, seemingly new failure mode (DCR never attempted at all).
Also possibly related: #881 documents what looks like the same underlying cause -- claude.ai appears to cache an authorization-server resolution keyed to the resource URL and/or org, which does not get invalidated when the server's protected-resource metadata changes (in #881's case, persisting 18+ hours and even affecting a brand-new, never-before-used URL for the same org). Our case may be a different symptom of the same caching bug: instead of redirecting to a stale-but-valid old provider, no registration request is sent at all after our resource metadata changed from pointing at an external provider (Auth0) to pointing at itself.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
評価
この issue はまだ評価されていません。