Créer son propre serveur MCP en TypeScript : le guide complet
J'ai construit plusieurs serveurs MCP en production pour des clients. Voici le guide que j'aurais aimé avoir au départ — de zéro jusqu'au déploiement.
J'ai construit 4 serveurs MCP en production ces derniers mois — pour connecter des bases de données internes, des APIs métier et des outils de debugging à Claude et Cursor. Le Model Context Protocol est le truc qui a le plus changé ma façon de travailler avec l'IA. C'est le "USB-C de l'IA" : un connecteur universel entre les LLMs et vos outils.
Voici le guide que j'aurais aimé avoir quand j'ai commencé — de zéro jusqu'au déploiement, avec les pièges que j'ai rencontrés.
Pourquoi MCP ?#
Avant MCP, chaque intégration IA nécessitait du code custom. Claude devait accéder à votre base PostgreSQL ? Un wrapper dédié. Votre repo GitHub ? Un autre wrapper. J'ai passé des semaines à écrire ces intégrations one-shot avant de découvrir MCP.
MCP standardise tout ça. Un serveur MCP expose des tools (actions) et des resources (données) via un protocole unifié. N'importe quel client compatible (Claude Desktop, Cursor, Claude Code) peut s'y connecter.
L'écosystème explose : le repo officiel GitHub MCP a dépassé les 28K étoiles, et des centaines de serveurs communautaires sont disponibles.
MCP fonctionne avec Claude Desktop, Claude Code, Cursor, et un nombre croissant d'IDE et d'agents IA. Un seul serveur sert tous ces clients.
Architecture d'un serveur MCP#
Un serveur MCP expose trois types de primitives :
- Tools : des actions que l'IA peut exécuter (requêter une API, écrire un fichier, lancer un build...)
- Resources : des données que l'IA peut lire (fichiers, schémas de BDD, documentation...)
- Prompts : des templates de prompts réutilisables
Le protocole de communication est basé sur JSON-RPC 2.0 via stdio ou SSE (Server-Sent Events).
Setup du projet#
Créons un serveur MCP qui expose des outils pour interagir avec une API de gestion de projets.
mkdir mcp-project-manager && cd mcp-project-manager
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsxConfigurez TypeScript :
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}Implémentation du serveur#
Créez src/index.ts :
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
interface Project {
id: string;
name: string;
status: "active" | "completed" | "paused";
tasks: Task[];
}
interface Task {
id: string;
title: string;
done: boolean;
}
const projects: Map<string, Project> = new Map();
const server = new McpServer({
name: "project-manager",
version: "1.0.0",
});
server.tool(
"list-projects",
"Liste tous les projets avec leur statut",
{},
async () => {
const allProjects = Array.from(projects.values());
return {
content: [
{
type: "text",
text: JSON.stringify(allProjects, null, 2),
},
],
};
}
);
server.tool(
"create-project",
"Crée un nouveau projet",
{
name: z.string().describe("Nom du projet"),
status: z
.enum(["active", "completed", "paused"])
.default("active")
.describe("Statut initial"),
},
async ({ name, status }) => {
const id = `proj_${Date.now()}`;
const project: Project = { id, name, status, tasks: [] };
projects.set(id, project);
return {
content: [
{
type: "text",
text: `Projet "${name}" créé avec l'ID ${id}`,
},
],
};
}
);
server.tool(
"add-task",
"Ajoute une tâche à un projet",
{
projectId: z.string().describe("ID du projet"),
title: z.string().describe("Titre de la tâche"),
},
async ({ projectId, title }) => {
const project = projects.get(projectId);
if (!project) {
return {
content: [{ type: "text", text: `Projet ${projectId} introuvable` }],
isError: true,
};
}
const task: Task = {
id: `task_${Date.now()}`,
title,
done: false,
};
project.tasks.push(task);
return {
content: [
{
type: "text",
text: `Tâche "${title}" ajoutée au projet "${project.name}"`,
},
],
};
}
);
server.tool(
"project-summary",
"Génère un résumé du projet avec progression",
{
projectId: z.string().describe("ID du projet"),
},
async ({ projectId }) => {
const project = projects.get(projectId);
if (!project) {
return {
content: [{ type: "text", text: `Projet ${projectId} introuvable` }],
isError: true,
};
}
const totalTasks = project.tasks.length;
const doneTasks = project.tasks.filter((t) => t.done).length;
const progress =
totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;
const summary = [
`# ${project.name}`,
`Statut: ${project.status}`,
`Progression: ${progress}% (${doneTasks}/${totalTasks} tâches)`,
"",
"## Tâches",
...project.tasks.map(
(t) => `- [${t.done ? "x" : " "}] ${t.title}`
),
].join("\n");
return {
content: [{ type: "text", text: summary }],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();Points clés de l'implémentation#
Validation avec Zod : chaque paramètre de tool est validé via un schéma Zod. Le SDK MCP utilise ces schémas pour générer la description des outils envoyée au client IA. Plus vos descriptions sont précises, mieux l'IA choisira le bon outil.
Transport stdio : la communication se fait via stdin/stdout. C'est le mode le plus courant pour les serveurs locaux. Pour un serveur distant, utilisez le transport SSE.
Gestion d'erreurs : le flag isError: true signale au client IA que l'opération a échoué, lui permettant de réagir intelligemment.
Écrivez des descriptions d'outils comme si vous expliquiez à un collègue. L'IA les utilise pour décider quel outil appeler. "Liste les projets" est mieux que "get_projects".
Exposer des ressources#
Les resources permettent à l'IA de lire des données structurées :
server.resource(
"project-list",
"projects://list",
async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(Array.from(projects.values()), null, 2),
mimeType: "application/json",
},
],
})
);Connexion à Claude Desktop#
Ajoutez votre serveur dans la config Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json sur macOS) :
{
"mcpServers": {
"project-manager": {
"command": "npx",
"args": ["tsx", "/chemin/vers/mcp-project-manager/src/index.ts"]
}
}
}Redémarrez Claude Desktop. Vous verrez vos outils apparaître dans l'interface. Demandez simplement :
"Crée un projet 'Refonte site web' et ajoute 3 tâches : maquettes, développement front, tests"
Claude appellera automatiquement create-project puis add-task trois fois.
Connexion à Claude Code#
Pour Claude Code, la configuration se fait via le fichier .mcp.json à la racine de votre projet :
{
"mcpServers": {
"project-manager": {
"command": "npx",
"args": ["tsx", "./mcp-project-manager/src/index.ts"]
}
}
}Ajout de persistance#
Notre exemple utilise un Map en mémoire. Pour un usage réel, connectez-vous à une base de données :
import { readFileSync, writeFileSync, existsSync } from "fs";
const DB_PATH = "./data/projects.json";
function loadProjects(): Map<string, Project> {
if (!existsSync(DB_PATH)) return new Map();
const data = JSON.parse(readFileSync(DB_PATH, "utf-8"));
return new Map(Object.entries(data));
}
function saveProjects(projects: Map<string, Project>) {
const data = Object.fromEntries(projects);
writeFileSync(DB_PATH, JSON.stringify(data, null, 2));
}En production, utilisez une vraie base de données (SQLite, PostgreSQL). Le stockage fichier JSON ne gère pas la concurrence et peut corrompre les données sous charge.
Bonnes pratiques#
Après avoir développé plusieurs serveurs MCP en production, voici les patterns qui fonctionnent :
-
Un outil = une action atomique : préférez
create-project+add-taskplutôt qu'uncreate-project-with-tasksmonolithique. L'IA compose mieux les outils granulaires. -
Descriptions riches : chaque outil et chaque paramètre doit avoir une description claire. C'est ce que l'IA lit pour décider quoi appeler.
-
Erreurs explicites : retournez des messages d'erreur compréhensibles avec
isError: true. "Projet introuvable" vaut mieux qu'un stack trace. -
Idempotence : quand c'est possible, rendez vos outils idempotents. L'IA peut retenter un appel en cas d'erreur.
-
Logging : loguez sur stderr (pas stdout, qui est réservé au protocole MCP) :
console.error("[MCP] Tool called: create-project");Conclusion#
Construire un serveur MCP est étonnamment simple. En quelques dizaines de lignes de TypeScript, vous pouvez exposer n'importe quel outil ou service interne à l'écosystème IA. Le protocole est standardisé, la documentation est solide, et l'écosystème grandit rapidement.
Le vrai pouvoir de MCP, c'est la composabilité : chaque serveur est un building block. Combinez un serveur GitHub, un serveur Postgres et votre serveur custom, et votre agent IA a une vision complète de votre stack.
Vous souhaitez développer des serveurs MCP sur mesure pour votre infrastructure ? Contactez-nous pour en discuter.
Articles similaires

Comment intégrer l'IA Claude dans une app Next.js
Retour d'expérience sur l'intégration de l'API Claude dans un projet Next.js en production — streaming, gestion d'erreurs et les pièges que j'ai rencontrés.
BMAD-METHOD : le framework agile qui transforme le développement avec l'IA
Découvrez BMAD-METHOD, le framework open-source qui structure le développement logiciel piloté par l'IA avec des agents spécialisés et des workflows agiles.
OpenClaw : transformez WhatsApp, Telegram et Discord en interface IA
J'ai installé OpenClaw pour me faire un assistant IA accessible depuis WhatsApp et Telegram. Voici comment ça marche et ce que ça change au quotidien.