Skip to content

Next.js MCP server in 2026: mcp-handler 2 and OAuth 2.1

By Cassini · AI · April 22, 2026 · 9 min read · Updated November 8, 2026

A dark app window with a lit port on its right edge; a line from it passes a lit lock, with a stack of pills hung below, to a large lit tile; four dashed branches end in hollow rings

This guide takes you from zero to a production-ready Model Context Protocol (MCP) server running inside an existing Next.js 16 SaaS. By the end you will have a route that speaks the stateless 2026-07-28 protocol, and 2025-era Streamable HTTP, to Claude, ChatGPT, Cursor, and VS Code, a typed tool that reads your database, OAuth 2.1 on the hot path, a deployment that holds up on serverless without a session store, and a working registration you can test against today.

We assume the product already ships. No rewrites. The MCP server mounts alongside your existing API routes and reuses the auth, database, and deployment pipeline you already run.

What you need before starting

  • Next.js 16.0 or later, App Router.
  • Node.js 22 or later. Node.js 20 reached end of life on 30 April 2026, and Vercel deprecated it on 1 October 2026.
  • A Vercel project with Fluid Compute enabled, or a Node server that can hold open long-running requests.
  • Redis only if you stay on mcp-handler 1.x. Version 2 keeps no session state, so it needs no store.
  • An OAuth 2.1 provider. Clerk, Better Auth, Auth.js, and WorkOS all ship MCP-aware flows; rolling your own is fine if you already run one.

Step 1. Install the packages

The Vercel-maintained mcp-handler wraps the official Model Context Protocol TypeScript SDK for Next.js route handlers. Version 2.0, released on 30 July 2026, is built on SDK v2 and serves the 2026-07-28 specification. Install it alongside the SDK's server package and Zod 4 for tool-input validation.

npm install mcp-handler@^2 @modelcontextprotocol/server@^2 zod@^4

The SDK is the canonical implementation of the protocol; @modelcontextprotocol/server replaces the old @modelcontextprotocol/sdk package in v2. mcp-handler is the thin adapter that turns a server definition into a Next.js route handler, a Nuxt handler, or a SvelteKit endpoint.

Step 2. Create the transport route

The 2026-07-28 specification makes the protocol stateless: the initialize handshake and the Mcp-Session-Id header are gone, and every request carries its protocol version and client capabilities. mcp-handler 2.x serves that natively and answers 2025-era Streamable HTTP clients from the same handler. The old HTTP+SSE transport is removed: /sse and /message return 410 Gone, so a client that still needs SSE keeps you on 1.x until it moves.

Create src/app/api/mcp/route.ts:

import { createMcpHandler } from "mcp-handler"
import { z } from "zod"

const handler = createMcpHandler((server) => {
  server.registerTool(
    "ping",
    {
      description: "Health check, returns pong",
      inputSchema: z.object({})
    },
    async () => ({ content: [{ type: "text", text: "pong" }] })
  )
})

export { handler as GET, handler as POST }

Export GET and POST. Tool calls travel over POST; the GET and DELETE session operations of 2025-era clients answer 405, which is expected now that nothing holds a session. The route path is yours to choose: the handler never reads it, so clients get the route's full URL.

Step 3. Expose a real tool with a typed schema

Tools are the verbs your SaaS hands to the agent. Each one declares a name, a description the model will read, a Zod object schema for arguments, and an async function that runs server-side on every call. In v2 the call is registerTool; the variadic server.tool(...) is gone.

server.registerTool(
  "list-invoices",
  {
    description: "Return the most recent invoices for the current workspace. Use when the user asks to see, list, or review invoices.",
    inputSchema: z.object({
      limit: z.number().int().min(1).max(100).default(25)
    })
  },
  async ({ limit }, ctx) => {
    const workspaceId = ctx.http?.authInfo?.extra?.workspaceId
    const rows = await db
      .from("invoices")
      .select("id, number, total, issued_at")
      .eq("workspace_id", workspaceId)
      .order("issued_at", { ascending: false })
      .limit(limit)
    return { content: [{ type: "text", text: JSON.stringify(rows.data) }] }
  }
)

Write the description the way a user would prompt it. The model selects tools by reading these strings, not by reading your implementation. Short, specific, verb-led. Declarative schemas let the client validate arguments before they ever reach your code, which cuts failure modes in half.

Step 4. Wire OAuth 2.1

The MCP 2026-07-28 specification requires remote servers to implement OAuth 2.1 with PKCE (SHA-256) and publish a Protected Resource Metadata document (RFC 9728) at /.well-known/oauth-protected-resource. Do not ship a remote MCP server without it. 2025 audits found more than a thousand public MCP endpoints exposed with weak or absent auth.

Wrap the handler in withMcpAuth and a token verifier:

import type { AuthInfo } from "@modelcontextprotocol/server"
import { withMcpAuth } from "mcp-handler"
import { verifyToken } from "@/lib/auth"

const auth = async (_req: Request, token?: string): Promise<AuthInfo | undefined> => {
  if (!token) return undefined
  const session = await verifyToken(token)
  if (!session) return undefined
  return {
    token,
    clientId: session.clientId,
    scopes: session.scopes,
    extra: { workspaceId: session.workspaceId, userId: session.userId }
  }
}

const authHandler = withMcpAuth(handler, auth, {
  required: true,
  resourceMetadataPath: "/.well-known/oauth-protected-resource"
})

export { authHandler as GET, authHandler as POST }

Clerk ships a drop-in withMcpAuth adapter; Better Auth, Auth.js, and WorkOS each document an MCP flow. If you roll your own, the critical parts are the well-known metadata document and SHA-256 PKCE verification; the specification explicitly forbids plain PKCE. mcp-handler's protectedResourceHandler serves that metadata document for you. The 2026-07-28 specification also deprecates Dynamic Client Registration in favour of Client ID Metadata Documents, which your authorization server, not the MCP route, has to support.

Step 5. Survive serverless: Fluid Compute, and Redis only on 1.x

A tool call can run for seconds or minutes while the response stays open. A function with a short limit cuts it off. On v2 that is the only serverless problem left.

Enable Fluid Compute on the project. It lets a single instance serve concurrent requests, which MCP exploits heavily because most tool calls are I/O-bound. Per Vercel's function limits, Pro and Enterprise functions default to 300 seconds and can go to 800 (1,800 in beta). Set the limit in the route file:

No Redis on v2. With no session in the protocol, any request can land on any instance, and the 1.x options redisUrl, maxDuration, and basePath are removed. If a tool needs state across calls, return an explicit handle (a job id, a draft id) and let the model pass it back as an argument.

// app/api/mcp/route.ts
export const maxDuration = 300 // seconds; Pro allows up to 800

If you stay on 1.x for an SSE client, Redis is still required: pass redisUrl to createMcpHandler and place the Redis instance in the same region as the deployment, because 1.x relays SSE traffic between instances through Redis pub/sub.

Step 6. Register the server with a client

For Claude Code, add the server to the project's .mcp.json:

{
  "mcpServers": {
    "your-saas": {
      "type": "http",
      "url": "https://your-saas.com/api/mcp"
    }
  }
}

Claude performs the OAuth handshake on first use and caches the token. Cursor, Claude.ai Custom Connectors, and ChatGPT Custom Connectors accept the same URL shape through their settings UI.

Verifying it works

  1. Send POST /api/mcp with a valid Bearer token and a tools/list JSON-RPC request. You should receive the list of your tools.
  2. Run the MCP Inspector with npx @modelcontextprotocol/inspector. Connect to your URL, authenticate, and invoke every tool.
  3. From Claude Code, type /mcp and confirm your server appears green. Trigger a tool and inspect the arguments the model sent.
  4. Stress it with 20 concurrent Inspector sessions. Latency should stay flat; if it climbs, Fluid Compute is off or the database sits in another region.

Common failures and fixes

  • Stream ended unexpectedly. The tool runs longer than the function's limit, or Fluid Compute is disabled. Raise maxDuration in the route file (up to 800 seconds on Pro) and redeploy.
  • Auth loops on every tool call. withMcpAuth returns undefined for a valid token. Usually the token audience or issuer does not match the Protected Resource Metadata you publish. Log both and align them.
  • Tool descriptions ignored by the model. They are written like code comments instead of user prompts. Rewrite each as a short sentence in the shape "do X when the user asks Y".
  • Sessions reset mid-flow. On 1.x, REDIS_URL is missing in one of your deployment environments; preview and production need the same variable. On 2.x there is no session to reset: carry state in a handle the tool returns.
  • 410 Gone on /sse. The client still uses the HTTP+SSE transport that mcp-handler 2.x removed. Point it at the Streamable HTTP URL, or keep that client on a 1.x deployment while it migrates.
  • Zod validation errors at the client. The tool schema requires a field the model does not know to send. Add a .default(), or restructure the description so the required field is obvious from natural language.

Going further

Once the server is up, the work shifts from protocol plumbing to tool design. The set of tools you expose becomes part of your product surface area, which is why the curation question matters more than the transport question. See our piece on what MCP is and when a SaaS needs one for the product argument, and our server versus client components decision tree for where the MCP route fits in a Next.js 16 codebase that already relies on server components for data.

Sources

Updated 2026-08-09: the current MCP specification is now 2026-07-28, which makes the protocol core stateless (the initialize handshake and the Mcp-Session-Id header are removed). OAuth 2.1 with SHA-256 PKCE and Protected Resource Metadata still apply.

Updated 2026-11-08: the code now targets mcp-handler 2.0 (released 30 July 2026), which serves the stateless 2026-07-28 protocol, drops the SSE transport and Redis, and uses registerTool; Node.js 22 replaces Node.js 20, now past end of life, and function limits follow Vercel's current 300 to 800 seconds.

Frequently asked questions

Rather have it done for you?

We design and build this kind of work for clients. Tell us what you need.