Créer une MCP App
Construisez des interfaces utilisateur interactives qui s'exécutent dans des hôtes compatibles MCP comme Claude Desktop. Une MCP App combine un outil MCP avec une ressource HTML pour afficher du contenu riche et interactif.
Concept fondamental : Outil + Ressource
Chaque MCP App nécessite deux parties liées ensemble :
- Outil - Appelé par l'LLM/hôte, retourne des données
- Ressource - Sert l'interface HTML groupée qui affiche les données
- Lien - Le
_meta.ui.resourceUride l'outil référence la ressource
L'hôte appelle l'outil → Le serveur retourne le résultat → L'hôte affiche l'interface de la ressource → L'interface reçoit le résultat
Arbre de décision pour démarrer rapidement
Sélection du framework
| Framework | Support SDK | Idéal pour |
|---|---|---|
| React | Hook useApp fourni |
Équipes familières avec React |
| Vanilla JS | Cycle de vie manuel | Applications simples, sans build |
| Vue/Svelte/Preact/Solid | Cycle de vie manuel | Préférence du framework |
Contexte du projet
Ajout à un serveur MCP existant :
- Importez
registerAppTool,registerAppResourcedu SDK - Ajoutez l'enregistrement de l'outil avec
_meta.ui.resourceUri - Ajoutez l'enregistrement de la ressource servant l'HTML groupé
Création d'un nouveau serveur MCP :
- Configurez le serveur avec un transport (stdio ou HTTP)
- Enregistrez les outils et ressources
- Configurez le système de build avec
vite-plugin-singlefile
Obtenir le code de référence
Clonez le dépôt SDK pour obtenir des exemples fonctionnels et une documentation API :
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
Modèles de framework
Apprenez et adaptez les exemples de /tmp/mcp-ext-apps/examples/basic-server-{framework}/ :
| Modèle | Fichiers clés |
|---|---|
basic-server-vanillajs/ |
server.ts, src/mcp-app.ts, mcp-app.html |
basic-server-react/ |
server.ts, src/mcp-app.tsx (utilise le hook useApp) |
basic-server-vue/ |
server.ts, src/App.vue |
basic-server-svelte/ |
server.ts, src/App.svelte |
basic-server-preact/ |
server.ts, src/mcp-app.tsx |
basic-server-solid/ |
server.ts, src/mcp-app.tsx |
Chaque modèle inclut :
server.tscomplet avecregisterAppTooletregisterAppResource- Application côté client avec tous les gestionnaires de cycle de vie
vite.config.tsavecvite-plugin-singlefilepackage.jsonavec toutes les dépendances requises.gitignoreexcluantnode_modules/etdist/
Référence API (Fichiers source)
Lisez la documentation JSDoc directement depuis /tmp/mcp-ext-apps/src/ :
| Fichier | Contenu |
|---|---|
src/app.ts |
Classe App, gestionnaires (ontoolinput, ontoolresult, onhostcontextchanged, onteardown), cycle de vie |
src/server/index.ts |
registerAppTool, registerAppResource, options de visibilité de l'outil |
src/spec.types.ts |
Toutes les définitions de type : McpUiHostContext, clés de variables CSS, modes d'affichage |
src/styles.ts |
applyDocumentTheme, applyHostStyleVariables, applyHostFonts |
src/react/useApp.tsx |
Hook useApp pour les applications React |
src/react/useHostStyles.ts |
Hooks useHostStyles, useHostStyleVariables, useHostFonts |
Exemples avancés
| Exemple | Motif démontré |
|---|---|
examples/shadertoy-server/ |
Entrée partielle en streaming + pause/play basé sur la visibilité (bonne pratique pour les grandes entrées) |
examples/wiki-explorer-server/ |
callServerTool pour la récupération de données interactive |
examples/system-monitor-server/ |
Motif de polling avec gestion d'intervalle |
examples/video-resource-server/ |
Ressources binaires/blob |
examples/sheet-music-server/ |
ontoolinput - traitement des arguments de l'outil avant la fin de l'exécution |
examples/threejs-server/ |
ontoolinputpartial - rendu streaming/progressif |
examples/map-server/ |
updateModelContext - maintenir le modèle informé de l'état de l'interface |
examples/transcript-server/ |
updateModelContext + sendMessage - mises à jour de contexte en arrière-plan + messages initiés par l'utilisateur |
examples/basic-host/ |
Implémentation d'hôte de référence utilisant AppBridge |
Remarques critiques sur l'implémentation
Ajout de dépendances
Utilisez npm install pour ajouter des dépendances plutôt que d'écrire manuellement les numéros de version :
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
Cela permet à npm de résoudre les versions compatibles les plus récentes. Ne spécifiez jamais les numéros de version de mémoire.
Exécution du serveur TypeScript
Utilisez tsx comme devDependency pour exécuter les fichiers serveur TypeScript :
npm install -D tsx
"scripts": {
"serve": "tsx server.ts"
}
Remarque : Les exemples du SDK utilisent bun mais les projets générés doivent utiliser tsx pour une meilleure compatibilité.
Ordre d'enregistrement des gestionnaires
Enregistrez TOUS les gestionnaires AVANT d'appeler app.connect() :
const app = new App({ name: 'My App', version: '1.0.0' })
// Enregistrez d'abord les gestionnaires
app.ontoolinput = (params) => {
/* gérer l'entrée */
}
app.ontoolresult = (result) => {
/* gérer le résultat */
}
app.onhostcontextchanged = (ctx) => {
/* gérer le contexte */
}
app.onteardown = async () => {
return {}
}
// Puis connectez
await app.connect()
Visibilité de l'outil
Contrôlez qui peut accéder aux outils via _meta.ui.visibility :
// Par défaut : visible au modèle et à l'application
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }
// Uniquement l'interface (masqué au modèle) - pour les boutons d'actualisation, soumissions de formulaires
_meta: { ui: { resourceUri, visibility: ["app"] } }
// Uniquement le modèle (l'application ne peut pas appeler)
_meta: { ui: { resourceUri, visibility: ["model"] } }
Intégration du style de l'hôte
Vanilla JS - Utilisez les fonctions d'aide :
import {
applyDocumentTheme,
applyHostStyleVariables,
applyHostFonts,
} from '@modelcontextprotocol/ext-apps'
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme)
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables)
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts)
}
React - Utilisez les hooks :
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react'
const { app } = useApp({ appInfo, capabilities, onAppCreated })
useHostStyles(app) // Injecte les variables CSS au document, rendant var(--*) disponible
Utilisation des variables en CSS - Après application, utilisez var() :
.container {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-family: var(--font-sans);
border-radius: var(--border-radius-md);
}
.code {
font-family: var(--font-mono);
font-size: var(--font-text-sm-size);
line-height: var(--font-text-sm-line-height);
color: var(--color-text-secondary);
}
.heading {
font-size: var(--font-heading-lg-size);
font-weight: var(--font-weight-semibold);
}
Groupes de variables clés : --color-background-*, --color-text-*, --color-border-*, --font-sans, --font-mono, --font-text-*-size, --font-heading-*-size, --border-radius-*. Voir src/spec.types.ts pour la liste complète.
Gestion de la zone sûre
Respectez toujours safeAreaInsets :
app.onhostcontextchanged = (ctx) => {
if (ctx.safeAreaInsets) {
const { top, right, bottom, left } = ctx.safeAreaInsets
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
}
}
Streaming d'entrée partielle
Pour les grandes entrées d'outil, utilisez ontoolinputpartial pour afficher la progression pendant la génération LLM. Le JSON partiel est réparé (toujours valide), permettant les mises à jour d'interface utilisateur progressives.
Spécification : ui/notifications/tool-input-partial
app.ontoolinputpartial = (params) => {
const args = params.arguments // JSON partiel réparé - toujours valide, les champs apparaissent lors de la génération
// Utilisez args directement pour le rendu progressif
}
app.ontoolinput = (params) => {
// Entrée complète finale - passer de l'aperçu au rendu complet
}
Cas d'usage :
| Motif | Exemple |
|---------|---------|
| Aperçu du code | Afficher le code en streaming dans <pre>, rendu à la fin (examples/shadertoy-server/) |
| Formulaire progressif | Remplir les champs du formulaire au fur et à mesure du streaming |
| Graphique en direct | Ajouter des points de données au graphique à mesure que le tableau croît |
| Rendu partiel | Afficher les données structurées incomplètes (tableaux, listes, arbres) |
Motif simple (aperçu du code) :
app.ontoolinputpartial = (params) => {
codePreview.textContent = params.arguments?.code ?? ''
codePreview.style.display = 'block'
canvas.style.display = 'none'
}
app.ontoolinput = (params) => {
codePreview.style.display = 'none'
canvas.style.display = 'block'
render(params.arguments)
}
Gestion des ressources basée sur la visibilité
Suspendez les opérations coûteuses (animations, WebGL, polling) quand la vue sort du viewport :
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animation.play() // ou : startPolling(), shaderToy.play()
} else {
animation.pause() // ou : stopPolling(), shaderToy.pause()
}
})
})
observer.observe(document.querySelector('.main'))
Mode plein écran
Demandez le plein écran via app.requestDisplayMode(). Vérifiez la disponibilité dans le contexte de l'hôte :
let currentMode: 'inline' | 'fullscreen' = 'inline'
app.onhostcontextchanged = (ctx) => {
// Vérifiez si le plein écran est disponible
if (ctx.availableDisplayModes?.includes('fullscreen')) {
fullscreenBtn.style.display = 'block'
}
// Suivez le mode actuel
if (ctx.displayMode) {
currentMode = ctx.displayMode
container.classList.toggle('fullscreen', currentMode === 'fullscreen')
}
}
async function toggleFullscreen() {
const newMode = currentMode === 'fullscreen' ? 'inline' : 'fullscreen'
const result = await app.requestDisplayMode({ mode: newMode })
currentMode = result.mode
}
Motif CSS - Supprimez le rayon de bordure en plein écran :
.main {
border-radius: var(--border-radius-lg);
overflow: hidden;
}
.main.fullscreen {
border-radius: 0;
}
Voir examples/shadertoy-server/ pour l'implémentation complète.
Erreurs courantes à éviter
- Gestionnaires après connect() - Enregistrez TOUS les gestionnaires AVANT d'appeler
app.connect() - Groupage en fichier unique manquant - Doit utiliser
vite-plugin-singlefile - Enregistrement de ressource oublié - L'outil ET la ressource doivent être enregistrés
- Lien resourceUri manquant - L'outil doit avoir
_meta.ui.resourceUri - Ignorer les insets de zone sûre - Gérez toujours
ctx.safeAreaInsets - Pas de secours textuel - Fournissez toujours un tableau
contentpour les hôtes non-UI - Styles codés en dur - Utilisez les variables CSS de l'hôte pour l'intégration de thème
- Pas de streaming pour les grandes entrées - Utilisez
ontoolinputpartialpour afficher la progression lors de la génération
Tests
Utilisation de basic-host
Testez les MCP Apps localement avec l'exemple basic-host :
# Terminal 1 : Construisez et exécutez votre serveur
npm run build && npm run serve
# Terminal 2 : Exécutez basic-host (depuis le dépôt cloné)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Ouvrez http://localhost:8080
Configurez SERVERS avec un tableau JSON d'URLs de serveur (par défaut : http://localhost:3001/mcp).
Débogage avec sendLog
Envoyez des journaux de débogage à l'application hôte (plutôt qu'à la console de développement de l'iframe) :
await app.sendLog({ level: 'info', data: 'Debug message' })
await app.sendLog({ level: 'error', data: { error: err.message } })