Server MCP su Next.js nel 2026: mcp-handler 2 e OAuth 2.1

Questa guida ti porta da zero a un server Model Context Protocol (MCP) pronto per la produzione che gira dentro un SaaS Next.js 16 esistente. Alla fine avrai una rotta che parla il protocollo stateless 2026-07-28, e lo Streamable HTTP dei client del 2025, con Claude, ChatGPT, Cursor e VS Code, un tool tipizzato che legge dal tuo database, OAuth 2.1 sul percorso caldo, un deploy che regge sul serverless senza uno store di sessione e una registrazione funzionante da testare oggi stesso.
Diamo per scontato che il prodotto sia già in produzione. Nessuna riscrittura. Il server MCP si monta accanto alle tue rotte API esistenti e riusa l'auth, il database e la pipeline di deploy che già fai girare.
Cosa ti serve prima di iniziare
- Next.js 16.0 o successivo, App Router.
- Node.js 22 o successivo. Node.js 20 è arrivato a fine vita il 30 aprile 2026 e Vercel l'ha deprecato il 1° ottobre 2026.
- Un progetto Vercel con Fluid Compute abilitato, oppure un server Node in grado di tenere aperte richieste di lunga durata.
- Redis solo se resti su mcp-handler 1.x. La versione 2 non tiene stato di sessione, quindi non le serve uno store.
- Un provider OAuth 2.1. Clerk, Better Auth, Auth.js e WorkOS spediscono tutti flussi consapevoli di MCP; farlo in casa va bene se ne hai già uno in produzione.
Step 1. Installa i pacchetti
Il pacchetto mcp-handler, mantenuto da Vercel, avvolge l'SDK TypeScript ufficiale del Model Context Protocol per i route handler di Next.js. La versione 2.0, uscita il 30 luglio 2026, poggia sull'SDK v2 e serve la specifica 2026-07-28. Installala insieme al pacchetto server dell'SDK e a Zod 4 per la validazione degli input dei tool.
npm install mcp-handler@^2 @modelcontextprotocol/server@^2 zod@^4L'SDK è l'implementazione canonica del protocollo; nella v2 @modelcontextprotocol/server prende il posto del vecchio @modelcontextprotocol/sdk. mcp-handler è l'adapter sottile che trasforma la definizione del server in un route handler Next.js, in un handler Nuxt o in un endpoint SvelteKit.
Step 2. Crea la rotta del trasporto
La specifica 2026-07-28 rende stateless il protocollo: l'handshake initialize e l'header Mcp-Session-Id non ci sono più, e ogni richiesta porta con sé versione del protocollo e capability del client. mcp-handler 2.x la serve in modo nativo e risponde dallo stesso handler ai client Streamable HTTP del 2025. Il vecchio trasporto HTTP+SSE è stato rimosso: /sse e /message rispondono 410 Gone, quindi un client che ha ancora bisogno di SSE ti tiene sulla 1.x finché non migra.
Crea 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, restituisce pong",
inputSchema: z.object({})
},
async () => ({ content: [{ type: "text", text: "pong" }] })
)
})
export { handler as GET, handler as POST }Esporta GET e POST. Le chiamate ai tool passano su POST; le operazioni di sessione GET e DELETE dei client del 2025 rispondono 405, ed è normale ora che nessuno tiene una sessione. Il percorso della rotta lo scegli tu: l'handler non lo legge mai, quindi ai client dai l'URL completo della rotta.
Step 3. Esponi un tool reale con uno schema tipizzato
I tool sono i verbi che il tuo SaaS consegna all'agente. Ognuno dichiara un nome, una descrizione che il modello leggerà, uno schema Zod a oggetto per gli argomenti e una funzione async che gira lato server a ogni chiamata. Nella v2 la chiamata è registerTool; il vecchio server.tool(...) variadico non esiste più.
server.registerTool(
"list-invoices",
{
description: "Restituisce le fatture più recenti del workspace corrente. Usalo quando l'utente chiede di vedere, elencare o rivedere le fatture.",
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) }] }
}
)Scrivi la descrizione come la scriverebbe l'utente al prompt. Il modello sceglie i tool leggendo queste stringhe, non leggendo la tua implementazione. Brevi, specifiche, guidate dal verbo. Gli schemi dichiarativi permettono al client di validare gli argomenti prima che tocchino il tuo codice, il che dimezza i failure mode.
Step 4. Collega OAuth 2.1
La specifica MCP 2026-07-28 richiede che i server remoti implementino OAuth 2.1 con PKCE (SHA-256) e pubblichino un documento di Protected Resource Metadata (RFC 9728) su /.well-known/oauth-protected-resource. Non spedire un server MCP remoto senza auth: gli audit del 2025 hanno trovato più di mille endpoint MCP pubblici esposti con auth debole o assente.
Avvolgi l'handler in withMcpAuth e un verifier di token:
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 spedisce un adapter withMcpAuth drop-in; Better Auth, Auth.js e WorkOS documentano ciascuno un flusso MCP. Se lo fai in casa, le parti critiche sono il documento di metadata well-known e la verifica PKCE SHA-256; la specifica vieta esplicitamente il PKCE plain. Il protectedResourceHandler di mcp-handler serve quel documento al posto tuo. La specifica 2026-07-28 depreca anche la Dynamic Client Registration a favore dei Client ID Metadata Document, che deve supportare il tuo authorization server, non la rotta MCP.
Step 5. Sopravvivere al serverless: Fluid Compute, e Redis solo sulla 1.x
Un tool call può durare secondi o minuti mentre la risposta resta aperta. Una funzione con un limite corto la taglia. Sulla v2 è l'unico problema di serverless rimasto.
Abilita Fluid Compute sul progetto. Permette a una singola istanza di servire richieste concorrenti, cosa che l'MCP sfrutta parecchio perché la maggior parte dei tool call è I/O-bound. Secondo i limiti delle funzioni di Vercel, sui piani Pro ed Enterprise il default è 300 secondi e si può salire a 800 (1.800 in beta). Imposta il limite nel file della rotta:
Niente Redis sulla v2. Senza sessione nel protocollo, ogni richiesta può arrivare su qualsiasi istanza, e le opzioni 1.x redisUrl, maxDuration e basePath sono state rimosse. Se un tool deve ricordare qualcosa fra una chiamata e l'altra, restituisci un handle esplicito (un id di job, un id di bozza) e lascia che il modello lo ripassi come argomento.
// app/api/mcp/route.ts
export const maxDuration = 300 // seconds; Pro allows up to 800Se resti sulla 1.x per un client SSE, Redis serve ancora: passa redisUrl a createMcpHandler e metti l'istanza Redis nella stessa regione del deploy, perché la 1.x smista il traffico SSE fra istanze con il pub/sub di Redis.
Step 6. Registra il server con un client
Per Claude Code, aggiungi il server al file .mcp.json del progetto:
{
"mcpServers": {
"your-saas": {
"type": "http",
"url": "https://your-saas.com/api/mcp"
}
}
}Claude esegue l'handshake OAuth al primo uso e cachea il token. Cursor, i Custom Connector di Claude.ai e quelli di ChatGPT accettano lo stesso formato di URL dalla loro UI di impostazioni.
Verificare che funzioni
- Manda
POST /api/mcpcon un Bearer token valido e una richiesta JSON-RPCtools/list. Dovresti ricevere l'elenco dei tuoi tool. - Esegui l'MCP Inspector con
npx @modelcontextprotocol/inspector. Connettiti al tuo URL, autenticati, invoca ogni tool. - Da Claude Code, digita
/mcpe conferma che il tuo server appare in verde. Attiva un tool e ispeziona gli argomenti che il modello ha mandato. - Stressalo con 20 sessioni Inspector concorrenti. La latency deve restare piatta; se sale, Fluid Compute è spento o il database sta in un'altra regione.
Fallimenti comuni e fix
- Stream ended unexpectedly. Il tool dura più del limite della funzione, oppure Fluid Compute è disabilitato. Alza
maxDurationnel file della rotta (fino a 800 secondi su Pro) e rideploy. - Loop di auth a ogni tool call.
withMcpAuthrestituisceundefinedper un token valido. Di solito l'audience o l'issuer del token non combaciano con i Protected Resource Metadata che pubblichi. Loggali entrambi e allineali. - Descrizioni dei tool ignorate dal modello. Sono scritte come commenti di codice anziché come prompt utente. Riscrivi ciascuna come una frase breve nella forma "fai X quando l'utente chiede Y".
- Sessioni che si resettano a metà flusso. Sulla 1.x manca
REDIS_URLin uno degli ambienti di deploy; preview e production devono avere la stessa variabile. Sulla 2.x non c'è nessuna sessione da resettare: porta lo stato in un handle restituito dal tool. - 410 Gone su
/sse. Il client usa ancora il trasporto HTTP+SSE che mcp-handler 2.x ha rimosso. Puntalo all'URL Streamable HTTP, oppure tieni quel client su un deploy 1.x mentre migra. - Errori di validazione Zod sul client. Lo schema del tool richiede un campo che il modello non sa di dover mandare. Aggiungi un
.default()o ristruttura la descrizione in modo che il campo richiesto emerga dal linguaggio naturale.
Per andare oltre
Una volta in piedi il server, il lavoro passa dal plumbing del protocollo al design dei tool. L'insieme dei tool che esponi diventa parte della superficie di prodotto, motivo per cui la domanda sulla curatela conta più di quella sul trasporto. Leggi il nostro pezzo su cos'è l'MCP e quando un SaaS ne ha bisogno per l'argomento di prodotto, e il nostro decision tree server versus client components per capire dove la rotta MCP si colloca in un codebase Next.js 16 che già si appoggia ai server component per i dati.
Aggiornato 2026-08-09: la specifica MCP attuale è ora la 2026-07-28, che rende stateless il core del protocollo (l'handshake initialize e l'header Mcp-Session-Id sono rimossi). Restano validi OAuth 2.1 con PKCE SHA-256 e i Protected Resource Metadata.
Aggiornato 2026-11-08: il codice ora usa mcp-handler 2.0 (uscito il 30 luglio 2026), che serve il protocollo stateless 2026-07-28, abbandona il trasporto SSE e Redis e usa registerTool; Node.js 22 sostituisce Node.js 20, ormai a fine vita, e i limiti delle funzioni seguono quelli attuali di Vercel, da 300 a 800 secondi.
Domande frequenti
No. Il server MCP si monta come route dinamica accanto alle tue route API esistenti e riusa l'auth, il database e la pipeline di deploy che già fai girare. La nuova route vive in /api/mcp/route.ts e, con mcp-handler 2.x, serve dallo stesso handler il protocollo stateless 2026-07-28 e lo Streamable HTTP dei client del 2025. Nessuna riscrittura, nessuna infrastruttura parallela, nessun deploy separato. Se il SaaS è su Next.js 16 App Router con Node 22+, il primo tool va online in un pomeriggio.
Sì per i server remoti. La specifica MCP 2026-07-28 richiede OAuth 2.1 con PKCE (SHA-256) e un Protected Resource Metadata document pubblicato su /.well-known/oauth-protected-resource. Il PKCE plain è esplicitamente vietato. Gli audit del 2025 hanno trovato più di mille endpoint MCP pubblici con auth debole o assente. Clerk, Better Auth, Auth.js e WorkOS hanno flussi MCP-aware integrati; farsi il proprio va bene se hai già un provider OAuth, ma il documento well-known è comunque obbligatorio.
Da mcp-handler 2.0 (30 luglio 2026) non ne ha più bisogno. La specifica 2026-07-28 ha tolto le sessioni dal protocollo, quindi ogni richiesta può arrivare su qualsiasi istanza serverless e l'handler non conserva nulla fra una chiamata e l'altra. Redis serve ancora con mcp-handler 1.x, che smista il vecchio trasporto SSE fra istanze con il pub/sub di Redis: passa l'URL a createMcpHandler({redisUrl}) e tieni Redis nella region del deploy. Con entrambe le versioni abilita Fluid Compute e imposta maxDuration (300 secondi di default su Pro, fino a 800).
Sì. MCP è un protocollo a livello di trasporto, non un protocollo proprietario. Lo stesso URL funziona per il .mcp.json di Claude Code, per i Custom Connectors di Claude.ai e ChatGPT, per Cursor, per VS Code. Ogni client esegue l'handshake OAuth al primo uso e mette in cache il token. Il server non sa quale client stia chiamando, e non ha bisogno di saperlo. Costruisci la superficie dei tool una sola volta, esponila a ogni agente MCP-aware che i tuoi utenti adottano.
SSE (HTTP+SSE, dalla specifica 2024-11-05) è il trasporto vecchio che teneva aperta una connessione; Streamable HTTP l'ha sostituito a marzo 2025 e la specifica 2026-07-28 l'ha reso stateless. mcp-handler 2.x serve solo Streamable HTTP: le richieste a /sse e /message rispondono 410 Gone. Esporta GET e POST da una sola route su /api/mcp. Se un client dipende ancora da SSE, tienilo su un deploy mcp-handler 1.x, con Redis, mentre migra.
Articoli correlati
19 aprile 2026 · 7 min di lettura
7 agosto 2026 · 8 min di lettura
2 novembre 2026 · 9 min di lettura