Créer un serveur MCP pour Claude Code et GitHub Copilot

Créez un seul serveur MCP en TypeScript et enregistrez-le dans Claude Code comme dans GitHub Copilot. Vous obtenez le code fonctionnel des outils, la configuration exacte de chaque client, une routine de débogage avec le MCP Inspector et un outil d’image qui appelle l’API PicassoIA.

Créer un serveur MCP pour Claude Code et GitHub Copilot
Cristian Da Conceicao
Fondateur de Picasso IA

Vous avez écrit un script qui vous fait gagner dix minutes par jour, et vous voulez maintenant que votre assistant d’IA l’exécute sans passer par un copier-coller. Créez-le une fois sous forme de serveur Model Context Protocol, et Claude Code et GitHub Copilot peuvent appeler les mêmes outils, car MCP est le langage commun qu’ils utilisent pour communiquer avec tout ce qui se trouve en dehors de l’éditeur. Ce guide pas à pas construit un petit serveur TypeScript, l’enregistre dans Claude Code, l’enregistre dans Copilot sous VS Code, et se termine par un véritable outil de génération d’images qui appelle l’API de PicassoIA. Prévoyez environ 40 minutes et à peu près 100 lignes de code.

Pourquoi un seul serveur vaut mieux que deux

Avant MCP, chaque assistant exigeait son propre format de plugin, son propre manifeste et ses propres règles de packaging. Un serveur MCP remplace tout cela par un processus unique qui annonce ce qu’il sait faire. Le client lance le processus, demande la liste de ses capacités et transmet cette liste au modèle. Le modèle décide ensuite, au cours de la conversation, quand un appel vaut la peine d’être fait.

Vue de dessus d’un bureau avec un schéma dessiné à la main, des cases et des flèches, à côté d’un ordinateur portable et d’un café

Le même protocole, deux clients

Un serveur peut exposer trois types de capacités :

  • Outils : des fonctions que le modèle peut appeler, comme add_note ou generate_image.
  • Ressources : des données en lecture seule que le client peut joindre à une conversation, comme un fichier journal ou un schéma.
  • Prompts : des modèles réutilisables que l’utilisateur déclenche volontairement.

Les outils concentrent aujourd’hui presque toute la valeur, c’est pourquoi cet article s’y limite. Claude Code et Copilot parlent tous deux les mêmes messages JSON-RPC sur les mêmes transports, ce qui signifie qu’un serveur qui fonctionne avec un client fonctionnera avec l’autre avec presque aucune modification.

Développeur debout à côté d’un tableau blanc couvert de notes adhésives disposées en trois colonnes

Là où les configurations diffèrent

Le serveur est identique. L’enregistrement, lui, ne l’est pas. Voici toute la différence dans un seul tableau :

ParamètreClaude CodeGitHub Copilot dans VS Code
Fichier de configuration.mcp.json dans le projet, ou ~/.claude.json.vscode/mcp.json, ou votre profil utilisateur
Propriété racinemcpServersservers
Ajout depuis le terminalclaude mcp addPalette de commandes : MCP: Add Server
Champ de transporttype (stdio, http, sse)type est obligatoire (stdio ou http)
SecretsOption --env ou expansion ${VAR}Bloc inputs avec ${input:id}
Où les outils s’exécutentToute sessionMode Agent uniquement

💡 Astuce : La propriété racine est le piège classique. Si vous collez une configuration Claude Code telle quelle dans VS Code, rien ne se charge, car Copilot cherche servers et non mcpServers.

Préparer le projet

Choisissez un dossier en dehors de votre dépôt principal afin que le serveur puisse servir plusieurs projets plus tard. Il vous faut Node.js 20 ou une version plus récente, ainsi qu’un terminal.

Gros plan sur les mains d’un développeur en train de taper, avec un éditeur de code flou en arrière-plan

Installer le SDK

mkdir team-notes-mcp && cd team-notes-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Ouvrez package.json et ajoutez "type": "module" ainsi que deux scripts, "build": "tsc" et "dev": "tsx src/index.ts". Créez ensuite un tsconfig.json :

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

Les exemples utilisent l’API @modelcontextprotocol/sdk 1.x avec McpServer et registerTool. Si une nouvelle version majeure modifie un chemin d’import, les concepts ci-dessous restent les mêmes.

Commencer avec stdio

MCP définit deux transports principaux. stdio signifie que le client lance votre serveur comme processus enfant et échange des messages via l’entrée et la sortie standard. Streamable HTTP signifie que le serveur tourne de son côté et que les clients se connectent par une URL. Commencez par stdio. Il ne nécessite ni port, ni couche d’authentification, ni hébergement, et les deux clients le prennent en charge d’emblée. Passez à HTTP uniquement lorsque plusieurs personnes doivent partager une même instance en cours d’exécution.

Écrire le serveur

Notre exemple est un petit serveur de notes d’équipe avec deux outils : l’un enregistre une note, l’autre les recherche. Il est assez court pour être lu en une minute et assez concret pour être utile.

Deux développeurs assis côte à côte, l’un montrant l’écran d’un ordinateur portable

Enregistrer un outil

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";
import { promises as fs } from "node:fs";
import path from "node:path";

const NOTES_FILE = path.join(process.env.NOTES_DIR ?? process.cwd(), "notes.json");

type Note = { id: number; text: string; tags: string[]; createdAt: string };

async function readNotes(): Promise<Note[]> {
  try {
    return JSON.parse(await fs.readFile(NOTES_FILE, "utf8"));
  } catch {
    return [];
  }
}

const server = new McpServer({ name: "team-notes", version: "1.0.0" });

server.registerTool(
  "add_note",
  {
    title: "Add note",
    description:
      "Save a short engineering note with optional tags. Use it when the user asks to remember a decision, a command or a bug.",
    inputSchema: {
      text: z.string().min(3).max(2000),
      tags: z.array(z.string()).default([]),
    },
  },
  async ({ text, tags }) => {
    const notes = await readNotes();
    const note: Note = {
      id: notes.length + 1,
      text,
      tags,
      createdAt: new Date().toISOString(),
    };
    await fs.writeFile(NOTES_FILE, JSON.stringify([...notes, note], null, 2));
    return { content: [{ type: "text", text: `Saved note #${note.id}` }] };
  }
);

server.registerTool(
  "search_notes",
  {
    title: "Search notes",
    description: "Find saved notes whose text or tags contain the query.",
    inputSchema: { query: z.string().min(1) },
  },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = (await readNotes()).filter(
      (n) =>
        n.text.toLowerCase().includes(q) ||
        n.tags.some((t) => t.toLowerCase().includes(q))
    );
    const text = hits.length
      ? hits.map((n) => `#${n.id} [${n.tags.join(", ")}] ${n.text}`).join("\n")
      : "No notes matched.";
    return { content: [{ type: "text", text }] };
  }
);

await server.connect(new StdioServerTransport());
console.error("team-notes MCP server running on stdio");

Exécutez npm run build. Vous disposez maintenant de dist/index.js, et ce fichier est la seule chose que les deux clients ont besoin de connaître.

Renvoyer des résultats propres

Le modèle lit tout ce que vous renvoyez, traitez donc la valeur de retour comme une interface. Gardez des résultats courts, structurés et honnêtes. Lorsqu’une erreur survient, ne levez pas une exception qui fait tomber la couche de transport. Renvoyez une erreur que le modèle peut lire et à laquelle il peut réagir :

return {
  isError: true,
  content: [{ type: "text", text: "notes.json is not valid JSON. Fix or delete it." }],
};

Un résultat isError permet à l’assistant de vous expliquer le problème ou de réessayer avec une autre entrée. Un plantage, lui, n’affiche qu’un vague bandeau « server disconnected ».

Garder stdout silencieux

C’est la cause la plus fréquente d’échec d’un premier serveur. Avec stdio, la sortie standard appartient au protocole. Un seul console.log perdu injecte du texte brut dans le flux JSON-RPC, et le client coupe la connexion. Écrivez vos journaux avec console.error, qui écrit sur stderr : les deux clients les captureront alors comme sortie de diagnostic.

💡 Astuce : Rédigez les descriptions d’outils comme des instructions destinées au modèle, et non comme de la documentation pour des humains. « Utilisez-le quand l’utilisateur demande de retenir une décision » fait choisir l’outil au bon moment. « Utilitaire de notes » ne le fait pas.

Connecter Claude Code

Gros plan sur une fenêtre de terminal affichée sur un écran d’ordinateur portable, reflétée dans une paire de lunettes

Ajouter le serveur avec la CLI

Une seule commande enregistre le serveur. Les options vont avant le nom, et un double tiret sépare le nom de la commande que Claude Code lancera :

claude mcp add --transport stdio --scope user \
  --env NOTES_DIR=/home/dev/notes \
  team-notes -- node /absolute/path/to/team-notes-mcp/dist/index.js

Utilisez un chemin absolu. Claude Code démarre le processus depuis le répertoire utilisé par votre session, donc les chemins relatifs cassent dès que vous ouvrez un autre projet. Vérifiez ensuite :

claude mcp list
claude mcp get team-notes

Dans une session, tapez /mcp pour afficher l’état de la connexion et la liste des outils. Posez une question naturelle, par exemple « Retenez que nous déployons le jeudi, ajoutez le tag release », et observez Claude Code demander l’autorisation d’appeler add_note.

Partager le serveur via .mcp.json

L’option de portée détermine qui accède au serveur. local le réserve à vous seul dans un projet, user le rend disponible dans tous les projets, et project écrit un fichier .mcp.json que vous pouvez committer afin que toute l’équipe en bénéficie. Voici une configuration partagée qui évite les chemins codés en dur :

{
  "mcpServers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${TEAM_NOTES_PATH}/dist/index.js"],
      "env": { "NOTES_DIR": "${NOTES_DIR:-.notes}" }
    }
  }
}

Chaque membre de l’équipe définit TEAM_NOTES_PATH une seule fois dans son shell. La forme ${NOTES_DIR:-.notes} fournit une valeur par défaut lorsque la variable est absente. Claude Code demande une approbation la première fois qu’il rencontre un serveur de portée projet, une protection judicieuse pour tout ce qui provient d’un dépôt.

Connecter GitHub Copilot

Développeur installé dans un fauteuil de bureau, souriant face à deux écrans en fin d’après-midi

Écrire .vscode/mcp.json

Créez .vscode/mcp.json dans votre espace de travail. Retenez la propriété racine différente et le type obligatoire :

{
  "servers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/tools/team-notes-mcp/dist/index.js"],
      "env": { "NOTES_DIR": "${workspaceFolder}/.notes" }
    }
  }
}

${workspaceFolder} rend le fichier portable, vous pouvez donc le committer. Pour les secrets, ajoutez un tableau inputs. VS Code demande la valeur une fois, la stocke de manière sécurisée et l’injecte :

{
  "inputs": [
    { "type": "promptString", "id": "picassoia-token", "description": "PicassoIA API token", "password": true }
  ],
  "servers": {
    "team-notes": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/tools/team-notes-mcp/dist/index.js"],
      "env": { "PICASSOIA_API_TOKEN": "${input:picassoia-token}" }
    }
  }
}

Passer en mode Agent

Copilot Chat s’ouvre par défaut en mode Ask, et les outils MCP ne se déclenchent qu’en mode Agent. Changez de mode dans le panneau de chat, ouvrez le sélecteur d’outils et vérifiez que team-notes apparaît avec ses deux outils cochés. S’il n’apparaît pas, exécutez MCP: List Servers depuis la palette de commandes, sélectionnez le serveur et lisez sa sortie. Redémarrez-le depuis le même menu après chaque reconstruction.

Copilot vous permet aussi de choisir parmi les modèles proposés par votre forfait, ce qui permet de tester le même serveur avec différents modèles. C’est un moyen peu coûteux de vérifier si vos descriptions d’outils sont assez claires pour chacun d’eux.

Tester et déboguer avant la mise en production

Profil de trois quarts d’un développeur barbu en bonnet, debout devant un bureau réglable près d’une fenêtre pluvieuse

Lancer le MCP Inspector

Avant d’accuser l’un ou l’autre client, testez le serveur seul. Le MCP Inspector officiel ouvre une page web locale où vous pouvez lister les outils, renseigner les arguments et voir les réponses brutes :

npx @modelcontextprotocol/inspector node dist/index.js

Appelez add_note avec un text vide. Votre schéma Zod doit le rejeter avec un message de validation lisible. Appelez ensuite search_notes avec un tag que vous venez d’enregistrer. Si les deux se comportent correctement ici, tout problème restant se situe dans la configuration du client et non dans votre code.

Corriger les pannes courantes

SymptômeCause probableCorrectif
Le serveur ne se connecte jamaisUn console.log a écrit sur stdoutPasser à console.error
« Command not found »Chemin relatif ou build manquantUtiliser un chemin absolu et exécuter npm run build
Outils absents dans CopilotLe chat est en mode AskPasser en mode Agent
L’outil existe mais n’est jamais choisiDescription vagueLa réécrire avec des formulations déclencheuses
Variable d’environnement videNon déclarée dans la configurationL’ajouter au bloc env
Les appels d’image échouent sous chargePlus de 5 tâches simultanéesMettre les appels en file d’attente dans l’outil

Donner à votre serveur un outil d’image

Les notes, c’est bien, mais la meilleure démonstration de MCP est un outil qui fait ce que l’assistant ne peut pas faire seul. La génération d’images convient bien : le modèle rédige un prompt précis, votre serveur le transforme en fichier, et l’URL revient directement dans la conversation.

Vue de dessus de quatre personnes pointant des schémas imprimés autour d’une longue table en chêne

Appeler l’API PicassoIA

L’API développeur de PicassoIA se trouve à https://api.picassoia.com/v1 et utilise un jeton Bearer qui commence par pia_sk_. Les prédictions sont asynchrones, sur le modèle de Replicate : vous en créez une, puis vous l’interrogez jusqu’à ce que son statut indique succeeded. Le modèle PicassoIA Image accepte un prompt pouvant atteindre 4 000 caractères et un aspect_ratio, et renvoie une liste d’URL d’images. Ajoutez cet outil avant la ligne server.connect :

const API = "https://api.picassoia.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
  "Content-Type": "application/json",
};

server.registerTool(
  "generate_image",
  {
    title: "Generate image",
    description: "Create an image from a text prompt with PicassoIA and return its URL.",
    inputSchema: {
      prompt: z.string().min(1).max(4000),
      aspect_ratio: z.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"]).default("16:9"),
    },
  },
  async ({ prompt, aspect_ratio }) => {
    const created = await fetch(`${API}/models/picassoia/picassoia-image/predictions`, {
      method: "POST",
      headers,
      body: JSON.stringify({ input: { prompt, aspect_ratio } }),
    }).then((r) => r.json());

    let prediction = created;
    while (["starting", "processing"].includes(prediction.status)) {
      const wait = prediction.eta?.next_poll_in_seconds ?? 2;
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      prediction = await fetch(`${API}/predictions/${created.id}`, { headers }).then((r) => r.json());
    }

    if (prediction.status !== "succeeded") {
      return {
        isError: true,
        content: [{ type: "text", text: `Generation ${prediction.status ?? "request failed"}` }],
      };
    }
    return { content: [{ type: "text", text: prediction.output[0] }] };
  }
);

Un compte autorise 5 prédictions simultanées, partagées entre tous les jetons et toutes les connexions. Une boucle qui lance dix images d’un coup atteindra donc ce plafond. Générez-les les unes après les autres dans l’outil, ou gardez une petite file d’attente.

💡 Astuce : Vérifiez les tarifs actuels et les conditions d’accès des forfaits sur la page de l’API PicassoIA avant de publier un serveur pour d’autres personnes. La documentation et la page tarifaire décrivent l’accès différemment : confirmez donc ce que votre propre forfait inclut.

Comment utiliser Sonnet 5 sur PicassoIA

Vos descriptions d’outils sont des prompts, et un modèle de langage est le meilleur éditeur pour les rédiger. Claude Sonnet 5 est un excellent choix pour cette tâche, et vous pouvez l’utiliser sur PicassoIA sans quitter le navigateur.

  1. Ouvrez la page Claude Sonnet 5 dans la collection de grands modèles de langage.
  2. Collez vos définitions d’outils, avec les noms, les descriptions et les schémas, dans le prompt.
  3. Demandez-lui de réécrire chaque description sous forme de courte instruction qui précise quand appeler l’outil et ce qu’il renvoie.
  4. Demandez dix entrées limites par outil, comme des chaînes vides, des textes très longs et des tags inhabituels.
  5. Passez ces entrées dans le MCP Inspector, corrigez chaque échec et recollez les descriptions améliorées dans votre code.

Pour un second avis sur une logique délicate, Claude Fable 5 et GPT 5.6 Sol figurent tous deux parmi les modèles proposés pour les tâches de programmation. Comparer leurs réécritures d’une même description révèle souvent quelle formulation est ambiguë.

Essayez par vous-même sur PicassoIA

Vous disposez désormais d’un serveur unique qui tourne dans deux assistants : des notes pour la mémoire, un outil d’image pour les résultats, et une routine de test qui garde les deux honnêtes. Le même schéma s’adapte à tout ce que vous pouvez encapsuler dans une fonction, des scripts de déploiement aux recherches en base de données.

Ordinateur portable et café allongé sur une table de café, avec un smartphone à côté

Commencez par l’outil d’image, car il donne un retour immédiat. Rédigez un prompt, appelez generate_image depuis Claude Code ou Copilot, et voyez le résultat en quelques secondes. Ensuite, ouvrez la page PicassoIA Image pour expérimenter directement avec les formats et les styles de prompt, ou parcourez tous les modèles disponibles sur picassoia.com/en/all-models. Votre première image n’est qu’à un prompt de distance.

Partager cet article

Choisissez votre langue