1. Ce que nous allons construire aujourd’hui et comment cela s’intègre à l’application
Rappelons notre application d’apprentissage : nous réalisons un assistant de choix de cadeaux. Dans les modules précédents, nous avions déjà :
- un widget dans ChatGPT (Next.js 16 + Apps SDK), qui affiche l’UI, l’état, et sait appeler callTool ;
- un backend simple (via Apps SDK / routes Next.js), qui renvoyait des stubs de cadeaux.
Nous voulons maintenant « externaliser le cerveau » de notre assistant dans un MCP-serveur séparé. Au final, le schéma ressemblera à ceci :
flowchart TD
subgraph ChatGPT
U[Utilisateur
dans le chat]
W["Widget de l’app
(Apps SDK)"]
end
subgraph Client MCP
C[ChatGPT MCP client]
end
subgraph OurServer[Notre serveur MCP]
T1[Outil : suggest_gifts]
R1[Ressource : gift_catalog]
P1[Prompt : birthday_template]
end
U --> W
W -- callTool --> C
C <-- JSON-RPC / HTTP --> OurServer
OurServer --> C
C --> W
Autrement dit :
- le modèle à l’intérieur de ChatGPT voit notre MCP-serveur comme un ensemble standard de tools/resources/prompts ;
- callTool depuis le widget devient logiquement un appel interne MCP ;
- notre serveur décrit les contrats (schémas, descriptions) et implémente la logique métier.
À la fin de ce cours, vous devriez avoir un projet Node/TypeScript séparé avec un serveur MCP qui :
- se lance en local avec une seule commande ;
- enregistre au moins un outil et une ressource ;
- renvoie des données pertinentes (même avec de simples mocks) ;
- est structuré de manière à pouvoir évoluer.
Le backend existant via Apps SDK/Next.js n’est pas réécrit pour l’instant : il reste en l’état, et nous lançons le serveur MCP comme un service séparé à côté. Plus tard, vous pourrez le « raccorder » à l’app ChatGPT et transférer progressivement la logique des cadeaux à sa place, au lieu des anciens stubs.
2. Stack : TypeScript + MCP SDK + transport HTTP
Nous écrirons un MCP-serveur en TypeScript pour Node.js. Le SDK officiel JS/TS pour MCP vit dans le paquet @modelcontextprotocol/sdk. Il gère pour vous la routine liée au JSON-RPC, à la validation et à la conversion des schémas : vous décrivez les arguments via des schémas Zod, et le SDK les convertit en JSON Schema, compréhensible par le modèle.
Côté transport, il nous faut une variante HTTP : ChatGPT communique avec des serveurs MCP distants via le réseau, et non via stdio/localement. La spécification MCP décrit un format standard « HTTP streaming » — en substance, l’évolution de l’ancien schéma HTTP+SSE. En pratique, c’est un seul endpoint HTTP qui gère la requête (POST/GET) et, si besoin, streame la réponse. Dans le SDK TypeScript pour MCP, il existe généralement un transport prêt à l’emploi pour ce format, que l’on peut brancher à Express ou Hono.
Pour rester concis, nous supposerons que nous avons :
- un objet serveur McpServer de @modelcontextprotocol/sdk ;
- un transport HTTP (par exemple, StreamableHttpServerTransport ou similaire), que l’on peut intégrer à Express.
Les noms exacts des classes peuvent légèrement varier selon les versions du SDK, mais architecturalement, c’est toujours :
- créer un objet serveur MCP ;
- y enregistrer tools/resources/prompts ;
- connecter le transport à l’application HTTP.
3. Structure du projet et préparation
Créons un dossier séparé pour le MCP-serveur. Il est pratique de le garder à côté de l’application front-end, mais comme un projet Node séparé :
chatgpt-gift-app/
app/ ← Next.js + Apps SDK (widget)
mcp-server/ ← notre serveur MCP
À l’intérieur de mcp-server :
mcp-server/
src/
server.ts ← point d’entrée du serveur MCP
gifts.ts ← logique métier de sélection de cadeaux
package.json
tsconfig.json
Nous ferons un exemple simple de gifts.ts un peu plus tard ; concentrons-nous d’abord sur server.ts.
Supposons que vous ayez déjà initialisé le projet :
mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk
tsconfig.json — tout à fait standard (esnext modules, target node, strict). Vous pouvez le reprendre d’un de vos projets TS.
4. Isoler la logique métier dans un module séparé
La tentation est grande d’écrire tout de suite server.registerTool(..., async () => {...}) et d’y entasser toute la logique. Mais il vaut mieux séparer dès le départ :
- un module qui ne sait rien de MCP, de JSON-RPC, etc. ;
- un module qui ne connaît que MCP, mais très peu la logique métier.
Dans src/gifts.ts, décrivons une fonction simple de suggestion de cadeaux :
// src/gifts.ts
export type GiftIdea = {
id: string;
title: string;
price: number;
occasion: string;
};
export type SuggestGiftsInput = {
age: number;
relationship: "friend" | "partner" | "child" | "coworker";
budget: number;
};
export function suggestGifts(input: SuggestGiftsInput): GiftIdea[] {
// pour l’instant, des mocks simples
return [
{
id: "book-1",
title: "Un livre sur son hobby préféré",
price: Math.min(input.budget, 30),
occasion: "generic",
},
{
id: "game-1",
title: "Jeu de société pour un groupe",
price: Math.min(input.budget, 50),
occasion: "party",
},
];
}
Cette fonction est pure : paramètres en entrée, tableau d’idées en sortie. Elle peut être testée par des tests unitaires, réutilisée ailleurs, et elle ne dépend d’aucun MCP. C’est exactement ce qui est recommandé : l’enrobage serveur d’un côté, les fonctions métier de l’autre.
5. Créer le MCP-serveur et brancher le transport HTTP
Passons maintenant au point d’entrée src/server.ts. Schématiquement, nous devons :
- créer une instance de serveur MCP ;
- y enregistrer les outils, ressources et prompts ;
- démarrer un serveur HTTP (par exemple, Express) et y brancher le transport MCP.
Commençons par un squelette :
// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/sdk/transport/streamable-http";
const app = express();
// 1. Créer le serveur MCP
const mcpServer = new McpServer({
name: "gift-assistant-mcp",
version: "0.1.0",
});
// 2. Nous enregistrerons plus tard tools/resources/prompts
// 3. Configurer le transport au-dessus de HTTP
const transport = new StreamableHttpServerTransport({
path: "/mcp", // endpoint MCP unique
app, // s’intègre dans l’application Express
});
transport.attach(mcpServer);
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});
Les noms concrets de la classe de transport peuvent varier, mais le pattern reste le même : vous créez un endpoint HTTP et vous y connectez le serveur MCP comme gestionnaire JSON-RPC au-dessus de HTTP/stream.
À ce stade, le serveur ne fait encore rien d’utile, mais il sait déjà :
- passer le MCP-handshake ;
- répondre aux requêtes de discovery de base (la liste tools/resources/prompts — pour l’instant vide).
Étape suivante : enregistrer le premier outil.
6. Enregistrer le tool suggest_gifts via le SDK MCP
L’Apps SDK officiel et la documentation MCP montrent le même pattern d’enregistrement d’un outil : la méthode registerTool, à laquelle vous passez un nom, un descripteur (titre, description, schéma d’arguments) et un gestionnaire.
Nous avons déjà décrit le type SuggestGiftsInput dans gifts.ts. Ajoutons maintenant un schéma Zod pour que le serveur puisse valider les arguments d’entrée et fournir automatiquement à la LLM un JSON Schema correct.
// src/server.ts (extrait)
import { z } from "zod";
import { suggestGifts } from "./gifts";
const suggestGiftsInputSchema = z.object({
age: z.number().int().min(0).max(120),
relationship: z.enum(["friend", "partner", "child", "coworker"]),
budget: z.number().min(0),
});
Enregistrons maintenant l’outil :
// toujours dans server.ts
mcpServer.registerTool(
"suggest_gifts",
{
title: "Suggest gift ideas",
description:
"Propose des idées de cadeaux selon l’âge, le type de relation et le budget.",
// Le SDK convertira le schéma Zod en JSON Schema pour le modèle
inputSchema: suggestGiftsInputSchema,
},
async ({ input }) => {
const ideas = suggestGifts(input);
const text = ideas
.map(
(g) =>
`• ${g.title} — ~${g.price} USD (occasion: ${g.occasion}, id: ${g.id})`
)
.join("\n");
return {
content: [
{
type: "text",
text,
},
],
// structuredContent peut être utilisé dans le widget
structuredContent: {
ideas,
},
};
}
);
Points clés :
- inputSchema — schéma Zod. Le SDK TS sait le transformer en JSON Schema et décrit ainsi automatiquement l’outil pour le modèle.
- Le gestionnaire reçoit un objet avec input (dont le type provient du schéma). À l’intérieur, vous pouvez appeler votre fonction métier.
- Dans le result, vous renvoyez le content — c’est le texte que le modèle verra comme résultat — et, si souhaité, un structuredContent avec une structure JSON que votre widget peut consommer.
Si, dans les modules précédents, vous avez déjà créé un outil via l’Apps SDK, ce code doit vous sembler familier : le pattern est identique, sauf qu’il vit maintenant dans un MCP-serveur séparé.
7. Ajouter la ressource gift_catalog pour les données
Les outils sont des actions. Il arrive que l’on veuille aussi fournir des données sous forme de ressource, afin que le modèle puisse les lire, y chercher, ou pour que votre widget puisse charger des modèles, des composants, etc. MCP décrit séparément la notion de ressources avec URI, types MIME et contenu.
Créons une ressource simple gift_catalog, qui renvoie une liste de cadeaux disponibles. Pour l’instant, ce seront les mêmes mocks, mais en vrai cela pourrait être une extraction de base de données ou un flux produit.
Commençons par le catalogue lui-même :
// src/gifts.ts (complément)
export const giftCatalog: GiftIdea[] = [
{
id: "book-1",
title: "Livre de programmation",
price: 25,
occasion: "learning",
},
{
id: "lego-1",
title: "Coffret LEGO",
price: 60,
occasion: "fun",
},
];
Enregistrons maintenant la ressource sur le serveur :
// src/server.ts (extrait)
import { giftCatalog } from "./gifts";
mcpServer.registerResource(
"gift_catalog",
{
title: "Gift catalog",
description: "Petit catalogue de cadeaux pour la démo et le débogage.",
mimeType: "application/json",
},
async () => {
return {
contents: [
{
uri: "mcp://gift-catalog",
mimeType: "application/json",
text: JSON.stringify(giftCatalog, null, 2),
},
],
};
}
);
Logiquement, voici ce qui se passe :
- le nom de ressource gift_catalog sera visible côté client lors du discovery (vous le verrez dans la liste des ressources dans l’inspecteur MCP) ;
- le descripteur contient une description lisible par un humain et un type MIME ;
- le gestionnaire renvoie un tableau contents avec un URI et du texte — c’est le format standard d’une ressource dans MCP.
Plus tard, vous pourrez :
- lire cette ressource côté client (par exemple, un agent ou un inspecteur) ;
- l’utiliser comme modèles/données pour l’UI ;
- expérimenter : comment le modèle utilise le catalogue existant pour expliquer des options à l’utilisateur.
8. Enregistrer un prompt simple
La troisième entité MCP — les prompts, des invites préconfigurées. Elles évitent de répéter de longs prompts système ou utilisateur, et permettent de les stocker côté serveur avec des noms.
Créons un mini-exemple : le prompt birthday_gift, que l’on pourra appeler comme « modèle pré-rempli de conversation au sujet d’un cadeau d’anniversaire ».
// src/server.ts (extrait)
mcpServer.registerPrompt("birthday_gift", {
title: "Birthday gift helper",
description: "Modèle de requête pour la sélection d’un cadeau d’anniversaire.",
messages: [
{
role: "system",
content:
"Tu es un assistant de recherche de cadeaux. Pose des questions de clarification et propose plusieurs options.",
},
{
role: "user",
content:
"J’ai besoin d’un cadeau d’anniversaire. Pose les questions nécessaires et aide-moi à choisir.",
},
],
});
Sous le capot, MCP permet aux clients :
- d’obtenir la liste des prompts (dans l’inspecteur, vous verrez birthday_gift) ;
- de demander son contenu et de l’utiliser comme invite de base pour le modèle.
À part, dans le module sur le system-prompt et les instructions, nous analysons en détail comment ces prompts se combinent avec les instructions globales de l’application. Ici, l’important est simplement de les « voir » comme une partie du serveur MCP.
9. Comment tout cela fonctionne au runtime
Assemblons l’ensemble.
Lorsque le client (par exemple, MCP Inspector ou ChatGPT) se connecte à notre endpoint HTTP /mcp :
- le handshake a lieu : client et serveur échangent des informations sur les capacités prises en charge (tools/resources/prompts, etc.) ;
- le client appelle les méthodes de discovery : il obtient la liste des outils, ressources et prompts avec leurs descriptions et schémas ;
- quand le modèle décide d’appeler un outil, il forme une requête JSON-RPC avec une méthode du type tools/call ou similaire — le SDK côté serveur la transforme en appel interne du gestionnaire registerTool ;
- le gestionnaire exécute la logique métier (chez nous, suggestGifts ou la délivrance de giftCatalog) et renvoie le résultat dans un format standardisé ;
- le SDK sérialise la réponse en JSON-RPC et l’envoie au client via le même transport HTTP/stream.
Tous les détails de JSON-RPC, la formation de l’id, le routage des méthodes, etc., restent à l’intérieur de @modelcontextprotocol/sdk. Pour vous, l’interface ressemble beaucoup à l’Apps SDK : vous travaillez avec registerTool/registerResource/registerPrompt et leurs gestionnaires sans vous soucier du protocole.
10. Exécution locale et premier test simple
Supposons que vous ayez ajouté tout ce qui précède. Il ne reste plus qu’à lancer.
Dans package.json, vous pouvez ajouter un script :
{
"scripts": {
"dev": "ts-node-dev src/server.ts"
}
}
Lançons :
npm run dev
Dans la console, vous devriez voir quelque chose comme :
MCP server listening on http://localhost:4000/mcp
Nous ferons une inspection complète et des appels manuels d’outils dans le prochain cours via MCP Inspector / MCP Jam. Mais même maintenant, vous pouvez faire un test smoke très simple via curl :
curl -X POST http://localhost:4000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Ce curl est un simple test smoke facultatif pour ceux qui aiment regarder des réponses JSON « brutes ». En développement réel, vous interagissez presque toujours avec le serveur MCP via un SDK et non en composant à la main des requêtes JSON-RPC.
Le nom exact de la méthode dépend de la version du protocole et du SDK, mais l’idée est que vous obtiendrez une liste JSON où, parmi les tools, vous verrez suggest_gifts. Si la méthode ne correspond pas — ce n’est pas grave : l’objectif de ce cours n’est pas de connaître tous les noms par cœur, mais que vous n’ayez pas peur de regarder des réponses JSON et d’en comprendre la structure, grâce aux cours précédents.
11. Connexion avec notre ChatGPT App et évolutions
Pour l’instant, le MCP-serveur vit de manière indépendante. Dans les prochains modules, vous :
- le connecterez à MCP Inspector et apprendrez à déboguer tools/resources/prompts séparément, sans toucher à ChatGPT ;
- configurerez l’app ChatGPT pour qu’elle voie ce MCP-serveur comme source d’outils ;
- transférerez une partie de la logique auparavant réalisée dans l’Apps SDK (par exemple, via des tools intégrés) vers la couche MCP ;
- ajouterez l’authentification, la journalisation, des scénarios en streaming — au-dessus du squelette existant.
Ce qui est important maintenant :
- vous avez un service séparé, responsable des « compétences » et des « données » de l’application ;
- ce service parle aux clients via le standard MCP, et non via un REST sur-mesure ;
- vous savez déjà enregistrer manuellement des outils, des ressources et des prompts, sans craindre le protocole.
12. Un mot sur la structure du code et les bonnes pratiques
Même sur un si petit exemple, on peut instaurer de bonnes habitudes.
Premièrement, gardez la configuration du serveur à part. Tout ce qui concerne le nom, la version, la journalisation, les réglages du transport (port, chemin /mcp) peut être extrait dans un petit module config.ts. Ensuite, lorsque vous déploierez sur Vercel ou derrière une MCP-gateway, vous devrez ajouter des variables d’environnement, et vous vous en remercierez.
Deuxièmement, essayez de garder les méthodes registerTool/registerResource/registerPrompt aussi « fines » que possible. Les descriptions de schémas, les textes et la logique métier — autant d’éléments qui se portent bien dans des fichiers séparés :
- gifts.ts — fonctions de sélection de cadeaux ;
- catalog.ts — travail avec le catalogue de produits ;
- prompts.ts — ensemble de prompts.
Le server.ts devient alors une sorte de « fournisseur MCP » qui assemble simplement le tout.
Troisièmement, souvenez-vous que le MCP-serveur est par nature réactif : il attend les connexions des clients et leurs requêtes. Cela signifie que toute opération bloquante ou trop longue dans les outils affectera directement l’UX dans ChatGPT. Dans les prochains modules, nous parlerons des timeouts, des opérations asynchrones et des réponses en streaming, mais dès maintenant, réfléchissez aux opérations qui peuvent partir en tâche de fond et à celles qui doivent répondre vite.
Insight : ChatGPT ne prend en charge qu’une partie de MCP
Il est important de comprendre : les ChatGPT Apps utilisent MCP comme transport et format, mais ne sont pas un client MCP complet. Si l’on ne lit que le protocole, on peut se faire de fausses idées sur le comportement au runtime.
Ce que promet le « MCP pur » :
- les ressources (resources) peuvent être lues dynamiquement, à la demande du client, et non une fois pour toutes ;
- le serveur peut envoyer des notifications resourceChanged/toolChanged et ainsi « pousser » des mises à jour sans redémarrage du client ;
- on peut construire un système assez souple, où l’ensemble tools/resources/prompts est piloté par des configs ou un état externe.
Dans le contexte des ChatGPT Apps, ce n’est pas le cas. Pour une application, l’image est beaucoup plus statique :
- lors de l’enregistrement de l’app, ChatGPT lit une fois la description de tous les tools et resources ;
- puis cette configuration est en pratique mise en cache comme partie de la version de l’application ;
- les mises à jour dynamiques via les notifications MCP ne sont pas prises en charge — la plateforme les ignore simplement.
13. Erreurs fréquentes lors de l’écriture de votre premier MCP-serveur
Erreur n° 1 : entasser toute la logique métier directement dans registerTool.
La tentation de « tout écrire vite fait dans le gestionnaire de l’outil » est grande, surtout dans un exemple pédagogique. Mais cela devient ensuite une usine à gaz illisible, mélangeant validation, accès BD et formatage de réponse. Mieux vaut extraire tout de suite les fonctions métier (suggestGifts, travail avec le catalogue) dans des modules séparés, et ne faire que « l’assemblage » dans le gestionnaire.
Erreur n° 2 : se lier en dur à des noms de méthodes JSON MCP spécifiques.
Parfois, les étudiants commencent à écrire if (method === "tools/list") et à parser le JSON à la main. Il ne faut pas faire ça : c’est le travail du SDK. La spec MCP et les noms de méthodes peuvent évoluer, et le SDK s’en charge. Utilisez registerTool, registerResource, registerPrompt et laissez la bibliothèque décider de la représentation JSON-RPC.
Erreur n° 3 : ignorer le transport et essayer d’alimenter ChatGPT avec un serveur stdio.
Le transport stdio est idéal pour des clients locaux comme des environnements desktop, où le client peut lancer le serveur comme sous-processus. Mais ChatGPT communique en HTTPS et a besoin d’un endpoint HTTP/stream. Tenter de « faire passer stdio » via un tunnel se termine mal. Pour une ChatGPT App, faites directement un transport HTTP (Streamable HTTP).
Erreur n° 4 : ignorer les types MIME et la structure des ressources.
Pour les ressources, le contenu n’est pas le seul élément important ; le type (mimeType) et l’URI le sont aussi. Si vous mettez partout text/plain et jetez des chaînes JSON au hasard, les clients (et les inspecteurs) auront plus de mal à comprendre de quelles données il s’agit. Essayez d’indiquer des types MIME corrects (application/json, text/html pour des modèles d’UI, etc.) et des URI stables.
Erreur n° 5 : utiliser le MCP-serveur comme un « HTTP API aléatoire ».
La tentation arrive parfois : « Puisque j’ai déjà Express, j’accroche aussi /api/whatever et j’y accède directement. » Mélanger l’endpoint MCP avec un REST arbitraire n’est pas une bonne idée : cela complique la configuration, le routage et la sécurité. Il est plus simple d’avoir un contrat clair : /mcp pour MCP, d’autres chemins pour d’autres besoins, ou même un autre service. En production, c’est particulièrement important pour configurer les gateways et l’auth. N’en faites pas un « HTTP API aléatoire » — un ensemble de routes sans lien avec le contrat MCP.
Erreur n° 6 : ne pas journaliser les messages MCP entrants et sortants.
Sans logs, un MCP-serveur devient une boîte noire : « quelque chose ne marche pas, mais je ne sais pas quoi ». Dès le premier serveur, il est utile d’écrire au moins dans le stderr des logs structurés compacts : méthode de l’outil, statut, temps d’exécution. L’essentiel est de ne pas journaliser de données sensibles ni de tokens ; nous en parlerons plus loin quand nous aborderons la sécurité.
Erreur n° 7 : essayer de tout déboguer d’un coup via ChatGPT, sans inspecteur.
Scénario fréquent : un étudiant écrit un MCP-serveur, le connecte tout de suite à une ChatGPT App, et « tout casse sans qu’on sache pourquoi ». Alors que l’inspecteur n’a même jamais été lancé. Résultat : il est difficile de comprendre si le problème vient du protocole, du serveur, de l’Apps SDK ou du comportement du modèle. La bonne voie : s’assurer d’abord que le MCP-serveur fonctionne correctement en isolation (via MCP Jam / Inspector), puis seulement le connecter à l’application.
GO TO FULL VERSION