Skip to content

feat!: migrate published packages to MCP SDK v2 - #327

Open
raulb wants to merge 7 commits into
mainfrom
raulb/spike-mcp-v2
Open

feat!: migrate published packages to MCP SDK v2#327
raulb wants to merge 7 commits into
mainfrom
raulb/spike-mcp-v2

Conversation

@raulb

@raulb raulb commented Jul 9, 2026

Copy link
Copy Markdown
Member

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, PR 1 of 3 in the MCP SDK v2 migration plan. 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.

@raulb raulb self-assigned this Jul 9, 2026
@raulb raulb changed the title chore: update to mcp v2 chore: update to the stateless protocol Jul 9, 2026
@coveralls

coveralls commented Jul 9, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31484919388

Coverage decreased (-0.001%) to 96.512%

Details

  • Coverage decreased (-0.001%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 3030
Covered Lines: 2945
Line Coverage: 97.19%
Relevant Branches: 382
Covered Branches: 348
Branch Coverage: 91.1%
Branches in Coverage %: Yes
Coverage Strength: 49.82 hits per line

💛 - Coveralls

@raulb raulb changed the title chore: update to the stateless protocol chore: prepare update to the stateless protocol Jul 9, 2026
@barryroodt

barryroodt commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Following up on the next steps you left in the catchup notes while you're out, I went ahead with the regression-check half of the ship-strategy question.

Pushed two commits: a merge of latest main plus one small test fix. Plain push, no force, your commits are untouched.

The merge was textually clean, but #334 (hide tools) landed on main with one semantic break against this branch. Its new src/tools/util.test.ts imports from the v1 monolithic SDK (@modelcontextprotocol/sdk/client/index.js, .../types.js), which this branch removes, so typecheck fails right after merging. Looks like a simple timing overlap between the two PRs: the file just needed porting to the @modelcontextprotocol/client package and its typed callTool result, matching the pattern you already used in server.test.ts (+4/-7).

Results on the merged branch:

  • Build green across all three packages (ESM/CJS/DTS)
  • mcp-utils 12/12, mcp-server-supabase 193/198 locally, including the ported util.test.ts and the 110-test server.test.ts covering the hide-tools behavior on the v2 SDK
  • The 5 local failures are the LLM e2e tests missing ANTHROPIC_API_KEY, which CI supplies
  • The postgrest suite was not rerun locally because it needs the local Supabase stack at :54321. Worth knowing: CI's test:coverage only runs mcp-server-supabase, so the postgrest tests are not exercised in CI either. You had them green pre-merge and the merge only touched postgrest via lockfile, but a local supabase start run before undrafting would close that gap.

Two more small things I noticed along the way:

  • The failing check job is not this branch. The management API spec drifted upstream after main's last sync (some id fields removed from the SAML provider types), so regenerating types produces a diff on any PR right now. I verified by regenerating locally against the live spec. That fix belongs to the type-sync workflow on main, so I left it out of this branch.
  • The PR description says 2.0.0-beta.2 but the branch now pins 2.0.0-beta.3 (both client and server in the catalog). Just flagging so the handoff records the right version, might be worth a one-word edit to the description.

None of this blocks anything, it just answers the regression half of your open question. From what I can see the SDK swap behaves identically on top of latest main, so merging this independently looks safe once CI confirms the e2e runs. We still want the merge-vs-rebase call and undrafting to be your deliberate call rather than something decided while you're away, so I'll leave it here. Very open to redoing this as a rebase instead if you prefer that history 🙂

@raulb raulb assigned barryroodt and unassigned raulb Aug 4, 2026
raulb and others added 6 commits August 11, 2026 13:02
Move all three published packages from the v1 monolithic
@modelcontextprotocol/sdk to the split v2 packages,
@modelcontextprotocol/server and @modelcontextprotocol/client.

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, and ExtractResult, and
createMcpServer now returns a bare Server rather than
Server<Request, Notification, Result>, because v2's Server class takes no
type parameters.
The hide-tools test added in #334 imports from the v1 monolithic SDK
(@modelcontextprotocol/sdk), which this branch removes. Port it to the
v2 client package and its typed callTool result, matching server.test.ts.
Assert the full sorted tools/list name set, the server identity and
declared capabilities, and one ordinary read-only list_projects call
against a loopback management API stub, so the stable SDK pin flip has
goldens to survive.

Also correct the missing-token comment: the server behavior is
unchanged, only the message the test's own client renders differs
between SDK majors.
Move the catalog from the 2.0.0-beta.3 prerelease to the released
2.0.0 line, and keep the caret range the previous
'@modelcontextprotocol/sdk': ^1.25.2 entry had. The catalog value is
substituted into every published peerDependencies entry, so an exact
pin would hand consumers an unsatisfiable peer whenever they depend on
any other 2.x of @modelcontextprotocol/server.

Also repoint the README samples, which still imported the v1 monolithic
package that this migration removes.
@barryroodt barryroodt changed the title chore: prepare update to the stateless protocol feat!: migrate published packages to MCP SDK v2 Aug 11, 2026
@raul

raul commented Aug 11, 2026

Copy link
Copy Markdown

Haha, using this unintended mention as an excuse to send @raulb a big hug 🤗 💜

@barryroodt
barryroodt marked this pull request as ready for review August 11, 2026 12:24
@barryroodt
barryroodt requested a review from a team as a code owner August 11, 2026 12:24
`ExpandRecursively` existed only to widen the intellisense of
`ExtractRequest`/`ExtractNotification`/`ExtractResult`, which this branch
removes because v2's `Server` takes no type parameters. It is still public
through `export * from './types.js'`, so leaving it would publish a type with
nothing left to widen and make its later removal a second breaking release.

Both READMEs' first example imports `@modelcontextprotocol/client`, which no
published manifest declares. Under v1 that resolved for free because
`@modelcontextprotocol/sdk` was the declared peer and npm auto-installs peers;
the v2 split moved the client into its own package, so the example no longer
runs after following the install instructions. Name it in both.

BREAKING CHANGE: `@supabase/mcp-utils` also stops exporting the type
`ExpandRecursively`.
@barryroodt

barryroodt commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Pushed two small things on top of e3f194d, plain push, no force. Dropped ExpandRecursively, which lost its only consumer when the Extract* types went. And named @modelcontextprotocol/client in both READMEs, since their first example imports it and no manifest installs it.

One thing worth your eyes. Malformed tools/call params now return -32602 where v1 returned -32603. I kept the new code instead of reverting: it's what the 2025-11-25 spec asks for, and v1's -32603 was an uncaught ZodError falling through to InternalError rather than a deliberate choice. I extended the BREAKING CHANGE footer to cover it.

On risk, I read the v1 client plus five other MCP clients and none of them branch on the error code. Something bespoke still could, so if you know of a gateway or log alert that reads MCP error codes, worth checking before we publish. Detail and the decision record are in the migration plan.

@barryroodt

barryroodt commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Ran the postgrest suite against a live stack, since it had never been shown green here.

4 passed, 3 failed, and identical on both sides: same three snapshots, byte-identical received values at base 302d2ad and at head. So the SDK swap moves nothing in postgrest.

The three failures are stale snapshots rather than anything from this PR. resources > read pins PostgREST's OpenAPI document at 12.2.0 and the current image serves 13.0.4. Refreshing them wants its own PR, otherwise we put unexplained snapshot churn in the migration diff.

@raulb

raulb commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Haha, using this unintended mention as an excuse to send @raulb a big hug 🤗 💜

OMG @raul !!! You gotta love the internet sometimes! Un abrazo grande

@raulb

raulb commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

@barryroodt just reviewed the latest commits. All make sense to me 👍. Can't approve because I was the one who opened the PR 🙈

@barryroodt

Copy link
Copy Markdown
Contributor

Thanks @raulb - with your blessing I'll "Approve" this PR but won't merge until the stacked PR has been approved as well.

Only once both have merged will I create a new release. This is so that I can use the new release in the related hosted MCP update (PR still pending).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants