AI and Automation

Come costruire un server MCP per un SaaS Next.js esistente

Una guida pragmatica per spedire un server MCP su un SaaS Next.js 16 in produzione: pacchetti, rotte, auth, stato di sessione e i fallimenti tipici.

22 aprile 20267 min di lettura
Come costruire un server MCP per un SaaS Next.js esistente

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 dinamica che parla Streamable HTTP a Claude, ChatGPT, Cursor e VS Code, un tool tipizzato che legge dal tuo database, OAuth 2.1 sul percorso caldo, stato di sessione appoggiato a Redis che sopravvive al serverless 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 20 o successivo.
  • Un progetto Vercel con Fluid Compute abilitato, oppure un server Node in grado di tenere aperte richieste di lunga durata.
  • Upstash Redis (o qualsiasi store compatibile con Redis) per coordinare le sessioni fra istanze serverless.
  • 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. Installalo insieme all'SDK e a Zod per la validazione degli input dei tool.

npm install mcp-handler @modelcontextprotocol/sdk zod

L'SDK è l'implementazione canonica del protocollo. 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 dinamica del trasporto

I server MCP parlano due trasporti. Streamable HTTP è il default dalla revisione delle specifiche di marzo 2025; SSE resta supportato per i client più vecchi. Un singolo segmento dinamico li gestisce entrambi.

Crea src/app/api/mcp/[transport]/route.ts:

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

const handler = createMcpHandler(
  (server) => {
    server.tool(
      "ping",
      "Health check, restituisce pong",
      {},
      async () => ({ content: [{ type: "text", text: "pong" }] })
    )
  },
  {},
  {
    basePath: "/api/mcp",
    streamableHttpEndpoint: "/mcp",
    sseEndpoint: "/sse"
  }
)

export { handler as GET, handler as POST, handler as DELETE }

Esporta GET, POST e DELETE. Streamable HTTP usa POST per le chiamate ai tool e GET per la ripresa dello stream; DELETE chiude in modo pulito la sessione. Se ne manca uno dei tre, alcuni client falliscono in silenzio.

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 per gli argomenti e una funzione async che gira lato server a ogni chiamata.

server.tool(
  "list-invoices",
  "Restituisce le fatture più recenti del workspace corrente. Usalo quando l'utente chiede di vedere, elencare o rivedere le fatture.",
  { limit: z.number().int().min(1).max(100).default(25) },
  async ({ limit }, { authInfo }) => {
    const rows = await db
      .from("invoices")
      .select("id, number, total, issued_at")
      .eq("workspace_id", authInfo.extra.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 2025-11-25 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 { withMcpAuth } from "mcp-handler"
import { verifyToken } from "@/lib/auth"

const auth = async (_req: Request, token: string | 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 }
  }
}

export const POST = withMcpAuth(handler, auth, { required: true })

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.

Step 5. Sopravvivere al serverless: Redis e Fluid Compute

Streamable HTTP tiene aperta una risposta mentre un tool call gira. Una funzione serverless Vercel di default la taglia. Due modifiche rimuovono il problema.

Abilita Fluid Compute sul progetto. Estende la vita dell'invocazione a 15 minuti e 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.

Aggiungi una Redis URL così l'handler può coordinarsi fra istanze. Senza, una sessione che inizia su una funzione non può riprendere su un'altra e i client cadono a metà chiamata.

const handler = createMcpHandler(
  register,
  {},
  {
    basePath: "/api/mcp",
    redisUrl: process.env.REDIS_URL,
    maxDuration: 900
  }
)

In produzione, piazza Upstash Redis nella stessa regione del tuo deploy. L'handler usa Redis pub/sub fra le chiamate GET e POST, quindi la latency qui si vede in ogni invocazione di tool.

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/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

  1. Manda POST /api/mcp/mcp con un Bearer token valido e il body JSON-RPC di initialize. Dovresti ricevere un oggetto capabilities che elenca i tuoi tool.
  2. Esegui l'MCP Inspector con npx @modelcontextprotocol/inspector. Connettiti al tuo URL, autenticati, invoca ogni tool.
  3. Da Claude Code, digita /mcp e conferma che il tuo server appare in verde. Attiva un tool e ispeziona gli argomenti che il modello ha mandato.
  4. Stressalo con 20 sessioni Inspector concorrenti. La latency deve restare piatta; se sale, la regione di Redis è sbagliata o Fluid Compute è spento.

Fallimenti comuni e fix

  • Stream ended unexpectedly. Fluid Compute è disabilitato o maxDuration è sotto i 300 secondi. Portalo a 900 e rideploy.
  • Loop di auth a ogni tool call. withMcpAuth restituisce undefined per 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. REDIS_URL manca in uno degli ambienti di deploy. Controlla la dashboard Vercel; preview e production devono avere la stessa variabile.
  • Capabilities vuote alla connessione. Hai esportato solo POST. Esporta anche GET e DELETE; l'initialize completa su POST ma il client fa GET per leggere lo stream.
  • 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.

Foto di Shubham Dhage su Unsplash

Domande frequenti

Devo riscrivere il mio SaaS Next.js per aggiungere un server MCP?
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/[transport]/route.ts e gestisce sia Streamable HTTP che SSE attraverso un singolo segmento dinamico. Nessuna riscrittura, nessuna infrastruttura parallela, nessun deploy separato. Se il SaaS è su Next.js 16 App Router con Node 20+, il primo tool va online in un pomeriggio.
Un server MCP ha bisogno di OAuth 2.1?
Sì per i server remoti. La specifica MCP 2025-11-25 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.
Perché un server MCP su Vercel ha bisogno di Redis?
Lo Streamable HTTP tiene aperta una risposta mentre un tool call gira. Senza Redis una sessione che inizia su un'istanza serverless non può riprendere su un'altra, e i client cadono a metà chiamata. Aggiungi Upstash Redis nella stessa region del deploy e passa l'URL a createMcpHandler({redisUrl}). L'handler usa Redis pub/sub tra GET e POST, quindi la latenza co-region si vede in ogni invocazione di tool. Abbinalo a Fluid Compute attivo (15 minuti di vita di invocazione) per un runtime di produzione stabile.
Un solo server MCP può servire Claude Code, ChatGPT e Cursor contemporaneamente?
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.
Qual è la differenza tra Streamable HTTP e SSE come trasporti MCP?
Streamable HTTP è il default dalla revisione di marzo 2025 della specifica. Usa POST per le tool call e GET per la ripresa dello stream, con DELETE per chiudere pulitamente una sessione. SSE (Server-Sent Events) è il trasporto vecchio, ancora supportato per client legacy. Un singolo segmento dinamico Next.js [transport] gestisce entrambi, quindi non devi scegliere uno solo. Esporta GET, POST e DELETE dall'handler. Se ne manca uno, alcuni client falliscono in silenzio.

Studio

Inizia un progetto.

Un partner unico per aziende, PA, startup e SaaS. Produzione più veloce, tecnologie moderne, costi ridotti. Un team, una fattura.