Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,33 @@ Needs `pg` installed (optional peer dependency) and a raw TCP socket: Node, Deno

See [`docs/postgres.md`](docs/postgres.md) for standalone composition with `withClaims`, the grants requirement, and current limits.

## MCP tools from your schema

> **Alpha.** `@supabase/server/mcp` tracks `@modelcontextprotocol/server` 2.x; generated tool names, schemas and annotations may change in a minor release.

`generateTools` reads the OpenAPI description PostgREST publishes for the caller and builds one MCP tool per operation — `list_`, `get_`, `create_`, `update_`, `delete_` for every table and view, one tool per database function — with descriptions from `COMMENT ON`. `registerTools` hands them to the official MCP SDK. Tools run through `ctx.supabase`, so RLS applies.

```ts
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'
import { withOAuthProtectedResource, withSupabase } from '@supabase/server'
import { generateTools, registerTools } from '@supabase/server/mcp'

Deno.serve(
withOAuthProtectedResource(
withSupabase({ auth: 'user' }, async (req, { supabase }) => {
const handler = createMcpHandler(async () => {
const server = new McpServer({ name: 'notes-mcp', version: '0.1.0' })
registerTools(server, await generateTools(supabase))
return server
})
return handler.fetch(req)
}),
),
)
```

Requires `@modelcontextprotocol/server` (optional peer) and `@supabase/supabase-js` 2.115.0+. See [`docs/mcp.md`](docs/mcp.md) for what is generated, annotations, filtering, and limitations.

## Environment Variables

Automatically available in Supabase Edge Functions:
Expand Down Expand Up @@ -546,6 +573,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
| `@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/mcp` | **Alpha.** `generateTools`, `registerTools` (MCP tools generated from the PostgREST schema) |
| `@supabase/server/peer/supabase-js` | Re-exported `supabase-js` types (`SupabaseClient`, `PostgrestError`, …) |

## Documentation
Expand All @@ -564,6 +592,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
| 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 generate MCP tools from my schema? | [`docs/mcp.md`](docs/mcp.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) |
Expand Down
125 changes: 103 additions & 22 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,64 @@ Defaults to the `SUPABASE_DB_URL` environment variable.

---

## @supabase/server/mcp

> **Alpha.** `@supabase/server/mcp` tracks `@modelcontextprotocol/server` 2.x.
> Generated tool names, input schemas and annotations may change in a minor
> release. Everything else in `@supabase/server` is stable.

Requires `@modelcontextprotocol/server` `^2.0.0` (optional peer dependency) and `@supabase/supabase-js` 2.115.0 or newer. See [`mcp.md`](mcp.md).

### generateTools

```ts
function generateTools<Database = unknown>(
supabase: SupabaseClient<Database>,
): Promise<Record<string, GeneratedTool>>
```

Fetches the OpenAPI description PostgREST publishes for the client's schema through `supabase.getOpenApiSpec()` — carrying the caller's token, so it describes what the caller's role can reach — and derives one tool per operation: `list_`, `get_`, `create_`, `update_` and `delete_` per table or view, and one tool per database function at `/rpc/<name>`. Descriptions come from `COMMENT ON`. Tools run through the same client, so Row Level Security applies.

Returns a record keyed by tool name. `tool.name` is authoritative; the key is an index.

Throws `ToolGenerationError` with code `SPEC_FETCH_FAILED` when the description cannot be read, or `TOOL_NAME_COLLISION` when two operations produce the same name.

### registerTools

```ts
function registerTools(
server: Pick<McpServer, 'registerTool'>,
tools: Record<string, GeneratedTool>,
): void
```

Calls `server.registerTool(name, config, handler)` for every tool. Each `inputSchema` is wrapped with the SDK's `fromJsonSchema()`, so the SDK validates arguments and advertises the schema in `tools/list`. `_meta` is not forwarded. A name the server already has surfaces as the SDK's own error.

### GeneratedTool

```ts
interface GeneratedTool {
name: string
description: string
inputSchema: Record<string, unknown> // JSON Schema
annotations: ToolAnnotations // from @modelcontextprotocol/server
_meta: ToolMeta
handler: (args: Record<string, unknown>) => Promise<CallToolResult>
}
```

### ToolMeta

```ts
interface ToolMeta {
kind: 'relation' | 'function'
name: string // the table, view, or function
method: 'GET' | 'POST' | 'PATCH' | 'DELETE'
}
```

---

## Types

### AuthMode
Expand Down Expand Up @@ -691,6 +749,21 @@ class AuthError extends SupabaseServerError {
}
```

### ToolGenerationError

```ts
class ToolGenerationError extends SupabaseServerError {
readonly status: 500
constructor(
message: string,
code?: string,
options?: SupabaseServerErrorOptions,
)
}
```

Thrown by `generateTools()` (`@supabase/server/mcp`).

### ErrorPayload

The JSON body every auto-responding layer returns, and the return type of `toJSON()`.
Expand Down Expand Up @@ -721,28 +794,31 @@ interface SupabaseServerErrorOptions {

## Error Code Constants

| Constant | Value | Class | Meaning |
| ----------------------------------- | ----------------------------------- | ----------- | -------------------------------------------------------------------- |
| `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error |
| `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set |
| `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found |
| `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key |
| `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found |
| `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key |
| `MissingResourceServerError` | `'MISSING_RESOURCE_SERVER'` | `EnvError` | `withOAuthProtectedResource` cannot derive a `resourceServer` |
| `MissingAuthorizationServerError` | `'MISSING_AUTHORIZATION_SERVER'` | `EnvError` | `withOAuthProtectedResource` cannot derive an authorization server |
| `MissingConnectionStringError` | `'MISSING_CONNECTION_STRING'` | `EnvError` | No Postgres connection string configured |
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error (401) |
| `MissingCredentialsError` | `'MISSING_CREDENTIALS'` | `AuthError` | Request carried no credentials at all (401) |
| `UnusableCredentialError` | `'UNUSABLE_CREDENTIAL'` | `AuthError` | A credential arrived but cannot be used (401) |
| `InvalidApiKeyError` | `'INVALID_API_KEY'` | `AuthError` | `apikey` matched no configured key (401) |
| `InvalidJwtError` | `'INVALID_JWT'` | `AuthError` | JWT failed verification (401) |
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | Fallback credential failure (401) |
| `JwksNotConfiguredError` | `'JWKS_NOT_CONFIGURED'` | `AuthError` | JWT sent but no JWKS configured (500) |
| `JwksFetchFailedError` | `'JWKS_FETCH_FAILED'` | `AuthError` | Remote JWKS unreachable or unusable (500) |
| `NoKeysConfiguredError` | `'NO_KEYS_CONFIGURED'` | `AuthError` | Auth mode no configured key can match (500) |
| `UnsupportedRoleError` | `'UNSUPPORTED_ROLE'` | `AuthError` | `withPostgresClient` will not assume the caller's `role` claim (500) |
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth (500) |
| Constant | Value | Class | Meaning |
| ----------------------------------- | ----------------------------------- | --------------------- | -------------------------------------------------------------------- |
| `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error |
| `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set |
| `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found |
| `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key |
| `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found |
| `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key |
| `MissingResourceServerError` | `'MISSING_RESOURCE_SERVER'` | `EnvError` | `withOAuthProtectedResource` cannot derive a `resourceServer` |
| `MissingAuthorizationServerError` | `'MISSING_AUTHORIZATION_SERVER'` | `EnvError` | `withOAuthProtectedResource` cannot derive an authorization server |
| `MissingConnectionStringError` | `'MISSING_CONNECTION_STRING'` | `EnvError` | No Postgres connection string configured |
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error (401) |
| `MissingCredentialsError` | `'MISSING_CREDENTIALS'` | `AuthError` | Request carried no credentials at all (401) |
| `UnusableCredentialError` | `'UNUSABLE_CREDENTIAL'` | `AuthError` | A credential arrived but cannot be used (401) |
| `InvalidApiKeyError` | `'INVALID_API_KEY'` | `AuthError` | `apikey` matched no configured key (401) |
| `InvalidJwtError` | `'INVALID_JWT'` | `AuthError` | JWT failed verification (401) |
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | Fallback credential failure (401) |
| `JwksNotConfiguredError` | `'JWKS_NOT_CONFIGURED'` | `AuthError` | JWT sent but no JWKS configured (500) |
| `JwksFetchFailedError` | `'JWKS_FETCH_FAILED'` | `AuthError` | Remote JWKS unreachable or unusable (500) |
| `NoKeysConfiguredError` | `'NO_KEYS_CONFIGURED'` | `AuthError` | Auth mode no configured key can match (500) |
| `UnsupportedRoleError` | `'UNSUPPORTED_ROLE'` | `AuthError` | `withPostgresClient` will not assume the caller's `role` claim (500) |
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth (500) |
| `ToolGenerationGenericError` | `'TOOL_GENERATION_ERROR'` | `ToolGenerationError` | Generic tool generation error (500) |
| `SpecFetchFailedError` | `'SPEC_FETCH_FAILED'` | `ToolGenerationError` | PostgREST OpenAPI description could not be read (500) |
| `ToolNameCollisionError` | `'TOOL_NAME_COLLISION'` | `ToolGenerationError` | Two operations produce the same tool name (500) |

Also exported: `ErrorSource` (`'@supabase/server'`) and `ErrorCodeHeader` (`'x-supabase-server-error'`).

Expand Down Expand Up @@ -781,6 +857,11 @@ const Errors: {
supportedRoles
}) => AuthError
[CreateSupabaseClientError]: (options?: { cause?: unknown }) => AuthError
[SpecFetchFailedError]: (failure: SpecFetchFailure) => ToolGenerationError
[ToolNameCollisionError]: (context: {
name: string
operations: readonly string[]
}) => ToolGenerationError
}
```

Expand Down
36 changes: 35 additions & 1 deletion docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ The status code and the `x-supabase-server-error` header are unaffected, and `me
Error
└── SupabaseServerError ← catch this for anything from @supabase/server
├── EnvError ← always status 500
└── AuthError ← status 401 or 500
├── AuthError ← status 401 or 500
└── ToolGenerationError ← always status 500
```

```ts
Expand Down Expand Up @@ -261,6 +262,38 @@ Set `SUPABASE_DB_URL`, or pass `connectionString` to the middleware — `details

Generic environment error. The default code when constructing an `EnvError` yourself.

## ToolGenerationError codes

Thrown by `generateTools()` from `@supabase/server/mcp` when MCP tools cannot be generated from the PostgREST description. Always `status: 500` — neither cause is the caller's fault.

| Code | Meaning |
| ------------------------------------------------- | --------------------------------------------------- |
| [`SPEC_FETCH_FAILED`](#spec_fetch_failed) | The PostgREST OpenAPI description could not be read |
| [`TOOL_NAME_COLLISION`](#tool_name_collision) | Two generated operations produce the same tool name |
| [`TOOL_GENERATION_ERROR`](#tool_generation_error) | Generic tool generation error |

### `SPEC_FETCH_FAILED`

`supabase.getOpenApiSpec()` did not return a Swagger 2.0 document. The `hint` branches on what happened:

- **404 or 406** — PostgREST is not serving an OpenAPI description. OpenAPI output is disabled on the project's Data API (`openapi-mode`), or the client URL does not point at a PostgREST endpoint.
- **401 or 403** — PostgREST rejected the credentials. The Supabase client must carry a valid API key and, for caller-scoped generation, the caller's access token.
- **0** — the request never reached PostgREST. Check the project URL and network connectivity.
- **A body without `swagger` and `definitions`** — another service answered, or OpenAPI output is disabled.
- **No `getOpenApiSpec` method** — the `@supabase/supabase-js` client predates 2.115.0. Upgrade.

`details.status` carries the HTTP status when there was a response; `cause` carries the `PostgrestError`.

### `TOOL_NAME_COLLISION`

Two operations would produce the same tool name — typically a database function named exactly like a generated relation tool, such as a function `list_notes` next to a table `notes`. Generation fails rather than silently replacing one.

Tool names are one namespace across tables, views and functions. Rename the database function, or revoke the role's privilege on one of the two so it leaves the description. `details.name` is the colliding name and `details.operations` names both operations.

### `TOOL_GENERATION_ERROR`

Generic tool generation error. The default code when constructing a `ToolGenerationError` yourself.

## How errors surface in each layer

| Function | Pattern | What happens on error |
Expand All @@ -276,6 +309,7 @@ Generic environment error. The default code when constructing an `EnvError` your
| `createContextClient()` | **Throws** | Throws `EnvError` |
| `createAdminClient()` | **Throws** | Throws `EnvError` |
| `withOAuthProtectedResource()` | **Throws** | Throws `EnvError` when required off Edge Functions and unconfigured |
| `generateTools()` | **Throws** | Throws `ToolGenerationError` (`@supabase/server/mcp`) |
| Hono `withSupabase()` | HTTPException | Throws `HTTPException` with `cause: AuthError` |

`verifyAuth()` also has the raw request in hand, so it adds diagnostics `verifyCredentials()` can't see — most usefully, an `Authorization` header that was present but unusable.
Expand Down
Loading
Loading