MCPTypeScriptGitHub CopilotVS CodeAI

Créer un serveur MCP personnalisé avec TypeScript pour étendre GitHub Copilot

Sloth255
Sloth255
·12 min read·2,668 words

Introduction

MCP (Model Context Protocol) est un protocole standard conçu par Anthropic pour connecter des modèles d'IA à des outils et à des sources de données externes. GitHub Copilot, Claude et Cursor, entre autres clients d'IA, le prennent en charge.

Un serveur MCP répond par exemple au besoin d'appeler une API interne depuis Copilot ou d'exécuter des commandes propres à un projet.

Cet article montre comment implémenter un serveur MCP en TypeScript, puis le connecter à GitHub Copilot dans VS Code. Il aborde l'installation, la conception des outils et les pratiques d'exploitation.


Fondamentaux du MCP

flowchart TD
  A[Agent Copilot dans VS Code] -->|MCP| B[Serveur MCP]
  B --> C[Outils externes]
  B --> D[API]
  B --> E[Base de données]

Un serveur MCP peut exposer trois types d'éléments.

Type Description Exemples
Outils Fonctions que le LLM peut appeler Appels API, opérations sur les fichiers
Ressources Données lisibles Documents, fichiers de configuration
Prompts Modèles de prompts réutilisables Revue de code, enquête sur incident

La communication s'effectue en JSON-RPC via stdio ou Streamable HTTP.


Principes et architecture de MCP

Pourquoi MCP a été créé

Avant MCP, chaque client d'IA disposait de sa propre intégration avec les systèmes externes. Réutiliser une même API interne depuis plusieurs clients imposait donc de développer une intégration par client.

[Avant MCP]
GitHub Copilot -> Intégration personnalisée A -> API interne
Claude         -> Intégration personnalisée B -> API interne (doublon)
Cursor         -> Intégration personnalisée C -> API interne (doublon)

[Avec MCP]
GitHub Copilot -+
Claude         +-> Serveur MCP -> API interne (une seule implémentation)
Cursor         -+

MCP réduit cette fragmentation au moyen d'un protocole standard. Après avoir implémenté un serveur, il n'est plus nécessaire de reproduire la même fonctionnalité pour chaque client. Les possibilités concrètes restent toutefois déterminées par la version de MCP, le transport et les mécanismes d'authentification pris en charge par chaque client d'IA.

Principe de base : les LLM décident, les serveurs exécutent

MCP repose sur une séparation nette des responsabilités.

  • LLM (comme Copilot) : interprète le contexte, choisit l'outil et construit ses arguments
  • Serveur MCP : exécute l'outil et renvoie le résultat

Un LLM n'accède pas directement aux systèmes externes. Chaque demande passe par une interface explicite d'appel d'outil ; l'authentification, la validation et la journalisation restent ainsi centralisées sur le serveur.

sequenceDiagram
  participant U as Utilisateur
  participant L as LLM (Copilot)
  participant S as Serveur MCP
  participant A as API externe

  U->>L: "Consulte les issues ouvertes"
  L->>S: tools/call search_github_issues
  S->>A: GET /search/issues
  A-->>S: Résultat JSON
  S-->>L: Résultat de l'exécution de l'outil
  L-->>U: Réponse mise en forme

Communication avec JSON-RPC 2.0

La communication repose sur JSON-RPC 2.0. Après la connexion, les messages suivent trois étapes.

Étape Méthode Descriptif
1 initialize Le client et le serveur échangent leurs versions et les fonctionnalités prises en charge
2 tools/list Le client récupère la liste des outils, avec leurs noms et schémas
3 tools/call Le client demande l'exécution d'un outil

Voici un exemple de demande et de réponse pour search_github_issues :

// Requête
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_github_issues",
    "arguments": {
      "query": "authentication",
      "repo": "owner/my-repo",
      "state": "open"
    }
  }
}
// Réponse
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"number\": 42, \"title\": \"Fix OAuth token refresh\", ...}]"
      }
    ]
  }
}

Le LLM reçoit ce content, le transforme en réponse en langage naturel et l'adresse à l'utilisateur.

Le rôle des transports

MCP définit deux transports standard : stdio et Streamable HTTP. L'ancien transport HTTP+SSE demeure disponible pour préserver la compatibilité, mais les nouvelles implémentations doivent adopter Streamable HTTP.

stdio Streamable HTTP
Fonctionnement Le client lance le serveur comme sous-processus et communique via l'entrée et la sortie standard Le client envoie et reçoit du JSON-RPC avec HTTP POST/GET ; SSE permet le streaming lorsque nécessaire
Cas d'usage Développement local et usage individuel Partage en équipe, gestion centralisée et plusieurs clients
Coût de démarrage Faible : le serveur peut démarrer à chaque appel Repose sur un service qui reste en exécution

Pour un usage local depuis VS Code, stdio est généralement le bon choix. La section « Choisir entre stdio et Streamable HTTP » précise ce choix plus loin.


Quand créer un serveur MCP plutôt que s'appuyer sur des skills

GitHub Copilot dans VS Code propose deux mécanismes Markdown pour étendre son comportement. Les fichiers d'instructions personnalisées (.github/copilot-instructions.md, .instructions.md et autres) définissent les consignes transmises à l'IA. Les skills d'agent (SKILL.md) définissent des workflows réutilisables, qui peuvent réunir procédures, scripts et ressources de support. Dans cet article, l'ensemble est désigné comme la « personnalisation par instructions ». Avant de créer un serveur MCP, vérifiez que ces mécanismes ne suffisent pas.

Cas résolus grâce à la personnalisation basée sur les instructions

  • Fournir à l'IA des informations statiques, comme les conventions de code ou l'organisation du projet
  • Réutiliser une procédure établie sous la forme d'un modèle de prompt
  • Définir un workflow sans accès à un système externe

La personnalisation par instructions ne requiert aucun serveur dédié. Un agent peut exécuter les scripts d'un skill avec des outils déjà présents, tel le terminal, mais l'exécution, les approbations et le format de sortie dépendent des capacités de l'environnement hôte. C'est le premier mécanisme à évaluer.

Un skill peut aussi expliquer comment appeler une API externe

Lorsque le besoin consiste à utiliser une API externe, documentez son appel dans un skill et demandez à Copilot de générer le code ou la commande curl correspondante.

<!-- Exemple de SKILL.md -->
Le point de terminaison de l'API interne de statut est https://api.internal/status.
Pour l'authentification, indiquez un jeton Bearer dans l'en-tête Authorization.
Exemple d'appel avec curl : curl -H "Authorization: Bearer $TOKEN" https://api.internal/status

Cette approche convient lorsque Copilot doit générer du code. Dans les environnements où un agent peut appeler une API au moyen du terminal ou d'une extension existante, la procédure d'exécution, l'approbation et la gestion des résultats restent à la charge de l'hôte.

La limite où le MCP est nécessaire

Un skill ne suffit plus dans les situations suivantes.

Situation Personnalisation basée sur les instructions MCP
Expliquer à Copilot comment appeler une API Oui
Définir une procédure d'appel d'API avec les outils fournis par l'hôte Oui
Exposer une API en tant qu'outil typé et dédié Non Oui
Standardiser l'authentification, la validation des entrées et le format des résultats sur le serveur Non Oui
Exposer la même fonctionnalité à plusieurs clients MCP Non Oui

Un skill indique à l'agent quoi faire et dans quel ordre. MCP fournit, lui, un contrat d'outil dédié et typé que les clients peuvent appeler. Le serveur exécute l'appel et renvoie son résultat dans un format défini dans le contexte de Copilot. Ce niveau de contrôle et la portabilité qui en résulte guident le choix.

Cas nécessitant un serveur MCP

Créez un serveur MCP lorsque la personnalisation par instructions seule ne permet pas de répondre à ces exigences.

Exigence Raison
Utiliser les résultats de l'API dans le raisonnement Copilot Les résultats de l'exécution entrent directement dans le contexte, permettant des questions de suivi et des transformations
Accéder à des ressources authentifiées Les jetons et informations d'identification peuvent rester dans des variables d'environnement sans être exposés aux utilisateurs
Effectuer des opérations d'écriture ou de mise à jour Opérations à effet de bord, comme créer des issues, mettre à jour des libellés ou modifier des enregistrements
Exiger des données en temps réel Récupérer l'état courant de la CI ou les dernières valeurs de la base de données, impossibles à conserver dans des fichiers statiques
Gérer des données volumineuses ou dynamiques Documents trop volumineux pour être inclus dans des prompts, ou données qui évoluent fréquemment
Effectuer des traitements ou conversions complexes Analyser des fichiers, agréger des données ou convertir des formats dans du code
Partager entre projets ou équipes Gérer de manière centralisée les outils appelés depuis plusieurs développeurs et référentiels

Flux de décision

flowchart TD
  A[Un besoin à couvrir] --> B{Un accès à un système<br/>externe est-il nécessaire ?}
  B -->|Non| C[La personnalisation par instructions peut suffire]
  B -->|Oui| D{Copilot doit-il recevoir le résultat<br/>et raisonner à partir de celui-ci ?}
  D -->|Non : générer le code suffit| C
  D -->|Oui| E{Authentification, effets de bord<br/>ou données dynamiques ?}
  E -->|Non| F[Vérifier si un serveur MCP existant<br/>peut répondre au besoin]
  E -->|Oui| G[Créer un serveur MCP]
  F -->|Aucun serveur adapté| G

La frontière entre la personnalisation par instructions et MCP tient à la nécessité de gérer, côté serveur, un contrat d'outil dédié, l'authentification et la validation des entrées.


Installation

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node tsx
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"]
}
// package.json (extrait)
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Implémenter un serveur MCP minimal

// src/index.ts
import { McpServer } from '@modelcontextprotocol/server'
import { serveStdio } from '@modelcontextprotocol/server/stdio'
import * as z from 'zod/v4'
import { fetchWeather } from './tools/weather.js'

interface GithubIssueSearchResponse {
  items: Array<{
    number: number
    title: string
    state: string
    html_url: string
  }>
}

function createServer(): McpServer {
  const server = new McpServer({
    name: 'my-custom-tools',
    version: '1.0.0',
  })

  server.registerTool(
    'get_weather',
    {
      description: 'Récupère la météo actuelle pour la ville indiquée',
      inputSchema: z.object({
        city: z.string().describe('Ville dont consulter la météo (par exemple Tokyo ou Osaka)'),
      }),
    },
    async ({ city }) => {
      const weather = await fetchWeather(city)
      return {
        content: [{
          type: 'text',
          text: `Météo actuelle à ${city} : ${weather.description}, ${weather.temp} °C`,
        }],
      }
    }
  )

  server.registerTool(
    'search_github_issues',
    {
      description: 'Recherche des issues dans un dépôt GitHub',
      inputSchema: z.object({
        query: z.string().describe('Requête de recherche'),
        repo: z.string().regex(/^[^/]+\/[^/]+$/).describe('Nom du dépôt (format owner/repo)'),
        state: z.enum(['open', 'closed', 'all']).default('open').describe('État de l’issue'),
      }),
    },
    async ({ query, repo, state }) => {
      const token = process.env.GITHUB_TOKEN
      if (!token) {
        return {
          content: [{ type: 'text', text: 'GITHUB_TOKEN n’est pas défini' }],
          isError: true,
        }
      }

      const stateQualifier = state === 'all' ? '' : ` state:${state}`
      const url = new URL('https://api.github.com/search/issues')
      url.searchParams.set(
        'q',
        `${query} is:issue repo:${repo}${stateQualifier}`
      )

      const response = await fetch(url.toString(), {
        headers: {
          Authorization: `Bearer ${token}`,
          Accept: 'application/vnd.github.v3+json',
        },
      })

      if (!response.ok) {
        return {
          content: [{ type: 'text', text: `Erreur de l’API GitHub : ${response.status}` }],
          isError: true,
        }
      }

      const data = await response.json() as GithubIssueSearchResponse
      const issues = data.items.slice(0, 5).map((issue) => ({
        number: issue.number,
        title: issue.title,
        state: issue.state,
        url: issue.html_url,
      }))

      return {
        content: [{ type: 'text', text: JSON.stringify(issues, null, 2) }],
      }
    }
  )

  return server
}

void serveStdio(createServer)
console.error('Le serveur MCP est démarré')

Exemples d'implémentation d'outils

API Météo (appel API externe)

// src/tools/weather.ts
interface WeatherData {
  description: string
  temp: number
  humidity: number
}

export async function fetchWeather(city: string): Promise<WeatherData> {
  const apiKey = process.env.OPENWEATHER_API_KEY
  if (!apiKey) throw new Error('OPENWEATHER_API_KEY n’est pas définie')

  const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric&lang=ja`

  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(`Impossible de récupérer les données météo : ${response.statusText}`)
  }

  const data = await response.json()
  return {
    description: data.weather[0].description,
    temp: Math.round(data.main.temp),
    humidity: data.main.humidity,
  }
}

Recherche de documents internes (fichiers locaux)

// src/tools/docs-search.ts
import * as fs from 'fs/promises'
import * as path from 'path'
import { pathToFileURL } from 'url'

interface DocResult {
  title: string
  excerpt: string
  url: string
}

export async function searchDocs(
  query: string,
  limit: number
): Promise<DocResult[]> {
  const docsDir = process.env.DOCS_DIR ?? './docs'
  const files = await fs.readdir(docsDir)
  const results: DocResult[] = []

  for (const file of files.filter((f) => f.endsWith('.md'))) {
    const content = await fs.readFile(path.join(docsDir, file), 'utf-8')

    if (content.toLowerCase().includes(query.toLowerCase())) {
      const lines = content.split('\n')
      // Ignore le front matter (le bloc --- initial) et prend le premier H1 comme titre
      let bodyStart = 0
      if (lines[0]?.trim() === '---') {
        const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---')
        bodyStart = end >= 0 ? end + 1 : 0
      }
      const h1 = lines.slice(bodyStart).find((l) => /^#\s/.test(l))
      const title = h1 ? h1.replace(/^#\s*/, '') : path.basename(file, '.md')
      const matchIndex = content.toLowerCase().indexOf(query.toLowerCase())
      const excerpt = content.slice(
        Math.max(0, matchIndex - 50),
        matchIndex + 150
      )

      results.push({
        title,
        excerpt,
        url: pathToFileURL(path.resolve(docsDir, file)).toString(),
      })

      if (results.length >= limit) break
    }
  }

  return results
}

Implémenter des ressources

Les ressources exposent des données en lecture seule que l'IA peut consulter. Elles conviennent aux informations auxquelles le modèle doit accéder de manière fiable : politiques internes, procédures opérationnelles ou documentation d'API.

// À ajouter au début de src/index.ts
import { readFile } from 'node:fs/promises'
import path from 'node:path'

// À ajouter avant return server dans createServer
server.registerResource(
  'project-guide',
  'docs://project-guide',
  {
    title: 'Guide du projet',
    description: 'Guide du projet à consulter pendant le développement',
    mimeType: 'text/markdown',
  },
  async (uri) => {
  const docsDir = process.env.DOCS_DIR ?? path.join(process.cwd(), 'docs')
  const content = await readFile(
    path.join(docsDir, 'project-guide.md'),
    'utf-8'
  )

  return {
    contents: [
      {
        uri: uri.href,
        mimeType: 'text/markdown',
        text: content,
      },
    ],
  }
  }
)

Les ressources sont particulièrement adaptées aux éléments suivants :

  • Guides de conception et conventions de codage
  • Runbooks opérationnels
  • Résumés des spécifications API
  • FAQ interne

Elles évitent de recopier ces informations dans chaque conversation.


Utiliser des prompts

Un serveur MCP peut exposer des prompts en complément des outils. Ils conviennent aux procédures et aux angles de revue que vous réutilisez régulièrement.

  • Prompts de revue de sécurité
  • Prompts pour générer des descriptions de pull requests
  • Prompts pour les premières étapes d'une enquête sur incident

Les outils exécutent des actions ; les prompts structurent une démarche. Cette distinction aide à sélectionner les éléments à exposer.


Connexion à VS Code

// .vscode/mcp.json
{
  "servers": {
    "my-custom-tools": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "${env:GITHUB_TOKEN}",
        "OPENWEATHER_API_KEY": "${env:OPENWEATHER_API_KEY}"
      }
    }
  }
}

Pendant le développement, tsx permet de l'exécuter directement, sans étape de build intermédiaire.

{
  "servers": {
    "my-custom-tools-dev": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "${workspaceFolder}/src/index.ts"]
    }
  }
}

Utilisation dans Copilot Chat

Après le redémarrage de VS Code, les outils MCP sont disponibles dans Copilot Chat.

"Quel temps fait-il à Tokyo ?"
  -> Copilot appelle automatiquement l'outil get_weather

"Trouve les issues ouvertes liées à \"memory leak\""
  -> Copilot appelle automatiquement l'outil search_github_issues

Vous pouvez aussi filtrer les outils par nom de serveur.

#my-custom-tools Liste les issues ouvertes liées à "authentication" dans owner/my-repo
  -> Appelle l'outil search_github_issues via ce serveur

Débogage

# Tester les outils de manière interactive avec MCP Inspector
npx @modelcontextprotocol/inspector npx tsx src/index.ts

Ouvrez dans un navigateur l'URL affichée par Inspector pour examiner les appels d'outils et leurs valeurs de retour.


Concevoir les outils

Définir d'abord ce que les outils ne doivent pas autoriser

Les outils MCP sont utiles, mais concentrer trop de pouvoir dans un seul outil crée un risque.

Mauvais exemple Bon exemple
manage_project list_open_issues, update_issue_labels
run_anything get_pipeline_status
execute_sql search_customers

Un outil est plus fiable lorsque son nom rend ses effets de bord explicites, que ses paramètres d'entrée sont limités et que son comportement en cas d'échec reste prévisible.

Concevoir inputSchema avec précision

Le schéma d'entrée d'un outil ne sert pas seulement à valider : il constitue aussi l'interface du LLM. Des descriptions imprécises réduisent la qualité de l'inférence des arguments.

const updateIssueInput = {
  type: 'object',
  properties: {
    repo: { type: 'string', pattern: '^[^/]+/[^/]+$' },
    issueNumber: { type: 'integer', minimum: 1 },
    labels: {
      type: 'array',
      items: { type: 'string' },
      maxItems: 10,
    },
  },
  required: ['repo', 'issueNumber', 'labels'],
  additionalProperties: false,
}

Points à respecter :

  • Rédiger des valeurs description précises
  • Utiliser enum dès que possible
  • Déclarer explicitement required
  • Utiliser additionalProperties: false pour écarter les paramètres imprévus

Répartir les responsabilités entre Zod et JSON Schema

inputSchema représente le contrat externe ; Zod assure une seconde validation à l'exécution. Pour les outils qui appellent une API ou une base de données externe, conservez impérativement cette validation côté serveur.

case 'update_issue': {
  // Définit avec JSON Schema la structure présentée au modèle,
  // puis valide avec Zod les valeurs effectivement acceptées
  const input = z.object({
    repo: z.string().regex(/^[^/]+\/[^/]+$/),
    issueNumber: z.number().int().positive(),
    labels: z.array(z.string()).max(10),
  }).parse(args)
  // ...
}

Structurer la gestion des erreurs

Classez au minimum les erreurs dans les trois catégories suivantes :

  • Erreurs d'entrée (échecs de validation)
  • Erreurs d'authentification/autorisation
  • Défaillances de services externes
function toToolErrorMessage(error: unknown): string {
  if (error instanceof z.ZodError) {
    return 'Entrée non valide. Vérifiez les champs obligatoires et le format des valeurs.'
  }

  if (error instanceof Error) {
    return `L’exécution de l’outil a échoué : ${error.message}`
  }

  return 'L’exécution de l’outil a échoué.'
}

Les messages renvoyés ne doivent pas exposer de détails internes superflus ; ils doivent indiquer clairement l'action à entreprendre.


Gérer les informations d'identification

Ne transmettez jamais de secrets par des prompts ou des arguments ; lisez-les depuis des variables d'environnement ou un environnement d'exécution sécurisé.

À faire À éviter
Lire les jetons API depuis env Inclure les secrets dans les arguments de l'outil
Utiliser des jetons au privilège minimal Écrire des jetons dans les journaux
Séparer les environnements de production et de développement Inclure les secrets dans les messages d'erreur

Gardez les autorisations minimales

  • Utiliser un jeton en lecture seule lorsqu'un outil ne fait que rechercher des issues GitHub
  • Limiter à SELECT les droits de l'utilisateur utilisé par un outil de lecture de base de données
  • Accorder uniquement l'autorisation de lecture des workflows à un outil de vérification des déploiements

Pour un usage en équipe, séparez les outils en lecture seule des outils de mise à jour, et isolez ceux destinés à la production.


Concevoir l'exploitation

Délais d'attente, tentatives et échecs partiels

Une expérimentation locale peut sembler fiable alors qu'un environnement de production est instable. Concevez le code en prévoyant les échecs.

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return await Promise.race([
    promise,
    new Promise<T>((_, reject) => {
      setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
    }),
  ])
}

Éléments minimaux :

  • Délais d'attente
  • Nombre maximal de tentatives
  • Limitation du débit
  • Normalisation des messages d'erreur

Observabilité et journalisation

Pour déboguer, connaître le seul outil appelé ne suffit pas. Enregistrez au moins les éléments suivants :

  • Nom de l'outil
  • Temps d'invocation et durée d'exécution
  • Catégorie succès/échec et cause de l'échec

Ne consignez toutefois pas un volume excessif de données : ne placez ni données personnelles, ni jetons d'accès, ni requêtes SQL complètes, ni réponses intégrales d'API externe dans les journaux.

Stratégie de test

Extraire l'implémentation des outils dans des fonctions permet de tester leur logique en dehors du protocole MCP.

// Isole la logique métier pour la rendre testable
export async function searchGithubIssues(input: SearchIssuesInput) {
  // Traitement effectif
}

server.registerTool(
  'search_github_issues',
  { inputSchema: SearchIssuesInputSchema },
  async (input) => formatResult(await searchGithubIssues(input))
)

Couches de test recommandées :

  1. Tests unitaires pour la validation des entrées
  2. Tests unitaires pour les gestionnaires d'outils
  3. Tests d'intégration qui couvrent le transport
  4. Vérification manuelle avec MCP Inspector

Choisir entre stdio et Streamable HTTP

Critère stdio Streamable HTTP
Configuration locale Très facile Un peu plus lourd
Audit centralisé Faible Élevé
Gouvernance de l'authentification Dépend de chaque utilisateur Plus simple
Débogage Simple Davantage de facteurs réseau
Distribution npm / binaire URL de service

Commencez avec stdio pour valider l'usage local. Passez à Streamable HTTP lorsque le partage en équipe ou la gestion centralisée le justifie. Pour une exposition par HTTP, implémentez la validation de l'en-tête Origin, l'écoute sur localhost et l'authentification.


Découper les serveurs MCP

Regrouper toutes les capacités sur un seul serveur étend rapidement ses responsabilités. Un découpage par domaine facilite l'exploitation.

  • Outils GitHub
  • Outils d'API internes
  • Outils de lecture de base de données
  • Serveurs de ressources dédiés à la documentation

Ce découpage simplifie aussi la gestion des autorisations et des variables d'environnement.


Échecs fréquents

1. Créer un outil polyvalent capable de tout faire

Des outils tels que run_anything ou execute_sql (SQL arbitraire) dégradent à la fois la sécurité et la reproductibilité.

2. Rendre inputSchema trop lâche

Le modèle construit alors les arguments moins fiablement, ce qui augmente le nombre d'appels en échec.

3. Recevoir des secrets comme arguments

Ils peuvent se retrouver dans les journaux ou dans l'historique de conversation.

4. Mélanger les opérations de lecture et de mise à jour sous la même autorisation

Le risque d'incident d'exploitation augmente.

5. Écrire des implémentations d'outils directement dans les gestionnaires

Les tests et la maintenance deviennent plus difficiles.


Une progression d'adoption pragmatique

Pour un premier serveur MCP, la progression suivante est réaliste :

  1. Créer un serveur local avec stdio uniquement
  2. Ajouter un ou deux outils en lecture seule
  3. Ajouter des ressources et des prompts
  4. Renforcer la journalisation et la gestion des erreurs
  5. Ajouter prudemment des outils de mise à jour, uniquement en cas de besoin

Cette progression maintient un équilibre entre sécurité et utilité.

Premiers outils pertinents :

  • Recherche de configuration de projet
  • Recherche de documents internes
  • Vérifications du statut CI
  • Recherche de pull requests et d'issues

N'ajoutez les outils de mise à jour qu'après avoir établi la confiance des utilisateurs et défini l'audit.


Conclusion

  • Les serveurs MCP communiquent en JSON-RPC via stdio ou Streamable HTTP ; le SDK @modelcontextprotocol/server simplifie leur implémentation en TypeScript.
  • La combinaison d'outils (actions), de ressources (données de référence) et de prompts (démarches réutilisables) étend les capacités de Copilot.
  • L'enregistrement d'un serveur dans .vscode/mcp.json le rend disponible depuis GitHub Copilot dans VS Code.
  • La conception d'un outil commence par ce qu'il ne doit pas autoriser ; un inputSchema précis améliore l'inférence du LLM.
  • Les secrets doivent rester dans les variables d'environnement et les autorisations doivent être minimales.
  • Commencez avec stdio et un ou deux outils en lecture seule, puis élargissez l'usage après avoir établi un retour d'expérience.

Références