Skip to content

Commit ead56f2

Browse files
authored
feat!: migrate published packages to MCP SDK v2 (#327)
## What kind of change does this PR introduce? Moves the three published packages off the v1 monolithic `@modelcontextprotocol/sdk` and onto the split v2 packages, `@modelcontextprotocol/server` + `@modelcontextprotocol/client`, on the released `2.0.0` line. Part of [AI-1044](https://linear.app/supabase/issue/AI-1044/migrate-mcp-server-supabase-to-mcp-sdk-v2), PR 1 of 3 in the [MCP SDK v2 migration plan](https://linear.app/supabase/document/mcp-sdk-v2-migration-plan-d19bb064d673). Rebased off @raulb 's original beta.3 work onto current `main`. ## What is the current behavior? `@supabase/mcp-utils`, `@supabase/mcp-server-supabase`, and `@supabase/mcp-server-postgrest` depend on `@modelcontextprotocol/sdk: ^1.25.2`, which predates the 2026-07-28 spec. ## What is the new behavior? Same wire behavior apart from one malformed-input error code, covered under **Wire delta** below. Nothing about serving changed: - `mcp-utils` registers its five handlers by method string (`tools/call`, `tools/list`, `resources/list`, `resources/templates/list`, `resources/read`) instead of by Zod request schema. v2 still validates and narrows each request through its codec, so this is a signature change, not a validation change. - `createMcpServer` returns a bare `Server`. v2's `Server` takes no type parameters, so the `ExtractRequest` / `ExtractNotification` / `ExtractResult` machinery that existed only to widen intellisense had nothing left to widen. - `mcp-server-postgrest` is compile-only, as the plan requires: imports moved, stdio entry and tool surface untouched. - No `serveStdio`, no HTTP handler, no era negotiation. Those are PR 2. ## Additional context **Catalog range.** The catalog entries are `^2.0.0` rather than an exact `2.0.0`. `peerDependencies` in all three packages is `"catalog:"`, and `pnpm publish` substitutes the catalog value verbatim, so an exact pin would ship consumers a peer they cannot satisfy alongside any other 2.x of `@modelcontextprotocol/server`. The old `^1.25.2` entry was a range for the same reason. The lockfile still resolves to exactly `2.0.0`. The plan's Task 1 Step 3 has been updated to record the caret range as the accepted value, so this is no longer a deviation from it. **Wire delta: malformed `tools/call` params return `-32602`, not `-32603`.** This is the one observable change between the two builds, and it is accepted as a spec correction rather than reverted. Full risk analysis is in the PR comments and recorded in the plan's PR 1 acceptance gate. In short: the 2025-11-25 spec classifies a request failing `CallToolRequest` schema validation as a Protocol Error with a worked `-32602` example, and the v2 SDK reserves `-32603` for a handler throwing something that is not a `ProtocolError`. v1's `-32603` came from an uncaught `ZodError` falling back to `InternalError`, so it was a side effect rather than a deliberate choice. `error instanceof McpError` still holds, the class and `name` are unchanged, and the failure is still a JSON-RPC error rather than an `isError` result. Only `.code` and the message prefix move. Type-checked callers cannot produce it, but untyped ones can: v1's `Client.callTool(params)` types `params` without runtime-validating `name` before it sends (`dist/esm/client/index.js:488-493`), so plain JavaScript, or TypeScript that lost the type through `any`, reaches this path through the ordinary public API. Note the separate case that did **not** change: arguments failing a tool's own zod schema still surface as an `isError` text result. **Verification.** Two independent checks, because the in-repo tests upgraded their own client alongside the server and so cannot prove backward compatibility on their own. 1. Wire goldens, committed before the pin flip and green on both sides of it: the full sorted 29-name `tools/list` set, server identity and declared capabilities, and one ordinary `list_projects` call against a loopback management API stub on port 0. Fully offline. 2. A throwaway probe holding the **client** fixed at v1 (`@modelcontextprotocol/sdk@1.30.0`, what a user on `^1.25.2` installs today) and varying only the server, driving `dist/transports/stdio.js` built from `main` and from this branch: ``` OLD serverInfo: {"name":"supabase","title":"Supabase","version":"0.10.0"} NEW serverInfo: {"name":"supabase","title":"Supabase","version":"0.10.0"} OLD capabilities: {"tools":{}} NEW capabilities: {"tools":{}} OLD tool count: 29 NEW tool count: 29 OLD list_projects result == NEW list_projects result (byte-for-byte) VERDICT: IDENTICAL ``` The missing-access-token path is identical too. `test/stdio.integration.ts` now asserts `Connection closed` where it used to assert `MCP error -32000`, and that is the test's own client upgrade rather than a server change: v1's error class prefixed the message with `MCP error ${code}: `, v2's passes it through bare. Held against the fixed v1 client, both builds return `MCP error -32000: Connection closed`. **Test results on `2.0.0`.** `mcp-utils` 12/12, `mcp-server-supabase` 215/215 across unit + integration, `format:check` clean, all three packages typecheck and build. Two suites need external services and fail identically before and after the pin flip: `mcp-server-postgrest/src/server.test.ts` wants a local Supabase stack on `127.0.0.1:54321` (7 failures, all `fetch failed`), and `test/e2e/*` wants `ANTHROPIC_API_KEY`. Worth noting `tests.yml` runs `test:coverage`, which filters to `@supabase/mcp-server-supabase`, so `mcp-utils` and `mcp-server-postgrest` only run locally. **Merging is not shipping.** `release.yml` runs release-please on push to `main`, which only opens a release PR. Hold that release PR until PR 3 is approved and merge-ready, per the plan's release gate. Platform develops against a `publish-preview` build in the meantime. BREAKING CHANGE: the peer dependency is now `@modelcontextprotocol/server` instead of `@modelcontextprotocol/sdk`, so consumers must install the new package. `@supabase/mcp-utils` also drops the exported types `ExtractRequest`, `ExtractNotification`, `ExtractResult` and `ExpandRecursively`, and `createMcpServer` returns a bare `Server` rather than `Server<Request, Notification, Result>`, because v2's `Server` class takes no type parameters. Because that returned value is now a v2 `Server`, a consumer who registered extra handlers on it must rewrite `server.setRequestHandler(SomeRequestSchema, ...)` as `server.setRequestHandler('some/method', ...)`; the v1 Zod-schema overload no longer exists. `InitData.clientCapabilities` now follows v2's `ClientCapabilities`, which is narrower than v1's and not assignable from it. Finally, a `tools/call` request whose `params` are malformed, meaning no `name` key or a non-string `name`, now returns JSON-RPC `-32602` with the message prefix `Invalid tools/call request: ` where v1 returned `-32603` with a bare stringified ZodError. --------- Co-authored-by: Barry Roodt <barry.roodt@supabase.io>
1 parent 8512069 commit ead56f2

19 files changed

Lines changed: 295 additions & 744 deletions

File tree

packages/mcp-server-postgrest/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,14 @@ pnpm add @supabase/mcp-server-postgrest
9292

9393
#### Example
9494

95-
The following example uses the [`StreamTransport`](../mcp-utils#streamtransport) to connect directly between an MCP client and server.
95+
The following example uses the [`StreamTransport`](../mcp-utils#streamtransport) to connect directly between an MCP client and server. It also needs `@modelcontextprotocol/client`, which is a separate package from the `@modelcontextprotocol/server` peer dependency and is not installed for you:
96+
97+
```bash
98+
npm i @modelcontextprotocol/client
99+
```
96100

97101
```ts
98-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
102+
import { Client } from '@modelcontextprotocol/client';
99103
import { StreamTransport } from '@supabase/mcp-utils';
100104
import { createPostgrestMcpServer } from '@supabase/mcp-server-postgrest';
101105

packages/mcp-server-postgrest/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@
3737
"@supabase/sql-to-rest": "^0.1.8"
3838
},
3939
"peerDependencies": {
40-
"@modelcontextprotocol/sdk": "catalog:",
40+
"@modelcontextprotocol/server": "catalog:",
4141
"zod": "catalog:"
4242
},
4343
"devDependencies": {
44-
"@modelcontextprotocol/sdk": "catalog:",
44+
"@modelcontextprotocol/client": "catalog:",
45+
"@modelcontextprotocol/server": "catalog:",
4546
"@supabase/auth-js": "^2.67.3",
4647
"@total-typescript/tsconfig": "^1.0.4",
4748
"@types/node": "^22.8.6",

packages/mcp-server-postgrest/src/server.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1+
import { Client } from '@modelcontextprotocol/client';
22
import { AuthClient } from '@supabase/auth-js';
33
import { StreamTransport } from '@supabase/mcp-utils';
44
import { describe, expect, test } from 'vitest';
5+
56
import { createPostgrestMcpServer } from './server.js';
67

78
// Requires local Supabase stack running

packages/mcp-server-postgrest/src/stdio.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
2-
3-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
42
import { parseArgs } from 'node:util';
3+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
4+
55
import { createPostgrestMcpServer } from './server.js';
66

77
async function main() {

packages/mcp-server-supabase/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,15 @@
6060
"openapi-fetch": "^0.13.5"
6161
},
6262
"peerDependencies": {
63-
"@modelcontextprotocol/sdk": "catalog:",
63+
"@modelcontextprotocol/server": "catalog:",
6464
"zod": "catalog:"
6565
},
6666
"devDependencies": {
6767
"@ai-sdk/anthropic": "catalog:",
6868
"@ai-sdk/mcp": "catalog:",
6969
"@electric-sql/pglite": "^0.2.17",
70-
"@modelcontextprotocol/sdk": "catalog:",
70+
"@modelcontextprotocol/client": "catalog:",
71+
"@modelcontextprotocol/server": "catalog:",
7172
"@total-typescript/tsconfig": "^1.0.4",
7273
"@types/common-tags": "^1.8.4",
7374
"@types/node": "^22.8.6",

packages/mcp-server-supabase/src/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1+
import { Client } from '@modelcontextprotocol/client';
22
import { StreamTransport } from '@supabase/mcp-utils';
33
import { describe, expect, test } from 'vitest';
4+
45
import {
56
ACCESS_TOKEN,
67
API_URL,

packages/mcp-server-supabase/src/management-api/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10082,6 +10082,13 @@ export interface operations {
1008210082
"application/openmetrics-text": string;
1008310083
};
1008410084
};
10085+
/** @description Project must be active and healthy, or metrics are not available for this project */
10086+
400: {
10087+
headers: {
10088+
[name: string]: unknown;
10089+
};
10090+
content?: never;
10091+
};
1008510092
/** @description Unauthorized */
1008610093
401: {
1008710094
headers: {

packages/mcp-server-supabase/src/server.test.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2-
import {
3-
CallToolResultSchema,
4-
type CallToolRequest,
5-
} from '@modelcontextprotocol/sdk/types.js';
1+
import { Client } from '@modelcontextprotocol/client';
2+
import type { CallToolRequestParams } from '@modelcontextprotocol/client';
63
import { StreamTransport } from '@supabase/mcp-utils';
74
import { codeBlock, stripIndent } from 'common-tags';
85
import gqlmin from 'gqlmin';
96
import { http, HttpResponse } from 'msw';
107
import { setupServer } from 'msw/node';
118
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
129
import { globalRegistry } from 'zod/v4';
10+
1311
import {
1412
ACCESS_TOKEN,
1513
API_URL,
@@ -102,9 +100,9 @@ async function setup(options: SetupOptions = {}) {
102100
*
103101
* Wrapper around the `client.callTool` method to handle the response and errors.
104102
*/
105-
async function callTool(params: CallToolRequest['params']) {
103+
async function callTool(params: CallToolRequestParams) {
106104
const output = await client.callTool(params);
107-
const { content } = CallToolResultSchema.parse(output);
105+
const { content } = output;
108106
const [textContent] = content;
109107

110108
if (!textContent) {
@@ -3893,7 +3891,7 @@ describe('tools', () => {
38933891
arguments: { schemas: ['public'] },
38943892
});
38953893

3896-
const result = CallToolResultSchema.parse(resultUntyped);
3894+
const result = resultUntyped;
38973895
const firstContent = result.content.at(0);
38983896
if (!firstContent) {
38993897
throw new Error('Expected content in tool response');

packages/mcp-server-supabase/src/tools/util.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2-
import {
3-
CallToolResultSchema,
4-
type CallToolRequest,
5-
} from '@modelcontextprotocol/sdk/types.js';
1+
import { Client } from '@modelcontextprotocol/client';
2+
import type { CallToolRequestParams } from '@modelcontextprotocol/client';
63
import { createMcpServer, StreamTransport } from '@supabase/mcp-utils';
74
import { describe, expect, test } from 'vitest';
85
import { z } from 'zod/v4';
@@ -38,9 +35,9 @@ async function setup(tools: Record<string, ReturnType<typeof injectableTool>>) {
3835
*
3936
* Wrapper around the `client.callTool` method to handle the response and errors.
4037
*/
41-
async function callTool(params: CallToolRequest['params']) {
38+
async function callTool(params: CallToolRequestParams) {
4239
const output = await client.callTool(params);
43-
const { content } = CallToolResultSchema.parse(output);
40+
const { content } = output;
4441
const [textContent] = content;
4542

4643
if (!textContent || textContent.type !== 'text') {

packages/mcp-server-supabase/src/transports/stdio.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
2-
3-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
42
import { parseArgs } from 'node:util';
3+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
4+
55
import packageJson from '../../package.json' with { type: 'json' };
66
import { createSupabaseApiPlatform } from '../platform/api-platform.js';
77
import { createSupabaseMcpServer } from '../server.js';

0 commit comments

Comments
 (0)