Distribuire un server MCP (Model Context Protocol) in produzione nel 2026 è un altro pianeta rispetto a farlo girare in locale con stdio. Quando il server deve servire più utenti, integrarsi con l'SSO aziendale, scalare orizzontalmente dietro un load balancer e — soprattutto — produrre log di audit conformi, le scelte architetturali contano davvero. Onestamente, è qui che si separano i prototipi dai deployment veri.
In questa guida vediamo come strutturare un'implementazione production-ready basata su Streamable HTTP, OAuth 2.1 con PKCE e un gateway MCP come punto di applicazione delle policy.
Do per scontato che tu conosca già le basi del protocollo. Se ti serve un'introduzione agli agenti che usano MCP, il nostro articolo sugli agenti IA con LangGraph, CrewAI e MCP è il punto di partenza naturale.
Perché stdio non basta più
Il trasporto stdio è perfetto per Claude Desktop o Claude Code in locale: il server eredita le credenziali del sistema operativo dell'utente e di autenticazione, beh, non se ne parla nemmeno. Tutto cambia quando il server diventa remoto.
- Identità multipla: più utenti, ciascuno con il proprio scope di permessi.
- Rete pubblica: il server è esposto via HTTP, e quindi deve gestire TLS, rate limiting e CORS.
- Scalabilità orizzontale: più repliche dietro un load balancer richiedono sessioni stateless (o un meccanismo di sticky session ben definito).
- Audit: ogni chiamata a tool deve essere tracciabile a un utente, a una richiesta e a un risultato.
La specifica MCP di novembre 2025 introduce Streamable HTTP come trasporto standard per server remoti, mandando in pensione il vecchio SSE puro. È un singolo endpoint (di solito /mcp) che accetta POST JSON-RPC e può rispondere in streaming via SSE quando serve. Semplice, e fa tutto quello che ti aspetti.
L'architettura di riferimento
Una distribuzione MCP enterprise nel 2026 si articola, di norma, su quattro livelli:
- Server MCP interni — uno per ciascun sistema critico (CRM, billing, ticketing, data warehouse). Ognuno vive nel tuo VPC, dietro il tuo IdP, con permessi a scope ridotto.
- Server MCP esterni fidati — server pubblicati da vendor SaaS (GitHub, Stripe, Slack, Linear), agganciati tramite un gateway controllato.
- Gateway MCP — punto unico di applicazione delle policy: autenticazione, autorizzazione, rate limiting, redaction di output sensibili, audit trail.
- Host agentico — il client che orchestra le chiamate (Claude Code, un agente Claude Agent SDK, una pipeline LangGraph).
Il gateway è il livello che fa la differenza tra un proof-of-concept e una distribuzione governata. Centralizza tutto ciò che, altrimenti, dovresti riscrivere in ogni singolo server. (Ed è esattamente lì che la maggior parte dei team si ritrova a perdere settimane.)
OAuth 2.1: i requisiti non negoziabili
Dalla specifica di giugno 2025, OAuth 2.1 è l'unico schema di autenticazione ammesso per i trasporti HTTP di MCP. Punto. I server MCP sono classificati come Resource Server e devono pubblicare la posizione del proprio Authorization Server tramite un endpoint .well-known/oauth-protected-resource.
I quattro pilastri obbligatori
- PKCE (Proof Key for Code Exchange): obbligatorio per tutti i client, non solo quelli pubblici. Elimina la fuga di authorization code.
- Niente flusso implicito: rimosso completamente. Solo Authorization Code Flow con PKCE.
- Authorization Server Metadata (RFC 8414): il server pubblica i propri endpoint in
/.well-known/oauth-authorization-server così il client li scopre dinamicamente.
- Dynamic Client Registration (RFC 7591): i client si registrano programmaticamente, senza setup manuale per ogni nuovo agente.
Validazione del token: l'errore più comune
Quando il tuo server MCP riceve un Bearer token, devi sempre verificare tre cose. Sempre.
- La firma (preferibilmente con chiave asimmetrica RS256 o ES256, mai HS256 in deployment multi-server).
- La claim
aud (audience): un token emesso per il server CRM non deve essere accettato dal server billing. Questo è uno degli errori che vedo ripetersi più spesso nelle code review.
- La claim
exp e gli scope: il token non deve essere scaduto e deve includere lo scope richiesto dal tool invocato.
Implementazione: Streamable HTTP con OAuth in TypeScript
Vediamo lo scheletro minimale di un server MCP remoto, basato sull'SDK ufficiale @modelcontextprotocol/sdk. Il server espone un tool get_customer protetto da un token Bearer.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import { mcpAuthMetadataRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
const app = express();
app.use(express.json());
const RESOURCE_URL = "https://mcp.azienda.it/mcp";
const AUTH_SERVER = "https://auth.azienda.it";
// 1. Pubblica i metadati Protected Resource (.well-known)
app.use(mcpAuthMetadataRouter({
resourceServerUrl: RESOURCE_URL,
authorizationServers: [AUTH_SERVER],
scopesSupported: ["mcp:read", "mcp:write", "crm:customers"],
}));
// 2. Middleware di validazione del Bearer token
const auth = requireBearerAuth({
verifier: {
verifyAccessToken: async (token) => {
// Introspection verso Keycloak / Auth0 / Okta
const res = await fetch(`${AUTH_SERVER}/oauth2/introspect`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `token=${token}`,
});
const data = await res.json();
if (!data.active) throw new Error("Token non valido");
if (data.aud !== RESOURCE_URL) throw new Error("Audience errata");
return { clientId: data.client_id, scopes: data.scope.split(" ") };
},
},
requiredScopes: ["mcp:read"],
});
// 3. Server MCP con un tool
const mcp = new McpServer({ name: "crm-server", version: "1.0.0" });
mcp.tool("get_customer",
{ customerId: { type: "string" } },
async ({ customerId }) => {
const customer = await crmClient.fetch(customerId);
return { content: [{ type: "text", text: JSON.stringify(customer) }] };
}
);
// 4. Endpoint Streamable HTTP protetto
app.post("/mcp", auth, async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await mcp.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(8080);
Due dettagli che, in produzione, fanno tutta la differenza:
sessionIdGenerator: undefined rende il server stateless: ogni richiesta si auto-contiene e il load balancer può instradare a qualunque replica. È il pattern raccomandato per il deployment serverless o multi-istanza.
- L'introspection del token è una chiamata extra a ogni richiesta. In produzione, usa la verifica locale con JWKS (chiave pubblica) e riserva l'introspection ai soli token opachi. Latency: enorme differenza.
Il gateway MCP: il livello che spesso manca
Implementare OAuth in ogni singolo server è ripetitivo e — diciamolo — pure rischioso. Un gateway MCP centralizza:
- Terminazione OAuth e propagazione dell'identità al server backend (header
X-User-Id, X-User-Email firmati).
- Rate limiting per utente e per tool.
- Redaction delle risposte: PII, segreti, contenuti sensibili filtrati prima di tornare all'agente.
- Audit log centralizzato in formato strutturato (JSON Lines verso un sistema SIEM).
- Routing: un'unica URL per l'agente, decine di server MCP dietro.
Due implementazioni di riferimento da studiare:
atrawog/mcp-oauth-gateway: gateway che aggiunge OAuth 2.1 a qualsiasi server MCP senza modificarne il codice. Wrappa server stdio esistenti e li espone via Streamable HTTP.
NapthaAI/http-oauth-mcp-server: implementazione Express + TypeScript che proxa OAuth verso un upstream con Dynamic Client Registration.
Pattern: propagazione dell'identità
Il gateway valida il token utente, poi rilascia un token interno a vita brevissima (60 secondi) con audience pari al server di backend. Il server MCP interno verifica solo questo token interno. In questo modo:
- Il backend non parla mai direttamente con l'IdP.
- Le credenziali utente non escono mai dal gateway.
- Il blast radius di un server compromesso è limitato a 60 secondi. Niente male.
Scalabilità orizzontale e sessioni
La specifica Streamable HTTP supporta le sessioni, ma per scalare in modo davvero stateless conviene rinunciarci dove possibile. Quando una sessione è inevitabile (per esempio un long-running tool che fa streaming di output), hai tre opzioni:
- Sticky session sul load balancer in base al
Mcp-Session-Id: semplice, ma rompe lo scale-down.
- Session store esterno (Redis): lo stato vive fuori dal processo, qualunque replica può servire la richiesta.
- Server stateless puri: ogni richiesta si auto-contiene; lo stato applicativo vive nel client agentico.
La roadmap MCP 2026 punta esplicitamente a rendere Streamable HTTP eseguibile in modalità completamente stateless su più istanze, con sessioni create, riprese e migrate in modo trasparente al client. È la direzione consigliata per qualunque nuovo progetto — non ho dubbi su questa scelta.
Osservabilità: cosa loggare
L'audit trail non è opzionale, e nel 2026 i requisiti di compliance per le azioni degli agenti AI si sono inaspriti parecchio. Per ogni invocazione di tool, persisti:
- Timestamp ISO 8601 con timezone.
- ID utente e ID sessione agente.
- Nome del server, nome del tool, versione.
- Argomenti di input (sanitizzati per PII).
- Risultato (troncato sopra una soglia, ma sempre con hash completo).
- Durata e codice di esito.
- Token claims rilevanti (
sub, aud, scope).
Servono due log: quello dell'host (cosa è stato approvato dall'utente) e quello del server (cosa è stato realmente eseguito). La discrepanza tra i due è il primo segnale di compromissione. Spesso, anche l'unico.
Errori e rate limiting: parlare al modello
Quando un server MCP avvolge un'API a monte rate-limited, un errore 429 da GitHub non deve far schiantare l'agente. Va tradotto in un risultato che il modello sa interpretare:
{
"isError": true,
"content": [{
"type": "text",
"text": "Rate limit raggiunto su GitHub API. Riprovare tra 30 secondi. Reset: 2026-05-16T14:32:00Z"
}]
}
Il modello vede questo come dato strutturato e può decidere di attendere, riprovare con backoff o passare a un tool alternativo. Le descrizioni vaghe dei tool e la gestione errori mancante sono, di gran lunga, le due cause più frequenti di bug in produzione.
Sandboxing dei server di terze parti
Un server MCP stdio gira con i permessi OS dell'host. Per qualsiasi server pubblicato da terzi, regole non negoziabili:
- Esegui in container con filesystem read-only e capability ridotte.
- Per server di provenienza incerta, valuta una sandbox WASM.
- Pinna le versioni con SHA: mai
latest in produzione.
- Disabilita network egress salvo le destinazioni esplicitamente necessarie.
L'approccio zero-trust per i server di terze parti è il default raccomandato dalla MCP working group nel 2026. E sinceramente, non vedo motivo per discostarsene.
Checklist di go-live
- [ ] Tutti i server remoti su Streamable HTTP, OAuth 2.1 con PKCE obbligatorio.
- [ ] Validazione di
aud, exp, firma asimmetrica su ogni richiesta.
- [ ] Gateway MCP davanti a tutti i server interni; propagazione identità via token a vita brevissima.
- [ ] Audit log strutturato verso SIEM, con hash dei payload completi.
- [ ] Rate limiting per utente e per tool, errori serializzati in modo model-friendly.
- [ ] Server stateless dove possibile, Redis-backed dove le sessioni sono necessarie.
- [ ] Server di terze parti in container o WASM sandbox, versioni pinnate.
- [ ] Human-in-the-loop obbligatorio per ogni azione irreversibile.
FAQ
Qual è la differenza tra SSE e Streamable HTTP in MCP?
SSE era il trasporto remoto originale ed è ora deprecato. Streamable HTTP usa un singolo endpoint POST con risposte opzionalmente in streaming via SSE: è più semplice da gestire dietro proxy e load balancer, supporta meglio i deployment stateless e ha sostituito SSE puro come standard dalla spec di novembre 2025.
Posso usare API key invece di OAuth 2.1?
Tecnicamente sì, per un singolo client controllato, ma esci dalla specifica MCP e perdi compatibilità con i client standard (Claude Desktop, Claude Code, mcp-remote). Per ogni deployment multi-utente o pubblico, OAuth 2.1 è obbligatorio. Le API key restano accettabili solo per service-to-service interno con accesso non utente.
Come gestisco i refresh token in un agente di lunga durata?
Il client agentico conserva refresh token e access token in uno store sicuro (mai in localStorage nel browser: usa cookie HttpOnly o storage in-memory). Quando l'access token scade, il client lo rinnova con il refresh token prima della prossima chiamata MCP. Il server non vede mai il refresh token. Mai.
Quante repliche servono per un server MCP in produzione?
Dipende dal carico, ma il minimo per alta disponibilità è due repliche dietro un load balancer, con health check sull'endpoint /mcp (richiesta JSON-RPC initialize). Per server stateless, autoscaling basato su CPU o richieste/secondo funziona bene; per server con sessioni, dimensiona considerando la memoria per sessione attiva.
Il gateway MCP è davvero necessario o è over-engineering?
Per uno o due server interni accessibili solo a un team ristretto, no — è probabilmente esagerato. Diventa necessario non appena hai più di tre server, più di dieci utenti, o requisiti di audit per compliance. Il gateway centralizza policy che altrimenti dovresti replicare in ogni server, ed è il punto naturale per redaction, rate limiting per utente e propagazione dell'identità.
Conclusione
MCP è passato in 18 mesi da "esperimento interessante" a infrastruttura critica per ogni sistema agentico aziendale. La differenza tra una proof-of-concept e una distribuzione di produzione si misura su quattro dimensioni: trasporto giusto (Streamable HTTP stateless), autenticazione corretta (OAuth 2.1 con PKCE e validazione audience), gateway come punto di policy, e audit completo. Le prime tre proteggono il presente; l'ultima ti salva quando — non se — qualcosa va storto.