Skip to content

Move the MCP server to the v2 transport packages #681

Description

@jrhizor

The MCP endpoint runs on @modelcontextprotocol/sdk v1.30 — McpServer plus
WebStandardStreamableHTTPServerTransport, driven directly:

// apps/web/src/lib/mcp/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";

The protocol has since split that package into @modelcontextprotocol/core,
@modelcontextprotocol/client, and @modelcontextprotocol/server, all at 2.0.0.
The v2 server offers createMcpHandler, which owns the HTTP shape — including
legacy: "reject" for clients still speaking the pre-Streamable transport.

Why this is a separate ticket

The better-auth 1.7 upgrade guide's MCP section says to adopt it:

Use the version 2 createMcpHandler with legacy: "reject", wrap it with
requireMcpAuth, and expose only POST.

That instruction is addressed to applications migrating off better-auth's
in-core MCP client and server adapters, which 1.7 deleted. We never used
those — we drive the official SDK ourselves — so nothing was removed under us
and #679 deliberately left this alone. Two further reasons it did not belong in
that PR:

  • requireMcpAuth cannot wrap our handler on any SDK version. It verifies a
    JWT against a JWKS URL, which the app cannot fetch when its public
    address is a published container port or a proxy, and it assumes a single
    credential where /api/mcp tries an API key first and an OAuth token second.
    verifyMcpAccessToken in packages/lib/src/auth/server.ts exists for exactly
    that reason and stays either way.
  • "Expose only POST" follows from the v2 handler's stateless mode. Our
    transport is already stateless (enableJsonResponse: true, no session id) and
    owns its own verb table, so GET and DELETE reach it and are answered per
    the protocol.

v1.30 is not deprecated and works: the e2e suite drives it with the official
client and the browser OAuth flow end to end. So this is upkeep, not a fix.

What it would touch

  • apps/web/src/lib/mcp/server.tsMcpServer and the transport
  • apps/web/src/lib/mcp/tools/* — 7 files; tool registration shape
  • apps/web/src/lib/mcp/__tests__/server.test.tsClient + InMemoryTransport
  • e2e/tests/shared/mcp.spec.tsClient + StreamableHTTPClientTransport
  • apps/web/src/routes/api/mcp.ts and api/mcp/$.ts — only if the verb table moves

Auth is untouched: resolveMcpAuth hands a Principal to handleMcpRequest,
and nothing below that boundary knows which SDK is underneath.

Worth checking while there

  • The v1 bundle is ~1 MB in the server output
    (.output/server/_libs/@modelcontextprotocol/sdk+[...].mjs). Whether the split
    packages shrink that is worth measuring, not assuming.
  • Whether v2 changes the negotiated protocol version, and whether that interacts
    with MCP 2026-07-28's Client ID Metadata Documents — better-auth offers
    cimd() for that, which we do not use (we run dynamic client registration
    instead, which the guide sanctions).

Done when

The e2e MCP suite passes unchanged — the official client connecting, listing
tools, reading a brand's numbers, and the full browser OAuth flow — with no
change to lib/mcp/auth.ts or packages/lib/src/auth/server.ts.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions