How discovery works
The MCP server publishes protected-resource metadata that tells the AI app which authorization server protects it. The authorization server publishes its OAuth endpoints and public signing keys. The MCP server’s RFC 9728 (OAuth protected-resource metadata) document should identify the canonical MCP URL and connector issuer:401 with a WWW-Authenticate: Bearer challenge containing resource_metadata="<URL of this document>". The MCP server publishes this document; the backend does not serve it. The resource must match the operator’s configured canonical URL exactly.
The client then reads authorization-server metadata and, when it needs to validate an access token, the JWKS (JSON Web Key Set, the public keys used to verify signed tokens):
https://api.example.com/oauth/connector. These protocol routes are at the API root and do not have /api/v1 equivalents. Do not use the root /.well-known/openid-configuration: that describes the separate legacy contact OIDC system.
Alpha caveat — discovery: real Claude and ChatGPT request captures are still pending. In particular, the exact discovery URLs each client probes, and whether either probes the root OpenID Connect path, are not confirmed.
Register the AI client
Dynamic client registration (DCR) gives the AI app a public client identity. Registration does not grant access to a hub. Clients use no client secret.201 and includes a case-sensitive client_id. There is no client secret or registration access token, and registrations cannot be edited; register again if the callback changes. An unused registration expires after 24 hours. After its first approved consent, it no longer expires.
Callbacks must be exact allowlist entries. There are no wildcards, prefix matches, port variations, or custom schemes. The code’s default entries are https://claude.ai/api/mcp/auth_callback and https://chatgpt.com/connector_platform_oauth_redirect; an operator can replace the full allowlist. ChatGPT also documents a callback shaped like https://chatgpt.com/connector/oauth/{callback_id}, but each actual URL must be added individually. Loopback callbacks are refused in production, even if listed.
If grant_types is omitted, registration uses the operator’s configured default. The documented default in the code is authorization-code only; clients receive refresh tokens only if their registration includes refresh_token.
Alpha caveat — callbacks and registration: the default callbacks come from vendor documentation, not recorded client requests. Exact callback URLs used by Claude and ChatGPT, and whether they request refresh_token during registration, are pending real captures. Confirm each connector’s actual callback with the operator before relying on these defaults.
Send the member through authorization and consent
The member starts in the AI app, signs in on the hub, and approves sharing access with that app. The AI app creates a Proof Key for Code Exchange (PKCE) verifier and its S256 (SHA-256) challenge, plus an unpredictablestate value. It opens the member’s browser at the authorization endpoint with the client ID, exact registered callback, challenge, state, and configured MCP resource:
curl in a production client. The verifier stays with the AI app; the challenge goes to authorize. The verifier must be 43–128 characters from A-Z a-z 0-9 - . _ ~. state is required and is returned unchanged. The callback must exactly match the registration. The resource string must exactly match a configured canonical URL or alias; the service does not normalize trailing slashes. Aliases resolve to the canonical URL for tokens.
The authorize request redirects the browser to the hub’s own /connect-client page. The hub’s backend-for-frontend (BFF) binds the request to that browser, then asks the member to sign in on the hub if needed and show explicit consent. The browser does not call the connector’s internal prepare endpoint directly. Once signed in, the hub BFF calls POST /api/oauth/requests/prepare with the request handle (rid), the member’s contact bearer, and its hub-edge proof (the secret and browser-bound headers that authenticate the hub BFF request). After the member approves or denies, the BFF calls POST /oauth/connector/consent. The consent endpoint returns JSON containing redirect_url; the hub BFF sends the browser there with a 303. On approval, that URL points to the registered callback with code, the original state, and iss; on denial, it carries error=access_denied with state and iss.
These are server-to-server BFF calls; they are not calls the AI client makes. The hub BFF sends the current browser nonce and canonical origin with both requests:
Authorization; prepare binds the browser and returns login_required. After sign-in, prepare returns consent_required and the latest CSRF (cross-site request forgery) token. The BFF then posts the member’s decision:
decision to deny when the member declines. The BFF must send the returned redirect_url to the browser with a 303 and must not forward the bearer or edge secret to the client callback.
Every connection requires explicit consent, including reconnecting to the same app. prompt=consent behaves like the default. prompt=select_account is refused with invalid_request; prompt=login and prompt=none are refused with login_required. The v1 door has no account chooser and cannot prove a fresh login for this request.
The authorization request lasts 10 minutes. After sign-in, the consent-ready step lasts 5 minutes at most and never past the original 10-minute deadline. If it expires or a bound value changes, the member must restart from the AI app.
Exchange the code for tokens
The AI app exchanges the one-use authorization code using the same client ID, callback, resource, and PKCE verifier. Send a form body:refresh_token, a refresh token:
hub_id is the mapped hub, its sub is the contact ID, and its scope is exactly mcp:access. It contains no email or profile. The MCP server must verify it with the connector JWKS and apply its own authorization for the contact. Do not treat it as a hub login or forward it to another resource.
There is one effective permission: mcp:access. Omitted or empty scope defaults to it. offline_access is accepted as an alias, alone or with mcp:access, but adds nothing and is never required; the granted scope remains exactly mcp:access. Other scopes are rejected.
Refresh, revoke, and reconnect
Access tokens last 300 seconds. A refresh token has a 7-day idle lifetime, renewed by rotation, and a 30-day absolute family lifetime from code exchange. Consent expires 90 days after approval. The 30-day family cap can end the connection before consent expires. A client without a refresh token must reconnect when its access token expires. Each refresh returns a new refresh token and spends the old one immediately. There is no grace window. Store the new refresh token before using anything else from that response. Reusing a spent refresh token revokes the whole family. If a client loses a successful refresh response, it must reconnect.200 response.
Alpha caveat — client recovery: real Claude and ChatGPT behavior after a lost refresh response,invalid_grant, and429is pending capture. The backend’s refresh rotation has no grace window, so clients should be prepared to reconnect afterinvalid_grant.
Let a member review and disconnect grants
A grant is the member’s approved connection between one client and one MCP resource on a hub. A hub’s Connected apps page can list a member’s live grants and disconnect one using the member’s own contact sign-in bearer. These endpoints stay mounted even when the connector feature flag is off, so members can clean up existing connections. Neither requires active membership in the hub; a banned or departed member can still review and disconnect. List live grants, newest first.page[size] accepts 1–100 and defaults to 20. Use the opaque meta.page.next_cursor as page[after] for the next page. Optional filter[hub_id] (up to 36 characters) and filter[resource] (up to 2048 characters) narrow results; the resource filter is an exact canonical MCP URL.
last_issued_at is not last use because the MCP validates tokens locally. The response never includes credentials, family IDs, or hashes. Escape client_name, which is untrusted text. Use page[after] only with a cursor returned in meta.page.next_cursor.
Disconnect a grant by its ID. A repeat returns 204 without another event. A successful disconnect revokes the grant and refresh families; access tokens already issued can still work at the MCP for up to 360 seconds while they expire.
Errors
OAuth routes use plain OAuth JSON and form bodies as specified; they do not useapplication/vnd.api+json. Before authorize validates the client and callback, errors are HTTP responses; after that point, authorization errors are a 302 to the validated callback with error, error_description, state when valid, and iss. The grants endpoints use the normal API error shape. For rate limits, honor Retry-After; the service fails closed if its rate-limit store is unavailable.
The hub BFF also receives these errors while preparing or recording consent:
invalid_request, invalid_client, invalid_token, untrusted_bff, session_not_eligible, access_denied, csrf_mismatch, consent_not_ready, restart_required, reconnect_required, request_expired, rate_limited (prepare), slow_down (consent), and temporarily_unavailable. For these, the hub should sign the member in again for invalid_token, restart from the AI app for restart_required, reconnect_required, or request_expired, call prepare again for csrf_mismatch or consent_not_ready, and wait for Retry-After for rate limits. untrusted_bff indicates a deployment fault. A BFF HTTP access_denied means the member is not eligible to connect on that hub; it differs from the redirect error=access_denied when the member presses Deny.
Alpha status and limits
The connector door is disabled by default withMIO_FEATURE_CONTACT_OAUTH_CONNECTORS_ENABLED=false. The operator must enable it and configure the issuer, resources, signing keys, callback policy, and hub-edge secret. When the flag is off, protocol endpoints and the hub prepare endpoint return 404; grants list and disconnect remain available.
The following behavior remains pending real client captures or resource-server confirmation:
- Exact callback URLs Claude and ChatGPT use, and whether they register refresh tokens.
- Whether clients send
resourceat authorize and token, whether they include a trailing slash, and which scope values they send. - Which discovery URLs each client probes.
- How clients handle lost refresh responses,
invalid_grant, and429.
prompt=login support, or client ID metadata document support.