Série MCP : Construire votre premier serveur MCP

Cet article de blog est le premier d’une série sur la construction de votre serveur MCP, son hébergement sur Azure et finalement son intégration dans un agent M365 Copilot.

  • Partie 1 — Construire votre premier serveur MCP
  • Partie 2 — Héberger le serveur MCP sur Azure (Azure Container Apps)
  • Partie 3 — Connecter le serveur MCP à un agent Microsoft 365 Copilot

Qu’est-ce que le Model Context Protocol ?

Le Model Context Protocol (MCP) est un standard ouvert, initialement introduit par Anthropic et maintenant largement adopté, qui définit comment les modèles d’IA communiquent avec des outils et des sources de données externes. Pensez-y comme un adaptateur universel — au lieu que chaque assistant IA ait besoin d’une intégration sur mesure pour chaque API ou service, MCP fournit un contrat unique et cohérent.

Pour Microsoft 365 Copilot spécifiquement, la prise en charge MCP signifie que vous pouvez exposer n’importe quelle capacité — interroger une base de données, appeler une API REST, lire depuis SharePoint, exécuter une logique métier — en tant qu’ensemble d’outils que Copilot peut découvrir et invoquer au nom de l’utilisateur.

Un serveur MCP expose trois primitives :

  • Outils (Tools) — des fonctions que Copilot peut appeler (par ex. getProjectStatus, createTicket)
  • Ressources (Resources) — des sources de données que le modèle peut lire (par ex. des fichiers, des enregistrements de base de données)
  • Invites (Prompts) — des templates d’invites réutilisables que le modèle peut utiliser

Pour la plupart des scénarios d’agent Copilot, les outils sont ce sur quoi vous vous concentrerez.

Prérequis

  • Node.js 20+
  • Azure CLI installé et connecté
  • Un tenant Microsoft 365 avec des licences Copilot
  • Docker Desktop (pour la Partie 2)
  • Familiarité de base avec les API REST

Créer le squelette du projet

Nous allons écrire le serveur en JavaScript simple, sans étape de compilation TypeScript nécessaire, ce qui garde les choses simples et fonctionne très bien avec l’inspecteur MCP pour les tests locaux.

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod express

Mettez à jour package.json pour utiliser les modules ESM :

{
  "type": "module",
  "scripts": {
    "dev": "node server.js",
    "start": "node server.js"
  }
}

Écrire le serveur

Créez server.js à la racine du projet. La décision de conception clé ici est un fichier unique pour les deux transports — STDIO pour le développement local et l’inspecteur MCP, HTTP pour Azure et Copilot. Une seule variable d’environnement contrôle lequel démarre :

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import express from "express";

// Données (remplacez par votre vraie source de données)
const projects = {
  "PRJ-001": { name: "Intranet Redesign",  status: "In Progress", owner: "Arne" },
  "PRJ-002": { name: "Copilot Rollout",    status: "Planning",    owner: "Simon"   },
  "PRJ-003": { name: "Purview Governance", status: "Completed",   owner: "Nico"  },
};

// Serveur MCP et outils (partagés entre les deux transports)
const server = new McpServer({ name: "project-status-server", version: "1.0.0" });

server.tool(
  "getProjectStatus",
  "Retrieves the current status and owner of a project by its ID.",
  { projectId: z.string().describe("The project ID, e.g. PRJ-001") },
  async ({ projectId }) => {
    const project = projects[projectId];
    if (!project) return { content: [{ type: "text", text: `Project ${projectId} not found.` }], isError: true };
    return { content: [{ type: "text", text: `**${project.name}** (${projectId})\nStatus: ${project.status}\nOwner: ${project.owner}` }] };
  }
);

server.tool(
  "updateProjectStatus",
  "Updates the status of a project.",
  {
    projectId: z.string().describe("The project ID"),
    status: z.enum(["Planning", "In Progress", "On Hold", "Completed"]).describe("The new status"),
  },
  async ({ projectId, status }) => {
    if (!projects[projectId]) return { content: [{ type: "text", text: `Project ${projectId} not found.` }], isError: true };
    projects[projectId].status = status;
    return { content: [{ type: "text", text: `Updated ${projectId} to "${status}".` }] };
  }
);

server.tool(
  "listProjects",
  "Returns a list of all projects with their current status.",
  {},
  async () => {
    const list = Object.entries(projects)
      .map(([id, p]) => `- **${id}** - ${p.name} (${p.status}, owner: ${p.owner})`)
      .join("\n");
    return { content: [{ type: "text", text: list }] };
  }
);

// Sélection du transport
if (process.env.TRANSPORT === "stdio") {
  // STDIO - pour le développement local et l'inspecteur MCP
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server running via STDIO");
} else {
  // HTTP - pour Azure et Copilot
  const app = express();
  app.use(express.json());

  app.get("/health", (_req, res) => res.json({ status: "ok" }));

  app.post("/mcp", async (req, res) => {
    const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
    res.on("finish", () => transport.close());
  });

  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => console.error(`MCP server running via HTTP on http://localhost:${PORT}`));
}

Quelques éléments à souligner :

  • Un fichier, deux transports. Les outils sont définis une fois et partagés. Le transport est choisi au démarrage via process.env.TRANSPORT.
  • console.error partout. En mode STDIO, stdout est réservé aux messages JSON-RPC. Tout console.log corrompt le protocole et provoque des erreurs d’analyse JSON cryptiques dans l’inspecteur. Utilisez toujours console.error pour la journalisation.
  • Pourquoi Express ? Le StreamableHTTPServerTransport du SDK MCP attend un corps de requête pré-analysé. Express gère cela proprement dès le départ.

Démarrez le serveur en mode HTTP :

node server.js

Tester le serveur

Option A - curl

Le transport MCP utilise les Server-Sent Events (SSE), il y a donc quelques éléments à bien paramétrer :

  • Passez -N (pas de mise en mémoire tampon), sinon curl reste silencieux
  • Incluez Accept: application/json, text/event-stream, sans cela vous obtenez une erreur 406 Not Acceptable
curl -N -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}"

Une réponse réussie ressemble à :

Option B - Inspecteur MCP (recommandé)

L’inspecteur MCP vous fournit une interface utilisateur appropriée dans le navigateur pour parcourir et appeler des outils de manière interactive.

npx @modelcontextprotocol/inspector

Configurez la connexion dans l’interface de l’inspecteur :

  • Type de transport : STDIO
  • Commande : node
  • Arguments : server.js
  • Répertoire de travail : chemin vers votre dossier de projet
  • Variables d’environnement : TRANSPORT = stdio

Cliquez sur Connecter et vous verrez les trois outils prêts à l’emploi.

Caution

L’inspecteur démarre votre serveur lui-même via STDIO — ne démarrez pas le serveur séparément avant de vous connecter. Assurez-vous également que TRANSPORT=stdio est défini dans les Variables d’environnement, sinon le serveur démarre en mode HTTP et l’inspecteur ne peut pas communiquer avec lui.

Une réponse réussie :