---
title: Chaque écran de cette appli SaaS est une MCP App
description: À AGNTCon + MCPCon North America, Mathew Goldsborough, de NimbleBrain, a montré une appli de tâches qui ressemble à n’importe quel tableau de bord SaaS, sauf que chaque panneau est une MCP App assemblée à l’exécution. Voici le côté hôte qu’il a construit, emplacements, facets, hooks de cycle de vie et tokens de thème partagés, lu à côté du code open source.
date: 2026-10-26
tags: MCP Apps, MCP, agents IA, web agentique
---

Mathew Goldsborough a affiché une appli de tâches à l’écran : une liste de choses à faire, une barre latérale, un panneau de chat calé à droite. On aurait dit n’importe quel tableau de bord SaaS. Puis il a annoncé à la salle que tout ce qu’on voyait était un serveur MCP ou une MCP App, assemblés à l’exécution. Son entreprise, NimbleBrain, construit l’hôte open source qui tient ces pièces ensemble, et son talk portait sur cet hôte : comment une appli trouve sa place à l’écran, affiche un compteur sans appel au modèle, apprend qu’elle vient d’être installée, et finit par ressembler à un seul produit.

C’était le dernier talk de la première journée d’AGNTCon + MCPCon North America, à San José, le 22 octobre. [Regardez-le sur le livestream à partir de 7:12:34](https://youtu.be/iNV5-YkPHW4?t=25954). Goldsborough est le fondateur et CEO de NimbleBrain, et ambassadeur de l’AAIF, le seul basé à Hawaï, ce qui lui permet d’ouvrir par un aloha. Ma [note sur le talk de Liad Yosef](/fr/notes/websites-lose-the-front-door.html) couvrait les MCP Apps côté client : un plan de salle qui revient dans votre assistant. Voici l’autre moitié, et je l’ai lue à côté du code et de la doc de NimbleBrain, qui sont publics.

<figure class="fd-fig nb nb-reveal" aria-labelledby="nb1-cap" data-desc="Une appli de tâches qui ressemble à un tableau de bord SaaS ordinaire : une barre latérale avec Conversations, Fichiers et Tâches, puis un groupe Applis avec À faire, Contacts et Mémoire ; une liste de tâches dans la zone principale avec un bouton Ajouter une tâche ; un panneau de chat à droite où l’utilisateur demande ce qui vient ensuite sur sa liste. La figure alterne entre ce qu’on voit et ce que c’est. Dans le second état, chaque zone porte une étiquette : la barre latérale, ce sont les vues de l’hôte puis les emplacements des applis ; la zone principale, la ressource ui:// du serveur de tâches dans une iframe dont les boutons sont des appels d’outils ; et le chat, la boucle de l’agent avec les skills chargées via MCP.">
<p class="ea-now nb-say2" aria-hidden="true"><span>Ce qu’on voit : une appli SaaS</span><span>Ce que c’est : des MCP Apps assemblées</span></p>
<div class="nb-win">
<p class="nb-win-bar"><i></i><i></i><i></i><span>Espace · Ops</span></p>
<div class="nb-win-body">
<div class="nb-side nb-zone">
<ul class="nb-side-list">
<li class="nb-side-h">Espace</li>
<li>Conversations</li>
<li>Fichiers</li>
<li>Tâches</li>
<li class="nb-side-h">Applis</li>
<li class="nb-side-on">À faire</li>
<li>Contacts</li>
<li>Mémoire</li>
</ul>
<p class="nb-tag">Vues de l’hôte, puis emplacements des applis</p>
</div>
<div class="nb-main nb-zone">
<p class="nb-main-h">À faire</p>
<ul class="nb-todo">
<li><i></i>Relire les brouillons sortants</li>
<li><i></i>Renouveler le domaine d’envoi</li>
<li class="nb-todo-done"><i></i>Synchroniser le CRM</li>
</ul>
<p class="nb-btn">+ Ajouter une tâche</p>
<p class="nb-tag">ui://todos dans une iframe. Les boutons sont des appels d’outils</p>
</div>
<div class="nb-chat nb-zone">
<p class="nb-chat-h">Chat</p>
<p class="nb-msg nb-msg-me">Qu’est-ce qui vient ensuite sur ma liste ?</p>
<p class="nb-msg">Relire les brouillons sortants.</p>
<p class="nb-tag">La boucle de l’agent, skills via MCP</p>
</div>
</div>
</div>
<figcaption id="nb1-cap">La révélation, redessinée. La disposition suit sa démo ; les tâches sont les miennes.</figcaption>
</figure>

## MCP est devenu un format de paquet

Son cadrage : chaque plateforme a eu son format de paquet, et une couche de composition autour. Les exécutables sur le bureau. Les fichiers WAR pour les applis web Java, « si vous avez eu la chance ou la malchance d’en faire ». Les paquets Debian pour Linux. Aujourd’hui MCP, qui selon lui n’a mûri que récemment assez pour porter tout ce dont un vrai produit a besoin.

Il compte trois pièces dans le paquet, et les appelle la triforce, pour les fans de Zelda. Les outils, les capacités qu’un serveur donne au modèle. Les MCP Apps, des interfaces riches et interactives livrées via MCP. Et les skills via MCP, le savoir-faire qui se charge avec les outils ; NimbleBrain avait bricolé sa propre version rudimentaire avant que l’extension existe. Ajoutez un `server.json` et deux ou trois extensions, et vous avez quelque chose qu’on peut livrer et composer.

La petite démo montre pourquoi ça compte. Demandez à un serveur de tâches « qu’est-ce qui vient ensuite sur ma liste ? » et un widget HTML s’affiche dans une iframe, pas un paragraphe de texte. Les skills arrivent avec. Chaque bouton sur lequel vous cliquez est un appel d’outil.

<figure class="fd-fig nb nb-pack" aria-labelledby="nb2-cap" data-desc="Les formats de paquet dans l’ordre : les exécutables pour le bureau, les fichiers WAR pour les applis web Java, les paquets Debian pour Linux, et maintenant MCP pour les agents et des applis entières. En dessous, les trois pièces que Goldsborough appelle la triforce, chacune dessinée comme un triangle du symbole de Zelda : les outils, ce que le serveur permet au modèle ; les MCP Apps, des interfaces affichées dans une iframe où chaque bouton est un appel d’outil ; et les skills, le savoir-faire chargé via MCP. Un server.json et deux ou trois extensions tiennent les trois.">
<ol class="nb-formats" aria-label="Formats de paquet">
<li><b>.exe</b><span>Le bureau</span></li>
<li><b>.war</b><span>Applis web Java</span></li>
<li><b>.deb</b><span>Linux</span></li>
<li class="nb-formats-now"><b>MCP</b><span>Les agents, et désormais des applis entières</span></li>
</ol>
<ol class="mp-trio">
<li><svg class="mp-grow-art nb-tri" viewBox="0 0 120 104" aria-hidden="true"><path class="nb-tri-on" d="M60,4 L88,52 L32,52 Z"/><path d="M32,52 L60,100 L4,100 Z"/><path d="M88,52 L116,100 L60,100 Z"/></svg><p class="mp-trio-n"><b>1</b>Outils</p><p class="mp-trio-what">Ce que le serveur permet au modèle</p></li>
<li><svg class="mp-grow-art nb-tri" viewBox="0 0 120 104" aria-hidden="true"><path d="M60,4 L88,52 L32,52 Z"/><path class="nb-tri-on" d="M32,52 L60,100 L4,100 Z"/><path d="M88,52 L116,100 L60,100 Z"/></svg><p class="mp-trio-n"><b>2</b>Applis</p><p class="mp-trio-what">Une interface dans une iframe ; chaque bouton est un appel d’outil</p></li>
<li><svg class="mp-grow-art nb-tri" viewBox="0 0 120 104" aria-hidden="true"><path d="M60,4 L88,52 L32,52 Z"/><path d="M32,52 L60,100 L4,100 Z"/><path class="nb-tri-on" d="M88,52 L116,100 L60,100 Z"/></svg><p class="mp-trio-n"><b>3</b>Skills</p><p class="mp-trio-what">Le savoir-faire, chargé via MCP</p></li>
</ol>
<p class="mp-strip"><b>Le paquet</b><span>Un server.json et deux ou trois extensions tiennent les trois.</span></p>
<figcaption id="nb2-cap">La diapo historique, puis sa triforce : un triangle par pièce.</figcaption>
</figure>

## Le chat, c’est le bac à sable de quelqu’un d’autre

Le hic, c’est que tout ça vit dans un chat. Le paquet est à vous, le serveur est à vous, mais le runtime ne vous appartient pas. Son image : vous entrez dans un bac à sable avec vos jouets, et quelqu’un vous dit comment jouer, où jouer, quelle hauteur font les murs et combien de sable vous avez.

Construire son propre hôte règle ça, et NimbleBrain l’a fait. Mais alors on réécrit la même plomberie à chaque fois. Et la spec elle-même est pensée pour un hôte qui parle à un serveur. Il n’y a pas grand-chose dedans pour un serveur MCP qui parle à un autre.

Il y a aussi un problème de public. Tout le monde fait du vibe coding dans Claude Code et construit son propre harness, a-t-il dit, mais les utilisateurs métier veulent toujours un tableau de bord, un bouton, un rapport, un graphique. Alors NimbleBrain a construit l’autre moitié.

<figure class="fd-fig nb nb-sandbox" aria-labelledby="nb3-cap" data-desc="Deux façons de faire tourner une MCP App aujourd’hui, chacune avec son coût. Dans un hôte de chat, c’est leur bac à sable : votre paquet et votre serveur, mais l’hôte décide où l’appli s’affiche, quelle taille elle a et ce qu’elle peut atteindre, et la spec couvre un hôte qui parle à un serveur, pas des serveurs qui se parlent. Dans votre propre hôte, le runtime est à vous, mais vous reconstruisez la même plomberie à chaque fois : identité, fichiers, conversations et routage des outils, encore, pour chaque produit. La réponse de NimbleBrain : construire cet hôte une fois, en open source.">
<div class="mp-two-row">
<div class="mp-two-card">
<p class="mp-two-who">Dans un hôte de chat</p>
<p class="mp-two-name">Leur bac à sable</p>
<svg class="mp-two-art" viewBox="0 0 300 100" aria-hidden="true"><rect class="nb-pit" x="40" y="14" width="220" height="76"/><rect class="nb-pit-sand" x="48" y="58" width="204" height="24"/><rect class="nb-pit-app" x="118" y="30" width="64" height="40"/><rect class="nb-pit-bar" x="118" y="30" width="64" height="8"/></svg>
<p class="mp-two-line">Votre paquet, votre serveur</p>
<p class="mp-two-line">Leurs murs : où elle s’affiche, sa taille, ce qu’elle atteint</p>
<p class="mp-two-line">Un hôte vers un serveur, rien de serveur à serveur</p>
</div>
<p class="mp-two-vs">ou</p>
<div class="mp-two-card">
<p class="mp-two-who">Dans votre propre hôte</p>
<p class="mp-two-name">La même plomberie, encore</p>
<svg class="mp-two-art" viewBox="0 0 300 100" aria-hidden="true"><g class="nb-pipe"><rect class="nb-pipe-app" x="38" y="12" width="56" height="20"/><rect x="38" y="38" width="56" height="12"/><rect x="38" y="56" width="56" height="12"/><rect x="38" y="74" width="56" height="12"/></g><g class="nb-pipe"><rect class="nb-pipe-app" x="122" y="12" width="56" height="20"/><rect x="122" y="38" width="56" height="12"/><rect x="122" y="56" width="56" height="12"/><rect x="122" y="74" width="56" height="12"/></g><g class="nb-pipe"><rect class="nb-pipe-app" x="206" y="12" width="56" height="20"/><rect x="206" y="38" width="56" height="12"/><rect x="206" y="56" width="56" height="12"/><rect x="206" y="74" width="56" height="12"/></g></svg>
<p class="mp-two-line">Le runtime est à vous</p>
<p class="mp-two-line">Identité, fichiers, conversations, routage : refaits par produit</p>
<p class="mp-two-line">Les utilisateurs métier ont toujours une boîte de chat</p>
</div>
</div>
<p class="mp-strip"><b>NimbleBrain</b><span>Construire l’hôte une fois, en open source, et laisser chaque serveur s’en servir.</span></p>
<figcaption id="nb3-cap">Ses deux problèmes côte à côte. Le bac à sable est son image ; les piles de plomberie sont les miennes.</figcaption>
</figure>

## L’autre moitié, c’est un hôte

NimbleBrain démarre sur un shell vide avec des points d’extension : des endroits de l’interface web où une MCP App peut s’afficher. Les applis intégrées viennent d’abord (conversations, fichiers, tâches planifiées), et les applis installées s’affichent à côté. Une appli ou deux cents. Cliquez sur l’une d’elles et sa MCP App s’affiche dans la zone principale. Vous n’êtes plus dans un chat, c’est un écran SaaS complet, et les boutons ne font pas d’appels REST. Ce sont des appels d’outils MCP. Sa thèse, dite avec un sourire : REST est mort, on peut tout faire via MCP.

Faites glisser le chat et la même appli tourne avec un agent. Vous voyez les appels d’outils et les skills qui se chargent, et l’agent peut agir. Ou laissez tomber l’interface web et branchez votre propre client MCP, parce que le runtime est aussi un serveur MCP.

Sous le shell se trouve ce qu’il appelle les primitives de l’hôte, ce dont chaque serveur a besoin et qu’il ne devrait pas construire lui-même : fichiers, conversations, routage des outils, identité. Tout se découpe en espaces de travail. La finance a Sage et Stripe, l’ingénierie a GitHub et Linear, et vous gardez le vôtre, privé. Les pièces s’échangent. Un client qui avait sa propre mémoire dans une base vectorielle l’a branchée, des gens de son équipe qui aiment Obsidian ont branché Obsidian, et Salesforce ou HubSpot, c’est un choix de connecteur.

Ça tourne partout : sur site, dans un cloud privé, sur leur service managé. Ça le place à côté des API internes et du Postgres que personne n’a touché depuis vingt ans. Ils mettent un petit broker MCP devant, et un seul runtime atteint à la fois le cloud public et le système historique.

Le dépôt confirme tout ça. Il est sous Apache 2.0, tourne sur Bun, est livré en deux images de conteneur, et `docker compose up` vous donne l’interface web sur le port 27246. La boucle de l’agent tourne jusqu’à ce que le modèle arrête d’appeler des outils, plafonnée à 10 itérations par défaut et 25 au maximum.

<figure class="fd-fig nb nb-stackfig" aria-labelledby="nb4-cap" data-desc="La pile de NimbleBrain en quatre couches. En haut, les portes d’entrée : le shell web, ou votre propre client MCP, comme un agent de code ou un harness. En dessous, les primitives de l’hôte que tous les serveurs partagent : fichiers, conversations, routage des outils, identité et espaces de travail. En dessous, la spec MCP : outils, ressources, applis et skills. Tout en bas, les serveurs : ceux de NimbleBrain (recherche web, mémoire, un CRM de contacts, une appli de prospection), les vôtres, n’importe quel serveur public, et un petit broker devant un Postgres de vingt ans. Les espaces de travail répartissent les connecteurs : la finance a Sage et Stripe, l’ingénierie GitHub et Linear, et un espace privé son propre GitHub. Ça tourne sur site, dans un cloud privé ou en service managé, avec n’importe quel modèle compatible OpenAI.">
<div class="nb-stack">
<div class="nb-layer nb-layer-door"><p class="nb-layer-k">Portes d’entrée</p><p class="nb-chips"><span>Le shell web</span><span>Votre client MCP</span><span>Agents de code, harness</span></p></div>
<div class="nb-layer nb-layer-host"><p class="nb-layer-k">Primitives de l’hôte</p><p class="nb-chips"><span>Fichiers</span><span>Conversations</span><span>Routage des outils</span><span>Identité</span><span>Espaces de travail</span></p></div>
<div class="nb-layer"><p class="nb-layer-k">La spec MCP</p><p class="nb-chips"><span>Outils</span><span>Ressources</span><span>Applis</span><span>Skills</span></p></div>
<div class="nb-layer nb-layer-srv"><p class="nb-layer-k">Serveurs</p><p class="nb-chips"><span>Les leurs : recherche, mémoire, CRM, prospection</span><span>Les vôtres</span><span>N’importe quel serveur public</span><span>Un broker vers un Postgres de 20 ans</span></p></div>
</div>
<ul class="nb-ws" aria-label="Espaces de travail">
<li><b>Finance</b><span>Sage · Stripe</span></li>
<li><b>Ingénierie</b><span>GitHub · Linear</span></li>
<li><b>Le mien</b><span>GitHub</span></li>
</ul>
<p class="mp-strip"><b>Tourne</b><span>Sur site, dans un cloud privé ou sur leur service managé, avec n’importe quel modèle compatible OpenAI.</span></p>
<figcaption id="nb4-cap">Sa diapo d’architecture en couches, avec l’exemple des espaces de travail en dessous.</figcaption>
</figure>

## Placements : l’appli choisit sa place

Alors comment une appli se retrouve-t-elle dans la barre latérale ? Elle le demande. NimbleBrain a défini une extension d’hôte sous son domaine inversé, `ai.nimblebrain/host`, et le manifeste d’une appli liste des placements : un slot, un libellé, une route, et la ressource `ui://` à afficher là.

```json
"ai.nimblebrain/host": {
  "placements": [
    { "slot": "sidebar.apps", "resourceUri": "ui://dashboard",
      "label": "Tasks", "route": "tasks" },
    { "slot": "settings", "resourceUri": "ui://settings" }
  ]
}
```

L’hôte lit ça au démarrage. Il ne sait rien du serveur, seulement l’interface. Quand vous cliquez sur Tasks, il charge cette ressource dans une iframe de la zone principale, à côté du chat ancré, à une URL comme `/w/ops/app/tasks`. La doc liste les slots : une entrée sous Applis dans la barre latérale, une page entière sans entrée de navigation, une section sur la page de réglages du connecteur, et un élément épinglé en bas du tiroir mobile. La barre latérale montre les quatre premières applis, puis un lien Tout voir. Si deux connecteurs réclament la même route, le premier la garde, et l’hôte journalise qui la détient.

Une entreprise qui veut une autre disposition peut changer le gabarit autour des slots et garder les mêmes applis.

<figure class="fd-fig nb nb-slots" aria-labelledby="nb5-cap" data-desc="Comment un placement devient un écran, en quatre étapes allumées tour à tour. Un : l’hôte lit le manifeste au démarrage, avec deux placements, une entrée sidebar.apps libellée Tasks avec la route tasks et la ressource ui://dashboard, et un placement settings avec ui://settings. Deux : l’entrée Tasks apparaît sous Applis dans la barre latérale. Trois : un clic ouvre /w/ops/app/tasks et l’hôte affiche ui://dashboard dans une iframe de la zone principale, à côté du chat ancré. Quatre : ui://settings s’affiche sur la page de réglages du connecteur.">
<p class="ea-now ea-say4" aria-hidden="true"><span>1 · L’hôte lit le manifeste</span><span>2 · Tasks obtient un slot</span><span>3 · Un clic affiche l’appli</span><span>4 · Les réglages ont leur page</span></p>
<div class="nb-slots-row">
<div class="nb-code nb-b1">
<span class="nb-cl">"placements": [</span>
<span class="nb-cl nb-b2">  { "slot": "sidebar.apps",</span>
<span class="nb-cl nb-b3">    "resourceUri": "ui://dashboard",</span>
<span class="nb-cl nb-b2">    "label": "Tasks",</span>
<span class="nb-cl nb-b3">    "route": "tasks" },</span>
<span class="nb-cl nb-b4">  { "slot": "settings",</span>
<span class="nb-cl nb-b4">    "resourceUri": "ui://settings" }</span>
<span class="nb-cl">]</span>
</div>
<div class="nb-shell">
<p class="nb-shell-url nb-b3">/w/ops/app/tasks</p>
<div class="nb-shell-body">
<ul class="nb-shell-side">
<li>Conversations</li>
<li>Fichiers</li>
<li class="nb-shell-h">Applis</li>
<li class="nb-shell-entry nb-b2">Tasks</li>
</ul>
<div class="nb-shell-main nb-b3"><b>ui://dashboard</b><span>Dans une iframe, à côté du chat ancré</span></div>
</div>
<div class="nb-shell-set nb-b4"><b>Réglages › Connecteurs › Tasks</b><span>ui://settings</span></div>
</div>
</div>
<figcaption id="nb5-cap">Le manifeste pilote le shell. Les noms de slots et la forme de la route viennent de la doc de NimbleBrain.</figcaption>
</figure>

## Facets : un compteur sans demander au modèle

C’est la partie que je volerais en premier. Un tableau de bord a besoin de chiffres : tâches bloquées, brouillons qui attendent une relecture. Vous pourriez demander au modèle. « Chaque fois que vous demandez au LLM, vous dépensez de plus en plus de tokens », a-t-il dit, alors les facets s’en passent.

Une facet est une ressource MCP ordinaire avec un marqueur dans son `_meta`. L’hôte liste les ressources du serveur, repère le marqueur, lit cette ressource, et récupère un seul nombre :

```json
{ "uri": "acme://facets/drafts-awaiting-review",
  "title": "Drafts awaiting review",
  "_meta": { "ai.nimblebrain/facets": { "level": "warning" } } }

→ resources/read →  { "count": 3 }
```

L’hôte l’affiche en une ligne, « 3 Drafts awaiting review », dans un panneau Needs attention sur la vue d’ensemble de l’espace, et un clic ouvre l’appli. La doc précise le reste. Le niveau (critical, warning ou info) fixe la couleur et l’ordre. Un zéro n’affiche rien. Une lecture plus lente que cinq secondes affiche la ligne comme indisponible sans casser les autres. Les compteurs sont mis en cache 60 secondes. Et le titre et le compteur sont traités comme des données non fiables qui n’atteignent jamais un modèle.

<figure class="fd-fig nb nb-facet" aria-labelledby="nb6-cap" data-desc="Le chemin d’une facet en quatre étapes allumées tour à tour. Un : l’hôte envoie resources/list au serveur. Deux : il repère une ressource dont le _meta porte le marqueur ai.nimblebrain/facets avec le niveau warning. Trois : il envoie resources/read et récupère un compteur de 3. Quatre : la vue d’ensemble de l’espace affiche la ligne 3 Drafts awaiting review. Sur le côté, le modèle est barré : il n’est pas sur ce chemin, donc aucun token n’est dépensé. En dessous, un panneau Needs attention avec une ligne par niveau, les titres tels que les serveurs les envoient : 2 Tasks blocked en critical, 3 Drafts awaiting review en warning, 1 Reply received en info.">
<p class="ea-now ea-say4" aria-hidden="true"><span>1 · Lister les ressources</span><span>2 · Repérer le marqueur</span><span>3 · Lire un nombre</span><span>4 · Afficher la ligne</span></p>
<div class="nb-facet-row">
<ol class="nb-flow">
<li class="nb-b1"><span class="nb-flow-who">Hôte → serveur</span><code>resources/list</code></li>
<li class="nb-b2"><span class="nb-flow-who">Une ressource porte</span><code>"ai.nimblebrain/facets": { "level": "warning" }</code></li>
<li class="nb-b3"><span class="nb-flow-who">Hôte → serveur</span><code>resources/read → { "count": 3 }</code></li>
<li class="nb-b4"><span class="nb-flow-who">Vue d’ensemble</span><b>3 Drafts awaiting review</b></li>
</ol>
<p class="nb-nomodel"><b>Le modèle</b><span>Pas sur ce chemin. Aucun token dépensé.</span></p>
</div>
<ul class="nb-attn" aria-label="Panneau Needs attention">
<li class="nb-attn-crit"><b>2</b>Tasks blocked<span>critical</span></li>
<li class="nb-attn-warn"><b>3</b>Drafts awaiting review<span>warning</span></li>
<li class="nb-attn-info"><b>1</b>Reply received<span>info</span></li>
</ul>
<figcaption id="nb6-cap">Les formes sur le fil et les trois niveaux viennent de la doc des facets ; les compteurs sont des exemples, les titres restent ceux qu’enverrait un serveur.</figcaption>
</figure>

## Lifecycle : l’installer achète un domaine

Certains serveurs doivent faire quelque chose ailleurs quand on les installe. L’appli de go-to-market de NimbleBrain achète des domaines et provisionne des boîtes mail dès que vous l’ajoutez à un espace de travail. C’est à deux ou trois couches du fournisseur, et aucun message MCP ne dit « tu viens d’être installé ».

L’extension lifecycle ne fait pas l’achat. C’est le crochet. Un serveur marque un outil pour chaque événement, `ready` et `removing`, et l’hôte l’appelle par un appel d’outil ordinaire. Installez-le, et le serveur entend « espace prêt », puis va acheter. Désinstallez-le, et `removing` arrive pendant que le serveur est encore joignable, pour qu’il libère ce qu’il a acheté.

C’est dans la doc que ça devient intéressant, parce qu’elle est honnête sur la livraison :

- `ready` arrive **au moins une fois**, et deux fois sur une installation neuve, dans un ordre incertain : une fois avec la raison `install`, une fois avec `resume` quand la connexion s’établit. Un redémarrage de l’hôte, c’est aussi `resume`, donc un redémarrage ne ressemble jamais à un nouveau client.
- Le handler doit donc être idempotent : « fais que ce soit vrai », un upsert, jamais un insert.
- `removing` est livré au mieux. Si l’hôte était éteint au moment de la désinstallation, il n’arrive jamais, donc un serveur qui laisse traîner un domaine sans lui compte sur un appel que rien ne garantit. Ajoutez votre propre balayage.
- Le handler doit seulement enregistrer l’intention et rendre la main. Un processus séparé, qui détient l’identifiant du fournisseur, fait l’achat lui-même, pour qu’un bug ou un appel d’outil injecté par prompt ne puisse pas atteindre cet identifiant.
- L’hôte cache les outils marqués à tous les autres, modèle compris. Un autre hôte pourrait ne pas le faire, donc la doc demande d’écrire chaque handler comme si n’importe quel appelant pouvait l’atteindre.

<figure class="fd-fig nb nb-life" aria-labelledby="nb7-cap" data-desc="L’extension lifecycle en trois moments allumés tour à tour. Installation : l’hôte appelle l’outil marqué ready avec la raison install, et le serveur enregistre que cet espace veut un domaine et des boîtes mail ; un processus séparé, qui détient l’identifiant du fournisseur, les achète. Redémarrage : l’hôte rappelle ready avec la raison resume, et le même upsert ne change rien. Désinstallation : l’hôte appelle l’outil marqué removing pendant que le serveur est encore joignable, et le serveur enregistre la libération ; comme removing est livré au mieux, un balayage maison rattrape les désinstallations dont il n’a jamais entendu parler. Le modèle ne voit jamais ces outils : l’hôte les cache à tous les autres appelants.">
<p class="ea-now nb-say3" aria-hidden="true"><span>Installation</span><span>Redémarrage</span><span>Désinstallation</span></p>
<ol class="nb-moments nb-cyc3">
<li><span class="nb-mo-k">Installation</span><code>ready · reason: "install"</code><span class="nb-mo-do">Enregistrer « veut un domaine et des boîtes mail ». Un processus séparé les achète.</span></li>
<li><span class="nb-mo-k">Redémarrage</span><code>ready · reason: "resume"</code><span class="nb-mo-do">Le même upsert. Rien de neuf à acheter.</span></li>
<li><span class="nb-mo-k">Désinstallation</span><code>removing</code><span class="nb-mo-do">Enregistrer la libération. Livré au mieux, donc un balayage rattrape le reste.</span></li>
</ol>
<p class="mp-strip"><b>Hôte seulement</b><span>Le modèle ne voit jamais ces outils. L’hôte les cache à tous les autres appelants.</span></p>
<figcaption id="nb7-cap">Son appli qui achète des domaines, passée au crible des règles de livraison de la doc lifecycle de NimbleBrain.</figcaption>
</figure>

## Dix applis devraient n’en faire qu’une

Il a montré deux applis faites par deux développeurs. L’un avait choisi le violet, l’autre le vert. Côte à côte, on aurait dit un patchwork.

La spec MCP Apps permet déjà à un hôte d’envoyer des variables de style à l’iframe. NimbleBrain les envoie, ajoute les tokens pour lesquels la spec n’a pas de clé, comme une couleur d’accent, et son SDK d’applis sous licence MIT, Synapse, les applique. Passez le shell en mode sombre et toutes les applis passent en sombre. « Combien de fois quelqu’un a essayé d’afficher une iframe dans un truc en mode sombre et a eu le mode clair ? » La même astuce donne des shells en marque blanche : les couleurs et les polices d’un client, c’est un kit de style, pas un rechargement d’appli.

Les toasts ont eu droit au même traitement. Le message de notification de la spec finit surtout en ligne de log, alors le bridge a une méthode `notify` avec un niveau et un titre, et le shell l’affiche comme ses propres toasts. Dans la doc, un succès reste cinq secondes, une info six, et les avertissements et les erreurs jusqu’à ce qu’on les ferme.

Basique, a-t-il reconnu. Mais c’est ce qui fait que dix ou douze serveurs donnent l’impression d’une seule appli plutôt que d’un assemblage cousu main.

<figure class="fd-fig nb nb-tokens" aria-labelledby="nb8-cap" data-desc="Deux applis faites par deux développeurs, une appli de tâches et une appli de prospection, en trois états qui se relaient. D’abord, deux looks : un développeur a pris le violet, l’autre le vert. Ensuite, l’hôte envoie ses tokens de thème et les deux applis prennent l’accent cobalt du shell. Enfin, le shell passe en mode sombre et les deux applis passent en sombre ensemble. En dessous, un toast envoyé avec la méthode notify du bridge, niveau success, titre Rapport exporté, description Enregistré dans Fichiers, affiché comme les toasts du shell.">
<p class="ea-now nb-say3" aria-hidden="true"><span>Deux développeurs, deux looks</span><span>Tokens de l’hôte : un seul look</span><span>Mode sombre : les deux basculent</span></p>
<div class="nb-apps">
<div class="nb-app nb-app-a"><p class="nb-app-h">À faire</p><p class="nb-app-row"><i></i>Relire les brouillons sortants</p><p class="nb-app-row"><i></i>Renouveler le domaine</p><p class="nb-app-btn">Ajouter une tâche</p></div>
<div class="nb-app nb-app-b"><p class="nb-app-h">Prospection</p><p class="nb-app-row"><i></i>3 brouillons à relire</p><p class="nb-app-row"><i></i>Boîte en préchauffe</p><p class="nb-app-btn">Envoyer le lot</p></div>
</div>
<p class="nb-toast"><span class="nb-toast-lv">success</span><b>Rapport exporté</b><span>Enregistré dans Fichiers.</span></p>
<figcaption id="nb8-cap">Applis schématiques. Le problème des deux couleurs et la bascule en mode sombre viennent du talk, le toast de la doc du bridge.</figcaption>
</figure>

## Ce que ça change pour mes cinquante applis

Je fais tourner une cinquantaine d’applis sur un Mac mini, chacune derrière [son propre port HTTPS Tailscale](/fr/notes/tailscale-serve-self-hosted-apps.html), chacune avec son front-end et son thème. Dix serveurs MCP vivent dans leurs propres dépôts à côté, la plupart de simples clients HTTP de l’API d’une appli, pour que mes agents puissent aussi utiliser les applis. Ce site en a un aussi : [mon portfolio derrière un serveur MCP](/fr/notes/portfolio-mcp-server.html).

Donc chaque appli que touchent mes agents a deux portes d’entrée, construites deux fois : une interface pour moi et un serveur MCP pour eux. Le paquet de Goldsborough, c’est une seule porte. Outils, interface et skills sont livrés ensemble, le shell ancre un chat à côté de chaque écran, et les boutons comme l’agent appellent les mêmes outils.

Hier, j’ai construit une page qui liste chaque appli par port et vérifie lesquelles répondent. C’est son catalogue sans la composition. Les facets s’y glisseraient parfaitement : un compteur de ce qui attend dans chaque appli, sur une seule page, sans modèle dans la boucle.

Deux réserves honnêtes. Je n’ai pas encore fait tourner NimbleBrain ; tout ce qui précède vient du talk, du dépôt et de la doc. Et les extensions sont celles de NimbleBrain, sous son propre préfixe. Un autre hôte les ignore, ce que la doc rend sûr par conception, mais alors on perd la fonctionnalité. Goldsborough a dit qu’ils voulaient contribuer là où le travail sur la spec se recoupe déjà.

Une réponse du Q&A mérite d’être gardée. À la question « pourquoi les MCP Apps et pas A2UI ? », il a répondu qu’ils avaient adopté les MCP Apps à leur lancement et n’avaient pas encore vu l’intérêt d’A2UI. Et ce n’est pas de l’UI générative. Vous concevez et construisez toujours l’appli. Ça ressemble à une appli CRUD, avec une couche d’agent fournie d’office.

<figure class="fd-fig nb nb-mine" aria-labelledby="nb9-cap" data-desc="Mon installation aujourd’hui face au paquet que décrit Goldsborough. Aujourd’hui, chaque appli a deux portes : un front-end avec son thème et son port, et un serveur MCP dans un autre dépôt qui appelle la même API, une cinquantaine d’applis et dix serveurs MCP en tout. En un paquet par appli : un server.json qui tient outils, interface et skills, un shell avec un chat et un look, et les boutons comme l’agent qui appellent les mêmes outils. Je n’ai pas encore fait tourner NimbleBrain ; ceci vient du talk et de la doc.">
<div class="mp-two-row">
<div class="mp-two-card">
<p class="mp-two-who">Mon installation aujourd’hui</p>
<p class="mp-two-name">Deux portes par appli</p>
<div class="nb-doors" aria-hidden="true"><span class="nb-mb">Front-end</span><span class="nb-mb nb-mb-mcp">Serveur MCP</span><span class="nb-mb nb-mb-api">L’API de l’appli</span></div>
<p class="mp-two-line">Un front-end par appli, son thème et son port</p>
<p class="mp-two-line">Un serveur MCP dans un autre dépôt, même API</p>
<p class="mp-two-line">Une cinquantaine d’applis, dix serveurs MCP</p>
</div>
<p class="mp-two-vs">vs</p>
<div class="mp-two-card mp-two-peter">
<p class="mp-two-who">Un paquet par appli</p>
<p class="mp-two-name">Une seule porte</p>
<div class="nb-pkg" aria-hidden="true"><p class="nb-pkg-k">server.json</p><p class="nb-chips"><span>Outils</span><span>Interface</span><span>Skills</span></p></div>
<p class="mp-two-line">Outils, interface et skills livrés ensemble</p>
<p class="mp-two-line">Les boutons et l’agent appellent les mêmes outils</p>
<p class="mp-two-line">Un shell, un chat, un look</p>
</div>
</div>
<p class="mp-strip"><b>Pas encore essayé</b><span>Je n’ai pas fait tourner NimbleBrain. C’est le talk, le dépôt et la doc.</span></p>
<figcaption id="nb9-cap">Mes chiffres face à son paquet. La carte de droite, c’est ce que j’obtiendrais, pas quelque chose que j’ai construit.</figcaption>
</figure>

## Essayez

- Regardez le talk : [le livestream d’AGNTCon + MCPCon à partir de 7:12:34](https://youtu.be/iNV5-YkPHW4?t=25954), environ 25 minutes avec le Q&A.
- Faites tourner l’hôte : [NimbleBrainInc/nimblebrain](https://github.com/NimbleBrainInc/nimblebrain), sous Apache 2.0. `docker compose up` avec une clé d’API Anthropic et un `instance.json` d’une ligne.
- Construisez une appli pour lui, ou pour n’importe quel hôte MCP Apps : [Synapse](https://github.com/NimbleBrainInc/synapse), sous MIT.
- Lisez les extensions : [placements](https://docs.nimblebrain.ai/host/placements/), [facets](https://docs.nimblebrain.ai/extensions/facets/), [lifecycle](https://docs.nimblebrain.ai/extensions/lifecycle/) et [le bridge des applis](https://docs.nimblebrain.ai/apps/bridge/).
- La spec en dessous : [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview).

Si vous avez livré une MCP App hors d’une fenêtre de chat, je veux voir l’hôte. Et si vous essayez les facets avant moi, dites-moi ce que vous avez compté.
