add-app-to-server

Par tldraw · tldraw

Cette compétence doit être utilisée lorsque l'utilisateur demande à « ajouter une app à mon serveur MCP », « ajouter une UI à mon serveur MCP », « ajouter une vue à mon outil MCP », « enrichir les outils MCP avec une UI », « ajouter une UI interactive à un serveur existant », « ajouter des MCP Apps à mon serveur », ou lorsqu'il a besoin d'ajouter des capacités d'UI interactive à un serveur MCP existant qui possède déjà des outils. Fournit des conseils pour analyser les outils existants et ajouter des ressources UI MCP Apps.

npx skills add https://github.com/tldraw/tldraw --skill add-app-to-server

Ajouter une UI à un serveur MCP

Enrichissez les outils d'un serveur MCP existant avec des interfaces utilisateur interactives en utilisant le SDK MCP Apps (@modelcontextprotocol/ext-apps).

Comment ça marche

Les outils existants sont associés à des ressources HTML qui s'affichent en ligne dans la conversation de l'hôte. L'outil continue de fonctionner pour les clients texte uniquement — l'UI est une amélioration, pas un remplacement. Chaque outil qui bénéficie d'une UI est lié à une ressource via _meta.ui.resourceUri, et l'hôte affiche cette ressource dans une iframe en sandbox quand l'outil est appelé.

Obtenir le code de référence

Clonez le dépôt du SDK pour accéder aux exemples fonctionnels et à la 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

Référence API (fichiers sources)

Lisez la documentation JSDoc directement depuis /tmp/mcp-ext-apps/src/ :

Fichier Contenu
src/app.ts Classe App, handlers (ontoolinput, ontoolresult, onhostcontextchanged, onteardown), cycle de vie
src/server/index.ts registerAppTool, registerAppResource, getUiCapability, options de visibilité des outils
src/spec.types.ts Toutes les définitions de types : McpUiHostContext, clés de variables CSS, modes d'affichage
src/styles.ts applyDocumentTheme, applyHostStyleVariables, applyHostFonts
src/react/useApp.tsx Hook useApp pour les apps React
src/react/useHostStyles.ts Hooks useHostStyles, useHostStyleVariables, useHostFonts

Exemples clés (patterns d'outils mixtes)

Ces exemples montrent des serveurs avec des outils App-améliorés et des outils simples — exactement le pattern que vous ajoutez :

Exemple Pattern
examples/map-server/ show-map (outil App) + geocode (outil simple)
examples/pdf-server/ display_pdf (outil App) + list_pdfs (outil simple) + read_pdf_bytes (outil app-only)
examples/system-monitor-server/ get-system-info (outil App) + poll-system-stats (outil de polling app-only)

Templates de frameworks

Apprenez et adaptez /tmp/mcp-ext-apps/examples/basic-server-{framework}/ :

Template 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

Étape 1 : Analyser les outils existants

Avant d'écrire du code, analysez les outils existants du serveur et déterminez lesquels bénéficient d'une UI.

  1. Lisez la source du serveur et listez tous les outils enregistrés
  2. Pour chaque outil, évaluez s'il bénéficierait d'une UI (retourne des données qui pourraient être visualisées, implique une interaction utilisateur, etc.) ou s'il convient en texte seul (recherches simples, fonctions utilitaires)
  3. Identifiez les outils qui pourraient devenir des helpers app-only (données que l'UI doit interroger/récupérer mais que le modèle n'a pas besoin d'appeler directement)
  4. Présentez l'analyse à l'utilisateur et confirmez les outils à améliorer

Framework de décision

Type de sortie d'outil Bénéfice UI Exemple
Données structurées / listes / tableaux Élevé — tableau interactif, recherche, filtrage Liste d'éléments, résultats de recherche
Métriques / nombres dans le temps Élevé — graphiques, jauges, tableaux de bord Statistiques système, analytiques
Média / contenu riche Élevé — visionneuse, lecteur, renderer Cartes, PDFs, images, vidéos
Texte simple / confirmations Faible — le texte suffit « Fichier créé », « Paramètre mis à jour »
Données pour d'autres outils Considérer app-only Points de sondage, chargeurs de chunks

Étape 2 : Ajouter des dépendances

npm install @modelcontextprotocol/ext-apps
npm install -D vite vite-plugin-singlefile

Plus les dépendances spécifiques au framework si nécessaire (p. ex. react, react-dom, @vitejs/plugin-react pour React).

Utilisez npm install pour ajouter des dépendances plutôt que d'écrire manuellement les numéros de version. Cela permet à npm de résoudre les dernières versions compatibles. Ne spécifiez jamais les numéros de version de mémoire.

Étape 3 : Configurer le pipeline de build

Configuration Vite

Créez vite.config.ts avec vite-plugin-singlefile pour bundler l'UI dans un seul fichier HTML :

import { defineConfig } from 'vite'
import { viteSingleFile } from 'vite-plugin-singlefile'

export default defineConfig({
    plugins: [viteSingleFile()],
    build: {
        outDir: 'dist',
        rollupOptions: {
            input: 'mcp-app.html', // un par UI, ou une entrée partagée
        },
    },
})

Point d'entrée HTML

Créez mcp-app.html (ou un par UI distincte si les outils ont besoin de vues différentes) :

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>MCP App</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="./src/mcp-app.ts"></script>
    </body>
</html>

Scripts de build

Ajoutez des scripts de build à package.json. L'UI doit être construite avant que le code du serveur ne la bundlise :

{
    "scripts": {
        "build:ui": "vite build",
        "build:server": "tsc",
        "build": "npm run build:ui && npm run build:server",
        "serve": "tsx server.ts"
    }
}

Étape 4 : Convertir les outils en outils App

Transformez les outils MCP simples en outils App avec UI.

Avant (outil MCP simple) :

server.tool('my-tool', { param: z.string() }, async (args) => {
    const data = await fetchData(args.param)
    return { content: [{ type: 'text', text: JSON.stringify(data) }] }
})

Après (outil App avec UI) :

import {
    registerAppTool,
    registerAppResource,
    RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server'

const resourceUri = 'ui://my-tool/mcp-app.html'

registerAppTool(
    server,
    'my-tool',
    {
        description: 'Shows data with an interactive UI',
        inputSchema: { param: z.string() },
        _meta: { ui: { resourceUri } },
    },
    async (args) => {
        const data = await fetchData(args.param)
        return {
            content: [{ type: 'text', text: JSON.stringify(data) }], // text fallback for non-UI hosts
            structuredContent: { data }, // structured data for the UI
        }
    }
)

Orientations clés :

  • Gardez toujours le tableau content avec un texte de secours pour les clients texte uniquement
  • Ajoutez structuredContent pour les données que l'UI doit afficher
  • Liez l'outil à sa ressource via _meta.ui.resourceUri
  • Laissez les outils qui ne bénéficient pas d'une UI inchangés — ils restent des outils simples

Étape 5 : Enregistrer les ressources

Enregistrez la ressource HTML afin que l'hôte puisse la récupérer :

import fs from 'node:fs/promises'
import path from 'node:path'

const resourceUri = 'ui://my-tool/mcp-app.html'

registerAppResource(
    server,
    {
        uri: resourceUri,
        name: 'My Tool UI',
        mimeType: RESOURCE_MIME_TYPE,
    },
    async () => {
        const html = await fs.readFile(
            path.resolve(import.meta.dirname, 'dist', 'mcp-app.html'),
            'utf-8'
        )
        return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }
    }
)

Si plusieurs outils partagent la même UI, ils peuvent référencer le même resourceUri et le même enregistrement de ressource.

Étape 6 : Construire l'UI

Enregistrement des handlers

Enregistrez TOUS les handlers AVANT d'appeler app.connect() :

import {
    App,
    PostMessageTransport,
    applyDocumentTheme,
    applyHostStyleVariables,
    applyHostFonts,
} from '@modelcontextprotocol/ext-apps'

const app = new App({ name: 'My App', version: '1.0.0' })

app.ontoolinput = (params) => {
    // Afficher l'UI en utilisant params.arguments et/ou params.structuredContent
}

app.ontoolresult = (result) => {
    // Mettre à jour l'UI avec le résultat final de l'outil
}

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)
    if (ctx.safeAreaInsets) {
        const { top, right, bottom, left } = ctx.safeAreaInsets
        document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`
    }
}

app.onteardown = async () => {
    return {}
}

await app.connect(new PostMessageTransport())

Styles de l'hôte

Utilisez les variables CSS de l'hôte pour l'intégration de thème :

.container {
    background: var(--color-background-secondary);
    color: var(--color-text-primary);
    font-family: var(--font-sans);
    border-radius: var(--border-radius-md);
}

Groupes de variables clés : --color-background-*, --color-text-*, --color-border-*, --font-sans, --font-mono, --font-text-*-size, --font-heading-*-size, --border-radius-*. Consultez src/spec.types.ts pour la liste complète.

Pour les apps React, utilisez plutôt les hooks useApp et useHostStyles — voir basic-server-react/ pour le pattern.

Améliorations optionnelles

Outils helpers app-only

Des outils que l'UI appelle mais que le modèle n'a pas besoin d'invoquer directement (sondage, pagination, chargement de chunks) :

registerAppTool(
    server,
    'poll-data',
    {
        description: 'Polls latest data for the UI',
        _meta: { ui: { resourceUri, visibility: ['app'] } },
    },
    async () => {
        const data = await getLatestData()
        return { content: [{ type: 'text', text: JSON.stringify(data) }] }
    }
)

L'UI appelle ces outils via app.callServerTool("poll-data", {}).

Configuration CSP

Si l'UI doit charger des ressources externes (polices, APIs, CDNs), déclarez les domaines :

registerAppResource(
    server,
    {
        uri: resourceUri,
        name: 'My Tool UI',
        mimeType: RESOURCE_MIME_TYPE,
        _meta: {
            ui: {
                connectDomains: ['api.example.com'], // cibles fetch/XHR
                resourceDomains: ['cdn.example.com'], // scripts, styles, images
                frameDomains: ['embed.example.com'], // iframes imbriquées
            },
        },
    },
    async () => {
        /* ... */
    }
)

Entrée partielle en streaming

Pour les grandes entrées d'outils, montrer la progression pendant la génération par le LLM :

app.ontoolinputpartial = (params) => {
    const args = params.arguments // JSON partiel corrigé - toujours valide
    // Afficher l'aperçu avec les données partielles
}

app.ontoolinput = (params) => {
    // Entrée complète finale - basculer au rendu complet
}

Dégradation gracieuse avec getUiCapability()

Enregistrez conditionnellement les outils App seulement quand le client supporte l'UI, en basculant sur les outils texte uniquement :

import {
    getUiCapability,
    registerAppTool,
    RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server'

server.server.oninitialized = () => {
    const clientCapabilities = server.server.getClientCapabilities()
    const uiCap = getUiCapability(clientCapabilities)

    if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
        // Le client supporte l'UI — enregistrer l'outil App
        registerAppTool(
            server,
            'my-tool',
            {
                description: 'Shows data with interactive UI',
                _meta: { ui: { resourceUri } },
            },
            appToolHandler
        )
    } else {
        // Client texte uniquement — enregistrer l'outil simple
        server.tool('my-tool', 'Shows data', { param: z.string() }, plainToolHandler)
    }
}

Mode plein écran

Permettez à l'UI de se développer en plein écran :

app.onhostcontextchanged = (ctx) => {
    if (ctx.availableDisplayModes?.includes('fullscreen')) {
        fullscreenBtn.style.display = 'block'
    }
    if (ctx.displayMode) {
        container.classList.toggle('fullscreen', ctx.displayMode === 'fullscreen')
    }
}

async function toggleFullscreen() {
    const newMode = currentMode === 'fullscreen' ? 'inline' : 'fullscreen'
    const result = await app.requestDisplayMode({ mode: newMode })
    currentMode = result.mode
}

Erreurs courantes à éviter

  1. Oublier le fallback texte content — Incluez toujours le tableau content avec du texte pour les hôtes non-UI
  2. Enregistrer les handlers après connect() — Enregistrez TOUS les handlers AVANT d'appeler app.connect()
  3. Oublier vite-plugin-singlefile — Sans lui, les assets ne se chargeront pas dans l'iframe sandboxée
  4. Oublier l'enregistrement des ressources — L'outil référence un resourceUri qui doit avoir une ressource correspondante
  5. Hardcoder les styles — Utilisez les variables CSS de l'hôte (var(--color-*)) pour l'intégration de thème
  6. Ne pas gérer les safe area insets — Appliquez toujours ctx.safeAreaInsets dans onhostcontextchanged

Tests

Utiliser basic-host

Testez le serveur amélioré avec l'exemple basic-host :

# Terminal 1 : Construire et exécuter votre serveur
npm run build && npm run serve

# Terminal 2 : Exécuter 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
# Ouvrir http://localhost:8080

Configurez SERVERS avec un tableau JSON d'URLs de votre serveur (défaut : http://localhost:3001/mcp).

Vérifier

  1. Les outils simples fonctionnent toujours et retournent une sortie texte
  2. Les outils App affichent leur UI dans l'iframe
  3. Le handler ontoolinput se déclenche avec les arguments de l'outil
  4. Le handler ontoolresult se déclenche avec le résultat de l'outil
  5. Les styles de l'hôte (thème, polices, couleurs) s'appliquent correctement

Skills similaires