Aller au contenu
Nicolas Demay
Créer ses propres serveurs MCP

Créer ses propres serveurs MCP

Publié le 10 min de lecture

Quand Anthropic a publié le Model Context Protocol, l’idée était simple : donner aux LLM un moyen propre d’interagir avec des services externes. Depuis, l’écosystème a explosé. Chaque éditeur a rapidement publié son propre serveur MCP, prêt à l’emploi.

C’est pratique. Mais pour moi, la vraie puissance des MCP n’est pas là. Elle est dans la possibilité de créer ses propres serveurs, des outils taillés sur mesure pour votre projet et votre infra. PHPUnit dans un container Docker spécifique, accès scopé à la base de données locale, analyse statique avec PHPStan, des outils que le modèle peut appeler directement, sans tâtonner.

C’est ce que j’utilise au quotidien avec Claude Code. Voici comment.

Le protocole MCP

Un point important : à mesure qu’on branche des serveurs MCP, tous les outils sont chargés dans le contexte d’entrée du modèle. Trois ou quatre serveurs, et ça peut représenter des milliers de tokens rien que pour décrire les outils disponibles. La fenêtre de contexte en prend un coup, et les performances avec.

Pour répondre à ce problème, Claude Code a introduit le Tool Search, un mécanisme de chargement différé (deferred loading), activé par défaut. Les définitions complètes des outils ne sont plus injectées au démarrage. Le modèle reçoit uniquement la liste des noms disponibles, et quand il a besoin d’un outil spécifique, il le recherche à la demande via un outil dédié (ToolSearch).

Ça réduit l’empreinte sur la fenêtre de contexte. Le compromis, c’est que parfois le modèle “oublie” qu’un outil existe et ne pense pas à le chercher. Mais il suffit de mentionner les outils dans l’orchestration.

MCP = outils, skills = orchestration

En parallèle des MCP, Claude Code propose les skills, des prompts complexes et réutilisables qui décrivent un workflow complet dans un fichier Markdown. Le modèle peut les invoquer comme une commande. On pourrait penser que les skills remplacent les MCP. Pourquoi s’embêter avec un serveur MCP quand un prompt bien rédigé fait le travail ?

Parce que les deux ne font pas la même chose. Un MCP expose des outils typés, des fonctions avec des paramètres définis, des retours structurés, un comportement déterministe. C’est l’équivalent d’une API. Quand le modèle appelle mcp__youtrack__get_issue avec un paramètre issueId: "PROJ-42", il sait exactement ce qu’il envoie et ce qu’il va recevoir.

Un skill expose de l’orchestration, un prompt qui décrit un workflow, des étapes, des décisions. Le skill peut référencer des outils MCP, des commandes bash, des fichiers à lire, dans un enchaînement logique.

Les deux se combinent naturellement. Dans mes skills, je référence directement les outils MCP par leur nom :

.claude/skills/commit/SKILL.md
---
name: commit
description: Create a commit with auto-generated message
allowed-tools: mcp__project__phpstan, mcp__project__phpunit
---

Le champ allowed-tools dans le frontmatter d’un skill déclare explicitement quels outils MCP le modèle peut utiliser pendant l’exécution du skill.

Le problème : un modèle aveugle hors du code

Par défaut, Claude Code sait lire et écrire des fichiers, et exécuter du bash. C’est déjà beaucoup. Mais il ne connaît pas les outils spécifiques de votre projet, PHPStan, PHPUnit, bin/console, votre base de données locale.

Il peut deviner des commandes bash. Mais bon, sans structure, sans contexte projet. Quand le modèle lance un docker compose exec php bin/console et que le container s’appelle php-fpm-worktree-feature-42, il se plante. Il tâtonne, il essaie des variantes, il perd du temps et du contexte.

C’est exactement là que les MCP brillent : en exposant des outils scopés au projet, on élimine toute ambiguïté.

Créer ses propres MCP

Beaucoup de services tiers ont publié leur propre serveur MCP. Mais la qualité varie. Quand JetBrains a publié le MCP YouTrack, je l’ai testé immédiatement, et j’ai été déçu. Il ne récupérait pas les médias associés aux tickets, retournait des erreurs sur certains appels, et manquait de fonctionnalités essentielles à mon workflow.

Comme YouTrack expose une API REST publique, j’ai demandé à Claude de créer un serveur MCP from scratch. En quelques minutes, j’avais un serveur Node.js fonctionnel avec exactement les outils dont j’avais besoin :

~/.claude/mcp-servers/youtrack/index.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const TOOLS = [
{
name: "get_issue",
description: "Retrieve a YouTrack issue with all details",
inputSchema: {
type: "object",
properties: {
issueId: {
type: "string",
description: "The issue ID (e.g., 'PROJECT-123')",
},
},
required: ["issueId"],
},
},
{
name: "get_issue_comments",
description: "Retrieve all comments for a YouTrack issue",
// ...
},
{
name: "get_issue_attachments",
description: "Retrieve media and attachments for an issue",
// ...
},
{
name: "update_issue",
description: "Update issue fields (state, assignee, etc.)",
// ...
},
];
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "get_issue": {
const response = await fetch(
`${YOUTRACK_URL}/api/issues/${args.issueId}?fields=idReadable,summary,description,fields(name,value(name))`,
{ headers: { Authorization: `Bearer ${YOUTRACK_TOKEN}` } }
);
const issue = await response.json();
return { content: [{ type: "text", text: JSON.stringify(issue) }] };
}
case "get_issue_comments":
// ...
case "get_issue_attachments":
// ...
case "update_issue":
// ...
}
});

Le principe est simple : on déclare les outils avec leur schéma d’entrée (ce que le modèle voit), puis on implémente le handler qui fait le vrai travail, ici un appel à l’API REST YouTrack. Le SDK MCP se charge du transport entre Claude Code et le serveur.

Pour que Claude Code découvre ce serveur, il suffit de le déclarer dans le settings.json, au niveau global ou au niveau projet :

~/.claude/settings.json
{
"mcpServers": {
"youtrack": {
"command": "node",
"args": ["/home/nicolas/.claude/mcp-servers/youtrack/index.js"],
"env": {
"YOUTRACK_URL": "https://youtrack.example.com",
"YOUTRACK_TOKEN": "perm:xxx"
}
}
}
}

Le champ command indique comment lancer le serveur, args passe les arguments, et env injecte les variables d’environnement nécessaires, typiquement les URLs et tokens d’API. Le serveur démarre automatiquement quand Claude Code se lance, et ses outils deviennent disponibles dans la session.

Quand je traite un ticket, je demande à Claude de parser le contenu complet, description, commentaires, pièces jointes. Tout le contexte métier du ticket est injecté en une seule commande. Pas de copier-coller depuis le navigateur, pas de résumé approximatif.

Le cas des worktrees Docker

Sur mes principaux projets, je travaille avec des git worktrees. Chaque branche tourne dans son propre dossier avec sa propre stack Docker. C’est très puissant pour paralléliser le travail, pendant qu’un agent travaille sur une feature, un autre peut tourner sur un fix urgent dans un worktree séparé.

Mais ça complexifie l’infrastructure. Chaque worktree a ses propres containers Docker avec des noms spécifiques. Quand le modèle a besoin de lancer PHPUnit ou d’accéder à la base de données, il ne sait pas quel container cibler. Il devine, il se trompe, il perd trois tentatives à tâtonner.

J’ai donc créé une panoplie de MCP custom qui exposent les outils essentiels, scopés au worktree courant :

mcp-server.js
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const exec = (cmd) => execSync(cmd, { encoding: "utf-8" });
const dc = `docker compose -f ${composeFile} exec`;
switch (name) {
case "phpunit": {
const path = args.testPath ? ` ${args.testPath}` : "";
const filter = args.filter ? ` --filter ${args.filter}` : "";
const output = exec(`${dc} php vendor/bin/phpunit${path}${filter}`);
return { content: [{ type: "text", text: parseTestOutput(output) }] };
}
case "console": {
const output = exec(`${dc} php bin/console ${args.command}`);
return { content: [{ type: "text", text: output }] };
}
case "mysql_query": {
if (!args.query.trim().toUpperCase().startsWith("SELECT")) {
return { content: [{ type: "text", text: "Error: only SELECT queries are allowed" }] };
}
const output = exec(`${dc} db mysql -e "${args.query}" ${dbName}`);
return { content: [{ type: "text", text: output }] };
}
}
});

Du coup, quand Claude ouvre un projet dans un worktree spécifique, il fait une recherche dans ses outils disponibles et cible directement le bon container, sans tâtonnement.

Contrôler l’output : protéger la fenêtre de contexte

Un avantage souvent sous-estimé des MCP custom : le post-traitement de l’output.

Quand on lance une suite de tests Behat via bash, l’output peut être massif, des centaines de lignes de verbose, de setup, de teardown. Sans contrôle, tout ça atterrit dans la fenêtre de contexte du modèle et pollue l’espace disponible pour le raisonnement.

Avec un outil MCP, on contrôle ce qui revient au modèle. Mon tool PHPUnit ne renvoie pas 200 lignes de verbose, il parse l’output et retourne uniquement les erreurs avec le fichier et la ligne concernés. C’est la différence entre :

Terminal window
# Bash brut — tout l'output arrive dans le contexte
docker compose exec php vendor/bin/phpunit
# 200+ lignes de sortie...

Et un outil MCP qui retourne :

{
"status": "failed",
"total": 142,
"passed": 139,
"failed": 3,
"errors": [
{"file": "tests/Service/InvoiceTest.php", "line": 47, "message": "Failed asserting that null is not null"},
{"file": "tests/Controller/OrderTest.php", "line": 112, "message": "Expected status 200, got 422"},
{"file": "tests/Repository/UserTest.php", "line": 83, "message": "Undefined method findByActive"}
]
}

Le modèle reçoit exactement ce dont il a besoin pour corriger, rien de plus. Essayez d’obtenir ça avec un skill qui appelle un CLI, c’est techniquement possible avec du bash et du grep, mais c’est fragile et verbeux.

La sécurité par le scope

Dans l’article précédent, j’avais expliqué comment interdire à Claude tout accès direct au CLI MySQL, permissions et hooks combinés pour bloquer les commandes dangereuses. Mais j’ai quand même besoin qu’il puisse interagir avec la base de données locale du projet.

C’est exactement là que le MCP custom prend tout son sens. L’outil mysql_query exposé par mon serveur ne peut cibler que la base du projet courant. Le modèle peut consulter le schéma, vérifier l’état d’un enregistrement, activer un feature flag en base, tout ce dont il a besoin pour travailler. Mais il ne peut pas aller au-delà, c’est naturellement scopé.

C’est une sécurité qu’un skill seul ne peut pas offrir. Un skill reste un prompt. Il peut suggérer au modèle de ne pas toucher à la prod, mais rien ne l’en empêche techniquement. Un MCP, c’est du code : la restriction est structurelle.

L’agent qui s’auto-équipe

Parce que soyons clairs : tous ces serveurs MCP que je vous montre depuis le début de l’article, ce n’est pas moi qui les écris. C’est Claude.

Quand j’arrive sur un nouveau projet avec une infrastructure Docker spécifique, je lui demande d’analyser le docker-compose.yml et de générer le serveur MCP correspondant. Il comprend la structure, identifie les services, et produit un serveur fonctionnel avec les outils adaptés. Ce besoin revient si souvent que j’ai créé un skill dédié (via le plugin officiel Anthropic /skill-creator) qui génère automatiquement ce serveur MCP pour n’importe quel projet.

Le modèle crée les outils dont il a besoin pour travailler efficacement sur le projet.

Impact sur le workflow quotidien

Bref, au quotidien la combinaison MCP + skills change la façon dont je travaille avec Claude Code.

Le modèle peut vérifier son propre code avant de proposer un commit. Dans mon skill de commit, le prompt lui impose de lancer PHPStan, et allowed-tools lui donne accès à l’outil MCP correspondant. Le modèle ne peut pas “oublier” de lancer l’analyse statique, c’est dans la procédure.

Côté tests, c’est pareil : pas besoin de copier-coller les erreurs, le MCP lui retourne directement les fichiers et lignes en erreur. Il corrige, il relance, il itère jusqu’à ce que ça passe.

Et puis il y a la base de données. Je m’en sers au quotidien comme interface directe :

Quel est l’email du user 35 ?

Quelles sont les dernières commandes avec le statut pending ?

Combien de clients ont activé le feature flag X ?

Il traduit ma question en SQL, interroge la base via l’outil scopé, et me renvoie la réponse. Plus besoin d’ouvrir un client MySQL pour une question rapide.

Du coup le modèle ne se contente plus de suggérer du code, il intervient directement sur le projet.

”Les MCP sont morts”

C’est le discours qu’on voit passer régulièrement chez les influenceurs IA. Supprimez vos serveurs MCP, migrez tout vers des skills, les MCP ne sont qu’un intermédiaire inutile.

Je pense qu’ils passent à côté de l’essentiel. Les serveurs MCP tiers, peut-être. Mais les MCP custom, ceux qu’on crée soi-même, taillés pour leur projet, leur stack, c’est là que tout se joue. Et si un service tiers que vous utilisez n’a pas encore publié son propre MCP mais expose une API publique, pas besoin d’attendre. Pas de MCP officiel, pas de CLI, un MCP custom reste la seule façon de donner accès au modèle. Les skills orchestrent, les MCP font le boulot.