Aller au contenu

Architecture MCP / WebMCP

Ce tutoriel est un voyage dans l’architecture de webmcp-auto-ui. Vous n’allez pas ecrire de code ici, mais vous allez comprendre comment chaque piece s’assemble. A la fin, vous saurez exactement ce qui se passe quand un utilisateur pose une question et qu’un widget apparait sur l’ecran.

Comprendre l’architecture complete de webmcp-auto-ui : les deux protocoles (MCP et WebMCP), leur symetrie, le prefixage uniforme, le lazy loading, le pipeline de schemas, et le resolver canonique.

  • Avoir lu le tutoriel Demarrer avec le boilerplate (ou avoir utilise l’app)
  • Comprendre ce qu’est un LLM et un appel d’outil (tool call)

Une comprehension profonde de l’architecture qui vous permettra de :

  • Debugger des problemes de routage d’outils
  • Ajouter de nouveaux serveurs MCP et WebMCP
  • Comprendre les logs de la boucle agent
  • Etendre le systeme avec de nouveaux protocoles

Le systeme repose sur deux protocoles symetriques :

  • MCP (Model Context Protocol) fournit les donnees distantes — requetes SQL, API REST, scraping, etc.
  • WebMCP fournit l’affichage local — widgets, canvas, interactions navigateur.

Chacun expose des tools (actions atomiques) et des recettes (guides de composition).

graph TB
subgraph MCP ["MCP (donnees distantes)"]
direction TB
M1[query_sql]
M2[fetch_document]
M3[search_recipes]
end
subgraph WebMCP ["WebMCP (affichage local)"]
direction TB
W1[widget_display]
W2[canvas]
W3[search_recipes]
end
LLM[Agent LLM] -->|HTTP| MCP
LLM -->|JS in-process| WebMCP
DimensionMCPWebMCP
RoleDonnees, API, basesAffichage, interaction
TransportHTTP Streamable / stdioAppels JS in-process
ExecutionServeur distantNavigateur local
LatenceReseau (50-500ms)Instantane (<1ms)
Exemples toolsquery_sql, fetch_documentwidget_display, canvas
RecettesDecrivent les donneesDecrivent la presentation
Package@webmcp-auto-ui/core@webmcp-auto-ui/core

Le design fondamental est la symetrie : le LLM ne distingue pas un tool MCP d’un tool WebMCP. Les deux protocoles exposent la meme interface :

  • search_recipes() — decouvrir les recettes disponibles
  • get_recipe() — obtenir le schema et les instructions
  • Des tools specifiques — executer des actions

Du point de vue du LLM, un appel MCP et un appel WebMCP suivent le meme cycle : decouverte, lecture du schema, execution. Seul le routage interne differe — la boucle agent dispatche vers le bon serveur selon le prefixe.

graph LR
LLM -->|tool_call| Router{Routeur}
Router -->|protocol = mcp| HTTP[McpClient.callTool via HTTP]
Router -->|protocol = webmcp| JS[WebMcpServer.executeTool en JS]
HTTP --> ServerMCP[Serveur distant]
JS --> Navigateur[Navigateur local]

Tous les tools suivent la convention de nommage :

{serverName}_{protocol}_{toolName}

Exemples concrets avec plusieurs serveurs connectes :

Tool prefixe completserverNameprotocoltoolName
tricoteuses_mcp_query_sqltricoteusesmcpquery_sql
tricoteuses_mcp_search_recipestricoteusesmcpsearch_recipes
datagouv_mcp_fetch_datasetdatagouvmcpfetch_dataset
autoui_webmcp_widget_displayautouiwebmcpwidget_display
autoui_webmcp_search_recipesautouiwebmcpsearch_recipes
designkit_webmcp_widget_displaydesignkitwebmcpwidget_display

Le routage dans la boucle agent parse ce prefixe avec une regex :

/^(.+?)_(mcp|webmcp)_(.+)$/

Puis dispatche :

  • protocol === 'mcp' —> McpClient.callTool(toolName, params)
  • protocol === 'webmcp' —> WebMcpServer.executeTool(toolName, params)

Ce prefixage garantit qu’il n’y a aucune collision de noms meme avec 10 serveurs connectes simultanement.

graph TD
Call["tricoteuses_mcp_query_sql(...)"] --> Parse[Regex parse]
Parse --> SN["serverName = tricoteuses"]
Parse --> P["protocol = mcp"]
Parse --> TN["toolName = query_sql"]
P -->|mcp| HTTP["McpClient(tricoteuses).callTool('query_sql', params)"]
P -->|webmcp| JS["WebMcpServer(nom).executeTool('toolName', params)"]

buildSystemPrompt(layers) genere un prompt recipe-driven adapte aux serveurs connectes. Le prompt impose un workflow strict en 4 etapes :

graph LR
A["1. Decouverte"] --> B["2. Lecture"]
B --> C["3. Execution"]
C --> D["4. Affichage"]
A -.- A1["search_recipes()"]
B -.- B1["get_recipe()"]
C -.- C1["query_sql(), fetch_data()..."]
D -.- D1["widget_display(), canvas()"]
  1. Decouverte — appeler search_recipes() pour trouver la recette pertinente
  2. Lecture — appeler get_recipe() pour lire les instructions
  3. Execution — suivre les instructions de la recette (fetch data, etc.)
  4. Affichage — utiliser widget_display, canvas, recall pour le rendu UI

Les listes d’outils aux etapes 1, 2 et 4 sont des placeholders : buildSystemPrompt injecte automatiquement les noms prefixes selon les layers connectes. Avec 2 serveurs MCP et 1 WebMCP, l’etape 1 contiendra par exemple :

tricoteuses_mcp_search_recipes(), datagouv_mcp_search_recipes(), autoui_webmcp_search_recipes()

Les apps peuvent passer un systemPrompt custom dans les options de runAgentLoop(). Quand il est fourni, il remplace le prompt genere. Cependant, le prompt unifie s’adapte aux serveurs presents et couvre la majorite des cas.


Au demarrage, la boucle agent n’expose pas tous les tools de tous les serveurs. Elle ne fournit que les tools de decouverte :

ProtocolTools exposes au depart
MCPsearch_recipes, get_recipe
WebMCPsearch_recipes, get_recipe, widget_display, canvas, recall

Les tools WebMCP d’action (widget_display, canvas, recall) sont toujours presents car ils sont necessaires pour afficher des resultats.

sequenceDiagram
participant LLM
participant AgentLoop as Agent Loop
participant MCP as Serveur MCP
Note over AgentLoop: Depart : seulement search_recipes, get_recipe
LLM->>AgentLoop: search_recipes("donnees")
AgentLoop->>MCP: search_recipes
MCP-->>AgentLoop: recettes trouvees
LLM->>AgentLoop: query_sql(...)
Note over AgentLoop: Premier appel a ce serveur !
AgentLoop->>AgentLoop: activateServerTools(serveur)
Note over AgentLoop: Tous les outils du serveur sont maintenant actifs
AgentLoop->>MCP: query_sql
MCP-->>AgentLoop: resultats

Quand le LLM appelle un tool d’un serveur pour la premiere fois, activateServerTools() ajoute tous les tools de ce serveur au jeu actif. Le serveur n’est active qu’une seule fois.

Avec 4 serveurs et 50 tools au total, le mode discovery expose environ 20 tools au lieu de 50. Cela represente une economie d’environ 3000-5000 tokens dans le prompt initial — significatif quand le budget est de 8K tokens par tour.


Les schemas des widgets suivent un pipeline en 4 etapes, du composant Svelte au runtime :

graph LR
A["interface Props (Svelte)"] -->|sync-schemas.ts| B["JSON Schema"]
B -->|frontmatter .md| C["Recette"]
C -->|registerWidget| D["WebMCP Server"]
D -->|widget_display| E["Validation runtime"]

Le script sync-schemas.ts maintient un mapping explicite entre chaque nom de widget et son fichier .svelte (ex: "stat"StatBlock.svelte, "profile"ProfileCard.svelte).

Quand le LLM appelle widget_display(name, params), le serveur WebMCP valide les params contre le JSON Schema avant de les passer au renderer :

sequenceDiagram
participant LLM
participant WS as WebMCP Server
participant V as validateJsonSchema
participant R as Renderer
LLM->>WS: widget_display('stat', {label: "Test"})
WS->>V: validate(params, schema)
alt Valide
V-->>WS: {valid: true}
WS->>R: rendre le widget
R-->>WS: {id: "w_abc123"}
WS-->>LLM: OK, id=w_abc123
else Invalide
V-->>WS: {valid: false, errors: [...]}
WS-->>LLM: Erreur + schema attendu
Note over LLM: Auto-correction au prochain tour
end

Si la validation echoue, le LLM recoit un message d’erreur avec le schema attendu, ce qui lui permet de corriger son appel.


Voici la sequence complete quand l’utilisateur demande : “Montre-moi le profil du depute Jean Dupont”.

sequenceDiagram
participant User as Utilisateur
participant App
participant AgentLoop as Agent Loop
participant LLM as LLM
participant MCP as Serveur MCP
participant WebMCP as WebMCP Server
User->>App: "Montre-moi le profil du depute Jean Dupont"
App->>AgentLoop: runAgentLoop message, options
Note over AgentLoop: Iteration 1 : Decouverte
AgentLoop->>LLM: prompt + tools discovery
LLM-->>AgentLoop: tool_call: tricoteuses_mcp_search_recipes
AgentLoop->>MCP: search_recipes
MCP-->>AgentLoop: recette "fiche-depute"
Note over AgentLoop: Iteration 2 : Lecture + Execution
AgentLoop->>LLM: resultat recettes
LLM-->>AgentLoop: tool_call: tricoteuses_mcp_get_recipe
AgentLoop->>MCP: get_recipe
MCP-->>AgentLoop: schema + instructions
Note over AgentLoop: Iteration 3 : Donnees
AgentLoop->>LLM: schema du widget
LLM-->>AgentLoop: tool_call: tricoteuses_mcp_query_sql
AgentLoop->>MCP: query_sql
MCP-->>AgentLoop: donnees JSON du depute
Note over AgentLoop: Iteration 4 : Affichage
AgentLoop->>LLM: donnees
LLM-->>AgentLoop: tool_call: autoui_webmcp_widget_display
AgentLoop->>WebMCP: widget_display
WebMCP-->>AgentLoop: id w_xyz
AgentLoop-->>App: AgentResult
App-->>User: Widget profile affiche !

Deux garde-fous evitent les boucles infinies :

  1. Compteur d’iterations sans rendu — apres 4 iterations sans widget_display, les tools de discovery sont retires du jeu actif. Apres 5 iterations, un message de nudge est injecte.
  2. maxIterations (defaut 5) — la boucle s’arrete meme si le LLM n’a pas termine.

Apres chaque iteration, les anciens tool_result sont comprimes : les textes de plus de 300 caracteres sont tronques a 200 avec un hint recall('id'). Le LLM peut recuperer le resultat complet via l’outil recall.


Plusieurs serveurs MCP et WebMCP coexistent grace au prefixage uniforme.

graph TB
Agent[Agent Loop] --> MCP1["tricoteuses_mcp_*"]
Agent --> MCP2["wikipedia_mcp_*"]
Agent --> WMCP1["autoui_webmcp_*"]
Agent --> WMCP2["designkit_webmcp_*"]
MCP1 --> S1[Serveur Tricoteuses]
MCP2 --> S2[Serveur Wikipedia]
WMCP1 --> W1[Widgets natifs]
WMCP2 --> W2[Widgets design]

Chaque serveur est un namespace complet. Si autoui et designkit exposent tous les deux un tool widget_display, le LLM voit :

  • autoui_webmcp_widget_display — widgets standards (stat, chart, map…)
  • designkit_webmcp_widget_display — widgets design (mockup, wireframe…)

Pas de confusion possible.


Les serveurs MCP n’utilisent pas toujours les noms exacts search_recipes et get_recipe. Le resolver canonique identifie les tools equivalents via 4 couches de matching :

CoucheStrategieExemple
Layer 1Correspondance exacte sur le nomsearch_recipes
Layer 2Decomposition (action, resource)list_skills —> action=list, resource=skills —> search_recipes
Layer 3Scan de la description pour keywordsdescription contient “recipe” + action “search”
Layer 4Fallback : pas de tool recette, liste les tools brutsserveur sans recettes
graph TD
Tool["list_skills"] --> L1{Layer 1: nom exact ?}
L1 -->|non| L2{Layer 2: decomposition ?}
L2 -->|action=list, resource=skills| Match["= search_recipes"]
L2 -->|non| L3{Layer 3: description ?}
L3 -->|non| L4["Layer 4: fallback (tools bruts)"]

Le resolver enregistre des alias dans une map locale :

// Si le serveur expose "list_skills" au lieu de "search_recipes"
aliasMap.set('serveur_mcp_search_recipes', 'serveur_mcp_list_skills');

Le system prompt utilise le nom canonique (search_recipes), et la boucle agent resout l’alias au moment de l’execution.


L’architecture par layers est concue pour accueillir de nouveaux types de serveurs sans modifier la boucle agent.

Un serveur WebMCP browser pourrait exposer notify, clipboard, share, download. Le LLM appellerait browser_webmcp_notify(...).

Un bridge natif pourrait exposer widget_display (rendu SwiftUI), haptic, speech. Meme convention : native_webmcp_*.

ToolLayer est un union discrimine par protocol. Ajouter un troisieme type = nouveau membre d’union + un cas dans buildToolsFromLayers().

// Aujourd'hui
export type ToolLayer = McpLayer | WebMcpLayer;
// Demain
export type ToolLayer = McpLayer | WebMcpLayer | WasmLayer;

graph TB
subgraph Navigateur
App[Application SvelteKit]
Agent[Agent Loop]
Canvas[Canvas Store]
Widgets[WidgetRenderer]
WebMCPs[Serveurs WebMCP locaux]
end
subgraph Cloud
LLMApi[API LLM distante]
MCP1[Serveur MCP 1]
MCP2[Serveur MCP 2]
end
App -->|message| Agent
Agent -->|prompt + tools| LLMApi
LLMApi -->|tool_calls| Agent
Agent -->|mcp calls| MCP1
Agent -->|mcp calls| MCP2
Agent -->|webmcp calls| WebMCPs
Agent -->|widget data| Canvas
Canvas -->|blocs reactifs| Widgets
Widgets -->|rendu DOM| App

ConceptImplementation
ProtocolesMCP (distant) + WebMCP (local), symetriques
Prefixage{server}_{protocol}_{tool}
LayersMcpLayer[] + WebMcpLayer[] = ToolLayer[]
Lazy loadingbuildDiscoveryTools() + activateServerTools()
System promptbuildSystemPrompt(layers) — dynamique
Pipeline schemasProps TS —> sync-schemas —> .md —> WebMCP
ValidationJSON Schema au runtime avant rendu
Multi-serveursNamespaces isoles, alias, filtrage recettes
Resolver canonique4 couches : exact, decomposition, description, fallback
Compression contexteTroncature + recall() pour long results
ExtensibiliteUnion discriminee ToolLayer, nouveau type

ProblemeCause probableSolution
”Unknown tool” dans les logsLe prefixe ne correspond a aucun serveurVerifiez que le serverName dans le layer correspond
Le LLM ignore un serveur MCPPas de recettes, le LLM ne sait pas quoi demanderAjoutez un fichier recipes.json au serveur
Boucle infinieLe LLM ne termine jamaisReduisez maxIterations ou verifiez le system prompt
Outils non visiblesLe serveur n’est pas dans les layersVerifiez que layers contient le layer du serveur

  • Implementer un nouveau protocole : etendez l’union ToolLayer et ajoutez un cas dans buildToolsFromLayers()
  • Creer un bridge natif : implementez un serveur WebMCP qui communique avec du code natif (Swift, Kotlin)
  • Optimiser le lazy loading : utilisez buildDiscoveryCache() pour pre-calculer les outils de decouverte