Construire un client MCP en TypeScript : se connecter à tout serveur d'outils IA

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

Vous avez appris à construire des serveurs MCP. Maintenant, construisez l'autre côté. Dans ce tutoriel, vous créerez un client MCP en TypeScript qui se connecte à tout serveur MCP, découvre ses capacités et appelle des outils de manière programmatique — la base pour construire vos propres applications alimentées par l'IA.

Ce que vous apprendrez

À la fin de ce tutoriel, vous serez capable de :

  • Comprendre l'architecture client-serveur MCP du point de vue du client
  • Créer un client MCP en TypeScript avec @modelcontextprotocol/sdk
  • Se connecter à des serveurs MCP via les transports stdio et SSE
  • Découvrir et appeler des outils exposés par tout serveur MCP
  • Lire des ressources et utiliser des prompts depuis les serveurs
  • Construire un client multi-serveurs qui se connecte à plusieurs serveurs simultanément
  • Créer une interface CLI interactive pour explorer MCP

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • Des connaissances en TypeScript (types, async/await, modules)
  • Un éditeur de code — VS Code ou Cursor recommandé
  • Une familiarité avec les concepts du protocole MCP (utile mais pas obligatoire)
  • Une compréhension basique de JSON-RPC

Qu'est-ce qu'un client MCP ?

Dans l'architecture du Model Context Protocol, le client est le composant qui se connecte aux serveurs MCP et consomme leurs capacités. Alors que les hôtes MCP comme Claude Desktop et Cursor ont des clients intégrés, vous pouvez construire votre propre client pour :

  • Intégrer les outils MCP dans vos applications — chatbots, outils CLI, pipelines d'automatisation
  • Construire des workflows IA personnalisés qui orchestrent plusieurs serveurs MCP
  • Tester et déboguer des serveurs MCP pendant le développement
  • Créer des interfaces spécialisées adaptées à des cas d'utilisation spécifiques

Aperçu de l'architecture

┌─────────────────────────────────────────────────┐
│  Votre application (hôte MCP)                   │
│                                                 │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  │
│  │  Client 1 │  │  Client 2 │  │  Client 3 │  │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  │
└────────┼──────────────┼──────────────┼──────────┘
         │              │              │
    ┌────▼────┐   ┌─────▼────┐  ┌─────▼────┐
    │Serveur A│   │Serveur B │  │Serveur C │
    │ (stdio) │   │  (SSE)   │  │ (stdio)  │
    └─────────┘   └──────────┘  └──────────┘

Chaque client maintient une connexion 1:1 avec un seul serveur. Votre application (l'hôte) gère plusieurs clients pour se connecter à plusieurs serveurs.


Étape 1 : Configuration du projet

Créez un nouveau projet TypeScript pour votre client MCP :

mkdir mcp-client && cd mcp-client
npm init -y

Installez les dépendances requises :

npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Voici ce que fait chaque package :

PackageObjectif
@modelcontextprotocol/sdkSDK officiel MCP avec la classe Client
zodValidation de schémas pour les arguments des outils
typescriptCompilateur TypeScript
tsxExécuter des fichiers TypeScript directement

Initialisez TypeScript :

npx tsc --init

Mettez à jour votre tsconfig.json :

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

Créez le répertoire source :

mkdir src

Ajoutez les scripts à package.json :

{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "build": "tsc",
    "dev": "tsx watch src/index.ts"
  }
}

Étape 2 : Créer un client MCP basique

Créez src/client.ts — le wrapper client principal :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 
export async function createMCPClient(
  serverCommand: string,
  serverArgs: string[] = [],
  env?: Record<string, string>
) {
  const transport = new StdioClientTransport({
    command: serverCommand,
    args: serverArgs,
    env: env ? { ...process.env, ...env } as Record<string, string> : undefined,
  });
 
  const client = new Client(
    {
      name: "my-mcp-client",
      version: "1.0.0",
    },
    {
      capabilities: {
        roots: { listChanged: true },
        sampling: {},
      },
    }
  );
 
  await client.connect(transport);
  return client;
}

Décomposons ce qui se passe ici :

  1. StdioClientTransport — lance le serveur MCP en tant que processus enfant et communique via stdin/stdout. C'est le transport le plus courant pour les serveurs MCP locaux.

  2. Client — l'instance du client MCP. Le premier argument définit les informations du client (nom et version). Le second déclare les capacités supportées par votre client.

  3. client.connect(transport) — établit la connexion, effectue le handshake MCP et négocie les capacités avec le serveur.


Étape 3 : Découvrir les capacités du serveur

Une fois connecté, la première chose que votre client devrait faire est de découvrir ce que le serveur offre. Créez src/discover.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function discoverCapabilities(client: Client) {
  const serverInfo = client.getServerVersion();
  console.log("Connecté au serveur :", serverInfo);
 
  const capabilities = client.getServerCapabilities();
  console.log("Capacités du serveur :", capabilities);
 
  if (capabilities?.tools) {
    const toolsResult = await client.listTools();
    console.log(`\n${toolsResult.tools.length} outils trouvés :`);
    for (const tool of toolsResult.tools) {
      console.log(`  - ${tool.name} : ${tool.description}`);
    }
  }
 
  if (capabilities?.resources) {
    const resourcesResult = await client.listResources();
    console.log(`\n${resourcesResult.resources.length} ressources trouvées :`);
    for (const resource of resourcesResult.resources) {
      console.log(`  - ${resource.uri} : ${resource.name}`);
    }
  }
 
  if (capabilities?.prompts) {
    const promptsResult = await client.listPrompts();
    console.log(`\n${promptsResult.prompts.length} prompts trouvés :`);
    for (const prompt of promptsResult.prompts) {
      console.log(`  - ${prompt.name} : ${prompt.description}`);
    }
  }
 
  return capabilities;
}

Cette fonction interroge trois catégories de fonctionnalités du serveur :

  • Outils — les fonctions exposées par le serveur (comme search_files, run_query, create_issue)
  • Ressources — les sources de données fournies par le serveur (comme les fichiers, les enregistrements de base de données, les réponses API)
  • Prompts — des modèles de prompts réutilisables avec des paramètres

Étape 4 : Appeler des outils

Les outils sont la fonctionnalité MCP la plus puissante. Ils permettent à votre client d'invoquer des fonctions côté serveur avec des arguments structurés. Créez src/tools.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function callTool(
  client: Client,
  toolName: string,
  args: Record<string, unknown> = {}
) {
  const result = await client.callTool({
    name: toolName,
    arguments: args,
  });
 
  if (result.isError) {
    throw new Error(
      `L'outil "${toolName}" a échoué : ${JSON.stringify(result.content)}`
    );
  }
 
  return result;
}
 
export async function listAndDescribeTools(client: Client) {
  const { tools } = await client.listTools();
 
  return tools.map((tool) => ({
    name: tool.name,
    description: tool.description,
    parameters: tool.inputSchema,
  }));
}

Exemple : Appeler un outil de recherche de fichiers

const result = await callTool(client, "search_files", {
  query: "authentication",
  path: "./src",
});
 
for (const content of result.content) {
  if (content.type === "text") {
    console.log(content.text);
  }
}

Gérer les différents types de contenu

Les outils MCP peuvent retourner différents types de contenu. Voici comment les gérer :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export function processToolResult(result: Awaited<ReturnType<Client["callTool"]>>) {
  const outputs: string[] = [];
 
  for (const content of result.content) {
    switch (content.type) {
      case "text":
        outputs.push(content.text);
        break;
      case "image":
        outputs.push(`[Image : ${content.mimeType}, ${content.data.length} octets]`);
        break;
      case "resource":
        outputs.push(`[Ressource : ${content.resource.uri}]`);
        break;
      default:
        outputs.push(`[Type de contenu inconnu]`);
    }
  }
 
  return outputs.join("\n");
}

Étape 5 : Lire les ressources

Les ressources fournissent un accès en lecture seule aux données exposées par le serveur. Créez src/resources.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function readResource(client: Client, uri: string) {
  const result = await client.readResource({ uri });
 
  for (const content of result.contents) {
    if (content.text) {
      return content.text;
    }
    if (content.blob) {
      return `[Données binaires : ${content.mimeType}]`;
    }
  }
 
  return null;
}
 
export async function listAllResources(client: Client) {
  const resources: Array<{ uri: string; name: string; description?: string }> = [];
 
  const result = await client.listResources();
  resources.push(...result.resources);
 
  let cursor = result.nextCursor;
  while (cursor) {
    const nextPage = await client.listResources({ cursor });
    resources.push(...nextPage.resources);
    cursor = nextPage.nextCursor;
  }
 
  return resources;
}

Modèles de ressources

Certains serveurs exposent des modèles de ressources — des URI paramétrées qui génèrent des ressources dynamiquement :

export async function listResourceTemplates(client: Client) {
  const result = await client.listResourceTemplates();
 
  for (const template of result.resourceTemplates) {
    console.log(`Modèle : ${template.uriTemplate}`);
    console.log(`  Nom : ${template.name}`);
    console.log(`  Description : ${template.description}`);
  }
 
  return result.resourceTemplates;
}

Par exemple, un serveur MCP GitHub pourrait exposer un modèle comme github://repos/{owner}/{repo}/issues/{number}. Votre client remplit les paramètres pour lire des ressources spécifiques.


Étape 6 : Travailler avec les prompts

Les prompts sont des modèles de messages réutilisables définis par les serveurs pour des tâches courantes. Créez src/prompts.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function getPrompt(
  client: Client,
  promptName: string,
  args: Record<string, string> = {}
) {
  const result = await client.getPrompt({
    name: promptName,
    arguments: args,
  });
 
  console.log(`Prompt : ${result.description}`);
 
  for (const message of result.messages) {
    console.log(`[${message.role}] :`);
    if (message.content.type === "text") {
      console.log(message.content.text);
    }
  }
 
  return result;
}

Les prompts sont utiles pour obtenir des instructions pré-construites depuis un serveur. Par exemple, un serveur MCP de base de données pourrait offrir un prompt write_query qui inclut le schéma de la base de données et les bonnes pratiques SQL.


Étape 7 : Construire un CLI interactif

Maintenant, combinons tout dans un CLI interactif. Créez src/index.ts :

import * as readline from "node:readline";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { discoverCapabilities } from "./discover.js";
import { callTool, listAndDescribeTools, processToolResult } from "./tools.js";
import { readResource, listAllResources } from "./resources.js";
import { getPrompt } from "./prompts.js";
 
class MCPExplorer {
  private client: Client | null = null;
  private rl: readline.Interface;
 
  constructor() {
    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });
  }
 
  private prompt(question: string): Promise<string> {
    return new Promise((resolve) => {
      this.rl.question(question, resolve);
    });
  }
 
  async connect(command: string, args: string[] = []) {
    console.log(`Connexion au serveur : ${command} ${args.join(" ")}`);
    this.client = await createMCPClient(command, args);
    await discoverCapabilities(this.client);
    console.log("\nConnexion établie. Tapez 'help' pour les commandes.\n");
  }
 
  async run() {
    if (!this.client) {
      const serverCmd = process.argv[2];
      const serverArgs = process.argv.slice(3);
 
      if (!serverCmd) {
        console.log("Utilisation : tsx src/index.ts <server-command> [args...]");
        console.log("Exemple : tsx src/index.ts npx -y @modelcontextprotocol/server-everything");
        process.exit(1);
      }
 
      await this.connect(serverCmd, serverArgs);
    }
 
    while (true) {
      const input = await this.prompt("mcp> ");
      const [command, ...params] = input.trim().split(" ");
 
      try {
        await this.handleCommand(command, params);
      } catch (error) {
        console.error("Erreur :", (error as Error).message);
      }
    }
  }
 
  private async handleCommand(command: string, params: string[]) {
    if (!this.client) return;
 
    switch (command) {
      case "help":
        this.showHelp();
        break;
 
      case "tools":
        const tools = await listAndDescribeTools(this.client);
        for (const tool of tools) {
          console.log(`\n${tool.name}`);
          console.log(`  ${tool.description}`);
          if (tool.parameters) {
            console.log(`  Paramètres : ${JSON.stringify(tool.parameters, null, 2)}`);
          }
        }
        break;
 
      case "call": {
        const toolName = params[0];
        if (!toolName) {
          console.log("Utilisation : call <tool-name> [json-args]");
          break;
        }
        const argsStr = params.slice(1).join(" ");
        const args = argsStr ? JSON.parse(argsStr) : {};
        const result = await callTool(this.client, toolName, args);
        console.log(processToolResult(result));
        break;
      }
 
      case "resources": {
        const resources = await listAllResources(this.client);
        for (const r of resources) {
          console.log(`  ${r.uri} — ${r.name}`);
        }
        break;
      }
 
      case "read": {
        const uri = params[0];
        if (!uri) {
          console.log("Utilisation : read <resource-uri>");
          break;
        }
        const content = await readResource(this.client, uri);
        console.log(content);
        break;
      }
 
      case "prompts": {
        const promptsResult = await this.client.listPrompts();
        for (const p of promptsResult.prompts) {
          console.log(`  ${p.name} : ${p.description}`);
        }
        break;
      }
 
      case "prompt": {
        const promptName = params[0];
        if (!promptName) {
          console.log("Utilisation : prompt <prompt-name> [json-args]");
          break;
        }
        const promptArgs = params.slice(1).join(" ");
        const parsedArgs = promptArgs ? JSON.parse(promptArgs) : {};
        await getPrompt(this.client, promptName, parsedArgs);
        break;
      }
 
      case "quit":
      case "exit":
        await this.client.close();
        this.rl.close();
        process.exit(0);
 
      default:
        console.log(`Commande inconnue : ${command}. Tapez 'help' pour les options.`);
    }
  }
 
  private showHelp() {
    console.log(`
Commandes disponibles :
  tools              Lister tous les outils disponibles
  call <name> [args] Appeler un outil avec des arguments JSON optionnels
  resources          Lister toutes les ressources disponibles
  read <uri>         Lire une ressource par URI
  prompts            Lister tous les prompts disponibles
  prompt <name>      Obtenir un prompt avec des arguments JSON optionnels
  help               Afficher ce message d'aide
  quit               Se déconnecter et quitter
    `);
  }
}
 
const explorer = new MCPExplorer();
explorer.run().catch(console.error);

Tester avec le serveur Everything

Le projet MCP fournit un serveur de test qui expose des outils, ressources et prompts exemples :

npx tsx src/index.ts npx -y @modelcontextprotocol/server-everything

Vous devriez voir une sortie comme :

Connecté au serveur : { name: "everything", version: "1.0.0" }
Capacités du serveur : { tools: {}, resources: {}, prompts: {} }

2 outils trouvés :
  - echo : Renvoie l'entrée
  - add : Additionne deux nombres

2 ressources trouvées :
  - file:///example.txt : Fichier exemple
  - file:///data.json : Données exemple

Connexion établie. Tapez 'help' pour les commandes.

mcp> call echo {"message": "Bonjour MCP !"}
Bonjour MCP !

mcp> call add {"a": 5, "b": 3}
8

Étape 8 : Connexion via le transport SSE

Certains serveurs MCP fonctionnent comme des services HTTP autonomes et utilisent les Server-Sent Events (SSE) pour la communication. Créez src/sse-client.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
 
export async function createSSEClient(serverUrl: string) {
  const transport = new SSEClientTransport(new URL(serverUrl));
 
  const client = new Client(
    {
      name: "my-mcp-client",
      version: "1.0.0",
    },
    {
      capabilities: {
        roots: { listChanged: true },
        sampling: {},
      },
    }
  );
 
  await client.connect(transport);
  return client;
}

Quand utiliser SSE vs Stdio

TransportCas d'utilisationFonctionnement
StdioServeurs locaux, outils CLILe client lance le serveur en processus enfant
SSEServeurs distants, services partagésLe client se connecte à un serveur HTTP en cours d'exécution

Pour les serveurs MCP distants (ceux qui tournent dans le cloud), SSE est le bon choix. Pour le développement local et les outils personnels, stdio est plus simple.


Étape 9 : Construire un client multi-serveurs

Les applications réelles ont souvent besoin de se connecter à plusieurs serveurs MCP simultanément. Créez src/multi-client.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { createSSEClient } from "./sse-client.js";
 
interface ServerConfig {
  name: string;
  transport: "stdio" | "sse";
  command?: string;
  args?: string[];
  url?: string;
  env?: Record<string, string>;
}
 
export class MultiServerClient {
  private clients = new Map<string, Client>();
 
  async addServer(config: ServerConfig) {
    let client: Client;
 
    if (config.transport === "stdio" && config.command) {
      client = await createMCPClient(
        config.command,
        config.args ?? [],
        config.env
      );
    } else if (config.transport === "sse" && config.url) {
      client = await createSSEClient(config.url);
    } else {
      throw new Error(`Configuration serveur invalide pour "${config.name}"`);
    }
 
    this.clients.set(config.name, client);
    console.log(`Connecté à "${config.name}"`);
    return client;
  }
 
  async listAllTools() {
    const allTools: Array<{
      server: string;
      name: string;
      description?: string;
    }> = [];
 
    for (const [serverName, client] of this.clients) {
      const capabilities = client.getServerCapabilities();
      if (!capabilities?.tools) continue;
 
      const { tools } = await client.listTools();
      for (const tool of tools) {
        allTools.push({
          server: serverName,
          name: tool.name,
          description: tool.description,
        });
      }
    }
 
    return allTools;
  }
 
  async callTool(
    serverName: string,
    toolName: string,
    args: Record<string, unknown> = {}
  ) {
    const client = this.clients.get(serverName);
    if (!client) {
      throw new Error(`Serveur "${serverName}" non trouvé`);
    }
 
    return client.callTool({ name: toolName, arguments: args });
  }
 
  async disconnectAll() {
    for (const [name, client] of this.clients) {
      await client.close();
      console.log(`Déconnecté de "${name}"`);
    }
    this.clients.clear();
  }
}

Exemple d'utilisation

import { MultiServerClient } from "./multi-client.js";
 
const manager = new MultiServerClient();
 
await manager.addServer({
  name: "filesystem",
  transport: "stdio",
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
});
 
await manager.addServer({
  name: "github",
  transport: "stdio",
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-github"],
  env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "" },
});
 
const allTools = await manager.listAllTools();
console.log("Tous les outils disponibles sur les serveurs :");
for (const tool of allTools) {
  console.log(`  [${tool.server}] ${tool.name} : ${tool.description}`);
}
 
const files = await manager.callTool("filesystem", "list_directory", {
  path: "/tmp",
});
 
await manager.disconnectAll();

Étape 10 : Intégration avec un LLM

La vraie puissance d'un client MCP apparaît quand vous le connectez à un LLM. Le client devient le pont entre le modèle IA et les outils externes. Voici un exemple simplifié avec le SDK Anthropic :

import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function runAgentLoop(
  anthropic: Anthropic,
  mcpClient: Client,
  userMessage: string
) {
  const { tools: mcpTools } = await mcpClient.listTools();
 
  const anthropicTools: Anthropic.Tool[] = mcpTools.map((tool) => ({
    name: tool.name,
    description: tool.description ?? "",
    input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
  }));
 
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];
 
  let response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    tools: anthropicTools,
    messages,
  });
 
  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter(
      (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
    );
 
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
    for (const toolUse of toolUseBlocks) {
      console.log(`Appel de l'outil : ${toolUse.name}`);
      const result = await mcpClient.callTool({
        name: toolUse.name,
        arguments: toolUse.input as Record<string, unknown>,
      });
 
      const textContent = result.content
        .filter((c): c is { type: "text"; text: string } => c.type === "text")
        .map((c) => c.text)
        .join("\n");
 
      toolResults.push({
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: textContent,
      });
    }
 
    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });
 
    response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      tools: anthropicTools,
      messages,
    });
  }
 
  const finalText = response.content
    .filter((block): block is Anthropic.TextBlock => block.type === "text")
    .map((block) => block.text)
    .join("\n");
 
  return finalText;
}

Cela crée une boucle agentique : le LLM reçoit le message utilisateur avec les outils MCP disponibles, décide quels outils appeler, votre client les exécute, et les résultats retournent au LLM jusqu'à ce qu'il produise une réponse finale.


Étape 11 : Gestion des erreurs et reconnexion

Les clients MCP en production nécessitent une gestion des erreurs robuste. Créez src/resilient-client.ts :

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
 
export class ResilientMCPClient {
  private client: Client | null = null;
  private command: string;
  private args: string[];
  private maxRetries: number;
 
  constructor(command: string, args: string[] = [], maxRetries = 3) {
    this.command = command;
    this.args = args;
    this.maxRetries = maxRetries;
  }
 
  async connect(): Promise<Client> {
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        this.client = await createMCPClient(this.command, this.args);
        return this.client;
      } catch (error) {
        console.error(`Tentative de connexion ${attempt} échouée :`, error);
        if (attempt === this.maxRetries) throw error;
        const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
        await new Promise((r) => setTimeout(r, delay));
      }
    }
    throw new Error("Échec de la connexion après toutes les tentatives");
  }
 
  async callToolSafely(
    toolName: string,
    args: Record<string, unknown> = {}
  ) {
    if (!this.client) {
      await this.connect();
    }
 
    try {
      return await this.client!.callTool({ name: toolName, arguments: args });
    } catch (error) {
      console.error(`Appel d'outil échoué, reconnexion en cours...`);
      await this.connect();
      return this.client!.callTool({ name: toolName, arguments: args });
    }
  }
 
  async disconnect() {
    if (this.client) {
      await this.client.close();
      this.client = null;
    }
  }
}

Patterns clés de gestion des erreurs

  1. Backoff exponentiel — attendre plus longtemps entre chaque tentative de reconnexion
  2. Reconnexion automatique — si un appel d'outil échoue à cause d'une connexion rompue, se reconnecter et réessayer
  3. Arrêt gracieux — toujours fermer le client quand c'est terminé pour nettoyer les processus enfants

Étape 12 : Support de fichier de configuration

Les vrais clients MCP chargent les configurations de serveurs depuis un fichier, tout comme Claude Desktop utilise claude_desktop_config.json. Créez src/config.ts :

import { readFile } from "node:fs/promises";
import { MultiServerClient } from "./multi-client.js";
 
interface MCPConfig {
  mcpServers: Record<
    string,
    {
      command: string;
      args?: string[];
      env?: Record<string, string>;
    }
  >;
}
 
export async function loadFromConfig(configPath: string) {
  const raw = await readFile(configPath, "utf-8");
  const config: MCPConfig = JSON.parse(raw);
 
  const manager = new MultiServerClient();
 
  for (const [name, server] of Object.entries(config.mcpServers)) {
    await manager.addServer({
      name,
      transport: "stdio",
      command: server.command,
      args: server.args,
      env: server.env,
    });
  }
 
  return manager;
}

Exemple de fichier de configuration (mcp-config.json) :

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

Ce format est identique à celui utilisé par Claude Desktop, ce qui facilite le partage des configurations de serveurs entre votre client personnalisé et Claude Desktop.


Tester votre implémentation

Utiliser le MCP Inspector

Le projet MCP fournit un outil d'inspection pour les tests :

npx @modelcontextprotocol/inspector

Cela lance une interface web où vous pouvez vous connecter aux serveurs, parcourir les outils/ressources/prompts et tester les appels de manière interactive.

Écrire des tests unitaires

import { describe, it, expect } from "vitest";
import { createMCPClient } from "./client.js";
 
describe("MCP Client", () => {
  it("devrait se connecter au serveur everything", async () => {
    const client = await createMCPClient("npx", [
      "-y",
      "@modelcontextprotocol/server-everything",
    ]);
 
    const version = client.getServerVersion();
    expect(version).toBeDefined();
 
    const { tools } = await client.listTools();
    expect(tools.length).toBeGreaterThan(0);
 
    await client.close();
  });
 
  it("devrait appeler l'outil echo", async () => {
    const client = await createMCPClient("npx", [
      "-y",
      "@modelcontextprotocol/server-everything",
    ]);
 
    const result = await client.callTool({
      name: "echo",
      arguments: { message: "test" },
    });
 
    expect(result.content).toBeDefined();
    await client.close();
  });
});

Dépannage

Problèmes courants

"Server process exited unexpectedly"

  • Vérifiez que la commande serveur fonctionne seule : npx -y @modelcontextprotocol/server-everything
  • Assurez-vous que le binaire du serveur est installé et dans le PATH
  • Vérifiez que Node.js 20+ est installé

"Transport error: connection refused"

  • Pour le transport SSE, vérifiez que l'URL du serveur est correcte et que le serveur tourne
  • Vérifiez les règles de pare-feu bloquant le port de connexion

"Tool not found"

  • Exécutez listTools() pour voir les noms d'outils disponibles
  • Les noms d'outils sont sensibles à la casse
  • Le serveur a peut-être mis à jour sa liste d'outils — revérifiez après reconnexion

"Invalid arguments"

  • Vérifiez le inputSchema de l'outil pour les paramètres requis
  • Assurez-vous que les types d'arguments correspondent (string vs number vs boolean)
  • Utilisez JSON.stringify() pour déboguer les arguments envoyés

Structure du projet

Voici la disposition finale du projet :

mcp-client/
├── src/
│   ├── index.ts           # Point d'entrée CLI
│   ├── client.ts          # Factory client Stdio
│   ├── sse-client.ts      # Factory client SSE
│   ├── multi-client.ts    # Gestionnaire multi-serveurs
│   ├── discover.ts        # Découverte des capacités
│   ├── tools.ts           # Utilitaires d'appel d'outils
│   ├── resources.ts       # Utilitaires de lecture de ressources
│   ├── prompts.ts         # Gestion des prompts
│   ├── resilient-client.ts # Wrapper de gestion des erreurs
│   └── config.ts          # Chargeur de fichier de configuration
├── mcp-config.json        # Configuration des serveurs
├── package.json
└── tsconfig.json

Prochaines étapes

Maintenant que vous avez un client MCP fonctionnel, envisagez de :

  • Ajouter plus de transports — le SDK MCP supporte aussi Streamable HTTP pour les déploiements modernes
  • Construire une interface web — créez un frontend Next.js qui se connecte aux serveurs MCP via votre client
  • Intégrer avec votre application IA — utilisez le pattern d'intégration LLM de l'étape 10 dans votre chatbot ou assistant
  • Créer un système de plugins — laissez les utilisateurs configurer dynamiquement les serveurs MCP auxquels se connecter
  • Ajouter de la journalisation et des métriques — suivez la latence des appels d'outils, les taux d'erreur et les patterns d'utilisation
  • Explorez le tutoriel serveur MCP pour construire vos propres serveurs

Conclusion

Vous avez construit un client MCP entièrement fonctionnel en TypeScript capable de :

  • Se connecter à tout serveur MCP via stdio ou SSE
  • Découvrir et appeler des outils, lire des ressources et utiliser des prompts
  • Gérer les connexions à plusieurs serveurs simultanément
  • Gérer les erreurs avec reconnexion automatique
  • S'intégrer avec des LLM pour des workflows d'appel d'outils agentiques
  • Charger les configurations de serveurs depuis des fichiers

L'écosystème MCP grandit rapidement, avec de nouveaux serveurs publiés chaque jour pour les bases de données, les API, les services cloud et les outils de développement. Votre client personnalisé vous donne un contrôle total sur la façon dont vos applications interagissent avec cet écosystème — sans être enfermé dans une application hôte spécifique.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur HTML-in-Canvas : Rendez de Vrais Éléments DOM dans un Canvas avec la Nouvelle API drawElement.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·