There are a lot of threads here about Vercel MCP returning 403 or zero teams, and I think they are being discussed as one problem when they are two. I spent some time measuring both. Everything below is reproducible with curl and no Vercel credentials.
The discovery layer is correct
GET https://mcp.vercel.com/.well-known/oauth-protected-resource resource: https://mcp.vercel.com/ authorization_servers: ["https://vercel.com"] scopes_supported: ["openid"]
GET https://vercel.com/.well-known/oauth-authorization-server authorization_endpoint: https://vercel.com/oauth/authorize token_endpoint: https://vercel.com/api/login/oauth/token registration_endpoint: https://vercel.com/api/login/oauth/register code_challenge_methods_supported: ["S256"] token_endpoint_auth_methods_supported: ["none"]An unauthenticated request to the server answers exactly as the MCP spec says it should:
POST https://mcp.vercel.com/ 401 WWW-Authenticate: Bearer error="invalid_token", error_description="No authorization provided", resource_metadata="https://mcp.vercel.com/.well-known/oauth-protected-resource"So far this is a textbook OAuth 2.1 public client setup: discovery, dynamic client registration, PKCE with S256.
Problem one: registration succeeds, authorization refuses
Registration only accepts localhost redirects:
POST /api/login/oauth/register {"redirect_uris":["https://example.com/callback"]} 400 {"error":"invalid_redirect_uri", "error_description":"The provided redirect URIs are not approved for use by this authorization server."}
POST /api/login/oauth/register {"redirect_uris":["http://localhost:8976/callback"]} 201 {"client_id":"cl_...", "token_endpoint_auth_method":"none", ...}That first response is the real answer to every "please allowlist my product for Vercel MCP OAuth" thread. A hosted client cannot register at all, and the error names the redirect URI rather than the approval policy, so people reasonably go looking at their callback URL.
The second one is stranger. Registration returns 201 with a client_id, and re-registering returns the same client_id, so it is not short-lived. But taking that client and that exact redirect URI to the authorize endpoint with a valid S256 challenge gives, for a signed-in user:
The app ID is invalid The app redirect URL is invalid
Both, for a client and a redirect URI Vercel's own registration endpoint had just issued and accepted. Signed out, the same URL renders the normal Authorize App screen, which is why this is easy to miss when testing.
RFC 7591 dynamic client registration exists so that clients do not need manual registration. The docs say Vercel MCP "only supports AI clients that have been reviewed and approved by Vercel", with a review form. Both things cannot be true at once: either the registration endpoint should not be advertised, or it should issue clients that can actually authorize. At minimum the error should say the client is not approved rather than blaming the app ID and the redirect URL.
Problem two: 0 teams and 403 with an explicit teamId
This is a different failure and it affects clients that are approved, which is why it only shows up for ChatGPT and Codex users.
The best diagnostic in these threads is not mine. found that calls succeed without a teamId and return 403 with re-authenticate to this scope when one is supplied, and the original reporter concluded the team scope is not being retained on the grant. It has been reported on both Hobby and Pro accounts, so it is not a plan limit.
Worth adding: there is no team scope anywhere in the OAuth layer. The protected resource advertises scopes_supported: ["openid"] and the authorization server advertises openid, email, profile, offline_access. So "re-authenticate to this scope" cannot refer to an OAuth scope, because none that covers a team exists. Whatever binds a grant to a team happens outside the advertised scope set, which would explain why reconnecting does not fix it: the user repeats the same consent and gets the same binding.
That also means a user has no way to see, before or after authorizing, which team their grant covers.
One small docs point
The docs say there are "public tools (available without authentication)". An unauthenticated initialize is refused with the 401 above, so the public tools cannot be reached without a token either.
What would help
- If approval is required, say so in the registration response or the authorize error instead of naming the app ID and redirect URI.
- Document which account or team a grant is bound to, and how to change it.
- Separate these two in support replies. They share a status code and nothing else.
Happy to be corrected on any of this. Everything above is from public endpoints and is reproducible.