Contribuer au projet
Ce guide documente les patterns a suivre et les pieges a eviter pour contribuer a WebMCP Auto-UI. Il est mis a jour au fil des incidents de production — chaque regle correspond a un bug reel, pas a une convention theorique.
Structure du monorepo
Section intitulée « Structure du monorepo »graph TD subgraph "Packages (npm workspaces)" CORE["core<br/>Types, MCP client, validation"] SDK["sdk<br/>HyperSkills, canvas store"] AGENT["agent<br/>Boucle agent, providers LLM"] UI["ui<br/>30+ widgets, composants agent"] end
subgraph "Apps" FLEX[flex — Composeur principal] VIEWER[viewer — Visionneur HyperSkills] SHOWCASE[showcase — Galerie widgets] RECIPES[recipes — Explorateur recettes] HOME[home — Page d'accueil] BOILER[boilerplate — Template starter] TODO[todo — Demo todo] end
CORE --> SDK CORE --> AGENT CORE --> UI SDK --> FLEX AGENT --> FLEX UI --> FLEXRegle fondamentale : reutiliser les packages
Section intitulée « Regle fondamentale : reutiliser les packages »Avant d’ecrire du code dans une app (apps/*), verifier d’abord si la fonctionnalite existe deja dans un package. Le tableau ci-dessous liste les imports disponibles :
| Besoin | Import | Package |
|---|---|---|
| Serveur WebMCP | createWebMcpServer | core |
| Client MCP | McpClient, McpMultiClient | core |
| Validation JSON Schema | validateJsonSchema | core |
| Montage widget vanilla | mountWidget | core |
| Boucle agent | runAgentLoop | agent |
| Provider LLM distant | RemoteLLMProvider | agent |
| Provider Gemma | WasmProvider | agent |
| Provider Ollama | LocalLLMProvider | agent |
| Outils de decouverte | buildDiscoveryTools, activateServerTools | agent |
| Resolution canonique | resolveCanonicalTools, toolAliasMap | agent |
| Suivi de tokens | TokenTracker | agent |
| Nano-RAG | ContextRAG | agent |
| Selecteur LLM | <LLMSelector> | ui |
| Chargement Gemma | <ModelLoader> | ui |
| Statut MCP | <McpStatus> | ui |
| Progression agent | <AgentProgress> | ui |
| Rendu widgets | <WidgetRenderer>, <BlockRenderer> | ui |
| Message bus FONC | bus | ui |
| Encodage HyperSkill | encode, decode | sdk |
| Canvas store | canvas via @webmcp-auto-ui/sdk/canvas | sdk |
Svelte 5 — Reactivite
Section intitulée « Svelte 5 — Reactivite »Les regles suivantes viennent de bugs reels de production. Chaque regle est illustree par le code fautif et le code correct.
Regle 1 : un $effect ne doit pas lire ET ecrire les memes etats
Section intitulée « Regle 1 : un $effect ne doit pas lire ET ecrire les memes etats »Symptome : effect_update_depth_exceeded en console, la page entiere se bloque (boutons sans effet, modals qui ne s’ouvrent pas).
Cause : Svelte 5 re-execute un $effect chaque fois qu’une de ses dependances reactives change. Si l’effet ecrit une valeur qu’il lit aussi, il se re-declenche lui-meme — boucle infinie — arret au bout de 5 iterations.
<!-- INCORRECT -- lit gemmaStatus ET l'ecrit -->$effect(() => { const llm = canvas.llm; if (gemmaStatus === 'ready') { // lit gemmaStatus → tracke gemmaStatus = 'idle'; // ecrit gemmaStatus → re-run ! } canvas.addMsg('system', llm);});
<!-- CORRECT -- untrack() pour les lectures non-declencheuses -->import { untrack } from 'svelte';
$effect(() => { const llm = canvas.llm; // seule dependance trackee untrack(() => { if (gemmaStatus === 'ready') { // lu mais pas tracke gemmaStatus = 'idle'; } canvas.addMsg('system', llm); });});Regle : isoler la ou les dependances qui doivent declencher le re-run, et wrapper tout le reste dans untrack().
Regle 2 : preferer $derived a $effect + $state pour les valeurs calculees
Section intitulée « Regle 2 : preferer $derived a $effect + $state pour les valeurs calculees »<!-- INCORRECT -- $state ecrit dans un $effect -->let paletteOpen = $state(true);$effect(() => { paletteOpen = canvas.mode === 'drag'; });
<!-- CORRECT si la valeur est read-only -->const paletteOpen = $derived(canvas.mode === 'drag');
<!-- CORRECT si la valeur est aussi ecrite manuellement (toggle utilisateur) -->let paletteOpen = $state(true);$effect(() => { paletteOpen = canvas.mode === 'drag'; });// OK car l'effet ne LIT PAS paletteOpen$derived est toujours preferable quand la valeur ne depend que d’autres etats reactifs. Utiliser $effect + $state uniquement quand la valeur est aussi modifiee par l’utilisateur (toggle, drag, etc.).
Regle 3 : eviter les $effect redondants avec onMount
Section intitulée « Regle 3 : eviter les $effect redondants avec onMount »<!-- INCORRECT -- double initialisation -->let skills = $state([]);$effect(() => { skills = listSkills(); }); // court-circuite par onMountonMount(() => { skills = listSkills(); });
<!-- CORRECT -- une seule source de verite -->let skills = $state([]);onMount(() => { skills = listSkills(); });Si listSkills() ne lit aucun etat reactif, le $effect ne se re-declenchera jamais apres le premier run. Il est strictement equivalent a onMount, mais plus trompeur a la lecture.
Regle 4 : passer le modele LLM explicitement au provider
Section intitulée « Regle 4 : passer le modele LLM explicitement au provider »// INCORRECT -- utilise toujours 'haiku' par defautreturn new RemoteLLMProvider({ proxyUrl: `${base}/api/chat` });
// CORRECT -- transmet le choix de l'utilisateurreturn new RemoteLLMProvider({ proxyUrl: `${base}/api/chat`, model: canvas.llm,});RemoteLLMProvider utilise model ?? 'haiku' comme valeur par defaut. Si l’utilisateur selecte sonnet dans le <LLMSelector>, il recevra quand meme haiku sans message d’erreur. Toujours passer model explicitement.
Boucle agent — loop.ts
Section intitulée « Boucle agent — loop.ts »onText n’est appele qu’a la derniere iteration
Section intitulée « onText n’est appele qu’a la derniere iteration »Par defaut, callbacks.onText est appele uniquement quand le LLM repond sans tool_use. Comme le system prompt force l’usage d’outils, ce callback n’est jamais atteint dans le flux normal.
Consequence : la bulle “thinking” reste figee sur le dernier outil appele.
Correctif applique dans loop.ts : appeler onText aussi quand il y a du texte intermediaire avant les tool_use :
// Texte intermediaire avant tool_use -- mise a jour liveif (lastText) callbacks.onText?.(lastText);Deploiement — Integrite du build
Section intitulée « Deploiement — Integrite du build »Toujours verifier le sha256 apres deploiement
Section intitulée « Toujours verifier le sha256 apres deploiement »Le script deploy.sh verifie automatiquement l’integrite apres chaque transfert. En cas de mismatch, le deploiement echoue avec rollback.
Pourquoi ? Sans cette verification, un deploiement peut “reussir” (pas d’erreur SCP, service active) mais servir l’ancien code si :
- Le build local etait perime
- Le SCP a ete interrompu silencieusement
- Le fichier cible etait en lecture seule
Toujours rebuilder les apps
Section intitulée « Toujours rebuilder les apps »Le script rebuild automatiquement les apps via npm run build avant de copier. Avant ce correctif, les packages etaient recompiles mais les apps conservaient leur ancien build/, et les correctifs Svelte etaient perdus.
Packages JS purs : wrapper type obligatoire
Section intitulée « Packages JS purs : wrapper type obligatoire »Quand un package npm est en JavaScript pur (sans types TypeScript), ne jamais l’importer directement dans le code TypeScript du projet. Creer un wrapper type dans le SDK :
// @ts-ignore — hyperskills is intentionally pure JSimport * as hs from 'hyperskills';
export const encode: (sourceUrl: string, content: string) => Promise<string> = hs.encode;export const decode: (urlOrParam: string) => Promise<{ sourceUrl: string; content: string }> = hs.decode;Pourquoi pas declare module ? Ignore par moduleResolution: "NodeNext" quand le JS est deja resolu.
Pourquoi pas .d.ts dans node_modules ? Supprime au prochain npm install.
Pourquoi pas allowJs ? Ne suffit pas avec strict: true + NodeNext.
Tests unitaires (Vitest)
Section intitulée « Tests unitaires (Vitest) »npm run test # Tous les testsnpm run test:watch # Mode watchnpm run test:coverage # Avec couvertureTests end-to-end (Playwright)
Section intitulée « Tests end-to-end (Playwright) »Les tests e2e verifient les apps deployees sur https://demos.hyperskills.net :
npx playwright test # Tous les testsnpx playwright test --grep "Composer" # Une suitenpx playwright test --grep "export" # Un test precisQuand lancer les tests :
- Apres chaque deploiement important
- Avant de marquer un bug comme resolu
- Apres un refactoring qui touche plusieurs composants
// INCORRECT -- peut cliquer avant hydratationawait page.goto(url);await page.click('button:has-text("export")');
// CORRECT -- attendre un element client-sideawait page.goto(url);await page.waitForSelector('select', { state: 'visible' });await page.waitForTimeout(500); // tick d'hydratationawait page.click('button:has-text("export")');Ce que les tests ne verifient pas
Section intitulée « Ce que les tests ne verifient pas »- Que le code deploye correspond au code local (c’est le role du sha256 dans
deploy.sh) - Les erreurs JavaScript silencieuses cote client (ajouter
page.on('pageerror')) - Les boucles reactives Svelte qui n’affectent pas les selecteurs CSS testes
Debug — Checklist “le correctif n’est pas en production”
Section intitulée « Debug — Checklist “le correctif n’est pas en production” »Quand un correctif semble applique localement mais pas visible en production, suivre cette checklist dans l’ordre :
-
Le build local est-il a jour ?
Fenêtre de terminal ls -la apps/flex/build/index.js # Verifier la date de modification -
Le fichier deploye correspond-il au build local ?
Fenêtre de terminal sha256sum apps/flex/build/index.jsssh bot "sha256sum /opt/webmcp-demos/flex/index.js"# Les deux hashes doivent etre identiques -
Le service a-t-il redemarre avec le bon fichier ?
Fenêtre de terminal ssh bot "systemctl status webmcp-flex --no-pager | head -20" -
Y a-t-il des erreurs JavaScript cote client ? Ouvrir la console du navigateur (
F12) sur l’URL de production.
Documentation
Section intitulée « Documentation »Synchronisation automatique
Section intitulée « Synchronisation automatique »Apres toute modification de code qui change les exports, les tokens ou les types de widgets :
npm run docs:syncLe CI verifie que la documentation est a jour a chaque push.
Diagrammes Mermaid
Section intitulée « Diagrammes Mermaid »Les diagrammes existants sont pre-rendus en SVG dans docs/starlight/public/diagrams/. Pour regenerer les SVG apres modification d’un diagramme :
npx @mermaid-js/mermaid-cli -i fichier.mmd -o docs/starlight/public/diagrams/fichier.svg --backgroundColor transparentWorkflow de contribution
Section intitulée « Workflow de contribution »1. Creer une branche
Section intitulée « 1. Creer une branche »git checkout -b feat/ma-featureConventions de nommage :
feat/: nouvelle fonctionnalitefix/: correction de bugrefactor/: restructuration sans changement fonctionneldocs/: documentation uniquement
2. Developper et tester
Section intitulée « 2. Developper et tester »npm run dev:flex # Developper sur l'app principalenpm run test # Lancer les tests unitairesnpm run check # Verifier les types TypeScript3. Commiter
Section intitulée « 3. Commiter »Utiliser le format Conventional Commits :
feat(agent): add context compaction via Nano-RAGfix(ui): prevent effect_update_depth_exceeded in ModelLoaderrefactor(core): simplify JSON Schema validationdocs(guide): update architecture diagramperf(onnxruntime): load WASM from CDN instead of bundling4. Pull Request
Section intitulée « 4. Pull Request »- Titre court et descriptif (< 70 caracteres)
- Description avec le contexte et les changements
- Lier les issues si applicable
Comment ajouter un nouveau widget ?
Section intitulée « Comment ajouter un nouveau widget ? »- Creer le composant Svelte dans
packages/ui/src/widgets/rich/ousimple/. - L’exporter dans
packages/ui/src/index.ts. - Ecrire une recette markdown avec frontmatter (schema JSON Schema).
- Enregistrer la recette dans
packages/agent/src/autoui-server.ts. - Lancer
npm run docs:syncpour mettre a jour la doc.
Comment ajouter un nouveau provider LLM ?
Section intitulée « Comment ajouter un nouveau provider LLM ? »- Creer un fichier dans
packages/agent/src/providers/. - Implementer l’interface
LLMProvider(methodechat). - L’exporter dans
packages/agent/src/index.ts. - Ajouter un cas dans la factory
createProvider.
Comment connecter un nouveau serveur MCP ?
Section intitulée « Comment connecter un nouveau serveur MCP ? »- Lancer le serveur MCP (doit exposer un endpoint SSE).
- Ajouter l’URL dans la configuration (
.env.localou parametres de l’app). - Le resolver canonique gerera automatiquement le mapping des noms d’outils.
Pourquoi les tests e2e testent les apps deployees et pas locales ?
Section intitulée « Pourquoi les tests e2e testent les apps deployees et pas locales ? »Parce que les bugs les plus dangereux surviennent entre le build local et le deploiement. Tester en production (avec des donnees de test) detecte les problemes d’integrite de build, de configuration nginx et de variables d’environnement manquantes.