
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 écrit quatre serveurs MCP utilisés en production. Leur code n'est pas public, mais la méthode l'est : c'est elle que je détaille ici. 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. Et chaque wrapper était à réécrire pour chaque outil IA.
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 a suivi : les éditeurs publient leur propre serveur, 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, transporté par stdio pour un serveur local ou par Streamable HTTP pour un serveur distant. L'ancien transport HTTP + SSE est déprécié depuis la révision du protocole de mars 2025 : à ne plus utiliser pour un nouveau serveur.
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 Streamable HTTP.
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.
Quand le serveur ne tourne plus sur votre poste mais sur une machine distante, le vrai sujet devient l'authentification : je l'ai raconté dans brancher un agent self-hosted sur Claude.
Vous souhaitez développer des serveurs MCP sur mesure pour votre infrastructure ? C'est l'un des chantiers types de mon accompagnement IA : prenons 30 minutes pour en discuter.
Related Articles

Brancher un agent self-hosted sur Claude sans lui donner les clés de la maison
Un agent qui tourne sur mon NAS, pilotable depuis Claude sur n'importe quel appareil, sans rien ouvrir sur Internet et sans droits root. Le pont MCP HTTP que j'ai écrit, et surtout les cinq murs pris en chemin.

BMAD en vrai : la méthode qui a changé mon idée avant le code
Ce que la méthode BMAD a changé sur un projet personnel dans l'immobilier : une autre niche, la concurrence américaine et les points de blocage, avant le code.

Donner une mémoire à Claude Code : l'architecture que j'utilise en mission
Une session Claude Code est excellente sur la tâche du jour et amnésique sur tout le reste. Voici le montage qui lui donne une mémoire : un vault Obsidian, des notes de session, et trois étages qu'il ne faut jamais confondre.