create-mcp-app

Par tldraw · tldraw

Cette skill doit être utilisée lorsque l'utilisateur demande à « créer une MCP App », « ajouter une UI à un MCP tool », « construire une MCP View interactive », « générer le squelette d'une MCP App », ou a besoin de conseils sur les patterns du MCP Apps SDK, l'enregistrement des UI-resources, le cycle de vie d'une MCP App ou l'intégration avec le host. Fournit des conseils complets pour construire des MCP Apps avec des UIs interactives.

npx skills add https://github.com/tldraw/tldraw --skill create-mcp-app

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 :

  1. Outil - Appelé par l'LLM/hôte, retourne des données
  2. Ressource - Sert l'interface HTML groupée qui affiche les données
  3. Lien - Le _meta.ui.resourceUri de 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, registerAppResource du 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.ts complet avec registerAppTool et registerAppResource
  • Application côté client avec tous les gestionnaires de cycle de vie
  • vite.config.ts avec vite-plugin-singlefile
  • package.json avec toutes les dépendances requises
  • .gitignore excluant node_modules/ et dist/

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

  1. Gestionnaires après connect() - Enregistrez TOUS les gestionnaires AVANT d'appeler app.connect()
  2. Groupage en fichier unique manquant - Doit utiliser vite-plugin-singlefile
  3. Enregistrement de ressource oublié - L'outil ET la ressource doivent être enregistrés
  4. Lien resourceUri manquant - L'outil doit avoir _meta.ui.resourceUri
  5. Ignorer les insets de zone sûre - Gérez toujours ctx.safeAreaInsets
  6. Pas de secours textuel - Fournissez toujours un tableau content pour les hôtes non-UI
  7. Styles codés en dur - Utilisez les variables CSS de l'hôte pour l'intégration de thème
  8. Pas de streaming pour les grandes entrées - Utilisez ontoolinputpartial pour 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 } })

Skills similaires