Talki Academy
Tutoriel28 min de lecture

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);

Configuration Claude Desktop

// ~/Library/Application Support/Claude/claude_desktop_config.json { "mcpServers": { "filesystem": { "command": "node", "args": ["/chemin/absolu/vers/filesystem-server/dist/index.js"], "env": { "NODE_ENV": "production" } } } }

Test du serveur

Une fois configuré et Claude Desktop redémarré :

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

Installation des dépendances

npm install @modelcontextprotocol/sdk zod pg npm install -D @types/pg # Variables d'environnement (.env) DATABASE_URL=postgresql://user:password@localhost:5432/dbname ALLOWED_TABLES=users,orders,products QUERY_TIMEOUT_MS=5000

Code complet (TypeScript)

// postgres-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { Pool } from "pg"; // Configuration const DATABASE_URL = process.env.DATABASE_URL!; const ALLOWED_TABLES = process.env.ALLOWED_TABLES?.split(",") || []; const QUERY_TIMEOUT = parseInt(process.env.QUERY_TIMEOUT_MS || "5000"); if (!DATABASE_URL) { throw new Error("DATABASE_URL manquante dans .env"); } if (ALLOWED_TABLES.length === 0) { throw new Error("ALLOWED_TABLES vide — aucune table accessible"); } // Pool de connexions PostgreSQL const pool = new Pool({ connectionString: DATABASE_URL, max: 10, // Max 10 connexions simultanées idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, statement_timeout: QUERY_TIMEOUT }); // Validation de sécurité SQL function isTableAllowed(tableName: string): boolean { return ALLOWED_TABLES.includes(tableName); } const server = new McpServer({ name: "postgres-server", version: "1.0.0" }); // RESOURCE : Schema de la base server.resource( "db://schema", "Schema des tables PostgreSQL autorisées", async () => { const client = await pool.connect(); try { const schemaQuery = ` SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = ANY($1) ORDER BY table_name, ordinal_position `; const result = await client.query(schemaQuery, [ALLOWED_TABLES]); return { contents: [{ uri: "db://schema", mimeType: "application/json", text: JSON.stringify(result.rows, null, 2) }] }; } finally { client.release(); } } ); // TOOL : Exécuter une requête SELECT server.tool( "query_database", "Exécute une requête SELECT en langage naturel", { query: z.string().describe("Requête SQL SELECT (read-only)"), params: z.array(z.any()).optional().describe("Paramètres de la requête préparée") }, async ({ query, params = [] }) => { // Validation : seulement SELECT autorisé const normalizedQuery = query.trim().toUpperCase(); if (!normalizedQuery.startsWith("SELECT")) { return { content: [{ type: "text", text: "Erreur : seules les requêtes SELECT sont autorisées" }], isError: true }; } // Vérifier que les tables interrogées sont dans la whitelist const tablesInQuery = ALLOWED_TABLES.filter(table => query.toLowerCase().includes(table.toLowerCase()) ); if (tablesInQuery.length === 0) { return { content: [{ type: "text", text: `Erreur : aucune table autorisée détectée. Tables disponibles : ${ALLOWED_TABLES.join(", ")}` }], isError: true }; } const client = await pool.connect(); try { const startTime = Date.now(); const result = await client.query(query, params); const duration = Date.now() - startTime; if (result.rows.length === 0) { return { content: [{ type: "text", text: "Requête exécutée avec succès — 0 résultats" }] }; } return { content: [{ type: "text", text: `Requête exécutée en ${duration}ms — ${result.rows.length} résultat(s) : ${JSON.stringify(result.rows, null, 2)}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur SQL : ${err.message}` }], isError: true }; } finally { client.release(); } } ); // TOOL : Statistiques de table server.tool( "table_stats", "Retourne le nombre de lignes et la taille d'une table", { tableName: z.string().describe("Nom de la table") }, async ({ tableName }) => { if (!isTableAllowed(tableName)) { return { content: [{ type: "text", text: `Table "${tableName}" non autorisée. Tables disponibles : ${ALLOWED_TABLES.join(", ")}` }], isError: true }; } const client = await pool.connect(); try { const countQuery = `SELECT COUNT(*) FROM ${tableName}`; const sizeQuery = ` SELECT pg_size_pretty(pg_total_relation_size($1)) AS size `; const [countResult, sizeResult] = await Promise.all([ client.query(countQuery), client.query(sizeQuery, [tableName]) ]); const count = countResult.rows[0].count; const size = sizeResult.rows[0].size; return { content: [{ type: "text", text: `Table : ${tableName} Nombre de lignes : ${count} Taille totale : ${size}` }] }; } finally { client.release(); } } ); // Démarrage du serveur async function main() { // Test de connexion au démarrage try { const client = await pool.connect(); await client.query("SELECT NOW()"); client.release(); console.error("✅ Connexion PostgreSQL OK"); } catch (err) { console.error("❌ Erreur connexion PostgreSQL:", err); process.exit(1); } const transport = new StdioServerTransport(); await server.connect(transport); console.error("✅ Serveur PostgreSQL MCP démarré"); console.error(`📊 Tables autorisées : ${ALLOWED_TABLES.join(", ")}`); } // Nettoyage à l'arrêt process.on("SIGINT", async () => { await pool.end(); process.exit(0); }); main().catch(console.error);

Configuration Claude Desktop

{ "mcpServers": { "postgres": { "command": "node", "args": ["/chemin/vers/postgres-server/dist/index.js"], "env": { "DATABASE_URL": "postgresql://user:password@localhost:5432/dbname", "ALLOWED_TABLES": "users,orders,products", "QUERY_TIMEOUT_MS": "5000" } } } }

Exemple d'utilisation

// 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
  • SELECT uniquement : validation stricte, aucun INSERT/UPDATE/DELETE/DROP
  • 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.

Installation

npm install @modelcontextprotocol/sdk zod node-fetch # .env GITHUB_TOKEN=ghp_votre_token_ici GITHUB_API_BASE=https://api.github.com

Code complet (TypeScript)

// github-api-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fetch from "node-fetch"; const GITHUB_TOKEN = process.env.GITHUB_TOKEN!; const GITHUB_API_BASE = process.env.GITHUB_API_BASE || "https://api.github.com"; if (!GITHUB_TOKEN) { throw new Error("GITHUB_TOKEN manquant dans .env"); } // Helper : appel API GitHub avec auth async function githubAPI(endpoint: string, options: any = {}) { const url = `${GITHUB_API_BASE}${endpoint}`; const headers = { "Authorization": `Bearer ${GITHUB_TOKEN}`, "Accept": "application/vnd.github+json", "User-Agent": "MCP-Server/1.0", ...options.headers }; const response = await fetch(url, { ...options, headers }); // Gestion rate limiting if (response.status === 403) { const rateLimitReset = response.headers.get("x-ratelimit-reset"); if (rateLimitReset) { const resetTime = new Date(parseInt(rateLimitReset) * 1000); throw new Error(`Rate limit atteint. Reset à ${resetTime.toISOString()}`); } } if (!response.ok) { const errorBody = await response.text(); throw new Error(`GitHub API error ${response.status}: ${errorBody}`); } return await response.json(); } const server = new McpServer({ name: "github-api", version: "1.0.0" }); // TOOL : Lister les repos d'un user server.tool( "list_repos", "Liste les repositories publics d'un utilisateur GitHub", { username: z.string().describe("Nom d'utilisateur GitHub") }, async ({ username }) => { try { const repos = await githubAPI(`/users/${username}/repos`); if (repos.length === 0) { return { content: [{ type: "text", text: `Aucun repository public pour @${username}` }] }; } const repoList = repos.map((r: any) => `- ${r.name} (${r.stargazers_count} ⭐) — ${r.description || "Pas de description"}` ).join("\n"); return { content: [{ type: "text", text: `Repositories de @${username} : ${repoList}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur : ${err.message}` }], isError: true }; } } ); // TOOL : Récupérer les issues d'un repo server.tool( "list_issues", "Liste les issues ouvertes d'un repository", { owner: z.string().describe("Propriétaire du repo"), repo: z.string().describe("Nom du repository"), state: z.enum(["open", "closed", "all"]).optional().describe("État des issues") }, async ({ owner, repo, state = "open" }) => { try { const issues = await githubAPI(`/repos/${owner}/${repo}/issues?state=${state}`); if (issues.length === 0) { return { content: [{ type: "text", text: `Aucune issue ${state} dans ${owner}/${repo}` }] }; } const issueList = issues.map((i: any) => `#${i.number} — ${i.title} (par @${i.user.login})` ).join("\n"); return { content: [{ type: "text", text: `Issues ${state} dans ${owner}/${repo} : ${issueList}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur : ${err.message}` }], isError: true }; } } ); // TOOL : Créer une issue (write operation) server.tool( "create_issue", "Crée une nouvelle issue dans un repository", { owner: z.string().describe("Propriétaire du repo"), repo: z.string().describe("Nom du repository"), title: z.string().describe("Titre de l'issue"), body: z.string().optional().describe("Description de l'issue") }, async ({ owner, repo, title, body = "" }) => { try { const issue = await githubAPI(`/repos/${owner}/${repo}/issues`, { method: "POST", body: JSON.stringify({ title, body }) }); return { content: [{ type: "text", text: `✅ Issue créée : #${issue.number} — ${issue.title} URL : ${issue.html_url}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur création issue : ${err.message}` }], isError: true }; } } ); // Démarrage async function main() { // Test de connexion au démarrage try { await githubAPI("/user"); console.error("✅ Authentification GitHub OK"); } catch (err) { console.error("❌ Erreur auth GitHub:", err); process.exit(1); } const transport = new StdioServerTransport(); await server.connect(transport); console.error("✅ Serveur GitHub API MCP démarré"); } main().catch(console.error);

Configuration Claude Desktop

{ "mcpServers": { "github": { "command": "node", "args": ["/chemin/vers/github-api-server/dist/index.js"], "env": { "GITHUB_TOKEN": "ghp_votre_token_personnel" } } } }

Utilisation

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

Installation

npm install @modelcontextprotocol/sdk zod cheerio node-fetch

Code complet (TypeScript)

// web-scraper-server.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fetch from "node-fetch"; import * as cheerio from "cheerio"; const server = new McpServer({ name: "web-scraper", version: "1.0.0" }); // TOOL : Scraper une page web server.tool( "scrape_page", "Récupère et parse le contenu HTML d'une page web", { url: z.string().url().describe("URL de la page à scraper"), selector: z.string().optional().describe("Sélecteur CSS optionnel pour extraire une partie spécifique") }, async ({ url, selector }) => { try { const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (compatible; MCP-Scraper/1.0)" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const html = await response.text(); const $ = cheerio.load(html); // Si un sélecteur est fourni, extraire seulement cette partie if (selector) { const selected = $(selector); if (selected.length === 0) { return { content: [{ type: "text", text: `Aucun élément trouvé pour le sélecteur "${selector}"` }] }; } const extractedText = selected.text().trim(); return { content: [{ type: "text", text: `Contenu extrait de ${url} (sélecteur: ${selector}) : ${extractedText}` }] }; } // Sinon, extraire le texte principal (body) const title = $("title").text().trim(); const bodyText = $("body").text().trim().replace(/\s+/g, " "); return { content: [{ type: "text", text: `Titre : ${title} Contenu : ${bodyText.slice(0, 2000)}...` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur scraping : ${err.message}` }], isError: true }; } } ); // TOOL : Extraire tous les liens d'une page server.tool( "extract_links", "Extrait tous les liens <a> d'une page web", { url: z.string().url().describe("URL de la page"), filterPattern: z.string().optional().describe("Regex pour filtrer les liens (ex: '.pdf$')") }, async ({ url, filterPattern }) => { try { const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (compatible; MCP-Scraper/1.0)" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const html = await response.text(); const $ = cheerio.load(html); const links: Array<{ text: string; href: string }> = []; $("a").each((_, el) => { const href = $(el).attr("href"); const text = $(el).text().trim(); if (href) { // Convertir les liens relatifs en absolus const absoluteUrl = new URL(href, url).href; // Filtrer si pattern fourni if (filterPattern) { const regex = new RegExp(filterPattern); if (regex.test(absoluteUrl)) { links.push({ text, href: absoluteUrl }); } } else { links.push({ text, href: absoluteUrl }); } } }); if (links.length === 0) { return { content: [{ type: "text", text: "Aucun lien trouvé" }] }; } const linkList = links.map(l => `- ${l.text || "(pas de texte)"} : ${l.href}`).join("\n"); return { content: [{ type: "text", text: `Trouvé ${links.length} lien(s) dans ${url} : ${linkList}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur extraction liens : ${err.message}` }], isError: true }; } } ); // TOOL : Extraire un tableau HTML server.tool( "extract_table", "Extrait un tableau HTML et le retourne en JSON", { url: z.string().url().describe("URL de la page"), tableIndex: z.number().optional().describe("Index du tableau (0 = premier tableau)") }, async ({ url, tableIndex = 0 }) => { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const html = await response.text(); const $ = cheerio.load(html); const tables = $("table"); if (tables.length === 0) { return { content: [{ type: "text", text: "Aucun tableau trouvé dans la page" }] }; } if (tableIndex >= tables.length) { return { content: [{ type: "text", text: `Index ${tableIndex} invalide — seulement ${tables.length} tableau(x) trouvé(s)` }] }; } const table = $(tables[tableIndex]); const rows: string[][] = []; table.find("tr").each((_, row) => { const cells: string[] = []; $(row).find("th, td").each((_, cell) => { cells.push($(cell).text().trim()); }); if (cells.length > 0) { rows.push(cells); } }); return { content: [{ type: "text", text: `Tableau extrait de ${url} : ${JSON.stringify(rows, null, 2)}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur extraction tableau : ${err.message}` }], isError: true }; } } ); // Démarrage async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("✅ Serveur Web Scraper MCP démarré"); } main().catch(console.error);

Configuration Claude Desktop

{ "mcpServers": { "scraper": { "command": "node", "args": ["/chemin/vers/web-scraper-server/dist/index.js"] } } }

Exemples d'utilisation

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

Serveur 5 : Multi-Tool Combiné (Filesystem + PostgreSQL + REST API)

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.

Code complet (TypeScript)

// multi-tool-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"; import { Pool } from "pg"; import fetch from "node-fetch"; // Configuration const ALLOWED_DIRS = process.env.ALLOWED_DIRS?.split(",") || []; const DATABASE_URL = process.env.DATABASE_URL; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const server = new McpServer({ name: "multi-tool-server", version: "1.0.0" }); // ========== FILESYSTEM TOOLS ========== if (ALLOWED_DIRS.length > 0) { function isPathAllowed(filePath: string): boolean { const normalized = path.resolve(filePath); return ALLOWED_DIRS.some(dir => normalized.startsWith(path.resolve(dir))); } server.tool( "read_file", "Lit un fichier texte", { path: z.string().describe("Chemin du fichier") }, async ({ path: filePath }) => { if (!isPathAllowed(filePath)) { return { content: [{ type: "text", text: `Accès refusé : ${filePath}` }], isError: true }; } try { const content = await fs.readFile(filePath, "utf-8"); return { content: [{ type: "text", text: content }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur lecture : ${err.message}` }], isError: true }; } } ); console.error(`✅ Filesystem tools activés (${ALLOWED_DIRS.length} répertoires)`); } // ========== POSTGRESQL TOOLS ========== let pool: Pool | null = null; if (DATABASE_URL) { pool = new Pool({ connectionString: DATABASE_URL, max: 5 }); server.tool( "query_database", "Exécute une requête SELECT", { query: z.string().describe("Requête SQL SELECT") }, async ({ query }) => { if (!query.trim().toUpperCase().startsWith("SELECT")) { return { content: [{ type: "text", text: "Seules les requêtes SELECT sont autorisées" }], isError: true }; } try { const result = await pool!.query(query); return { content: [{ type: "text", text: `${result.rows.length} résultat(s) :\n${JSON.stringify(result.rows, null, 2)}` }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur SQL : ${err.message}` }], isError: true }; } } ); console.error("✅ PostgreSQL tools activés"); } // ========== GITHUB API TOOLS ========== if (GITHUB_TOKEN) { async function githubAPI(endpoint: string, options: any = {}) { const url = `https://api.github.com${endpoint}`; const headers = { "Authorization": `Bearer ${GITHUB_TOKEN}`, "Accept": "application/vnd.github+json", ...options.headers }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { throw new Error(`GitHub API error ${response.status}`); } return await response.json(); } server.tool( "list_github_repos", "Liste les repos d'un utilisateur GitHub", { username: z.string().describe("Nom d'utilisateur") }, async ({ username }) => { try { const repos = await githubAPI(`/users/${username}/repos`); const repoList = repos.map((r: any) => `- ${r.name} (${r.stargazers_count} ⭐)`).join("\n"); return { content: [{ type: "text", text: repoList }] }; } catch (err: any) { return { content: [{ type: "text", text: `Erreur GitHub : ${err.message}` }], isError: true }; } } ); console.error("✅ GitHub API tools activés"); } // ========== DÉMARRAGE ========== async function main() { // Test connexions au démarrage if (pool) { try { await pool.query("SELECT NOW()"); } catch (err) { console.error("❌ Erreur connexion PostgreSQL:", err); process.exit(1); } } if (GITHUB_TOKEN) { try { await githubAPI("/user"); } catch (err) { console.error("❌ Erreur auth GitHub:", err); process.exit(1); } } const transport = new StdioServerTransport(); await server.connect(transport); console.error("✅ Serveur Multi-Tool MCP démarré"); } process.on("SIGINT", async () => { if (pool) await pool.end(); process.exit(0); }); main().catch(console.error);

Configuration Claude Desktop

{ "mcpServers": { "multi-tool": { "command": "node", "args": ["/chemin/vers/multi-tool-server/dist/index.js"], "env": { "ALLOWED_DIRS": "/Users/username/Documents,/Users/username/Projects", "DATABASE_URL": "postgresql://user:password@localhost:5432/dbname", "GITHUB_TOKEN": "ghp_your_token" } } } }

Avantages du serveur multi-tool

  • 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

Erreur 4 : "Rate limit exceeded" (GitHub, OpenAI, etc.)

Symptôme : l'API externe retourne 429 ou 403.

Solution :

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

→ Formation Claude API pour Développeurs (3 jours, MCP inclus, financement OPCO)

Formez votre equipe a l'IA

Nos formations sont financables OPCO — reste a charge potentiel : 0€.

Voir les formationsVerifier eligibilite OPCO