feat!: migrate published packages to MCP SDK v2 - #327
Conversation
Coverage Report for CI Build 31484919388Coverage decreased (-0.001%) to 96.512%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
|
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 Results on the merged branch:
Two more small things I noticed along the way:
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 🙂 |
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.
a6733a1 to
e3f194d
Compare
|
Haha, using this unintended mention as an excuse to send @raulb a big hug 🤗 💜 |
`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`.
|
Pushed two small things on top of One thing worth your eyes. Malformed 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. |
|
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 The three failures are stale snapshots rather than anything from this PR. |
|
@barryroodt just reviewed the latest commits. All make sense to me 👍. Can't approve because I was the one who opened the PR 🙈 |
|
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). |
What kind of change does this PR introduce?
Moves the three published packages off the v1 monolithic
@modelcontextprotocol/sdkand onto the split v2 packages,@modelcontextprotocol/server+@modelcontextprotocol/client, on the released2.0.0line.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-postgrestdepend 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-utilsregisters 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.createMcpServerreturns a bareServer. v2'sServertakes no type parameters, so theExtractRequest/ExtractNotification/ExtractResultmachinery that existed only to widen intellisense had nothing left to widen.mcp-server-postgrestis compile-only, as the plan requires: imports moved, stdio entry and tool surface untouched.serveStdio, no HTTP handler, no era negotiation. Those are PR 2.Additional context
Catalog range. The catalog entries are
^2.0.0rather than an exact2.0.0.peerDependenciesin all three packages is"catalog:", andpnpm publishsubstitutes 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.2entry was a range for the same reason. The lockfile still resolves to exactly2.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/callparams 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 failingCallToolRequestschema validation as a Protocol Error with a worked-32602example, and the v2 SDK reserves-32603for a handler throwing something that is not aProtocolError. v1's-32603came from an uncaughtZodErrorfalling back toInternalError, so it was a side effect rather than a deliberate choice.error instanceof McpErrorstill holds, the class andnameare unchanged, and the failure is still a JSON-RPC error rather than anisErrorresult. Only.codeand the message prefix move. Type-checked callers cannot produce it, but untyped ones can: v1'sClient.callTool(params)typesparamswithout runtime-validatingnamebefore it sends (dist/esm/client/index.js:488-493), so plain JavaScript, or TypeScript that lost the type throughany, 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 anisErrortext 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.
tools/listset, server identity and declared capabilities, and one ordinarylist_projectscall against a loopback management API stub on port 0. Fully offline.@modelcontextprotocol/sdk@1.30.0, what a user on^1.25.2installs today) and varying only the server, drivingdist/transports/stdio.jsbuilt frommainand from this branch:The missing-access-token path is identical too.
test/stdio.integration.tsnow assertsConnection closedwhere it used to assertMCP error -32000, and that is the test's own client upgrade rather than a server change: v1's error class prefixed the message withMCP error ${code}:, v2's passes it through bare. Held against the fixed v1 client, both builds returnMCP error -32000: Connection closed.Test results on
2.0.0.mcp-utils12/12,mcp-server-supabase215/215 across unit + integration,format:checkclean, 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.tswants a local Supabase stack on127.0.0.1:54321(7 failures, allfetch failed), andtest/e2e/*wantsANTHROPIC_API_KEY. Worth notingtests.ymlrunstest:coverage, which filters to@supabase/mcp-server-supabase, somcp-utilsandmcp-server-postgrestonly run locally.Merging is not shipping.
release.ymlruns release-please on push tomain, 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 apublish-previewbuild in the meantime.BREAKING CHANGE: the peer dependency is now
@modelcontextprotocol/serverinstead of@modelcontextprotocol/sdk, so consumers must install the new package.@supabase/mcp-utilsalso drops the exported typesExtractRequest,ExtractNotification,ExtractResultandExpandRecursively, andcreateMcpServerreturns a bareServerrather thanServer<Request, Notification, Result>, because v2'sServerclass takes no type parameters. Because that returned value is now a v2Server, a consumer who registered extra handlers on it must rewriteserver.setRequestHandler(SomeRequestSchema, ...)asserver.setRequestHandler('some/method', ...); the v1 Zod-schema overload no longer exists.InitData.clientCapabilitiesnow follows v2'sClientCapabilities, which is narrower than v1's and not assignable from it. Finally, atools/callrequest whoseparamsare malformed, meaning nonamekey or a non-stringname, now returns JSON-RPC-32602with the message prefixInvalid tools/call request:where v1 returned-32603with a bare stringified ZodError.