@vantageos/backend-doctor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +88 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +94 -0
  4. package/dist/contract.d.ts +18 -0
  5. package/dist/contract.js +29 -0
  6. package/dist/coverage-cli.d.ts +2 -0
  7. package/dist/coverage-cli.js +17 -0
  8. package/dist/coverage.d.ts +32 -0
  9. package/dist/coverage.js +77 -0
  10. package/dist/detectors/oracle-axis1.d.ts +86 -0
  11. package/dist/detectors/oracle-axis1.js +221 -0
  12. package/dist/detectors/oracle-axis2.d.ts +22 -0
  13. package/dist/detectors/oracle-axis2.js +149 -0
  14. package/dist/detectors/predicates.d.ts +19 -0
  15. package/dist/detectors/predicates.js +334 -0
  16. package/dist/detectors/shared.d.ts +56 -0
  17. package/dist/detectors/shared.js +103 -0
  18. package/dist/detectors/source/_scan.d.ts +43 -0
  19. package/dist/detectors/source/_scan.js +82 -0
  20. package/dist/detectors/source/backfill.d.ts +27 -0
  21. package/dist/detectors/source/backfill.js +211 -0
  22. package/dist/detectors/source/contract.d.ts +33 -0
  23. package/dist/detectors/source/contract.js +385 -0
  24. package/dist/detectors/source/identity.d.ts +47 -0
  25. package/dist/detectors/source/identity.js +410 -0
  26. package/dist/detectors/source/jobs.d.ts +10 -0
  27. package/dist/detectors/source/jobs.js +209 -0
  28. package/dist/detectors/source/lifecycle.d.ts +6 -0
  29. package/dist/detectors/source/lifecycle.js +147 -0
  30. package/dist/detectors/source/readbudget.d.ts +6 -0
  31. package/dist/detectors/source/readbudget.js +133 -0
  32. package/dist/detectors/source/refusal.d.ts +6 -0
  33. package/dist/detectors/source/refusal.js +125 -0
  34. package/dist/detectors/source/security.d.ts +37 -0
  35. package/dist/detectors/source/security.js +392 -0
  36. package/dist/detectors/source/writesafety.d.ts +14 -0
  37. package/dist/detectors/source/writesafety.js +345 -0
  38. package/dist/detectors/standard/_doc.d.ts +66 -0
  39. package/dist/detectors/standard/_doc.js +152 -0
  40. package/dist/detectors/standard/r41-frontier.d.ts +58 -0
  41. package/dist/detectors/standard/r41-frontier.js +176 -0
  42. package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
  43. package/dist/detectors/standard/r46-returns-validator.js +474 -0
  44. package/dist/detectors/standard/standard-doc.d.ts +12 -0
  45. package/dist/detectors/standard/standard-doc.js +271 -0
  46. package/dist/detectors/structural.d.ts +14 -0
  47. package/dist/detectors/structural.js +83 -0
  48. package/dist/engine-types.d.ts +10 -0
  49. package/dist/engine-types.js +1 -0
  50. package/dist/not-checkable.d.ts +22 -0
  51. package/dist/not-checkable.js +37 -0
  52. package/dist/oracle.d.ts +33 -0
  53. package/dist/oracle.js +133 -0
  54. package/dist/registry.d.ts +11 -0
  55. package/dist/registry.js +140 -0
  56. package/dist/report.d.ts +35 -0
  57. package/dist/report.js +138 -0
  58. package/dist/severity.d.ts +30 -0
  59. package/dist/severity.js +121 -0
  60. package/dist/tally.d.ts +57 -0
  61. package/dist/tally.js +152 -0
  62. package/dist/trigger.d.ts +47 -0
  63. package/dist/trigger.js +143 -0
  64. package/fixtures/backend-standard-51ec7af1.md +648 -0
  65. package/fixtures/backend-standard-a9d7877b.md +629 -0
  66. package/fixtures/mcp-app-standard-a74a042b.md +279 -0
  67. package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
  68. package/fixtures/mcp-server-standard-a74a042b.md +434 -0
  69. package/fixtures/mcp-standard-a74a042b.md +354 -0
  70. package/fixtures/vp-1200-green/.pin +1 -0
  71. package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
  72. package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
  73. package/fixtures/vp-1200-red/.pin +1 -0
  74. package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
  75. package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
  76. package/fixtures/vp-by-tool-day158.csv +190 -0
  77. package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
  78. package/fixtures/vp-convex/.pin +1 -0
  79. package/fixtures/vp-convex/bun.lock +783 -0
  80. package/fixtures/vp-convex/convex/auth.config.ts +8 -0
  81. package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
  82. package/fixtures/vp-convex/convex/convex.config.ts +14 -0
  83. package/fixtures/vp-convex/convex/credentials.ts +591 -0
  84. package/fixtures/vp-convex/convex/crons.ts +72 -0
  85. package/fixtures/vp-convex/convex/dashboard.ts +199 -0
  86. package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
  87. package/fixtures/vp-convex/convex/issues.ts +544 -0
  88. package/fixtures/vp-convex/convex/kb.ts +305 -0
  89. package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
  90. package/fixtures/vp-convex/convex/kbShared.ts +56 -0
  91. package/fixtures/vp-convex/convex/licenses.ts +443 -0
  92. package/fixtures/vp-convex/convex/memories.ts +406 -0
  93. package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
  94. package/fixtures/vp-convex/convex/messages.ts +971 -0
  95. package/fixtures/vp-convex/convex/migrations.ts +239 -0
  96. package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
  97. package/fixtures/vp-convex/convex/oauth.ts +1608 -0
  98. package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
  99. package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
  100. package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
  101. package/fixtures/vp-convex/convex/schema.ts +1229 -0
  102. package/fixtures/vp-convex/convex/tasks.ts +2782 -0
  103. package/fixtures/vp-r31-green/.pin +1 -0
  104. package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
  105. package/fixtures/vp-r31-red/.pin +1 -0
  106. package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
  107. package/fixtures/vr-by-tool-day159.csv +89 -0
  108. package/package.json +39 -0
@@ -0,0 +1,279 @@
1
+ # MCP App Standard — Complément App
2
+
3
+ Version : v2.2 — 2026-08-16. Aligné standard GA MCP Apps `2026-07-28`. Table de détecteurs + table de source A-1..A-7.
4
+ Owner : gamma
5
+ Status : en vigueur — non déclaré fini tant que l'opérateur ne l'a pas dit.
6
+ Références parent : `resources/references/mcp-standard.md` (le tronc), `resources/references/mcp-server-standard.md` (le complément serveur).
7
+ Source benchmark : `analysis/mcp-apps-benchmark-2026-04-26.md`.
8
+
9
+ Source canonique : `resources/references/mcp-app-standard.md`. **Ce document est un complément.** Il ne porte QUE ce qui est propre à une MCP App — repli markdown, bundle single-file, bac à sable, accessibilité, budgets de performance, clé UI GA. Tout le reste — bilinguisme (`C-1`), SemVer (`C-3`), suite d'évaluation dont le cas dégradé (`C-5`), obtention vérifiable de l'artefact (`C-7`) — vit dans le tronc et ne se réécrit jamais ici.
10
+
11
+ **Ce que gouverne ce document** : une MCP App, c'est-à-dire un serveur MCP qui retourne, en plus du contenu texte, une UI interactive rendue dans le client. **Où passe la frontière** : une App EST un serveur — elle tient le tronc, le complément serveur ENTIER (règles `S-*` : transport, protocole, découverte, autorisation, sécurité), ET ce complément. Ce document ne porte que la couche visuelle qui s'ajoute au serveur.
12
+
13
+ ---
14
+
15
+ ## Règles propres à l'App (`A-1` à `A-6`)
16
+
17
+ 1. **`A-1` — Repli markdown obligatoire (dégradation gracieuse — graceful degradation)** dans `content[]` à côté de la ressource `ui://`. Un client sans couche UI reçoit une réponse texte équivalente et significative, sans erreur. Aucune exception. *(C'est aussi ce que doit un serveur pur qui retourne une App : la règle vit ici, le complément serveur y renvoie.)*
18
+ 2. **`A-2` — Bundle single-file** via `vite-plugin-singlefile` (ou équivalent) : HTML + JS + CSS dans un seul fichier servi via `ui://`. Aucun fetch externe au runtime UI — tout est passé en props ou demandé via le bridge.
19
+ 3. **`A-3` — Bac à sable respecté.** Pas de `window.parent.*`, pas de `localStorage` direct : tout passe par le bridge. Le modèle double-iframe sandbox-proxy (ci-dessous) applique une CSP dérivée par ressource — strictement plus strict, il s'ajoute à cette règle.
20
+ 4. **`A-4` — WCAG AA** validé en CI via `axe-core`, 0 violation bloquante avant release. Sémantique HTML native, ARIA sur tout interactif non-évident, contraste AA, navigation clavier, `prefers-reduced-motion`.
21
+ 5. **`A-5` — Budgets de performance respectés** : bundle < 250 KB gzipped, initial render < 1s LAN, roundtrip bridge < 500ms p95, mémoire steady-state < 50 MB. La moitié statique (taille du bundle) est mesurable sur l'artefact ; render/roundtrip demandent une sonde runtime.
22
+ 6. **`A-6` — Clé UI GA obligatoire (gate D6).** La ressource UI est liée par la clé imbriquée `_meta.ui.resourceUri` via `registerAppTool` (`@modelcontextprotocol/ext-apps`). Les formes dépréciées `_meta["ui/resourceUri"]` (plate) et l'id communautaire `io.modelcontextprotocol/ui` sont refusées en écriture. Le détecteur mcp-doctor D6 gate cette conformité en CI : 0 App publiée sans la clé GA.
23
+
24
+ ### Seconde source — pratique établie MCP
25
+
26
+ 7. **`A-7` — Consentement humain avant un outil destructif.** L'App/hôte est le point d'application du contrôle humain : elle gate tout appel d'outil dont les annotations indiquent un comportement destructif ou non idempotent — OU qui ne porte aucune annotation — derrière une confirmation utilisateur explicite, jamais une auto-exécution silencieuse. Les annotations d'une source non épinglée sont non fiables (`C-15`) : une App ne conditionne pas cette confirmation sur `destructiveHint=false` seul. *(Source : spec MCP 2025-06-18 `server/tools` §User Interaction Model — « there SHOULD always be a human in the loop with the ability to deny tool invocations » ; SHOULD.)*
27
+
28
+ ### Traçabilité — règles App → objectifs
29
+
30
+ | Règle | production | entreprise | sécurité | jetons | cloisonnement |
31
+ |---|:---:|:---:|:---:|:---:|:---:|
32
+ | A-1 repli markdown obligatoire | ✓ | ✓ | | ✓ | |
33
+ | A-2 bundle single-file, aucun fetch externe | ✓ | | ✓ | | |
34
+ | A-3 bac à sable respecté | | | ✓ | | ✓ |
35
+ | A-4 WCAG AA, 0 violation bloquante | ✓ | ✓ | | | |
36
+ | A-5 budgets de performance | ✓ | | | ✓ | |
37
+ | A-6 clé UI GA obligatoire | ✓ | ✓ | | | |
38
+ | A-7 consentement avant outil destructif | | ✓ | ✓ | | |
39
+
40
+ **Objectif faiblement servi côté App : multi-tenant.** Seul `A-3` y touche, par la bande (le bac à sable empêche une App de lire l'état d'une autre). Rien n'oblige une App à déclarer de quelle organisation proviennent les données qu'elle rend. Le cloisonnement est garanti en amont, par le serveur (`S-5`) et son backend (`R-11`), et l'App en hérite — c'est la frontière du tronc §7, pas une lacune de ce complément.
41
+
42
+ ### Spécifications de détecteur (règles App `A-1`..`A-7`)
43
+
44
+ L'instrument reste gelé ; on spécifie ce qu'un détecteur lira. La colonne Genre porte le jeu fermé du tronc §5.2 — **`statique`** · **`dynamique`** · **`gate`**, seuls ou combinés par `+` — et rien d'autre (pour une App, `statique` lit le bundle/le code UI ; `dynamique` observe l'App servie/rendue ; `gate` = vérification au point de livraison, CI). La part non mécanique se lit dans les colonnes Rouge/Vert, jamais dans Genre.
45
+
46
+ | Règle | Le check lit | Rouge | Vert | Genre |
47
+ |---|---|---|---|---|
48
+ | A-1 | le `content[]` servi à côté de la ressource `ui://` | une ressource `ui://` sans repli texte équivalent | `ui://` + repli markdown significatif présents | dynamique |
49
+ | A-2 | le bundle servi | fetch externe au runtime UI, ou bundle multi-fichiers | HTML+JS+CSS en un seul fichier, aucun fetch externe | statique |
50
+ | A-3 | le code UI | `window.parent.*` ou `localStorage` direct | tout passe par le bridge, CSP double-iframe respectée | statique |
51
+ | A-4 | le résultat `axe-core` en CI | une violation WCAG AA bloquante | 0 violation bloquante | statique + gate |
52
+ | A-5 | la taille du bundle (statique) + render/roundtrip (sonde) | bundle > 250 KB gz, render > 1s, roundtrip p95 > 500ms, mémoire > 50 MB | sous tous les budgets | statique + dynamique |
53
+ | A-6 | la clé imbriquée `_meta.ui.resourceUri` | forme plate `_meta["ui/resourceUri"]` ou id communautaire `io.modelcontextprotocol/ui` en écriture | clé GA imbriquée via `registerAppTool` | statique + gate |
54
+ | A-7 | la logique de gate de l'App/hôte | un outil destructif ou non annoté auto-exécuté sans confirmation | confirmation humaine explicite avant un tel appel, jamais conditionnée sur `destructiveHint=false` seul | statique |
55
+
56
+ ### Traçabilité — règles App → source
57
+
58
+ Chaque règle nomme sa source, une cellule non vide par règle.
59
+
60
+ | Règle | Source |
61
+ |---|---|
62
+ | A-1..A-6 | source une — nos produits + standard GA MCP Apps `2026-07-28` (clé `_meta.ui.resourceUri`, bundle single-file, bac à sable double-iframe) |
63
+ | A-7 | pratique — spec MCP 2025-06-18 `server/tools` §User Interaction Model |
64
+
65
+ ---
66
+
67
+ ## Principe directeur
68
+
69
+ Une MCP App retourne une UI interactive rendue dans le client (Claude web/desktop, ChatGPT, VS Code Insiders, Goose, Postman, MCPJam). Le standard GA (`2026-07-28`) lie la ressource UI à son tool via la clé imbriquée `_meta.ui.resourceUri`, écrite par `registerAppTool`. Les formes plates et l'ancien id communautaire sont DÉPRÉCIÉS : tolérés en lecture pour rétrocompat, refusés en écriture (`A-6`). C'est un produit visuel — la qualité d'exécution UI distingue un serveur moyen d'un serveur qui convertit.
70
+
71
+ ---
72
+
73
+ ## File structure
74
+
75
+ ```
76
+ mcp-app-name/
77
+ ├── .claude-plugin/plugin.json
78
+ ├── src/
79
+ │ ├── index.ts # MCP server entry — register tools + UI resources
80
+ │ ├── server.ts
81
+ │ ├── tools/ # Tools qui retournent ui:// resources
82
+ │ ├── ui/
83
+ │ │ ├── components/ # React / Vue / Svelte
84
+ │ │ ├── styles/
85
+ │ │ ├── i18n/ # en.json + fr.json (tronc §5.1)
86
+ │ │ └── entry.tsx
87
+ │ ├── schemas/
88
+ │ └── lib/
89
+ ├── tests/ # unit / component / integration / a11y / visual
90
+ ├── evals/evals.json # (tronc §5.5, cas "client sans support UI" — C-5)
91
+ ├── docs/ # README.md, README.fr.md, ui-spec.md, architecture.md
92
+ ├── vite.config.ts # Bundle single-file (A-2)
93
+ ├── package.json
94
+ ├── mcp.json
95
+ └── CHANGELOG.md
96
+ ```
97
+
98
+ Seuls `src/ui/`, `src/tools/*render*`, et `vite.config.ts` sont propres à l'App ; le socle serveur (`server.ts`, `mcp.json`, transport, protocole) obéit au complément serveur, et l'arborescence commune au tronc.
99
+
100
+ ---
101
+
102
+ ## UI components supported
103
+
104
+ | Catégorie | Composants ElPi Corp standardisés |
105
+ |---|---|
106
+ | **Display** | Cards, KPI tiles, Tables interactives, Lists |
107
+ | **Charts** | Mermaid, Recharts (line/bar/pie/scatter), Heatmaps |
108
+ | **Visualisations 3D** | Three.js, CesiumJS (cas vantage-radar) |
109
+ | **Forms** | Inputs, selects, multi-step wizards, file upload |
110
+ | **Media** | PDF viewer, image viewer, audio player, video player |
111
+ | **Layout** | Tabs, accordions, modals, drawers |
112
+ | **Feedback** | Toast, progress bars, loading states |
113
+
114
+ Frameworks autorisés : React (default), Vue, Svelte, Preact, Solid, vanilla. Pas d'Angular (lourdeur bundle). Stack default : **React 19 + Tailwind v4 + lit-ui**.
115
+
116
+ ---
117
+
118
+ ## Component definitions
119
+
120
+ ```ts
121
+ // src/tools/render-dashboard.ts
122
+ import { z } from "zod";
123
+ import { renderUiResource } from "@modelcontextprotocol/ext-apps/server";
124
+
125
+ export const inputSchema = z.object({
126
+ scoreRange: z.tuple([z.number(), z.number()]).default([0, 100]),
127
+ locale: z.enum(["en", "fr"]).default("en")
128
+ });
129
+
130
+ export const tool = {
131
+ name: "render_dashboard",
132
+ description: "Render the vantage-radar 9-angles dashboard. Use this whenever the user asks for a 'dashboard', 'visual report', or 'matrix view' — even if they don't say 'render' explicitly.",
133
+ inputSchema,
134
+ handler: async (input) => {
135
+ const data = await loadDashboardData(input);
136
+ const fallbackMarkdown = renderMarkdownFallback(data); // A-1
137
+ const ui = await renderUiResource({ bundlePath: "dist/ui/dashboard.html", props: { data, locale: input.locale } });
138
+ return { content: [ { type: "text", text: fallbackMarkdown }, { type: "resource", resource: ui } ] };
139
+ }
140
+ };
141
+ ```
142
+
143
+ Le nommage, les schémas, l'écriture de la description bilingue obéissent au tronc (`C-2`, `C-4`, §5.7). Règles propres à l'App : bundle = single HTML file (`A-2`) ; props via `window.__MCP_PROPS__` ; aucun fetch externe au runtime.
144
+
145
+ ### Clé UI GA imbriquée (`A-6`)
146
+
147
+ `renderUiResource()` attache la forme **nested** `_meta.ui` — jamais la clé plate `_meta["ui/resourceUri"]`, deprecated :
148
+
149
+ > « **Deprecation notice:** The flat `_meta["ui/resourceUri"]` format is deprecated. Use `_meta.ui.resourceUri` instead. The deprecated format will be removed before GA. » (apps.mdx line 347)
150
+
151
+ ```ts
152
+ // resources/list handler — forme nested obligatoire
153
+ {
154
+ uri: `ui://vp/v1/${primitive}`,
155
+ name: primitive,
156
+ mimeType: "text/html;profile=mcp-app",
157
+ _meta: { ui: DEFAULT_UI_META } // ← nested, jamais _meta["ui/resourceUri"]
158
+ }
159
+ ```
160
+
161
+ Toute App qui n'utilise pas `renderUiResource()` reproduit cette forme nested à l'identique — un server qui émet la clé plate est sur un chemin de suppression avant la GA.
162
+
163
+ ---
164
+
165
+ ## Render lifecycle
166
+
167
+ 1. **Déclaration** — le tool déclare une ressource `ui://` et la lie via `_meta.ui.resourceUri` (`A-6`).
168
+ 2. **Exécution** — le LLM appelle le tool.
169
+ 3. **Rendu** — le client récupère et affiche la ressource dans un iframe sandboxé ; communication bidirectionnelle via `postMessage` / JSON-RPC.
170
+
171
+ Lors du handshake `initialize`, le serveur déclare `extensions: ["io.modelcontextprotocol/ui"]` (voir `mcp-server-standard.md`, `mcp.json`). Si le client ne supporte pas l'extension, le serveur retourne uniquement `content[type=text]` (repli markdown, `A-1`). Aucune erreur côté client.
172
+
173
+ ---
174
+
175
+ ## Double-iframe sandbox-proxy CSP model
176
+
177
+ Spec source : `specification/2026-01-26/apps.mdx`, `modelcontextprotocol/ext-apps` (lines 109-163, 413-485, 1233-1325). Le rendu passe par **deux iframes imbriquées**.
178
+
179
+ Une iframe **outer** (« sandbox-proxy ») négocie le handshake avec le host, puis charge une iframe **inner** sandboxée qui porte le contenu HTML du `ui://`. La CSP appliquée à l'inner est dérivée, par ressource, des métadonnées déclarées par le tool — elle n'est pas statique.
180
+
181
+ | Métadonnée resource | Directive CSP inner iframe |
182
+ |---|---|
183
+ | `csp.connectDomains` | `connect-src` |
184
+ | `csp.resourceDomains` | `img-src`, `script-src`, `style-src`, `font-src`, `media-src` |
185
+ | (domaines autorisés à s'embarquer) | `frame-src` |
186
+ | `csp.baseUri` | `base-uri` |
187
+
188
+ ### Règles
189
+
190
+ - Le host MUST enforcer la CSP dérivée (apps.mdx line 274).
191
+ - Le host SHOULD logger la config CSP appliquée pour audit (apps.mdx line 287).
192
+ - Toute App déclarant des domaines externes (images, fonts, scripts tiers) les déclare explicitement dans `csp.connectDomains` / `csp.resourceDomains` — un domaine non déclaré est bloqué par construction.
193
+ - Ce modèle est strictement plus strict que le bac à sable simple de `A-3` ; il s'y ajoute.
194
+
195
+ ---
196
+
197
+ ## Interaction model + state
198
+
199
+ Le bridge `@modelcontextprotocol/ext-apps/app-bridge` expose une API JSON-RPC bidirectionnelle.
200
+
201
+ ```ts
202
+ import { useBridge } from "@modelcontextprotocol/ext-apps/react";
203
+ function ScoreSlider() {
204
+ const bridge = useBridge();
205
+ const onChange = (value: number) => bridge.callTool("update_threshold", { value });
206
+ return <input type="range" onChange={/* … */} />;
207
+ }
208
+ ```
209
+
210
+ - Toute interaction qui mute l'état serveur → `bridge.callTool`. Toute mutation locale (UI state, animations) → state local.
211
+ - Pas de side-effect direct (fetch externe, localStorage) sans passer par le bridge (`A-3`).
212
+ - Latence cible round-trip bridge : < 500ms p95 (`A-5`).
213
+
214
+ | Niveau d'état | Lieu | Use case |
215
+ |---|---|---|
216
+ | **Server state** | MCP server | Source of truth métier |
217
+ | **UI state** | React/Vue local | Animations, hover, focus, accordions |
218
+ | **Bridge cache** | `@modelcontextprotocol/ext-apps` | Réponses tool récentes, dédoublonnage |
219
+
220
+ Le serveur EST le store : pas de Redux/Zustand pour l'état métier, pas de duplication server↔UI. Optimistic UI autorisé pour les mutations à faible enjeu.
221
+
222
+ ---
223
+
224
+ ## Accessibility (WCAG AA — `A-4`)
225
+
226
+ - Sémantique HTML native (button, nav, section, table) — pas de `<div onClick>`.
227
+ - ARIA labels obligatoires sur tout interactif non-évident.
228
+ - Contraste AA (4.5:1 texte normal, 3:1 texte large).
229
+ - Navigation clavier complète, focus visible.
230
+ - Screen reader : test minimum VoiceOver (macOS) ou NVDA (Windows) sur le flux principal.
231
+ - `prefers-reduced-motion` respecté.
232
+ - `axe-core` en CI, 0 violation bloquante.
233
+
234
+ ---
235
+
236
+ ## Performance constraints (`A-5`)
237
+
238
+ | Métrique | Cible | Mesure |
239
+ |---|---|---|
240
+ | Bundle size (gzipped) | < 250 KB | `vite build` size-limit |
241
+ | Initial render | < 1s LAN | Playwright trace |
242
+ | Tool roundtrip via bridge | < 500ms p95 | Tracing custom |
243
+ | Memory steady-state | < 50 MB | DevTools profiler |
244
+
245
+ Code splitting par composant lourd, lazy load des charts/3D/éditeurs, target ES2022, images AVIF/WebP ou SVG inline.
246
+
247
+ ---
248
+
249
+ ## Testing (spécifique App)
250
+
251
+ Au-delà des tests communs (tronc §5.2) :
252
+
253
+ - **Component** : Vitest + Testing Library, ≥ 1 test par composant exposé, ≥ 1 test par locale.
254
+ - **Integration (MCP roundtrip)** : spawn server stdio + client mock + render bundle dans Playwright headless. Assert : tool appelé → resource `ui://` retournée → composant rendu → interaction → callback tool reçu.
255
+ - **Accessibility** : `axe-core` en CI, 0 violation bloquante (`A-4`) ; screen reader manuel par release majeure.
256
+ - **Visual regression** : Playwright screenshot diffs, threshold 1 % pixel diff.
257
+ - **Eval suite** (`C-5`, tronc) : les 3 cas incluent le cas dégradé « client sans support UI → fallback markdown » (`A-1`) et le cas erreur « data manquante → état d'erreur UI lisible ».
258
+
259
+ ---
260
+
261
+ ## Examples — Elastic case study (référence du benchmark)
262
+
263
+ Elastic a publié 3 MCP Apps (Security, Observability, Search) — premier cas production-grade documenté.
264
+
265
+ - **Security** : triage alertes SOC, dashboards inline avec entités cliquables, timelines. Node.js server, React UI, single-file bundle.
266
+ - **Observability** : diagnostic SRE, graphes de topologie interactifs, drill-down par service. Reuse ElPi Corp : drill-down hiérarchique pour vantage-radar (9 angles → score → sources → preuves).
267
+ - **Search** : création de dashboards en langage naturel, éditeur drag-and-drop. Reuse : pattern « natural language to UI ».
268
+
269
+ **Patterns à répliquer** : bundle single-file, fallback markdown systématique, drill-down hiérarchique, entités cliquables qui re-trigger des tools. **Anti-patterns** : setup multi-étapes, mauvaise traduction auto FR, UI sans fallback texte, monétisation prématurée.
270
+
271
+ ---
272
+
273
+ ## ElPi Corp additions (au-delà de la spec MCP Apps)
274
+
275
+ - **SELLABLE AS** dans le README — chaque App cible un nom de plugin marketplace.
276
+ - **VantageRegistry registration** : `register_component` category `mcp-app` à chaque release (`C-14`, tronc §5.6).
277
+ - **Stack canonique** : React 19 + Tailwind v4 + lit-ui + Recharts + axe-core. Tout fork justifie son écart.
278
+ - **Bilingual i18n shared package** : `@elpi/mcp-app-i18n` pour partager les strings entre apps du même domaine.
279
+ - **Premier candidat pilote** : vantage-radar Phase 2 (matrice 9 angles + scores en MCP App). Économie estimée vs web app séparée : 3-6 semaines de dev frontend.
@@ -0,0 +1,259 @@
1
+ # MCP Extension Standard — Complément extension
2
+
3
+ Version : v2.1 — 2026-08-16. Protocole visé `2026-07-28`, plancher `2025-06-18`. Table de détecteurs + table de source E-1..E-6.
4
+ Owner : gamma
5
+ Status : en vigueur — non déclaré fini tant que l'opérateur ne l'a pas dit.
6
+ Référence parent : `resources/references/mcp-standard.md` (le tronc).
7
+
8
+ Source canonique : `resources/references/mcp-extension-standard.md`. **Ce document est un complément.** Il ne porte QUE ce qui est propre à une extension MCP — identifier, dégradation gracieuse, unicité de type, hooks de cycle de vie, absence d'effet global, timeouts. Tout le reste — bilinguisme (`C-1`), SemVer (`C-3`), suite d'évaluation dont le cas dégradé (`C-5`), et les déclarations de la surface servie (`C-8` à `C-11`) — vit dans le tronc et ne se réécrit jamais ici.
9
+
10
+ **Ce que gouverne ce document** : une extension MCP, c'est-à-dire un composant additionnel qui étend un serveur existant sans en modifier le code source — middleware, tool extension, ou transport adapter. **Où passe la frontière** : le serveur hôte porte le protocole (`server/discover`, `resultType`, autorisation — voir `mcp-server-standard.md`) ; l'extension ne fait que respecter le contrat du résultat qu'elle intercepte et ne casse jamais le serveur qui l'embarque.
11
+
12
+ ---
13
+
14
+ ## Règles propres à l'extension (`E-1` à `E-6`)
15
+
16
+ 1. **`E-1` — Identifier reverse-DNS unique.** Préfixe `com.elpi/`, `com.<bu>/`, ou identifier officiel MCP (`io.modelcontextprotocol/*`). Collision = refus de publication.
17
+ 2. **`E-2` — Dégradation gracieuse toujours `true`.** Une extension ne casse JAMAIS le serveur hôte. Un client qui ne déclare pas l'extension voit le serveur fonctionner normalement. `graceful_degradation: true` dans `extension.json`, sans exception.
18
+ 3. **`E-3` — Une extension = un type** (middleware XOR tool-extension XOR transport). Pas de mix. `extension.json.type` a une cardinalité de 1.
19
+ 4. **`E-4` — Hooks déclarés = hooks implémentés.** Le champ `lifecycle` de `extension.json` correspond exactement aux fonctions de hook exportées. Pas de hook fantôme, pas de hook implémenté non déclaré.
20
+ 5. **`E-5` — Aucune mutation de l'identifier d'une autre extension, aucun effet global.** Sandboxé par contexte. Une extension ne modifie ni l'état ni l'identité d'une autre.
21
+ 6. **`E-6` — Timeouts respectés.** Un hook qui dépasse son timeout est skipped avec un warning log, jamais un blocker du serveur hôte. Défauts : `init` 5s, `beforeCall`/`afterCall` 1s, `onError` 500ms — surchargés via `extension.json.timeouts`.
22
+
23
+ ### Traçabilité — règles extension → objectifs
24
+
25
+ | Règle | production | entreprise | sécurité | jetons | cloisonnement |
26
+ |---|:---:|:---:|:---:|:---:|:---:|
27
+ | E-1 identifier reverse-DNS unique | ✓ | ✓ | | | |
28
+ | E-2 dégradation gracieuse toujours vraie | ✓ | | | | |
29
+ | E-3 une extension = un type | | ✓ | | | |
30
+ | E-4 hooks déclarés = hooks implémentés | ✓ | ✓ | | | |
31
+ | E-5 aucune mutation d'identifier, aucun effet global | | | ✓ | | ✓ |
32
+ | E-6 timeouts respectés | ✓ | | | | |
33
+
34
+ **Objectif faiblement servi côté extension : token-efficient.** Aucune des six règles propres ne borne ce qu'une extension ajoute au contexte — la surface qu'un middleware injecte dans une réponse, le coût des tools qu'une tool-extension ajoute au catalogue du serveur hôte. Cette borne est portée par le tronc : une tool-extension qui ajoute des tools est soumise à `C-13` (plafond de contexte) au même titre que le serveur hôte, et son `tokens_estime_contexte` compte dans le plafond du catalogue composé.
35
+
36
+ ### Spécifications de détecteur (règles extension `E-1`..`E-6`)
37
+
38
+ L'instrument reste gelé ; on spécifie ce qu'un détecteur lira. La colonne Genre porte le jeu fermé du tronc §5.2 — **`statique`** · **`dynamique`** · **`gate`**, seuls ou combinés par `+` — et rien d'autre (pour une extension, `statique` lit l'arbre + `extension.json` ; `dynamique` observe l'extension greffée sur un serveur hôte en marche). La part non mécanique se lit dans les colonnes Rouge/Vert, jamais dans Genre.
39
+
40
+ | Règle | Le check lit | Rouge | Vert | Genre |
41
+ |---|---|---|---|---|
42
+ | E-1 | l'`identifier` de `extension.json` | id reverse-DNS absent, mal formé, ou en collision | id reverse-DNS unique et valide (`com.elpi/`, `com.<bu>/`, `io.modelcontextprotocol/*`) | statique |
43
+ | E-2 | `extension.json.graceful_degradation` + le comportement du serveur hôte sans l'extension | `graceful_degradation ≠ true`, ou le serveur hôte casse quand le client ne déclare pas l'extension | `true` + serveur hôte fonctionnel sans l'extension | statique + dynamique |
44
+ | E-3 | `extension.json.type` | `type` absent ou de cardinalité > 1 (mix middleware/tool/transport) | exactement un type | statique |
45
+ | E-4 | le champ `lifecycle` vs les fonctions de hook exportées | un hook déclaré non exporté, ou un hook exporté non déclaré | correspondance exacte déclaré ↔ implémenté | statique |
46
+ | E-5 | les écritures de l'extension vers l'identité/l'état d'une autre | mutation de l'identifier ou de l'état d'une autre extension ; effet global | sandboxé par contexte, aucun effet hors de soi | statique |
47
+ | E-6 | la gestion des timeouts de hook | un hook dépassant son timeout bloque le serveur hôte au lieu d'être skippé + warn | skip + warning log, aux défauts déclarés (`init` 5s, etc.) ou surcharges `extension.json.timeouts` | dynamique |
48
+
49
+ ### Traçabilité — règles extension → source
50
+
51
+ Chaque règle nomme sa source, une cellule non vide par règle.
52
+
53
+ | Règle | Source |
54
+ |---|---|
55
+ | E-1..E-6 | source une — nos produits + délta de spec `2026-07-28` sur le contrat d'extension (`extension.json`, cycle de vie des hooks, négociation par capability `extensions`) |
56
+
57
+ ---
58
+
59
+ ## 2026-07-28 protocol delta — impact sur les extensions
60
+
61
+ Changement par changement, ce qui touche réellement le contrat des extensions.
62
+
63
+ ### S'applique aux extensions
64
+
65
+ - **`resultType` préservé** ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). Un middleware dont `afterCall` mute `ctx.output` DOIT préserver le champ `resultType` du résultat original — jamais le stripper ni le retyper. Casser `resultType` casse la négociation MRTR du serveur hôte.
66
+ - **`CacheableResult` sur les listes** ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)). Une tool-extension qui ajoute des tools à `tools/list` fournit `ttlMs`/`cacheScope` cohérents avec la réponse composée. Un middleware de caching respecte `cacheScope: "private"` par défaut et ne promeut jamais un résultat `"private"` en `"public"`.
67
+ - **Négociation par capability `extensions`** — confirmée stable dans `2026-07-28` (minor change #1). Le mécanisme d'identifier (`extensions: ["io.modelcontextprotocol/ui"]`) est inchangé.
68
+ - **Streamable HTTP sans session** ([SEP-2567](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567), [SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)) — UNIQUEMENT la catégorie transport adapter. Ne plus s'appuyer sur `Mcp-Session-Id` ni un état par connexion ; tout état inter-appels transite par un handle serveur explicite. `minProtocolVersion` reste `2025-06-18`.
69
+ - **Dépréciation HTTP+SSE** ([SEP-2596](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596)) — tout transport adapter SSE migre vers Streamable HTTP avant la fin de la fenêtre (12 mois minimum).
70
+
71
+ ### Hors-scope pour les extensions (explicitement)
72
+
73
+ - **`server/discover` RPC** ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)) — responsabilité du serveur HÔTE (`S-4`). L'extension expose son plancher via `extension.json.compatibility.minProtocolVersion`, mécanisme inchangé.
74
+ - **Suppression handshake `initialize` / stateless par-requête** — concerne le protocole client↔serveur hôte. Les hooks `beforeCall`/`afterCall` opèrent sur le `ctx` déjà résolu, ne participent pas au handshake.
75
+ - **Dépréciation Roots/Sampling/Logging** ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)) — feature de capability serveur, aucun hook de ce standard ne l'implémente.
76
+ - **MRTR / `InputRequiredResult`** au-delà du champ `resultType` (ci-dessus) — orchestration portée par le serveur hôte et le client.
77
+
78
+ ---
79
+
80
+ ## File structure
81
+
82
+ ```
83
+ mcp-extension-name/
84
+ ├── .claude-plugin/plugin.json
85
+ ├── src/
86
+ │ ├── index.ts # Entry point — exporte la factory + identifier
87
+ │ ├── extension.ts # Implémentation principale
88
+ │ ├── hooks/ # before-call.ts, after-call.ts, on-error.ts
89
+ │ ├── i18n/ # Table de locales (tronc §5.1)
90
+ │ └── lib/
91
+ ├── tests/ # unit / integration (composition) / fixtures (tronc §5.2)
92
+ ├── evals/evals.json # (tronc §5.5, cas dégradé "extension absente" — C-5)
93
+ ├── docs/ # README.md, README.fr.md, compose.md
94
+ ├── examples/ # basic.ts, stack.ts
95
+ ├── package.json
96
+ ├── extension.json # Manifest extension (propre à l'extension)
97
+ ├── LICENSE
98
+ └── CHANGELOG.md
99
+ ```
100
+
101
+ Seuls `src/extension.ts`, `src/hooks/`, `examples/`, et `extension.json` sont propres à l'extension ; le reste obéit au tronc.
102
+
103
+ ---
104
+
105
+ ## Extension types (3 catégories)
106
+
107
+ ### 1. Middleware
108
+
109
+ Intercepte requests / responses. Use cases : logging, rate limiting, caching, retries, redaction PII, traduction FR↔EN, audit trail.
110
+
111
+ ```ts
112
+ import { defineMiddleware } from "@elpi/mcp-extension-sdk";
113
+
114
+ export default defineMiddleware({
115
+ identifier: "com.elpi/middleware-redact-pii",
116
+ beforeCall: async (ctx) => { ctx.input = redactPII(ctx.input); return ctx; },
117
+ afterCall: async (ctx) => { ctx.output = redactPII(ctx.output); return ctx; }
118
+ });
119
+ ```
120
+
121
+ ### 2. Tool extension
122
+
123
+ Ajoute des tools à un serveur existant sans modifier son code. Les tools ajoutés obéissent au tronc : nommage (`C-2`), schémas (`C-4`), description bilingue (`C-1`, §5.7), autorité/verbe/mécanisme/pagination déclarés (`C-8` à `C-11`).
124
+
125
+ ```ts
126
+ import { defineToolExtension } from "@elpi/mcp-extension-sdk";
127
+
128
+ export default defineToolExtension({
129
+ identifier: "com.elpi/github-mcp-fr-helpers",
130
+ baseServer: "github/github-mcp-server",
131
+ tools: [ { name: "summarize_pr_fr", description: "…", inputSchema, outputSchema, handler } ]
132
+ });
133
+ ```
134
+
135
+ ### 3. Transport adapter
136
+
137
+ Transport custom (SSE, WebSocket, ZeroMQ, named pipes). Use case : intégrations enterprise, air-gapped, bridges vers clients non-MCP. Un adapter Streamable HTTP suit le délta `2026-07-28` (sans session, ci-dessus).
138
+
139
+ ```ts
140
+ import { defineTransport } from "@elpi/mcp-extension-sdk";
141
+
142
+ export default defineTransport({
143
+ identifier: "com.elpi/transport-websocket",
144
+ open: async (config) => { /* … */ }, send: async (m) => { /* … */ },
145
+ receive: async () => { /* … */ }, close: async () => { /* … */ }
146
+ });
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Manifest — extension.json
152
+
153
+ ```json
154
+ {
155
+ "identifier": "com.elpi/<reverse-dns-style>",
156
+ "name": "mcp-extension-name",
157
+ "version": "0.1.0",
158
+ "type": "middleware|tool-extension|transport",
159
+ "description": "EN description.",
160
+ "description_fr": "Description FR.",
161
+ "compatibility": {
162
+ "minProtocolVersion": "2025-06-18",
163
+ "compatibleServers": ["*"],
164
+ "incompatibleWith": []
165
+ },
166
+ "lifecycle": ["init", "before_call", "after_call", "on_error", "shutdown"],
167
+ "graceful_degradation": true,
168
+ "ui_extension_id": null
169
+ }
170
+ ```
171
+
172
+ ### Validation rules (propres à l'extension)
173
+
174
+ | Champ | Règle |
175
+ |---|---|
176
+ | `identifier` | Reverse-DNS unique (`E-1`). |
177
+ | `type` | Une et une seule des 3 catégories (`E-3`). |
178
+ | `compatibleServers` | Glob patterns (`["*"]` = universel). |
179
+ | `graceful_degradation` | Toujours `true` (`E-2`). |
180
+ | `lifecycle` | Correspond **exactement** aux hooks réellement implémentés — aucun hook déclaré non exporté, aucun hook exporté non déclaré (`E-4`). |
181
+
182
+ *(le nommage du `name`, la description bilingue, la version obéissent au tronc — `C-2`, `C-1`, `C-3`.)*
183
+
184
+ ---
185
+
186
+ ## Lifecycle hooks
187
+
188
+ ```ts
189
+ type Lifecycle = {
190
+ init?: (ctx: ServerContext) => Promise<void>;
191
+ beforeCall?: (ctx: CallContext) => Promise<CallContext>;
192
+ afterCall?: (ctx: CallContext) => Promise<CallContext>;
193
+ onError?: (ctx: ErrorContext) => Promise<ErrorContext | void>;
194
+ shutdown?: () => Promise<void>;
195
+ };
196
+ ```
197
+
198
+ - `init` : une fois au boot, idempotent. Échec = extension désactivée + warning + serveur continue (`E-2`).
199
+ - `beforeCall` : peut muter `ctx.input`, doit retourner le contexte. Throw = abort de l'appel.
200
+ - `afterCall` : peut muter `ctx.output`, doit retourner le contexte. Préserve `resultType` (délta ci-dessus).
201
+ - `onError` : transforme ou avale une erreur ; `void` = erreur originale propagée.
202
+ - `shutdown` : best-effort, pas garanti en kill -9.
203
+
204
+ **Timeouts (`E-6`)** : `init` 5s, `beforeCall`/`afterCall` 1s, `onError` 500ms. Au-delà : warning, hook skip, exécution continue — jamais un blocker.
205
+
206
+ ---
207
+
208
+ ## Composition (plusieurs extensions sur un serveur)
209
+
210
+ ```
211
+ Client request
212
+ ↓
213
+ [ext1.beforeCall] → [ext2.beforeCall] → [ext3.beforeCall]
214
+ ↓ Tool handler ↓
215
+ [ext3.afterCall] → [ext2.afterCall] → [ext1.afterCall]
216
+ ↓
217
+ Client response
218
+ ```
219
+
220
+ LIFO sur `afterCall` : la première extension chargée est la dernière à transformer la sortie.
221
+
222
+ ### Règles de composition
223
+
224
+ - Ordre déclaratif dans la config serveur (pas de magic).
225
+ - Aucune dépendance implicite : si une extension dépend d'une autre, le déclarer dans `extension.json.requires`.
226
+ - Conflit d'identifier : erreur fatale au boot.
227
+ - Une extension ne mute JAMAIS l'identifier d'une autre — sandboxé par contexte (`E-5`).
228
+
229
+ ---
230
+
231
+ ## Examples — patterns clés
232
+
233
+ ### Middleware « redact-pii » (interne ElPi Corp)
234
+
235
+ Hooks `beforeCall` (input scrubbing) + `afterCall` (output scrubbing). Compatible `["*"]`. Distribution npm `@elpi/mcp-ext-redact-pii`.
236
+
237
+ ### Tool extension « github-mcp-fr-helpers »
238
+
239
+ Étend `github/github-mcp-server` avec `summarize_pr_fr`, `triage_issues_fr`, `weekly_digest_fr`. Compatible `["github/github-mcp-server@^1.0"]`. SELLABLE AS `perello-github-fr`.
240
+
241
+ ### Transport « websocket » (référence officielle)
242
+
243
+ Identifier `io.modelcontextprotocol/transport-websocket` (officiel) ou `com.elpi/transport-websocket`. Use case : bridge MCP → frontend web. Hors scope Phase 1.
244
+
245
+ ### Composition ElPi Corp standard
246
+
247
+ ```
248
+ [redact-pii] → [rate-limit-token-bucket] → [logger-structured]
249
+ ```
250
+
251
+ Chaque extension est un repo séparé, distribuable séparément. L'orchestrateur compose dans la config serveur.
252
+
253
+ ---
254
+
255
+ ## ElPi Corp additions (au-delà de la spec MCP)
256
+
257
+ - **SELLABLE AS** dans le README — chaque extension cible un nom de plugin marketplace.
258
+ - **VantageRegistry registration** : `register_component` category `mcp-extension` à chaque release stable (`C-14`, tronc §5.6).
259
+ - **Stack reference** : ElPi Corp publie une stack canonique (redact-pii + rate-limit + logger) que tout serveur commercial embarque par défaut.