From e7d103291d3735049e7444697c9c66b8ac51d9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Barroso?= Date: Thu, 10 Sep 2026 13:19:12 +0200 Subject: [PATCH 1/2] docs: add MCP server guide and surface the OAuth entry point in the skill docs/mcp.md explains the pipeline form for MCP servers (withOAuthProtectedResource before the withSupabase gate), how the public URLs are derived on Edge Functions and configured elsewhere, the Supabase Auth prerequisites, the inline-handler inference gotcha, and the stateless limits. The agent skill gains the @supabase/server/oauth-protected-resource entry point and a docs-table row pointing at the new page. docs(mcp): drop the private sandbox link, match the guide title docs(mcp): address review on the MCP guide - CLI 2.117.0 injects SUPABASE_FUNCTION_SLUG only; SUPABASE_PUBLIC_URL is honored when set but not set by the CLI. - The asymmetric-key requirement comes from withSupabase verifying user JWTs against the JWKS, not from OAuth 2.1. - Document OAuthProtectedResourceConfig, UrlOption, fromSupabaseUrl and the escape hatches in api-reference.md, and link there. - Show withSupabase and a writing tool, the onerror hook for factory errors, the OPTIONS preflight row, the root-path-without-slug case, and that the one-shot limits belong to the handler, not the library. --- docs/api-reference.md | 56 +++++++++++++++ docs/mcp.md | 116 ++++++++++++++++++++++++++++++++ skills/supabase-server/SKILL.md | 40 +++++------ 3 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 docs/mcp.md diff --git a/docs/api-reference.md b/docs/api-reference.md index ef18cca..7487139 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -443,6 +443,62 @@ Defaults to the `SUPABASE_DB_URL` environment variable. --- +## @supabase/server/oauth-protected-resource + +> **Alpha.** The config shape, the contributed context key, and the metadata +> route may change in a minor release. + +Also re-exported from `@supabase/server`. See [`docs/mcp.md`](mcp.md) for the MCP server walkthrough. + +### withOAuthProtectedResource + +```ts +function withOAuthProtectedResource( + config?: OAuthProtectedResourceConfig, +): Entry<{ oauthProtectedResource: OAuthProtectedResourceContribution }> +function withOAuthProtectedResource(handler: FetchHandler): FetchHandler +function withOAuthProtectedResource( + config: OAuthProtectedResourceConfig, + handler: FetchHandler, +): FetchHandler +``` + +OAuth 2.1 Protected Resource behavior (RFC 9728) for the wrapped handler. Answers `GET` and `OPTIONS` on any path ending in `/oauth-protected-resource` with the metadata document and a permissive CORS preflight; adds `WWW-Authenticate: Bearer resource_metadata="…"` to a `401` from below unless the handler already set that header; passes everything else through. Runs before the `withSupabase` gate; placing it directly after `withSupabase` with a credentialed auth mode is refused when the stack is built. + +Contributes `ctx.oauthProtectedResource.resourceMetadataUrl`, the resolved absolute URL of the metadata document. + +### OAuthProtectedResourceConfig + +| Option | Type | Default on Supabase Edge Functions | Default elsewhere | +| --------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resourceServer` | `UrlOption` | Public origin from `X-Forwarded-*` (or `SUPABASE_PUBLIC_URL`) + `/functions/v1/{SUPABASE_FUNCTION_SLUG}` | None. Throws `MissingResourceServerError` (`MISSING_RESOURCE_SERVER`). | +| `authorizationServer` | `UrlOption` | Public origin + `/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each + `/auth/v1`. Throws `MissingAuthorizationServerError` (`MISSING_AUTHORIZATION_SERVER`) if neither is set. | + +`UrlOption` is `string | ((req: Request) => string)`. Without `SUPABASE_FUNCTION_SLUG` the resource path is reconstructed from the request path with `/functions/v1` restored; a request at the root path with no slug throws `MissingResourceServerError`. + +### fromSupabaseUrl + +```ts +function fromSupabaseUrl(supabaseUrl: string): string +``` + +Turns a project URL (`https://.supabase.co`) into its Auth issuer (`…/auth/v1`) for `authorizationServer`. Tolerates a value that already carries the `/auth/v1` path. + +### resourceMetadataResponse / unauthorizedResponse + +```ts +function resourceMetadataResponse( + req: Request, + options?: { resource?: string; authorizationServers?: string[] }, +): Response +function unauthorizedResponse( + req: Request, + options?: { resourceMetadataUrl?: string }, +): Response +``` + +The building blocks behind the middleware, for custom routing. Defaults derive from the request as above. + ## Types ### AuthMode diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..2fcbe13 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,116 @@ +# MCP servers + +> **Alpha.** `withOAuthProtectedResource` and composing `withSupabase` as a +> `pipeline` entry track `@supabase/middleware` 0.x. The config shape, the +> contributed context key, and the metadata route may change in a minor +> release. The nested `withOAuthProtectedResource(withSupabase(config, handler))` +> form is stable. + +An MCP server your app exposes to its users is an HTTP endpoint that MCP clients (Claude, ChatGPT, Cursor, VS Code, Claude Code) call after an OAuth 2.1 flow. `@supabase/server` covers the two Supabase-specific parts: OAuth discovery for the client, and turning the user's token into an RLS-scoped Supabase client for your tools. The MCP transport and the tools come from any MCP library. + +```ts +import { pipeline } from '@supabase/middleware' +import { withOAuthProtectedResource, withSupabase } from '@supabase/server' +import { createMcpHandler, McpServer } from '@modelcontextprotocol/server' +import { z } from 'zod/v4' +import type { Database } from './database.types' + +export default { + fetch: pipeline( + [withOAuthProtectedResource(), withSupabase({ auth: 'user' })], + async (req, { supabase }) => { + const handler = createMcpHandler( + () => { + const server = new McpServer({ name: 'todos', version: '0.1.0' }) + server.registerTool( + 'create_todo', + { + description: 'Create a todo', + inputSchema: z.object({ title: z.string() }), + }, + async ({ title }) => { + const { data, error } = await supabase + .from('todos') + .insert({ title }) + .select() + .single() + if (error) throw new Error(error.message) + return { content: [{ type: 'text', text: JSON.stringify(data) }] } + }, + ) + return server + }, + // Factory errors (duplicate tool name, failed schema fetch) surface as a + // bare 500 otherwise; log them so they reach the function logs. + { onerror: (error) => console.error('MCP request failed', error) }, + ) + return handler.fetch(req) + }, + ), +} +``` + +Pass the `Database` generic (from `supabase gen types typescript`) when tools write. Without it the client is `SupabaseClient` and `insert({ title })` fails type-checking with the argument resolved to `never[]`; reads compile either way. + +## The two entries + +Order matters. + +`withOAuthProtectedResource()` runs **before** the auth gate, so it sees unauthenticated requests. It does two things: + +| Request | Response | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `GET {resource}/oauth-protected-resource` | RFC 9728 Protected Resource Metadata (`resource`, `authorization_servers`) | +| `OPTIONS {resource}/oauth-protected-resource` | `204` with permissive CORS headers, so browser-based clients can read the document cross-origin | +| Any response from below with status `401` | Adds `WWW-Authenticate: Bearer resource_metadata="…"` unless the handler already set one | + +That header is how a client that hit a `401` finds the metadata, and through it the authorization server, without guessing URLs. It is generic OAuth middleware; nothing in it is MCP-specific. + +`withSupabase({ auth: 'user' })` is the gate. Requests without a valid user JWT get a `401` (which the entry above enriches); requests with one reach the handler with `ctx.supabase` scoped to that user, so RLS applies to everything the tools do. + +The handler is passed inline. Passing a separately declared function typed `(req: Request, ctx: SupabaseContext) => Promise` makes TypeScript collapse the inferred context to `object` (TS2345); wrap it as `(req, ctx) => handleMcp(req, ctx)` instead. + +## URLs + +On Supabase Edge Functions the metadata is derived with no configuration: the public origin from the gateway's `X-Forwarded-*` headers (`SUPABASE_PUBLIC_URL` wins when set, but the CLI does not set it), the path as `/functions/v1/{SUPABASE_FUNCTION_SLUG}`, and the issuer as `{origin}/auth/v1`. Supabase CLI 2.117.0 or later injects the slug locally. Without a slug the path is reconstructed from the request, which works for a function served at `/functions/v1/{name}`; a function served at the root path `/` with no slug throws `MISSING_RESOURCE_SERVER`, because there is no function segment to restore. + +Anywhere else, a Next.js route handler, a Worker, a plain server, the app's origin is unrelated to the project's, so pass both URLs: + +```ts +import { withOAuthProtectedResource, fromSupabaseUrl } from '@supabase/server' + +withOAuthProtectedResource({ + resourceServer: (req) => new URL(req.url).origin + '/api/mcp', + authorizationServer: fromSupabaseUrl('https://.supabase.co'), +}) +``` + +- `resourceServer`: the public URL of this endpoint. Required off Edge Functions; `MissingResourceServerError` (`MISSING_RESOURCE_SERVER`) otherwise. +- `authorizationServer`: the OAuth issuer. Falls back to `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each with `/auth/v1`; `MissingAuthorizationServerError` (`MISSING_AUTHORIZATION_SERVER`) if neither is set. `fromSupabaseUrl(projectUrl)` builds it from a project URL. A non-Supabase OAuth 2.1 server (Clerk, WorkOS, Auth0) works too. + +Both accept a string or `(req: Request) => string` (`UrlOption`). The full config: + +| Option | Type | Default on Edge Functions | Default elsewhere | +| --------------------- | ----------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `resourceServer` | `UrlOption` | `{public origin}/functions/v1/{slug}` | none; throws `MissingResourceServerError` | +| `authorizationServer` | `UrlOption` | `{public origin}/auth/v1` | `SUPABASE_PUBLIC_URL`, then `SUPABASE_URL`, each `+ /auth/v1`; else throws `MissingAuthorizationServerError` | + +The middleware contributes `ctx.oauthProtectedResource.resourceMetadataUrl`, the resolved metadata URL, to the downstream context. + +## Supabase Auth prerequisites + +The project must have the OAuth 2.1 server enabled, dynamic client registration on (MCP clients register themselves), an asymmetric signing key (ES256 or RS256), and a consent screen hosted in the app's frontend. The signing key requirement comes from this library, not from OAuth: `withSupabase({ auth: 'user' })` verifies user JWTs against the project JWKS and rejects legacy HS256 tokens (see [`docs/auth-modes.md`](auth-modes.md#legacy-keys-and-jwts-are-not-supported)). On Edge Functions set `verify_jwt = false` for the function in `config.toml`; the gate does the verification, and the gateway must let the unauthenticated discovery request through. + +## Escape hatches + +For custom routing, the pieces behind the middleware are exported on `@supabase/server/oauth-protected-resource`: `resourceMetadataResponse(req, options?)` returns the metadata document, `unauthorizedResponse(req, options?)` returns a `401` with the `WWW-Authenticate` header. + +## Limits + +The one-request, one-response shape above is the handler's, not the library's: building a `McpServer` per request with `createMcpHandler` gives Streamable HTTP in its simplest form, with no server-initiated stream, so no MCP sampling and no elicitations that need an open channel. `@supabase/server` does not constrain the transport; a stateful transport composes the same way. + +## See also + +- [Deploy MCP servers](https://supabase.com/docs/guides/ai-tools/byo-mcp) on supabase.com, the end-to-end guide +- [MCP Server block](https://supabase.com/library/docs/headless/mcp-server), an installable Edge Function built on this +- [`docs/api-reference.md`](api-reference.md#withoauthprotectedresource) for `OAuthProtectedResourceConfig` and the error catalogue diff --git a/skills/supabase-server/SKILL.md b/skills/supabase-server/SKILL.md index 9cbb266..ee79c48 100644 --- a/skills/supabase-server/SKILL.md +++ b/skills/supabase-server/SKILL.md @@ -34,11 +34,12 @@ Server-side utilities for Supabase. Handles auth, client creation, and context i ## Entry points -| Import | Deno / Edge Functions | Provides | -| -------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| `@supabase/server` | `npm:@supabase/server` | `withSupabase`, `createSupabaseContext`, types, errors | -| `@supabase/server/core` | `npm:@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `resolveEnv`, `createContextClient`, `createAdminClient` | -| `@supabase/server/adapters/hono` | `npm:@supabase/server/adapters/hono` | `withSupabase` (Hono middleware variant) | +| Import | Deno / Edge Functions | Provides | +| ------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@supabase/server` | `npm:@supabase/server` | `withSupabase`, `createSupabaseContext`, types, errors | +| `@supabase/server/core` | `npm:@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `resolveEnv`, `createContextClient`, `createAdminClient` | +| `@supabase/server/adapters/hono` | `npm:@supabase/server/adapters/hono` | `withSupabase` (Hono middleware variant) | +| `@supabase/server/oauth-protected-resource` | `npm:@supabase/server/oauth-protected-resource` | **Alpha.** `withOAuthProtectedResource`, `fromSupabaseUrl`, `resourceMetadataResponse`, `unauthorizedResponse` — OAuth 2.1 discovery for MCP servers; see `docs/mcp.md` | ## Quick starts @@ -413,17 +414,18 @@ The full documentation lives in the `docs/` directory of the `@supabase/server` - **If working inside the SDK repo:** `docs/` is at the project root. - **If the package is installed as a dependency:** look in `node_modules/@supabase/server/docs/`. -| Question | Doc file | -| ------------------------------------------------------------------- | ------------------------------- | -| How do I create a basic endpoint? | `docs/getting-started.md` | -| What auth modes are available? Array syntax? Named keys? | `docs/auth-modes.md` | -| Which framework adapters exist? How do I contribute one? | `src/adapters/README.md` | -| How do I use this with Hono? | `docs/adapters/hono.md` | -| How do I use this with H3 / Nuxt? | `docs/adapters/h3.md` | -| How do I use low-level primitives for custom flows? | `docs/core-primitives.md` | -| How do environment variables work across runtimes? | `docs/environment-variables.md` | -| How do I handle errors? What codes exist? | `docs/error-handling.md` | -| How do I get typed database queries? | `docs/typescript-generics.md` | -| How do I use this with `@supabase/ssr` (Next.js, SvelteKit, Remix)? | `docs/ssr-frameworks.md` | -| What's the complete API surface? | `docs/api-reference.md` | -| What security decisions does this package make? | `docs/security.md` | +| Question | Doc file | +| ------------------------------------------------------------------------------------- | ------------------------------- | +| How do I create a basic endpoint? | `docs/getting-started.md` | +| What auth modes are available? Array syntax? Named keys? | `docs/auth-modes.md` | +| Which framework adapters exist? How do I contribute one? | `src/adapters/README.md` | +| How do I use this with Hono? | `docs/adapters/hono.md` | +| How do I use this with H3 / Nuxt? | `docs/adapters/h3.md` | +| How do I use low-level primitives for custom flows? | `docs/core-primitives.md` | +| How do environment variables work across runtimes? | `docs/environment-variables.md` | +| How do I handle errors? What codes exist? | `docs/error-handling.md` | +| How do I get typed database queries? | `docs/typescript-generics.md` | +| How do I use this with `@supabase/ssr` (Next.js, SvelteKit, Remix)? | `docs/ssr-frameworks.md` | +| How do I build an MCP server my users connect to (OAuth discovery, RLS-scoped tools)? | `docs/mcp.md` | +| What's the complete API surface? | `docs/api-reference.md` | +| What security decisions does this package make? | `docs/security.md` | From 6595eabf617071e8b19331065c5fca24da49dc08 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 11 Sep 2026 18:03:43 +0300 Subject: [PATCH 2/2] docs: list mcp.md in README and complete the metadata row --- README.md | 67 +++++++++++++++++++++++++++-------------------------- docs/mcp.md | 12 +++++----- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3c5fdff..4ae311f 100644 --- a/README.md +++ b/README.md @@ -531,42 +531,43 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like ## Exports -| Export | What's in it | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `@supabase/server` | `withSupabase`, `createSupabaseContext` | -| `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` | -| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) | -| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) | -| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) | -| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) | -| `@supabase/server/middleware/client` | **Alpha.** `withSupabaseClient` (RLS-scoped `ctx.supabase` client) | -| `@supabase/server/middleware/admin-client` | **Alpha.** `withSupabaseAdminClient` (`ctx.supabaseAdmin`, bypasses RLS) | -| `@supabase/server/middleware/claims` | **Alpha.** `withClaims` (JWKS-verified `ctx.jwtClaims`) | -| `@supabase/server/middleware/required-claims` | **Alpha.** `withRequiredClaims` (user-mode auth gate, non-null `ctx.jwtClaims`) | -| `@supabase/server/middleware/postgres` | **Alpha.** `withPostgresClient` (RLS-scoped `ctx.postgres` client) | -| `@supabase/server/middleware/postgres-admin` | **Alpha.** `withPostgresAdminClient` (`ctx.postgresAdmin`, bypasses RLS) | -| `@supabase/server/oauth-protected-resource` | **Alpha.** `withOAuthProtectedResource`, `fromSupabaseUrl`, `resourceMetadataResponse`, `unauthorizedResponse` | -| `@supabase/server/peer/supabase-js` | Re-exported `supabase-js` types (`SupabaseClient`, `PostgrestError`, …) | +| Export | What's in it | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `@supabase/server` | `withSupabase`, `createSupabaseContext` | +| `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` | +| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) | +| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) | +| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) | +| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) | +| `@supabase/server/middleware/client` | **Alpha.** `withSupabaseClient` (RLS-scoped `ctx.supabase` client) | +| `@supabase/server/middleware/admin-client` | **Alpha.** `withSupabaseAdminClient` (`ctx.supabaseAdmin`, bypasses RLS) | +| `@supabase/server/middleware/claims` | **Alpha.** `withClaims` (JWKS-verified `ctx.jwtClaims`) | +| `@supabase/server/middleware/required-claims` | **Alpha.** `withRequiredClaims` (user-mode auth gate, non-null `ctx.jwtClaims`) | +| `@supabase/server/middleware/postgres` | **Alpha.** `withPostgresClient` (RLS-scoped `ctx.postgres` client) | +| `@supabase/server/middleware/postgres-admin` | **Alpha.** `withPostgresAdminClient` (`ctx.postgresAdmin`, bypasses RLS) | +| `@supabase/server/oauth-protected-resource` | **Alpha.** `withOAuthProtectedResource`, `fromSupabaseUrl`, `resourceMetadataResponse`, `unauthorizedResponse`; see [`docs/mcp.md`](docs/mcp.md) | +| `@supabase/server/peer/supabase-js` | Re-exported `supabase-js` types (`SupabaseClient`, `PostgrestError`, …) | ## Documentation -| Question | Doc file | -| ------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| How do I create a basic endpoint? | [`docs/getting-started.md`](docs/getting-started.md) | -| What auth modes are available? Array syntax? Named keys? | [`docs/auth-modes.md`](docs/auth-modes.md) | -| Which framework adapters exist? How do I contribute one? | [`src/adapters/README.md`](src/adapters/README.md) | -| How do I use this with Hono? | [`docs/adapters/hono.md`](docs/adapters/hono.md) | -| How do I use this with H3 / Nuxt? | [`docs/adapters/h3.md`](docs/adapters/h3.md) | -| How do I use this with Elysia? | [`docs/adapters/elysia.md`](docs/adapters/elysia.md) | -| How do I use this with NestJS? | [`docs/adapters/nestjs.md`](docs/adapters/nestjs.md) | -| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) | -| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) | -| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) | -| How do I get typed database queries? | [`docs/typescript-generics.md`](docs/typescript-generics.md) | -| How do I run raw SQL scoped to the caller by RLS? | [`docs/postgres.md`](docs/postgres.md) | -| How do I use this with `@supabase/ssr` (Next.js, SvelteKit, Remix)? | [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) | -| What's the complete API surface? | [`docs/api-reference.md`](docs/api-reference.md) | -| Does this library support legacy API keys or HS256 JWTs? | [`docs/auth-modes.md`](docs/auth-modes.md#legacy-keys-and-jwts-are-not-supported) | +| Question | Doc file | +| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| How do I create a basic endpoint? | [`docs/getting-started.md`](docs/getting-started.md) | +| What auth modes are available? Array syntax? Named keys? | [`docs/auth-modes.md`](docs/auth-modes.md) | +| Which framework adapters exist? How do I contribute one? | [`src/adapters/README.md`](src/adapters/README.md) | +| How do I use this with Hono? | [`docs/adapters/hono.md`](docs/adapters/hono.md) | +| How do I use this with H3 / Nuxt? | [`docs/adapters/h3.md`](docs/adapters/h3.md) | +| How do I use this with Elysia? | [`docs/adapters/elysia.md`](docs/adapters/elysia.md) | +| How do I use this with NestJS? | [`docs/adapters/nestjs.md`](docs/adapters/nestjs.md) | +| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) | +| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) | +| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) | +| How do I get typed database queries? | [`docs/typescript-generics.md`](docs/typescript-generics.md) | +| How do I run raw SQL scoped to the caller by RLS? | [`docs/postgres.md`](docs/postgres.md) | +| How do I use this with `@supabase/ssr` (Next.js, SvelteKit, Remix)? | [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md) | +| How do I build an MCP server my users connect to (OAuth discovery, RLS-scoped tools)? | [`docs/mcp.md`](docs/mcp.md) | +| What's the complete API surface? | [`docs/api-reference.md`](docs/api-reference.md) | +| Does this library support legacy API keys or HS256 JWTs? | [`docs/auth-modes.md`](docs/auth-modes.md#legacy-keys-and-jwts-are-not-supported) | ## Development diff --git a/docs/mcp.md b/docs/mcp.md index 2fcbe13..0bb225f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -58,13 +58,13 @@ Order matters. `withOAuthProtectedResource()` runs **before** the auth gate, so it sees unauthenticated requests. It does two things: -| Request | Response | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| `GET {resource}/oauth-protected-resource` | RFC 9728 Protected Resource Metadata (`resource`, `authorization_servers`) | -| `OPTIONS {resource}/oauth-protected-resource` | `204` with permissive CORS headers, so browser-based clients can read the document cross-origin | -| Any response from below with status `401` | Adds `WWW-Authenticate: Bearer resource_metadata="…"` unless the handler already set one | +| Request | Response | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `GET {resource}/oauth-protected-resource` | RFC 9728 Protected Resource Metadata (`resource`, `authorization_servers`, `bearer_methods_supported`) | +| `OPTIONS {resource}/oauth-protected-resource` | `204` with permissive CORS headers, so browser-based clients can read the document cross-origin | +| Any response from below with status `401` | Adds `WWW-Authenticate: Bearer resource_metadata="…"` unless the handler already set one | -That header is how a client that hit a `401` finds the metadata, and through it the authorization server, without guessing URLs. It is generic OAuth middleware; nothing in it is MCP-specific. +That header is how a client that hit a `401` finds the metadata, and through it the authorization server, without guessing URLs. It is generic OAuth middleware; the only MCP-specific detail is that the preflight allows the `mcp-protocol-version` request header. `withSupabase({ auth: 'user' })` is the gate. Requests without a valid user JWT get a `401` (which the entry above enriches); requests with one reach the handler with `ctx.supabase` scoped to that user, so RLS applies to everything the tools do.