Transport HTTP e Deployment
Introduzione
Finora hai usato il transport STDIO, dove il client lancia il server come processo figlio. In questo capitolo passerai al transport Streamable HTTP, che permette di eseguire il server come servizio indipendente accessibile via rete.
STDIO vs HTTP: Quando Usare Cosa
Server HTTP con Express
L'SDK MCP fornisce un'integrazione diretta con Express tramite StreamableHTTPServerTransport.
Setup
Aggiungi le dipendenze:
npm install express
npm install -D @types/express
Implementazione
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
import { z } from "zod";
// Crea il server MCP
const server = new McpServer({
name: "notes-http-server",
version: "1.0.0",
});
// Registra i tool (stessa API di STDIO)
const notes: Map<string, string> = new Map();
server.tool(
"add-note",
"Aggiunge una nuova nota",
{
title: z.string(),
content: z.string(),
},
async ({ title, content }) => {
notes.set(title, content);
return {
content: [{ type: "text", text: `Nota "${title}" salvata.` }],
};
},
);
// --- Transport HTTP ---
const app = express();
app.use(express.json());
// Crea transport con sessioni
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
// Connetti il server MCP al transport
await server.connect(transport);
// Endpoint MCP: POST (messaggi client -> server)
app.post("/mcp", async (req, res) => {
await transport.handleRequest(req, res, req.body);
});
// Endpoint MCP: GET (SSE stream server -> client)
app.get("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// Endpoint MCP: DELETE (terminazione sessione)
app.delete("/mcp", async (req, res) => {
await transport.handleRequest(req, res);
});
// Health check
app.get("/health", (_req, res) => {
res.json({ status: "ok", server: "notes-http-server" });
});
// Avvia il server HTTP
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
console.log(`Server MCP HTTP in ascolto su http://localhost:${PORT}/mcp`);
});
Punti Chiave
Session ID Generator: ogni connessione client riceve un ID sessione univoco. Il client lo include nell'header Mcp-Session-Id per tutte le richieste successive.
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
Tre endpoint su /mcp:
POST /mcp— il client invia messaggi JSON-RPC (request, notification, response)GET /mcp— il client apre un SSE stream per ricevere notifiche push dal serverDELETE /mcp— il client termina la sessione esplicitamente
server.connect() una sola volta: a differenza di STDIO dove ogni connessione crea un nuovo server, in HTTP il server si connette al transport una volta e gestisce tutte le sessioni.
Client HTTP
Per connettersi a un server HTTP, usa StreamableHTTPClientTransport:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({
name: "http-client",
version: "1.0.0",
});
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
);
await client.connect(transport);
// Da qui in poi, stessa API di STDIO
const tools = await client.listTools();
console.log("Tool disponibili:", tools.tools.map((t) => t.name));
const result = await client.callTool({
name: "add-note",
arguments: { title: "Test", content: "Contenuto di prova" },
});
console.log("Risultato:", result);
await client.close();
Gestione della Sessione
Il StreamableHTTPClientTransport gestisce automaticamente:
- Invio dell'header
Mcp-Session-Iddopo l'inizializzazione - Invio dell'header
MCP-Protocol-Versionin tutte le richieste - Riconnessione in caso di sessione scaduta (HTTP 404)
Pattern Stateful: Un Server per Molti Client
In produzione, il server HTTP gestisce sessioni multiple:
import { randomUUID } from "node:crypto";
// Mappa sessioni -> stato
const sessions = new Map<string, { notes: Map<string, string> }>();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => {
const sessionId = randomUUID();
sessions.set(sessionId, { notes: new Map() });
return sessionId;
},
});
Per accedere alla sessione corrente dall'handler di un tool, usa i metadata del transport o un pattern di dependency injection.
Dual Transport: STDIO + HTTP
Un server professionale supporta entrambi i transport, decidendo in base alla configurazione:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
// Crea e configura il server MCP (tool, resource, prompt)
function createServer(): McpServer {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
// ... registra tool ...
return server;
}
// Avvio condizionale
const mode = process.env.MCP_TRANSPORT ?? "stdio";
if (mode === "http") {
const server = createServer();
const app = express();
app.use(express.json());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);
app.post("/mcp", async (req, res) => await transport.handleRequest(req, res, req.body));
app.get("/mcp", async (req, res) => await transport.handleRequest(req, res));
app.delete("/mcp", async (req, res) => await transport.handleRequest(req, res));
app.get("/health", (_, res) => res.json({ status: "ok" }));
const port = process.env.PORT ?? 3000;
app.listen(port, () => console.log(`HTTP server on port ${port}`));
} else {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server avviato su STDIO");
}
Configurazione:
MCP_TRANSPORT=stdio(default) — per Claude DesktopMCP_TRANSPORT=http PORT=3000— per deployment HTTP
Sicurezza del Transport HTTP
Validazione Origin
Per prevenire attacchi DNS rebinding, valida l'header Origin:
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowed = ["http://localhost:3000", "https://myapp.example.com"];
if (origin && !allowed.includes(origin)) {
res.status(403).json({ error: "Origin non consentito" });
return;
}
next();
});
Binding Locale
Per server locali, binda solo su localhost:
app.listen(port, "127.0.0.1", () => {
console.log(`Server in ascolto solo su localhost:${port}`);
});
Autenticazione
Per server remoti, implementa autenticazione con Bearer token:
app.use("/mcp", (req, res, next) => {
const auth = req.headers.authorization;
if (!auth?.startsWith("Bearer ")) {
res.status(401).json({ error: "Token mancante" });
return;
}
const token = auth.slice(7);
if (!isValidToken(token)) {
res.status(403).json({ error: "Token non valido" });
return;
}
next();
});
Riepilogo
In questo capitolo hai imparato:
- Le differenze tra transport STDIO e Streamable HTTP
- Come creare un server HTTP con Express e
StreamableHTTPServerTransport - La gestione delle sessioni con
sessionIdGenerator - Come creare un client HTTP con
StreamableHTTPClientTransport - Il pattern dual-transport per supportare sia STDIO che HTTP
- Misure di sicurezza: validazione Origin, binding locale, autenticazione
Prossimo: Persistenza con SQLite