diff --git a/apps/docs/content/guides/ai-tools/byo-mcp.mdx b/apps/docs/content/guides/ai-tools/byo-mcp.mdx index b144d498663..e00e6f2b222 100644 --- a/apps/docs/content/guides/ai-tools/byo-mcp.mdx +++ b/apps/docs/content/guides/ai-tools/byo-mcp.mdx @@ -343,6 +343,37 @@ Every client that implements the MCP authorization specification discovers your Users can review and revoke connected clients through the [OAuth grant management](/docs/guides/auth/oauth-server/oauth-flows#managing-user-grants) endpoints. The [Headless App block](/library/docs/tanstack/headless-app) ships an `/agents` page that does this. +## Use a custom domain + +With a [custom domain](/docs/guides/platform/custom-domains) active, clients can reach your MCP server at `https://api.example.com/functions/v1/mcp`. Supabase Auth keeps its issuer on the project domain, `https://.supabase.co/auth/v1`. `withOAuthProtectedResource()` builds the Auth URL from the domain the request arrived on, so on the custom domain it advertises `https://api.example.com/auth/v1`. That doesn't match the issuer, and MCP clients that validate the Auth metadata stop at discovery. + +If your clients can use the project URL, `https://.supabase.co/functions/v1/mcp` needs no changes. To serve MCP on the custom domain too, set the Auth server explicitly: + +1. Point `authorizationServer` at an environment variable instead of letting it default to the request's domain: + + ```ts + [ + withOAuthProtectedResource({ authorizationServer: Deno.env.get('MCP_AUTH_ISSUER') }), + withSupabase({ auth: 'user' }), + ], + ``` + +2. Store the issuer as a secret, and redeploy: + + ```bash + supabase secrets set MCP_AUTH_ISSUER=https://.supabase.co/auth/v1 + supabase functions deploy mcp + ``` + +Leave `MCP_AUTH_ISSUER` unset locally. An unset value falls back to the default, which is correct on the local stack. The `resource` URL still follows the domain each client uses, so the project URL and the custom domain both keep working. + +To confirm the fix, compare `authorization_servers` from the resource metadata with `issuer` from Auth's discovery document. They must match: + +```bash +curl https://api.example.com/functions/v1/mcp/oauth-protected-resource +curl https://.supabase.co/auth/v1/.well-known/oauth-authorization-server +``` + ## Run it outside Edge Functions The same pipeline mounts in any runtime that speaks `Request` in, `Response` out: a Next.js route handler, a SvelteKit endpoint, Cloudflare Workers, or a plain Node, Bun, or Deno server. Off Edge Functions there are no forwarded headers to derive the public URLs from, so pass them explicitly: diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index 57bf5fb08aa..3ec9ff1b06d 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -102,6 +102,7 @@ Before you activate your domain, prepare your applications and integrations for - Supabase Auth will use the custom domain immediately once activated. - OAuth flows will advertise the custom domain as a callback URL. - SAML will use the custom domain instead. This means that the `EntityID` of your project has changed, and this may cause SAML with existing identity providers to stop working. +- MCP servers that use Supabase Auth need `authorizationServer` set explicitly to the project Auth issuer, not the custom domain. See [Use a custom domain](/docs/guides/ai-tools/byo-mcp#use-a-custom-domain) in the BYO-MCP guide. To prevent issues for your users, follow these steps: