MCP en Pratique : 5 Serveurs MCP à Déployer Aujourd'hui
Tutoriel pratique avec 5 exemples de serveurs MCP production-ready. Filesystem MCP sécurisé, PostgreSQL MCP avec pooling, REST API MCP avec auth, web scraper custom, multi-tool MCP. Code complet, déploiement, troubleshooting.
Par Talki Academy·Mis a jour le 3 avril 2026
De la théorie à la pratique
Vous avez lu notre guide théorique sur MCP. Vous comprenez l'architecture Host-Client-Server. Vous savez ce que sont les resources, tools, et prompts. Maintenant, vous voulez du code qui fonctionne.
Cet article vous donne 5 serveurs MCP prêts pour la production, avec le code complet, les patterns de sécurité, les configurations de déploiement, et les erreurs courantes à éviter. Chaque exemple est testé avec Claude 4.5 Sonnet et Claude Desktop (avril 2026).
⚠️ Prérequis : Node.js 20+ ou Python 3.11+, SDK MCP installé, une clé API Claude pour tester. Si vous débutez avec MCP, lisez d'abord le guide théorique — cet article suppose que vous connaissez les bases.
Ce que vous allez apprendre
Comment sécuriser un serveur filesystem pour éviter les accès non autorisés
Comment gérer une pool de connexions PostgreSQL avec gestion d'erreurs robuste
Comment wrapper une REST API externe avec authentification
Comment construire un custom tool (web scraper) from scratch
Comment combiner plusieurs outils dans un serveur MCP multi-tool
Comment débugger les erreurs MCP les plus fréquentes
Serveur 1 : Filesystem MCP Sécurisé
Le serveur filesystem expose vos fichiers locaux à Claude. Sans précautions, c'est une faille de sécurité massive. Ce serveur implémente 3 règles critiques : whitelist de répertoires, validation des chemins, et lecture seule par défaut.
Code complet (TypeScript)
// filesystem-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";
// CONFIGURATION DE SÉCURITÉ
const ALLOWED_DIRS = [
"/Users/username/Documents/notes",
"/Users/username/Projects"
];
// Fonction de validation de chemin
function isPathAllowed(filePath: string): boolean {
const normalized = path.resolve(filePath);
return ALLOWED_DIRS.some(dir => normalized.startsWith(path.resolve(dir)));
}
const server = new McpServer({
name: "filesystem-secure",
version: "1.0.0"
});
// RESOURCE : Liste des fichiers d'un répertoire
server.resource(
"fs://list",
"Liste des fichiers dans les répertoires autorisés",
async () => {
const allFiles: string[] = [];
for (const dir of ALLOWED_DIRS) {
try {
const files = await fs.readdir(dir);
allFiles.push(...files.map(f => path.join(dir, f)));
} catch (err) {
console.error(`Erreur lecture ${dir}:`, err);
}
}
return {
contents: [{
uri: "fs://list",
mimeType: "application/json",
text: JSON.stringify(allFiles, null, 2)
}]
};
}
);
// TOOL : Lire un fichier (read-only)
server.tool(
"read_file",
"Lit le contenu d'un fichier texte",
{
path: z.string().describe("Chemin absolu du fichier à lire")
},
async ({ path: filePath }) => {
// Validation sécurité
if (!isPathAllowed(filePath)) {
return {
content: [{
type: "text",
text: `Erreur : accès refusé. Répertoires autorisés : ${ALLOWED_DIRS.join(", ")}`
}],
isError: true
};
}
try {
const content = await fs.readFile(filePath, "utf-8");
const stats = await fs.stat(filePath);
return {
content: [{
type: "text",
text: `Fichier : ${filePath}
Taille : ${stats.size} octets
Modifié : ${stats.mtime.toISOString()}
Contenu :
${content}`
}]
};
} catch (err: any) {
return {
content: [{
type: "text",
text: `Erreur lecture fichier : ${err.message}`
}],
isError: true
};
}
}
);
// TOOL : Rechercher dans les fichiers (grep-like)
server.tool(
"search_files",
"Recherche un pattern dans tous les fichiers autorisés",
{
pattern: z.string().describe("Expression régulière à chercher"),
caseInsensitive: z.boolean().optional().describe("Ignorer la casse")
},
async ({ pattern, caseInsensitive = false }) => {
const results: Array<{ file: string; matches: string[] }> = [];
const regex = new RegExp(pattern, caseInsensitive ? "gi" : "g");
for (const dir of ALLOWED_DIRS) {
try {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isFile()) {
try {
const content = await fs.readFile(filePath, "utf-8");
const matches = content.match(regex);
if (matches && matches.length > 0) {
results.push({
file: filePath,
matches: matches.slice(0, 5) // Limiter à 5 matches par fichier
});
}
} catch {
// Ignorer les fichiers binaires
}
}
}
} catch (err) {
console.error(`Erreur scan ${dir}:`, err);
}
}
if (results.length === 0) {
return {
content: [{
type: "text",
text: `Aucun résultat pour le pattern "${pattern}"`
}]
};
}
return {
content: [{
type: "text",
text: `Trouvé ${results.length} fichier(s) correspondant à "${pattern}" :
${results.map(r => `${r.file}: ${r.matches.length} match(es)`).join("\n")}`
}]
};
}
);
// Démarrage du serveur
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("✅ Serveur filesystem MCP sécurisé démarré");
console.error(`📁 Répertoires autorisés : ${ALLOWED_DIRS.join(", ")}`);
}
main().catch(console.error);
// Dans Claude Desktop, tapez :
"Lis le fichier /Users/username/Documents/notes/2026-01-15.md"
// Claude appellera automatiquement l'outil read_file
// Résultat attendu : contenu du fichier avec métadonnées
"Cherche tous les fichiers contenant le mot 'MCP'"
// Claude appellera search_files
// Résultat : liste des fichiers + nombre de matches
Points de sécurité critiques
Whitelist stricte : seuls les répertoires dans ALLOWED_DIRS sont accessibles
Validation de chemin : path.resolve() empêche les attaques path traversal (../../etc/passwd)
Read-only par défaut : aucun outil d'écriture exposé (ajoutez write_file seulement si nécessaire)
Gestion d'erreurs : les erreurs filesystem ne crashent pas le serveur
Limitation de résultats : search_files limite à 5 matches/fichier pour éviter l'overload
💡 Pro tip : Pour un serveur filesystem en entreprise, stockez ALLOWED_DIRSdans une variable d'environnement et ajoutez un système de permissions par utilisateur (via OAuth).
Serveur 2 : PostgreSQL MCP avec Connection Pooling
Ce serveur connecte Claude à votre base PostgreSQL. Il implémente un pool de connexions, des requêtes préparées, un timeout sur les queries, et une whitelist de tables accessibles.
// Dans Claude Desktop :
"Combien d'utilisateurs sont inscrits depuis janvier 2026 ?"
// Claude génère et exécute :
// SELECT COUNT(*) FROM users WHERE created_at >= '2026-01-01'
"Quelle est la commande moyenne par client ?"
// Claude génère :
// SELECT AVG(total_amount) FROM orders
Sécurité PostgreSQL
Requêtes préparées : tous les paramètres utilisent $1, $2... pour prévenir les injections SQL
Whitelist de tables : seules les tables dans ALLOWED_TABLES sont accessibles
Timeout : les requêtes longues sont annulées après 5s par défaut
Connection pooling : max 10 connexions, auto-release après usage
Credentials sécurisés : DATABASE_URL dans env vars, jamais en dur dans le code
⚠️ En production : créez un utilisateur PostgreSQL read-only dédié avec GRANT SELECT ON tables TO mcp_readonly_user. Ne réutilisez jamais votre user admin.
Serveur 3 : REST API MCP avec Authentification
Ce serveur wrappe une API REST externe (exemple : GitHub API) et gère l'authentification, le rate limiting, et les erreurs HTTP.
// Dans Claude Desktop :
"Liste les repos de @anthropics"
// Claude appelle list_repos({ username: "anthropics" })
"Quelles sont les issues ouvertes dans anthropics/anthropic-sdk-python ?"
// Claude appelle list_issues({ owner: "anthropics", repo: "anthropic-sdk-python" })
"Crée une issue dans mon repo test/demo avec le titre 'Bug dans le README'"
// Claude appelle create_issue avec les bons paramètres
Points clés REST API
Authentification centralisée : le token est injecté dans tous les appels via headers
Rate limiting : détection du header x-ratelimit-reset et erreur explicite
Gestion d'erreurs HTTP : status codes différents de 2xx sont catchés et loggés
User-Agent : obligatoire pour GitHub API (sinon 403)
Write operations opt-in : create_issue est un outil séparé, pas exposé par défaut
💡 Adaptable à n'importe quelle API : remplacez GitHub par Notion, Stripe, Jira, etc. Le pattern apiCall() + gestion d'auth + error handling fonctionne partout.
Serveur 4 : Web Scraper Custom MCP
Ce serveur custom scrappe une page web et extrait du contenu structuré. Idéal pour transformer des sites non-API en sources de données pour Claude.
// Dans Claude Desktop :
"Récupère le contenu de https://news.ycombinator.com/"
// Claude appelle scrape_page({ url: "..." })
"Extrais tous les liens PDF de https://example.com/documentation"
// Claude appelle extract_links({ url: "...", filterPattern: "\.pdf$" })
"Extrais le premier tableau de https://example.com/pricing"
// Claude appelle extract_table({ url: "...", tableIndex: 0 })
Considérations légales et éthiques
⚠️ Attention au scraping :
Respectez le fichier robots.txt du site cible
N'abusez pas : limitez la fréquence des requêtes (rate limiting)
Certains sites interdisent le scraping dans leurs ToS
Préférez toujours une API officielle si disponible
Ce serveur combine les 3 patterns précédents dans un seul serveur MCP. Idéal pour éviter de gérer plusieurs processus et centraliser l'accès à vos outils métier.
Un seul processus : moins de mémoire, plus simple à gérer
Configuration centralisée : toutes les env vars dans un seul endroit
Activation conditionnelle : les tools ne sont ajoutés que si les credentials sont fournis
Logs unifiés : tous les outils loggent dans le même flux stderr
Inconvénients
Si un outil crash, tout le serveur redémarre
Plus difficile à débugger (logs entremêlés)
Code plus complexe à maintenir
💡 Recommandation : commencez avec des serveurs séparés (un par outil). Consolidez en multi-tool seulement si vous avez >5 serveurs et que la gestion devient lourde.
Troubleshooting & Erreurs Courantes
Erreur 1 : "Server not responding" dans Claude Desktop
Symptôme : le serveur MCP apparaît comme déconnecté dans Claude Desktop.
Causes :
Le chemin vers le script dans claude_desktop_config.json est incorrect
Le script n'est pas exécutable (chmod +x manquant pour Python)
Node.js ou Python n'est pas dans le PATH
Le serveur crash au démarrage (erreur dans le code)
Solution :
# 1. Vérifiez que le script démarre manuellement
node /chemin/vers/server.js
# Ou pour Python :
python3 /chemin/vers/server.py
# 2. Vérifiez les logs Claude Desktop
# macOS : ~/Library/Logs/Claude/
# Windows : %APPDATA%\Claude\logs\
# 3. Testez avec l'inspecteur MCP
npx @modelcontextprotocol/inspector node server.js
Erreur 2 : "Tool call failed" ou "isError: true"
Symptôme : Claude appelle l'outil mais reçoit une erreur.
Causes :
Validation Zod échoue (mauvais type de paramètre)
Exception non catchée dans le handler de l'outil
Timeout (base de données trop lente, API externe down)
Solution :
// Toujours wrapper les appels externes dans try/catch
server.tool("my_tool", "...", schema, async (params) => {
try {
const result = await externalAPI.call(params);
return { content: [{ type: "text", text: result }] };
} catch (err: any) {
// Logguer l'erreur sur stderr (visible dans les logs Claude Desktop)
console.error("Erreur my_tool:", err);
return {
content: [{ type: "text", text: `Erreur : ${err.message}` }],
isError: true // Important : signale à Claude que c'est une erreur
};
}
});
Erreur 3 : "Cannot read property 'xyz' of undefined"
Symptôme : crash du serveur avec stack trace Node.js.
Cause : accès à une propriété inexistante (par ex. result.rows[0].name quand rows est vide).
Solution :
// Toujours vérifier l'existence avant d'accéder
const result = await db.query("SELECT ...");
if (result.rows.length === 0) {
return { content: [{ type: "text", text: "Aucun résultat" }] };
}
const firstRow = result.rows[0];
// Maintenant firstRow existe, safe d'accéder firstRow.name
// Détecter le rate limit et informer Claude
if (response.status === 429) {
const retryAfter = response.headers.get("retry-after"); // en secondes
const message = retryAfter
? `Rate limit atteint. Réessayer dans ${retryAfter}s.`
: "Rate limit atteint. Réessayer plus tard.";
return {
content: [{ type: "text", text: message }],
isError: true
};
}
Erreur 5 : Serveur MCP consomme trop de mémoire
Symptôme : le processus Node.js/Python utilise plusieurs Go de RAM.
Causes :
Connection pooling mal configuré (trop de connexions ouvertes)
Cache non vidé (accumulation de données en mémoire)
Fuite mémoire dans le code (event listeners non supprimés)
Solution :
// PostgreSQL : limiter le pool
const pool = new Pool({
max: 5, // Max 5 connexions (pas 100!)
idleTimeoutMillis: 30000 // Fermer les connexions idle après 30s
});
// Toujours release les clients
const client = await pool.connect();
try {
await client.query("...");
} finally {
client.release(); // CRITIQUE : ne jamais oublier
}
Debug mode : logs verbeux
Pour activer les logs de debug du SDK MCP :
// Dans claude_desktop_config.json, ajoutez :
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/chemin/vers/server.js"],
"env": {
"DEBUG": "mcp:*" // Active tous les logs MCP
}
}
}
}
// Ou en ligne de commande :
DEBUG=mcp:* node server.js
Questions fréquentes
Quelle est la différence entre stdio et HTTP pour un serveur MCP ?
stdio (standard input/output) lance le serveur MCP comme un processus local connecté au client via pipes. Idéal pour Claude Desktop et les IDE. HTTP avec SSE (Server-Sent Events) permet de déployer un serveur MCP distant accessible via réseau. Recommandation : stdio pour dev et usage local, HTTP/SSE pour serveurs partagés en entreprise.
Comment sécuriser un serveur MCP en production ?
5 règles : (1) N'exposez jamais vos credentials en dur — utilisez des variables d'environnement ou des secrets managers, (2) Validez toutes les entrées avec Zod ou Pydantic, (3) Limitez les permissions des outils (lecture seule par défaut, écriture sur opt-in explicite), (4) Loggez tous les appels d'outils pour audit, (5) Pour HTTP : activez HTTPS, authentification API key, et rate limiting.
Puis-je combiner plusieurs serveurs MCP dans Claude Desktop ?
Oui. Claude Desktop permet de charger plusieurs serveurs MCP simultanément. Exemple : un serveur filesystem pour lire vos notes, un serveur PostgreSQL pour vos données clients, et un serveur GitHub pour vos repos. Claude aura accès à tous les tools et resources exposés par les 3 serveurs. Attention : chaque serveur est un processus indépendant, gérez la mémoire en conséquence.
Comment débugger un serveur MCP qui ne répond pas ?
Workflow de debug : (1) Vérifiez les logs Claude Desktop (menu Développeur > Afficher les logs), (2) Testez le serveur en ligne de commande : `node server.js` doit démarrer sans erreur, (3) Vérifiez le transport : stdio doit loguer sur stderr (pas stdout), (4) Utilisez l'inspecteur MCP : `npx @modelcontextprotocol/inspector node server.js`, (5) Activez le mode verbose dans le SDK : `DEBUG=mcp:* node server.js`.
Quel langage choisir pour un serveur MCP : TypeScript ou Python ?
TypeScript : meilleur typage avec Zod, async natif, parfait pour APIs REST et intégrations Node.js. Python : idéal pour data science, ML, scripts sysadmin, et si votre équipe maîtrise Python. Les deux SDKs sont feature-complete. Recommandation : TypeScript pour serveurs métier et web, Python pour data et automatisation.
Pour aller plus loin
Ces 5 serveurs MCP couvrent 80% des besoins en entreprise : accès fichiers, bases de données, APIs REST, scraping, et combinaisons multi-outils. Pour maîtriser MCP de A à Z avec exercices pratiques et déploiement en production :