Construire un serveur MCP de A à Z : tools, resources, prompts, validation, transports stdio et HTTP — pour connecter Claude à n'importe quel système externe.
On part de zéro. À la fin de ce chapitre, Claude Code sera connecté à votre serveur et pourra appeler votre premier outil.
mkdir mon-mcp && cd mon-mcp
npm init -ynpm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node@modelcontextprotocol/sdk — le SDK officiel MCPzod — validation de schéma (indispensable en pratique)tsx — exécute du TypeScript directement sans build{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}{
"name": "mon-mcp",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}"type": "module" — on écrit en ESM. Le SDK MCP utilise ESM, votre code aussi.
mon-mcp/
├── src/
│ ├── index.ts ← point d'entrée, setup serveur + transport
│ ├── tools/
│ │ └── index.ts ← définition des tools
│ └── resources/
│ └── index.ts ← définition des resources
├── package.json
└── tsconfig.json// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
// ── Créer le serveur ──────────────────────────────────────────────────
const server = new Server(
{
name: 'mon-mcp', // identifiant du serveur
version: '1.0.0',
},
{
capabilities: {
tools: {}, // ce serveur expose des tools
},
}
)
// ── Déclarer les tools disponibles ───────────────────────────────────
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'add',
description: 'Additionne deux nombres',
inputSchema: {
type: 'object',
properties: {
a: { type: 'number', description: 'Premier nombre' },
b: { type: 'number', description: 'Deuxième nombre' },
},
required: ['a', 'b'],
},
},
],
}))
// ── Implémenter les tools ─────────────────────────────────────────────
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
if (name === 'add') {
const a = args?.a as number
const b = args?.b as number
return {
content: [
{
type: 'text',
text: `${a} + ${b} = ${a + b}`,
},
],
}
}
throw new Error(`Tool inconnu : ${name}`)
})
// ── Démarrer avec le transport stdio ──────────────────────────────────
const transport = new StdioServerTransport()
await server.connect(transport)Avant de connecter Claude, tester que le serveur répond :
# Envoyer une requête JSON-RPC manuellement
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx tsx src/index.tsRéponse attendue :
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "add",
"description": "Additionne deux nombres",
"inputSchema": { ... }
}
]
}
}Si vous voyez ça, le serveur fonctionne.
Éditer ~/.claude/settings.json (ou .claude/settings.json pour le projet) :
{
"mcpServers": {
"mon-mcp": {
"command": "npx",
"args": ["tsx", "/chemin/absolu/vers/mon-mcp/src/index.ts"]
}
}
}Ou avec le build compilé :
{
"mcpServers": {
"mon-mcp": {
"command": "node",
"args": ["/chemin/absolu/vers/mon-mcp/dist/index.js"]
}
}
}Redémarrer Claude Code. Au démarrage, il lance le processus npx tsx ... et communique via stdio.
Dans Claude Code :
> liste les outils MCP disponiblesClaude doit mentionner add parmi les outils. Puis :
> utilise l'outil add avec 17 et 25Claude appelle le tool et retourne 17 + 25 = 42.
Un tool qui requête une API externe :
// Dans ListToolsRequestSchema handler, ajouter dans le tableau tools :
{
name: 'get_weather',
description: 'Retourne la météo actuelle pour une ville',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'Nom de la ville (ex: Paris, Lyon)',
},
},
required: ['city'],
},
},
// Dans CallToolRequestSchema handler :
if (name === 'get_weather') {
const city = args?.city as string
const res = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1`
)
const data = await res.json() as any
const current = data.current_condition[0]
return {
content: [
{
type: 'text',
text: JSON.stringify({
city,
temp_c: current.temp_C,
feels_like_c: current.FeelsLikeC,
description: current.weatherDesc[0].value,
humidity: current.humidity,
}, null, 2),
},
],
}
}> quel temps fait-il à Bordeaux ?Claude appelle get_weather({ city: "Bordeaux" }) et interprète les données retournées.
Un tool peut retourner plusieurs blocs de contenu :
return {
content: [
{
type: 'text',
text: '## Résultats de la requête',
},
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
{
type: 'text',
text: `${results.length} résultats trouvés.`,
},
],
}Types de content disponibles : text, image (base64), resource (référence à une resource MCP).
Le transport stdio utilise stdin/stdout pour le protocole. console.log sur stdout casse le protocole. Pour logger :
// Toujours sur stderr
console.error('[mon-mcp] serveur démarré')
console.error('[mon-mcp] tool appelé :', name, args)Ou utiliser server.sendLoggingMessage() :
await server.sendLoggingMessage({
level: 'info',
data: `Tool ${name} appelé avec ${JSON.stringify(args)}`,
})Les logs MCP apparaissent dans les logs de Claude Code sans polluer la communication.
Le serveur tourne, Claude s'y connecte et appelle les tools. Le chapitre suivant rend les tools robustes : validation typée avec Zod, gestion d'erreurs structurée, tools asynchrones complexes.