Construire un serveur MCP de A à Z : tools, resources, prompts, validation, transports stdio et HTTP — pour connecter Claude à n'importe quel système externe.
Le transport est le canal de communication entre le client MCP et votre serveur. Deux options principales : stdio (processus local, communication par stdin/stdout) et HTTP Streamable (serveur HTTP, connexions réseau). Chacun a son cas d'usage.
Stdio est le transport par défaut et le plus simple. Claude lance votre serveur comme un processus enfant et communique via stdin/stdout.
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
const transport = new StdioServerTransport()
await server.connect(transport)
// Le processus reste en vie tant que la connexion est ouverteAvantages :
settings.jsonLimites :
Quand l'utiliser : outils personnels, scripts locaux, intégrations qui accèdent à la machine locale (fichiers, DB locale, processus).
HTTP Streamable (le transport moderne recommandé par Anthropic) expose le serveur MCP sur une URL HTTP. Les clients se connectent via HTTP, les réponses utilisent Server-Sent Events pour le streaming.
import express from 'express'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
const app = express()
app.use(express.json())
// Une session = un transport
const transports = new Map<string, StreamableHTTPServerTransport>()
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined
if (sessionId && transports.has(sessionId)) {
// Session existante — réutiliser le transport
const transport = transports.get(sessionId)!
await transport.handleRequest(req, res, req.body)
return
}
// Nouvelle session
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: (id) => {
transports.set(id, transport)
},
})
transport.onclose = () => {
if (transport.sessionId) {
transports.delete(transport.sessionId)
}
}
const mcpServer = createServer() // votre fonction qui crée et configure le Server
await mcpServer.connect(transport)
await transport.handleRequest(req, res, req.body)
})
// Endpoint GET pour SSE (streaming)
app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string
const transport = transports.get(sessionId)
if (!transport) {
res.status(404).json({ error: 'Session introuvable' })
return
}
await transport.handleRequest(req, res)
})
// Endpoint DELETE pour fermer une session
app.delete('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string
const transport = transports.get(sessionId)
if (transport) {
await transport.close()
transports.delete(sessionId)
}
res.status(200).end()
})
app.listen(3000, () => {
console.error('[mcp] Serveur HTTP démarré sur http://localhost:3000')
})Installation supplémentaire :
npm install express
npm install -D @types/express{
"mcpServers": {
"mon-mcp-remote": {
"url": "http://localhost:3000/mcp"
}
}
}Pour un serveur déployé :
{
"mcpServers": {
"mon-mcp-prod": {
"url": "https://mcp.monapp.fr/mcp",
"headers": {
"Authorization": "Bearer ${MON_MCP_TOKEN}"
}
}
}
}Pour les serveurs HTTP exposés, l'authentification est critique.
app.use('/mcp', (req, res, next) => {
const auth = req.headers.authorization
if (!auth?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Token manquant' })
return
}
const token = auth.slice(7)
if (token !== process.env.MCP_SECRET_TOKEN) {
res.status(403).json({ error: 'Token invalide' })
return
}
next()
})app.use('/mcp', (req, res, next) => {
const apiKey = req.headers['x-api-key']
if (!apiKey || !validApiKeys.has(apiKey as string)) {
res.status(403).json({ error: 'API key invalide' })
return
}
next()
})Dans settings.json côté client :
{
"mcpServers": {
"mon-mcp": {
"url": "https://mcp.monapp.fr/mcp",
"headers": {
"x-api-key": "${MCP_API_KEY}"
}
}
}
}Avec stdio, un serveur = un client. Avec HTTP, plusieurs clients partagent le même serveur — mais chaque session doit être isolée.
// src/server-factory.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { registerTools } from './tools/index.js'
import { registerResources } from './resources/index.js'
export function createServer(): Server {
const server = new Server(
{ name: 'mon-mcp', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {} } }
)
registerTools(server)
registerResources(server)
return server
}Chaque requête de nouvelle session crée un nouveau Server via createServer(). Les sessions sont indépendantes.
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/index.js"]# Build et run
npm run build
docker build -t mon-mcp .
docker run -p 3000:3000 -e MCP_SECRET_TOKEN=xxx mon-mcplocation /mcp {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# Nécessaire pour SSE (streaming)
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# Headers standard
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Timeout long pour les connexions SSE
proxy_read_timeout 3600s;
}proxy_buffering off est critique — sans ça, Nginx bufferise le SSE et les messages n'arrivent pas en temps réel.
npm install -g pm2
pm2 start dist/index.js --name mon-mcp
pm2 save
pm2 startup # démarrage au boot| Critère | stdio | HTTP Streamable |
|---|---|---|
| Usage | Local, personnel | Partagé, équipe, remote |
| Setup | Minimal | Serveur HTTP requis |
| Sécurité réseau | Pas d'exposition | À configurer (HTTPS, auth) |
| Multi-clients | Non | Oui |
| Latence | Très faible | Variable selon réseau |
| Déploiement | Pas nécessaire | VPS, Docker, cloud |
Règle simple : stdio pour les outils personnels locaux, HTTP pour tout ce qui est partagé ou déployé.
// Toujours charger depuis process.env — fonctionne en stdio et HTTP
const DB_URL = process.env.DATABASE_URL
if (!DB_URL) {
console.error('DATABASE_URL manquant')
process.exit(1)
}Stdio : les env vars passent via settings.json :
{
"mcpServers": {
"mon-mcp": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"DATABASE_URL": "postgresql://localhost/mydb"
}
}
}
}HTTP : les env vars viennent du .env ou du système.
Les transports sont maîtrisés. Le dernier chapitre assemble tout en un projet complet : un serveur MCP pour gérer un projet de développement — tickets, utilisateurs, déploiements, métriques.