Lorsque les serveurs de visualisation existants ne couvrent pas vos besoins — types de graphiques personnalisés, interactions de drill-down complexes, structures de données propriétaires — vous construisez le vôtre. Cette note couvre l’implémentation : côté serveur TypeScript, SDK client, et les outils de build qui produisent un bundle déployable.
La configuration générale du serveur MCP est couverte dans MCP Server Project Setup. Cette note se concentre spécifiquement sur l’extension MCP Apps — le mécanisme de ressource ui:// et le SDK client qui rend l’UI interactive.
Prérequis
Node.js 18+ et deux packages au-delà du SDK MCP standard :
npm install @modelcontextprotocol/sdk @modelcontextprotocol/ext-appsLe package @modelcontextprotocol/ext-apps fournit registerAppTool, registerAppResource et RESOURCE_MIME_TYPE côté serveur, plus la classe App pour le bundle côté client.
Côté serveur : enregistrer l’outil d’application
Le serveur enregistre deux éléments : un outil avec des métadonnées UI, et une ressource qui sert le bundle HTML.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";import * as fs from "fs/promises";
const server = new McpServer({ name: "Analytics Dashboard", version: "1.0.0" });const resourceUri = "ui://dashboard/main.html";
// Enregistrer l'outil avec les métadonnées UIregisterAppTool(server, "show-dashboard", { title: "Analytics Dashboard", description: "Display interactive metrics dashboard. Use when the user asks for a visualization, chart, or dashboard of their data.", inputSchema: { type: "object", properties: { metrics: { type: "array", description: "Array of metric objects to display", items: { type: "object" } }, chartType: { type: "string", enum: ["bar", "line", "scatter", "pie"], description: "Visualization type" } }, required: ["metrics"] }, _meta: { ui: { resourceUri } }}, async (args) => ({ content: [{ type: "text", text: JSON.stringify(args.metrics) }]}));
// Enregistrer la ressource HTMLregisterAppResource(server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { const html = await fs.readFile("./dist/index.html", "utf-8"); return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; });Quelques points importants dans ce pattern :
Le gestionnaire d’outil retourne des données comme un bloc de contenu texte. La couche UI gère le rendu — la fonction de l’outil ne génère pas de HTML. Cette séparation garde la logique serveur propre et testable.
_meta.ui.resourceUri dans le schéma de l’outil est ce qui déclenche le comportement App dans l’hôte. Sans lui, l’outil est simplement un outil MCP standard. Avec lui, l’hôte sait qu’il doit précharger et rendre la ressource ui://.
Le gestionnaire de ressource lit le fichier HTML à l’exécution. Cela signifie que vous pouvez mettre à jour le bundle UI sans modifier le code du serveur.
Côté client : le SDK App
À l’intérieur du bundle HTML, la classe App de @modelcontextprotocol/ext-apps gère la communication avec l’hôte. Le bundle doit l’importer et configurer les gestionnaires d’événements avant d’appeler app.connect().
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App({ name: "Dashboard App", version: "1.0.0" });
// Reçoit le payload de données initial quand l'outil s'exécuteapp.ontoolresult = (result) => { const rawText = result.content?.find(c => c.type === "text")?.text; if (!rawText) return; const data = JSON.parse(rawText); renderChart(data);};
// Appelé depuis les interactions UI — changements de filtre, drill-downs, sélections de segmentasync function loadSegmentDetails(segment: string) { const result = await app.callServerTool({ name: "get-segment-details", arguments: { segment } }); const details = JSON.parse(result.content?.[0]?.text ?? "{}"); updateVisualization(details);}
// Informe l'IA de ce que l'utilisateur vient de faire — permet des questions de suivi contextuellesfunction onUserFilter(dimension: string, value: string) { app.updateModelContext({ action: "filtered", dimension, value, timestamp: new Date().toISOString() });}
app.connect();L’appel app.callServerTool() est ce qui permet une véritable interactivité — cliquer sur une barre dans votre graphique rappelle le serveur pour récupérer des données de détail, qui restituent la visualisation. C’est le modèle d’interaction qui différencie MCP Apps d’un export de graphique statique.
app.updateModelContext() est optionnel mais vaut la peine d’être implémenté si les utilisateurs posent des questions de suivi après avoir interagi avec la visualisation. Sans lui, l’IA ne sait pas ce que l’utilisateur a fait dans l’UI ; avec lui, “pourquoi le mobile a-t-il chuté ?” arrive avec l’état du filtre déjà attaché.
Outils de build
L’hôte nécessite un seul fichier HTML autonome. Aucun lien CDN externe, aucun fichier de style ou de script séparé — tout est intégré. Vite avec vite-plugin-singlefile gère cela :
import { defineConfig } from "vite";import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({ plugins: [viteSingleFile()], build: { outDir: "dist", // S'assurer que la sortie est adaptée à un usage inline assetsInlineLimit: Infinity, },});npm add -D vite vite-plugin-singlefilenpm run build# dist/index.html est votre bundle déployableLe SDK MCP Apps fournit des modèles de démarrage pour React, Vue, Svelte, Preact, Solid et vanilla JS. Pour la visualisation de données spécifiquement, React s’associe bien avec des bibliothèques comme Recharts ou Nivo ; vanilla JS avec Chart.js ou D3 est plus léger et évite la surcharge du framework dans le bundle.
Ajouter un outil de drill-down
Les serveurs de visualisation personnalisés ont souvent besoin de plus d’un outil — l’outil d’affichage principal et un ou plusieurs outils pour récupérer des données de détail. Enregistrez des outils supplémentaires sans _meta (ce sont des outils MCP ordinaires, pas des outils App) :
server.tool("get-segment-details", { description: "Fetch detailed breakdown data for a specific segment. Called when user drills down into a chart segment.", inputSchema: { type: "object", properties: { segment: { type: "string", description: "Segment identifier to detail" }, dateRange: { type: "object", properties: { start: { type: "string" }, end: { type: "string" } } } }, required: ["segment"] }}, async (args) => { // Interroger votre source de données const data = await fetchSegmentDetails(args.segment, args.dateRange); return { content: [{ type: "text", text: JSON.stringify(data) }] };});Ces outils secondaires n’ont pas besoin de métadonnées UI car ils sont appelés depuis le SDK App (app.callServerTool()), pas directement par l’IA.
Structure du projet
my-viz-server/├── package.json├── tsconfig.json├── server.ts # Serveur MCP — enregistrement des outils, gestionnaire de ressource├── vite.config.ts # Configuration de build en fichier unique├── src/│ ├── main.ts # Configuration SDK App, gestionnaires d'événements│ ├── chart.ts # Logique de visualisation│ └── App.tsx # Composant React (si React est utilisé)└── dist/ └── index.html # Bundle compilé — servi comme ressource ui://Le serveur et le client sont dans le même référentiel car ils doivent rester synchronisés — le format de sortie de l’outil du serveur doit correspondre à ce que le client s’attend à recevoir via ontoolresult. Une incompatibilité de version entre ce que l’outil retourne et ce que le client peut analyser est une source courante d’échecs silencieux.
Tests
Testez en couches :
- Tests unitaires des gestionnaires d’outils directement — appelez les fonctions async avec des données de test, vérifiez que la sortie JSON est conforme à vos attentes.
- Testez le bundle de manière isolée — ouvrez
dist/index.htmldans un navigateur et injectez des événementsontoolresultfictifs. Vérifiez le rendu sans avoir besoin d’un serveur MCP actif. - Tests d’intégration avec MCP Inspector —
npx @modelcontextprotocol/inspector node server.js. Vérifiez que l’outil apparaît, que la ressource se charge, et que l’invocation basique de l’outil fonctionne. - Testez dans le client réel — connectez à Claude Desktop ou Claude Code. Vérifiez que l’UI se rend dans la conversation et que l’interactivité fonctionne.
La note MCP Server Testing and Debugging couvre le workflow de test en trois étapes complet. Pour les Apps spécifiquement, ne sautez pas le test dans le client — le sandbox iframe et le canal de communication postMessage introduisent des modes d’échec que l’Inspector ne détecte pas.