iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Node.js · Intermédiaire

Créer un serveur MCP en Node.js

Construire un serveur MCP de A à Z : tools, resources, prompts, validation, transports stdio et HTTP — pour connecter Claude à n'importe quel système externe.

Node.jsTypeScriptMCPClaude Code
01Comprendre MCP02Premier serveur MCP03Tools avancés — validation, erreurs, structure04Resources — exposer du contenu05Prompts — templates réutilisables06Transports et déploiement07Projet complet — MCP de gestion de projet
Chapitre 3·25 min

Tools avancés — validation, erreurs, structure

Le squelette du chapitre précédent ne valide rien et explose sur n'importe quelle entrée inattendue. En production, les tools ont besoin de validation stricte, d'erreurs informatives, et d'une architecture qui reste lisible à mesure que le nombre de tools grandit.

Validation avec Zod

Sans validation, args?.city as string est une bombe à retardement. Si Claude envoie undefined, un objet, ou un nombre — le cast TypeScript ne protège pas à l'exécution.

Zod valide et type simultanément :

import { z } from 'zod'
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
 
// Schéma Zod
const GetWeatherSchema = z.object({
  city: z.string().min(1).max(100),
  unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
})
 
// Dans le handler CallToolRequestSchema :
if (name === 'get_weather') {
  const parsed = GetWeatherSchema.safeParse(args)
 
  if (!parsed.success) {
    throw new McpError(
      ErrorCode.InvalidParams,
      `Paramètres invalides : ${parsed.error.message}`
    )
  }
 
  const { city, unit } = parsed.data  // typé correctement
  // ...
}

safeParse retourne { success: true, data } ou { success: false, error } — jamais d'exception. On choisit quoi faire avec l'erreur.

McpError — erreurs structurées

Les erreurs MCP ont un code standardisé que les clients comprennent.

import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
 
// Codes disponibles
ErrorCode.ParseError          // JSON invalide
ErrorCode.InvalidRequest      // requête malformée
ErrorCode.MethodNotFound      // tool inexistant
ErrorCode.InvalidParams       // paramètres invalides
ErrorCode.InternalError       // erreur serveur
// Paramètre manquant ou invalide
throw new McpError(ErrorCode.InvalidParams, 'city est requis et doit être une string')
 
// Ressource introuvable
throw new McpError(ErrorCode.InvalidParams, `Utilisateur ${id} introuvable`)
 
// Erreur interne
throw new McpError(ErrorCode.InternalError, `Erreur DB : ${err.message}`)

Claude reçoit le message d'erreur et peut l'expliquer à l'utilisateur ou adapter son comportement.

Retourner une erreur dans le contenu

Différence importante : throw McpError = erreur de protocole (tool n'a pas pu s'exécuter). Retourner isError: true = tool s'est exécuté mais a rencontré un cas d'erreur métier.

// Erreur métier — le tool a fonctionné, mais le résultat est une erreur
return {
  content: [
    {
      type: 'text',
      text: `Utilisateur ${id} introuvable dans la base de données.`,
    },
  ],
  isError: true,
}

Claude sait que c'est une erreur et adapte sa réponse — sans que ça brise le flux de la conversation.

Règle :

  • throw McpError → problème de protocole ou de paramètres invalides
  • return { isError: true } → résultat négatif normal (404, validation métier)

Architecture multi-tools : le pattern registry

Avec 10+ tools, un if/else géant dans le handler devient illisible. Pattern recommandé : un registre de tools.

// src/tools/types.ts
import { z } from 'zod'
 
export interface ToolDefinition {
  name: string
  description: string
  inputSchema: object
  schema: z.ZodType<any>
  handler: (args: any) => Promise<ToolResult>
}
 
export interface ToolResult {
  content: Array<{ type: string; text: string }>
  isError?: boolean
}
// src/tools/weather.ts
import { z } from 'zod'
import type { ToolDefinition } from './types.js'
 
const schema = z.object({
  city: z.string().min(1),
  unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
})
 
export const weatherTool: ToolDefinition = {
  name: 'get_weather',
  description: 'Météo actuelle pour une ville',
  inputSchema: {
    type: 'object',
    properties: {
      city: { type: 'string', description: 'Nom de la ville' },
      unit: { type: 'string', enum: ['celsius', 'fahrenheit'], default: 'celsius' },
    },
    required: ['city'],
  },
  schema,
  handler: async (rawArgs) => {
    const { city, unit } = schema.parse(rawArgs)
 
    const res = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=j1`)
    if (!res.ok) {
      return {
        content: [{ type: 'text', text: `Ville "${city}" introuvable.` }],
        isError: true,
      }
    }
 
    const data = await res.json() as any
    const current = data.current_condition[0]
    const temp = unit === 'celsius' ? current.temp_C : current.temp_F
 
    return {
      content: [
        {
          type: 'text',
          text: `${city} : ${temp}°${unit === 'celsius' ? 'C' : 'F'}, ${current.weatherDesc[0].value}`,
        },
      ],
    }
  },
}
// src/tools/index.ts
import { weatherTool } from './weather.js'
import { searchUsersTool } from './users.js'
import { createTicketTool } from './tickets.js'
import type { ToolDefinition } from './types.js'
 
export const tools: Map<string, ToolDefinition> = new Map([
  [weatherTool.name, weatherTool],
  [searchUsersTool.name, searchUsersTool],
  [createTicketTool.name, createTicketTool],
])
// src/index.ts — les handlers deviennent triviaux
import { tools } from './tools/index.js'
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
 
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: Array.from(tools.values()).map((t) => ({
    name: t.name,
    description: t.description,
    inputSchema: t.inputSchema,
  })),
}))
 
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params
 
  const tool = tools.get(name)
  if (!tool) {
    throw new McpError(ErrorCode.MethodNotFound, `Tool inconnu : ${name}`)
  }
 
  try {
    return await tool.handler(args)
  } catch (err) {
    if (err instanceof McpError) throw err
    throw new McpError(
      ErrorCode.InternalError,
      err instanceof Error ? err.message : 'Erreur interne'
    )
  }
})

Ajouter un tool = créer un fichier, l'importer dans tools/index.ts. Zéro modification des handlers.

Types de retour avancés

Retourner une image

import { readFile } from 'fs/promises'
 
return {
  content: [
    {
      type: 'image',
      data: (await readFile('/tmp/chart.png')).toString('base64'),
      mimeType: 'image/png',
    },
    {
      type: 'text',
      text: 'Graphique des ventes Q1 2026.',
    },
  ],
}

Claude peut analyser l'image si le modèle utilisé est multimodal.

Retourner une resource

Référencer une resource exposée par le même serveur :

return {
  content: [
    {
      type: 'resource',
      resource: {
        uri: 'db://users/42',
        mimeType: 'application/json',
        text: JSON.stringify(user),
      },
    },
  ],
}

Tools avec opérations longues

Pour des opérations qui prennent du temps, envoyer des notifications de progression :

handler: async (args, extra) => {
  // extra.signal — AbortSignal pour annulation
  // Envoyer des logs de progression via le serveur
  await server.sendLoggingMessage({ level: 'info', data: 'Démarrage du build...' })
 
  const result = await longRunningBuild(args.branch, { signal: extra?.signal })
 
  await server.sendLoggingMessage({ level: 'info', data: 'Build terminé.' })
 
  return {
    content: [{ type: 'text', text: `Build ${result.id} terminé en ${result.duration}s` }],
  }
},

Inputs complexes : tableaux et objets imbriqués

Zod gère toute la complexité de schéma :

const DeploySchema = z.object({
  service: z.string(),
  environment: z.enum(['dev', 'staging', 'production']),
  config: z.object({
    replicas: z.number().int().min(1).max(10).default(2),
    memory: z.string().regex(/^\d+[MG]$/, 'Format: 512M ou 2G').default('512M'),
    envVars: z.record(z.string()).optional(),
  }).default({}),
  dryRun: z.boolean().default(false),
})
 
// inputSchema correspondant (pour la découverte MCP)
inputSchema: {
  type: 'object',
  properties: {
    service: { type: 'string' },
    environment: { type: 'string', enum: ['dev', 'staging', 'production'] },
    config: {
      type: 'object',
      properties: {
        replicas: { type: 'number', default: 2 },
        memory: { type: 'string', default: '512M' },
        envVars: { type: 'object', additionalProperties: { type: 'string' } },
      },
    },
    dryRun: { type: 'boolean', default: false },
  },
  required: ['service', 'environment'],
}

Tester les tools en isolation

Avant de connecter Claude, tester les handlers directement :

// src/tools/weather.test.ts
import { weatherTool } from './weather.js'
 
// Test basique
const result = await weatherTool.handler({ city: 'Paris', unit: 'celsius' })
console.log(result)
 
// Test erreur
const errorResult = await weatherTool.handler({ city: '' })
console.log(errorResult)
npx tsx src/tools/weather.test.ts

Pas besoin de lancer le serveur complet pour tester un handler.


Les tools sont robustes et bien structurés. Le chapitre suivant couvre les resources — le moyen d'exposer du contenu que Claude peut lire à sa demande, plutôt que d'attendre un appel de tool.

Précédent
Premier serveur MCP
Suivant
Resources — exposer du contenu

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main