@cyanmycelium/mcp-broker 1.2.0 → 1.3.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 (49) hide show
  1. package/.mcp-broker.example/CONFIGURATION-EN.md +300 -44
  2. package/.mcp-broker.example/CONFIGURATION-FR.md +313 -44
  3. package/.mcp-broker.example/README.md +73 -2
  4. package/.mcp-broker.example/config.json +18 -9
  5. package/.mcp-broker.example/config.stdio-bridge.json +16 -0
  6. package/README.md +407 -27
  7. package/dist/bin.js +215 -20
  8. package/dist/bin.js.map +1 -1
  9. package/dist/chunk-BZUZYXVA.js +5955 -0
  10. package/dist/chunk-BZUZYXVA.js.map +1 -0
  11. package/dist/grammars/claude/en.json +12 -0
  12. package/dist/grammars/claude/fr.json +12 -0
  13. package/dist/grammars/default/en.json +40 -0
  14. package/dist/grammars/default/fr.json +40 -0
  15. package/dist/grammars/default/zh.json +40 -0
  16. package/dist/index.d.ts +991 -25
  17. package/dist/index.js +1 -1
  18. package/package.json +3 -3
  19. package/src/auth/index.ts +3 -1
  20. package/src/auth/provider.auth.ts +126 -8
  21. package/src/authorization/policy.engine.ts +11 -2
  22. package/src/authorization/policy.types.ts +25 -1
  23. package/src/bin.ts +325 -28
  24. package/src/broker/adapters/broker.adapter.diagnose.ts +45 -0
  25. package/src/broker/adapters/broker.adapter.guide.ts +108 -0
  26. package/src/broker/aggregate/aggregate.server.ts +82 -15
  27. package/src/broker/aggregate/provider.client.session.ts +85 -11
  28. package/src/broker/behaviors/broker.behavior.diagnose.ts +47 -0
  29. package/src/broker/behaviors/broker.behavior.guide.ts +79 -0
  30. package/src/broker/broker.context.ts +65 -0
  31. package/src/broker/broker.diagnostics.ts +495 -0
  32. package/src/broker/broker.guides.ts +1029 -0
  33. package/src/broker/broker.server.ts +23 -7
  34. package/src/broker/broker.slots.ts +36 -0
  35. package/src/broker/grammars/claude/en.json +12 -0
  36. package/src/broker/grammars/claude/fr.json +12 -0
  37. package/src/broker/grammars/default/en.json +40 -0
  38. package/src/broker/grammars/default/fr.json +40 -0
  39. package/src/broker/grammars/default/zh.json +40 -0
  40. package/src/config.ts +191 -4
  41. package/src/index.ts +38 -3
  42. package/src/remote.transports.ts +127 -10
  43. package/src/remote.upstream.ts +4 -1
  44. package/src/ws/ws.interfaces.ts +148 -3
  45. package/src/ws/ws.tunnel.builder.ts +63 -1
  46. package/src/ws/ws.tunnel.ts +1150 -173
  47. package/web/README.md +31 -4
  48. package/dist/chunk-FTDKH2C4.js +0 -3670
  49. package/dist/chunk-FTDKH2C4.js.map +0 -1
@@ -24,15 +24,27 @@ Quelques règles de lecture :
24
24
  - Les chemins de fichiers relatifs sont résolus depuis le dossier
25
25
  `.mcp-broker/`.
26
26
 
27
+ Chaque section ci-dessous porte le nom de la clé qu'elle explique, et non un
28
+ numéro de ligne, pour que le guide reste juste quand le fichier d'exemple
29
+ grossit.
30
+
31
+ Une chose à savoir avant tout le reste : **le modèle est livré avec
32
+ `auth.enabled: false`**. Une copie fraîche démarre et répond à tous les clients,
33
+ ce qui est exactement ce qu'il faut le temps de prendre ses marques. Tout le
34
+ bloc `auth` reste présent, comme référence pour le jour où vous l'activerez.
35
+ Lisez [`docs/authorization.md`](../../../../docs/authorization.md) avant de le
36
+ faire.
37
+
27
38
  ## La carte mentale
28
39
 
29
- Le fichier répond à cinq questions :
40
+ Le fichier répond à six questions :
30
41
 
31
42
  1. Où le broker écoute-t-il ?
32
43
  2. Comment chiffre-t-il les connexions ?
33
- 3. Comment reconnaît-il les clients et les fournisseurs ?
34
- 4. Que peut faire chaque client, et sur quelles ressources ?
35
- 5. Quels serveurs MCP locaux ou empaquetés doit-il charger ?
44
+ 3. Quelles pages web, s'il y en a, ont le droit de l'appeler ?
45
+ 4. Comment reconnaît-il les clients et les fournisseurs ?
46
+ 5. Que peut faire chaque client, et sur quelles ressources ?
47
+ 6. Quels serveurs MCP locaux ou empaquetés doit-il charger ?
36
48
 
37
49
  Le bloc `auth` est le plus important pour la sécurité. Il se lit ainsi :
38
50
 
@@ -86,7 +98,7 @@ Cette séparation est essentielle :
86
98
  - les ressources décrivent où cela est permis ;
87
99
  - les sujets décrivent à qui cela est permis.
88
100
 
89
- ## Lignes 1 à 5 : paramètres généraux
101
+ ## Paramètres généraux
90
102
 
91
103
  ```json
92
104
  {
@@ -127,17 +139,71 @@ Nom logique affiché par les outils d'introspection du broker.
127
139
 
128
140
  - Il aide à distinguer plusieurs brokers.
129
141
  - Il n'a aucun effet sur l'autorisation.
142
+ - **Utilisable seulement par l'API programmatique aujourd'hui.** Le tunnel
143
+ l'honore, mais le broker en ligne de commande n'a pas encore de moyen de le
144
+ transmettre : le renseigner ici ne change donc rien. Passez par
145
+ `IWsTunnelOptions.brokerName` si vous en avez besoin dès maintenant.
146
+
147
+ ## Origines navigateur autorisées
148
+
149
+ ```json
150
+ "allowedOrigins": ["https://app.factory.local", "https://mcp.factory.local"]
151
+ ```
152
+
153
+ La liste des origines de pages web autorisées à appeler ce broker en HTTP. Elle
154
+ est vérifiée sur `/<slot>/mcp`, `/<slot>/sse` et `/<slot>/messages`.
155
+
156
+ Trois règles à retenir, parce que chacune surprend quelqu'un :
157
+
158
+ 1. **Absent veut dire fermé.** Sans `allowedOrigins`, toute requête portant un
159
+ en-tête `Origin` est refusée avec un `403`. C'est volontaire : sans ce
160
+ contrôle, n'importe quelle page ouverte dans le navigateur de l'utilisateur
161
+ pourrait piloter votre broker.
162
+ 2. **Une requête sans en-tête `Origin` passe toujours.** Claude Desktop,
163
+ l'Inspector MCP et tous les SDK côté serveur n'en envoient pas : c'est
164
+ pourquoi tout semble parfait jusqu'au premier navigateur.
165
+ 3. **Être servie par ce broker n'exempte de rien.** Une page chargée depuis le
166
+ montage `www` reste une origine navigateur et doit figurer dans la liste.
167
+
168
+ La comparaison est littérale : le schéma et le port font partie de la valeur.
169
+ `https://app.factory.local` ne correspond ni à `http://app.factory.local` ni à
170
+ `https://app.factory.local:8443`, et une barre oblique finale ne correspond
171
+ jamais. Ce modèle définit `tls.cert`/`tls.key`, le broker parle donc HTTPS et les
172
+ entrées utilisent `https://`. Retirez le bloc TLS et elles doivent devenir
173
+ `http://...:3001`.
130
174
 
131
- ## Lignes 7 à 11 : chemins HTTP et WebSocket
175
+ Une expression régulière remplace la liste quand les origines ne sont pas
176
+ connues à l'avance :
177
+
178
+ ```json
179
+ "allowedOrigins": { "pattern": "^https://[a-z0-9-]+\\.factory\\.local$" }
180
+ ```
181
+
182
+ `MCP_BROKER_ALLOWED_ORIGINS` remplace le fichier par une liste séparée par des
183
+ virgules. Cette variable ne peut pas porter la forme `pattern` : une expression
184
+ régulière ne survit pas à un découpage sur les virgules.
185
+
186
+ ## Chemins HTTP et WebSocket
132
187
 
133
188
  ```json
134
189
  "paths": {
135
- "provider": "/provider",
136
- "client": "/",
137
- "mcp": "/mcp"
190
+ "provider": "/provider",
191
+ "providers": "/providers",
192
+ "client": "/",
193
+ "mcp": "/mcp",
194
+ "sse": "/sse",
195
+ "messages": "/messages"
138
196
  }
139
197
  ```
140
198
 
199
+ Les six clés sont honorées, et chacune est aussi réglable par une variable
200
+ d'environnement, qui l'emporte : `MCP_BROKER_PROVIDER_PATH`,
201
+ `MCP_BROKER_PROVIDERS_PATH`, `MCP_BROKER_CLIENT_PATH`, `MCP_BROKER_MCP_PATH`,
202
+ `MCP_BROKER_SSE_PATH`, `MCP_BROKER_MESSAGES_PATH`.
203
+
204
+ En changer un déplace le point d'entrée pour tout le monde : le SDK fournisseur,
205
+ les clients, et les URL affichées au démarrage. N'y touchez pas sans raison.
206
+
141
207
  ### `paths.provider`
142
208
 
143
209
  Préfixe WebSocket utilisé par un fournisseur qui se connecte au broker.
@@ -148,7 +214,35 @@ Exemple :
148
214
  wss://mcp.factory.local/provider/spoony-00452
149
215
  ```
150
216
 
151
- Le fournisseur demande ici le slot `spoony-00452`.
217
+ Le fournisseur demande ici le slot `spoony-00452`. La socket transporte des
218
+ trames JSON-RPC nues, un fournisseur par socket. Dans
219
+ `@cyanmycelium/mcp-broker-provider`, c'est `DirectTransport`.
220
+
221
+ ### `paths.providers`
222
+
223
+ Chemin WebSocket exact (aucun nom de slot n'y est ajouté) utilisé par un
224
+ fournisseur qui porte plusieurs slots sur une seule socket, en enveloppant
225
+ chaque trame dans une enveloppe qui nomme le slot. Dans
226
+ `@cyanmycelium/mcp-broker-provider`, c'est `MultiplexTransport`.
227
+
228
+ ```text
229
+ wss://mcp.factory.local/providers
230
+ ```
231
+
232
+ `paths.provider` et `paths.providers` diffèrent d'une lettre et ne sont **pas
233
+ interchangeables** : ce qui les sépare est le format des trames, pas seulement
234
+ l'URL. Brancher un `MultiplexTransport` sur `/provider/<nom>`, ou un
235
+ `DirectTransport` sur `/providers`, est l'erreur d'intégration la plus fréquente.
236
+ Le broker nomme désormais l'incohérence et refuse la socket au lieu de rester
237
+ muet, mais autant faire juste du premier coup :
238
+
239
+ | Point d'entrée | Trames | Transport |
240
+ |---------------------|--------------------|----------------------|
241
+ | `/provider/<nom>` | JSON-RPC nu | `DirectTransport` |
242
+ | `/providers` | enveloppes | `MultiplexTransport` |
243
+
244
+ `/providers/<nom>` n'est ni l'un ni l'autre : c'est compris comme une connexion
245
+ *cliente* sur un slot littéralement nommé `providers/<nom>`.
152
246
 
153
247
  ### `paths.client`
154
248
 
@@ -169,10 +263,84 @@ Avec le slot `spoony-00452`, l'URL devient :
169
263
  https://mcp.factory.local/spoony-00452/mcp
170
264
  ```
171
265
 
172
- Les chemins `providers`, `sse` et `messages` ne sont pas redéfinis dans cet
173
- exemple. Le broker utilise donc leurs valeurs par défaut.
266
+ C'est le transport à privilégier. Les deux suivants sont l'ancien couple.
267
+
268
+ ### `paths.sse`
269
+
270
+ Suffixe du flux SSE historique, ouvert en `GET`. Le broker y répond par un
271
+ événement `endpoint` qui porte l'URL où poster.
272
+
273
+ ```text
274
+ https://mcp.factory.local/spoony-00452/sse
275
+ ```
276
+
277
+ ### `paths.messages`
278
+
279
+ Suffixe où le client SSE historique `POST`e ses requêtes JSON-RPC, en paire avec
280
+ le flux ci-dessus.
281
+
282
+ ```text
283
+ https://mcp.factory.local/spoony-00452/messages
284
+ ```
285
+
286
+ Ces deux points d'entrée historiques sont soumis au même contrôle
287
+ `allowedOrigins` que `/<slot>/mcp`.
288
+
289
+ ## Surveillance des fournisseurs
290
+
291
+ ```json
292
+ "providerHeartbeatIntervalMs": 30000,
293
+ "providerRequestTimeoutMs": 60000,
294
+ "providerTakeover": "liveness"
295
+ ```
296
+
297
+ Trois clés facultatives. Les valeurs montrées sont celles par défaut : vous
298
+ pouvez supprimer le bloc entier. Elles sont explicitées ici parce que, quand un
299
+ fournisseur se comporte mal, ce sont ces trois-là qu'on règle.
300
+
301
+ ### `providerHeartbeatIntervalMs`
302
+
303
+ Intervalle entre deux pings envoyés à chaque socket fournisseur. Un fournisseur
304
+ qui rate un intervalle complet est déconnecté et son slot libéré. `0` désactive
305
+ le mécanisme. Aussi `MCP_BROKER_PROVIDER_HEARTBEAT_MS`.
306
+
307
+ Sans lui, une socket dont le pair a disparu sans fermer proprement (onglet tué,
308
+ portable mis en veille, VPN coupé) reste ouverte pour le système pendant environ
309
+ deux heures, durant lesquelles le broker annonce le slot comme connecté et
310
+ refuse toutes les tentatives de reconnexion.
311
+
312
+ Soyons honnêtes sur ce que cela prouve : un pong est renvoyé par la pile réseau
313
+ du pair, pas par le JavaScript de la page. Cela détecte un processus, une machine
314
+ ou un chemin réseau mort, pas un fournisseur connecté qui ne répond simplement
315
+ pas. Pour cela, voir la clé suivante.
316
+
317
+ ### `providerRequestTimeoutMs`
318
+
319
+ Délai au bout duquel le broker abandonne l'attente d'une réponse du fournisseur
320
+ et renvoie une erreur JSON-RPC nommant le slot. `0` désactive le délai. Aussi
321
+ `MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS`.
322
+
323
+ Augmentez-le si vous hébergez des outils réellement longs. Diminuez-le si vous
324
+ préférez une erreur à un client qui attend indéfiniment, car c'est l'alternative :
325
+ un onglet mis en veille par le navigateur est un fournisseur connecté qui ne
326
+ répond à rien.
174
327
 
175
- ## Lignes 13 à 16 : TLS
328
+ ### `providerTakeover`
329
+
330
+ Ce qui se passe quand un fournisseur se connecte à un slot déjà tenu par une
331
+ autre socket.
332
+
333
+ - `"reject"` : le tenant garde toujours le slot.
334
+ - `"liveness"` (défaut) : le tenant ne le garde que tant qu'il répond aux pings.
335
+ - `"always"` : le nouveau venu l'emporte, mais uniquement si l'authentification
336
+ des fournisseurs est configurée et qu'il s'est authentifié sous le même
337
+ principal que le tenant. Sans authentification des fournisseurs, le broker
338
+ retombe sur `"liveness"` et le dit, car une reprise inconditionnelle
339
+ permettrait à quiconque atteint l'URL d'évincer le vrai fournisseur.
340
+
341
+ Aussi `MCP_BROKER_PROVIDER_TAKEOVER`.
342
+
343
+ ## TLS
176
344
 
177
345
  ```json
178
346
  "tls": {
@@ -202,7 +370,7 @@ Cette clé est secrète. Elle ne doit jamais être ajoutée au dépôt Git.
202
370
  Le broker doit pouvoir lire les deux fichiers. Une paire certificat et clé
203
371
  incorrecte empêche le démarrage en HTTPS.
204
372
 
205
- ## Lignes 18 à 23 : fichiers web statiques
373
+ ## Fichiers web statiques
206
374
 
207
375
  ```json
208
376
  "www": {
@@ -215,10 +383,25 @@ incorrecte empêche le démarrage en HTTPS.
215
383
 
216
384
  ### `www.open`
217
385
 
218
- Indique si le broker doit ouvrir automatiquement le navigateur.
386
+ Indique si le broker doit ouvrir automatiquement le navigateur au démarrage.
387
+
388
+ - `false` (ou absent) convient aux serveurs, conteneurs et environnements
389
+ headless.
390
+ - `true` ouvre la racine du broker, `https://localhost:3001/`.
391
+ - Une chaîne ouvre une page précise : `"/app/index.html"`, ou une URL absolue
392
+ sur l'origine de ce broker.
219
393
 
220
- - `false` convient aux serveurs, conteneurs et environnements headless.
221
- - `true` est pratique en développement local.
394
+ Une URL sur une autre origine est refusée avec un message sur la sortie
395
+ d'erreur, comme toute autre chaîne : une valeur transmise telle quelle à la
396
+ commande « ouvrir ceci » du système peut lancer un fichier local ou une
397
+ application enregistrée, et rien dans le démarrage d'un broker n'exige de visiter
398
+ un autre hôte. Ouvrez-la vous-même.
399
+
400
+ Le navigateur ne s'ouvre que si une entrée `www.mounts` couvre réellement le
401
+ chemin résolu. Sinon, le broker indique quels préfixes sont montés au lieu de
402
+ lancer un navigateur sur un `404`.
403
+
404
+ `MCP_BROKER_OPEN` accepte les mêmes valeurs (`"1"` pour la racine).
222
405
 
223
406
  ### `www.mounts`
224
407
 
@@ -240,11 +423,11 @@ Ce bloc ne protège pas automatiquement une interface web. Les routes MCP sont
240
423
  protégées par `auth`, mais une application web statique doit aussi être conçue
241
424
  pour ne pas exposer de secret.
242
425
 
243
- ## Lignes 25 à 35 : activation OAuth
426
+ ## Activation OAuth
244
427
 
245
428
  ```json
246
429
  "auth": {
247
- "enabled": true,
430
+ "enabled": false,
248
431
  "publicBaseUrl": "https://mcp.factory.local",
249
432
  "authorizationServers": [
250
433
  "https://identity.factory.local"
@@ -259,13 +442,23 @@ pour ne pas exposer de secret.
259
442
 
260
443
  ### `auth.enabled`
261
444
 
262
- Active l'authentification OAuth des clients.
445
+ Active l'authentification OAuth des clients. **Ce modèle la livre désactivée.**
263
446
 
264
- - `true` exige un bearer token valide.
265
- - `false` conserve le mode historique sans authentification.
447
+ - `false` (la valeur livrée) conserve le mode historique sans authentification :
448
+ tous les clients atteignent tous les slots. À utiliser sur un réseau de
449
+ confiance, et le temps de faire fonctionner le reste.
450
+ - `true` exige un bearer token valide sur chaque requête cliente. Le reste du
451
+ bloc doit alors décrire un vrai serveur d'autorisation : avec `enabled: true`
452
+ et les valeurs d'exemple `identity.factory.local` encore en place, le broker
453
+ répond `401` à tous les clients avec un défi pointant vers un hôte qui
454
+ n'existe pas, ce qui est une façon déroutante d'occuper un après-midi.
266
455
  - Une politique détaillée n'est utile que si les clients possèdent une
267
456
  identité authentifiée.
268
457
 
458
+ Tout ce qui suit (`roles`, `assignments`, `denies`, `slotResources`,
459
+ `toolCapabilities`) est inerte tant que `enabled` vaut `false`. C'est conservé
460
+ dans le modèle comme exemple travaillé, pas parce que cela agit.
461
+
269
462
  ### `auth.publicBaseUrl`
270
463
 
271
464
  Adresse publique utilisée par les clients pour joindre le broker.
@@ -352,7 +545,7 @@ Pour accorder cet accès, ajoutez par exemple :
352
545
  Le JWT devra alors posséder à la fois le scope `broker:admin` et le groupe
353
546
  `broker-administrators`.
354
547
 
355
- ## Lignes 36 à 40 : claims JWT transformés en sujets
548
+ ## Claims JWT transformés en sujets
356
549
 
357
550
  ```json
358
551
  "subjectMapping": {
@@ -419,7 +612,7 @@ client:local-ai-assistant
419
612
  Un même appel peut donc posséder plusieurs identités en même temps, par exemple
420
613
  un utilisateur, deux groupes et une application cliente.
421
614
 
422
- ## Lignes 41 à 64 : rôles et capacités
615
+ ## Rôles et capacités
423
616
 
424
617
  Un rôle répond uniquement à la question « que peut-on faire ? ». Il ne contient
425
618
  jamais de chemin de ressource.
@@ -493,7 +686,7 @@ Déclarer un rôle ne l'accorde à personne. Dans le fichier d'exemple, aucune
493
686
  affectation n'utilise `administrator`. Personne n'est donc administrateur par
494
687
  ce seul bloc.
495
688
 
496
- ## Lignes 65 à 78 : affectations
689
+ ## Affectations
497
690
 
498
691
  Une affectation répond à la phrase :
499
692
 
@@ -561,7 +754,7 @@ site Paris, sans pouvoir appeler les outils.
561
754
 
562
755
  Les expressions régulières ne sont pas acceptées.
563
756
 
564
- ## Lignes 79 à 89 : interdiction explicite
757
+ ## Interdiction explicite
565
758
 
566
759
  ```json
567
760
  "denies": [
@@ -591,7 +784,7 @@ quelle que soit la position des règles dans le fichier.
591
784
  Utilisez `"capabilities": ["*"]` pour interdire toute capacité sur une
592
785
  ressource précise.
593
786
 
594
- ## Lignes 90 à 93 : noms techniques et ressources stables
787
+ ## Noms techniques et ressources stables
595
788
 
596
789
  ```json
597
790
  "slotResources": {
@@ -629,7 +822,7 @@ Un slot non déclaré est normalement converti en `/<nom-du-slot>`. Pour un
629
822
  environnement industriel, il est préférable de déclarer explicitement les
630
823
  mappings afin de conserver des identités stables.
631
824
 
632
- ## Lignes 94 à 99 : classification globale des outils
825
+ ## Classification globale des outils
633
826
 
634
827
  ```json
635
828
  "toolCapabilities": {
@@ -656,7 +849,7 @@ générique `mcp.tools.call`.
656
849
  Cette valeur par défaut explique pourquoi le rôle `maintenance` contient aussi
657
850
  `mcp.tools.call`.
658
851
 
659
- ## Lignes 100 à 104 : classification spécifique à une zone
852
+ ## Classification spécifique à une zone
660
853
 
661
854
  ```json
662
855
  "providerToolCapabilities": {
@@ -680,7 +873,7 @@ valeur globale. Cette redondance est volontairement pédagogique. Dans un vrai
680
873
  déploiement, ce bloc est surtout utile si le même nom d'outil n'a pas le même
681
874
  niveau de risque selon le fournisseur ou la zone.
682
875
 
683
- ## Lignes 105 à 107 : audit
876
+ ## Audit
684
877
 
685
878
  ```json
686
879
  "audit": {
@@ -698,16 +891,31 @@ Passez temporairement à `true` pour comprendre une politique ou diagnostiquer
698
891
  un problème. Les journaux contiennent la décision et les identifiants de
699
892
  politique, jamais le bearer token ni le secret fournisseur.
700
893
 
701
- ## Ligne 108 : secret partagé des fournisseurs
894
+ ## Secret partagé des fournisseurs
702
895
 
703
896
  ```json
704
897
  "providerSecret": "change-me"
705
898
  ```
706
899
 
707
- Ce secret authentifie les serveurs MCP qui se connectent à `/provider/<slot>`
708
- ou `/providers`.
900
+ **Volontairement absent du modèle livré.** Ajoutez la clé dans `auth` pour
901
+ activer l'authentification des fournisseurs.
709
902
 
710
- Il est indépendant des bearer tokens des clients.
903
+ Ce secret authentifie les serveurs MCP qui se connectent à `/provider/<slot>`
904
+ ou `/providers`. Chaque fournisseur doit alors le présenter dans
905
+ `X-Provider-Token` ou `Authorization: Bearer`, faute de quoi il est refusé dès la
906
+ poignée de main WebSocket.
907
+
908
+ Il est indépendant des bearer tokens des clients, et surtout il n'est **pas**
909
+ gouverné par `auth.enabled` : dès qu'il est défini, l'authentification des
910
+ fournisseurs est active, même avec OAuth désactivé. C'est pourquoi le modèle ne
911
+ le livre pas. Laissé en place avec sa valeur d'exemple, il refuserait tous les
912
+ fournisseurs sur un broker que le lecteur croit grand ouvert.
913
+
914
+ Une conséquence à anticiper : le constructeur `WebSocket` du navigateur ne peut
915
+ pas poser d'en-têtes de requête, donc un fournisseur hébergé dans une page web ne
916
+ peut pas présenter ce secret du tout. Avec `providerSecret` défini, les
917
+ fournisseurs navigateur sont exclus ; il leur faut l'authentification
918
+ fournisseur désactivée, ou un reverse proxy authentifiant en amont.
711
919
 
712
920
  La valeur `change-me` est uniquement un placeholder. En production :
713
921
 
@@ -722,14 +930,15 @@ chemins de ressources. Pour limiter chaque appareil à son propre sous-arbre,
722
930
  utilisez un `IProviderAuthenticator` personnalisé qui renvoie un
723
931
  `IProviderPrincipal.allowedResources`.
724
932
 
725
- ## Lignes 111 à 117 : serveur MCP local lancé par le broker
933
+ ## Serveur MCP local lancé par le broker
726
934
 
727
935
  ```json
728
936
  "stdioUpstreams": [
729
937
  {
730
- "name": "fs",
731
- "command": "npx",
732
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
938
+ "name": "fs",
939
+ "command": "npx",
940
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
941
+ "aggregate": true
733
942
  }
734
943
  ]
735
944
  ```
@@ -757,10 +966,58 @@ Arguments transmis au programme :
757
966
  Accorder un accès au filesystem est sensible. Limitez `/data` au strict
758
967
  nécessaire.
759
968
 
760
- Ajoutez `"aggregate": true` si ce provider doit aussi apparaître dans `_all`.
761
- Sans cette propriété, cet upstream stdio reste accessible par son slot direct.
969
+ ### `aggregate`
970
+
971
+ `true` fait entrer ce fournisseur dans le slot réservé `_all`, en plus de son
972
+ propre `/fs/mcp`. Sans cette propriété, cet upstream stdio reste accessible par
973
+ son slot direct uniquement.
974
+
975
+ Notez l'asymétrie : les entrées `stdioUpstreams` ne rejoignent **pas** `_all` par
976
+ défaut, alors que les entrées `mcpServers` et `mcpbBundles` le font. Renseignez
977
+ la clé explicitement dans les deux cas et vous n'aurez jamais à vous en
978
+ souvenir.
979
+
980
+ ## Passerelle stdio vers un hôte MCP
981
+
982
+ Absent de `config.json`, mais c'est la raison pour laquelle on règle `aggregate`
983
+ en premier lieu. Un second fichier de ce dossier,
984
+ `config.stdio-bridge.json`, ajoute une clé :
985
+
986
+ ```json
987
+ "stdioProvider": "_all"
988
+ ```
989
+
990
+ Avec elle, le broker se comporte aussi comme un serveur MCP stdio : il lit du
991
+ JSON-RPC sur son entrée standard et écrit les réponses sur sa sortie standard,
992
+ reliant un hôte comme Claude Desktop à un slot. Pointez la configuration de
993
+ l'hôte sur ce fichier :
994
+
995
+ ```json
996
+ {
997
+ "command": "npx",
998
+ "args": ["-y", "@cyanmycelium/mcp-broker"],
999
+ "env": { "MCP_BROKER_CONFIG": "/chemin/absolu/.mcp-broker/config.stdio-bridge.json" }
1000
+ }
1001
+ ```
1002
+
1003
+ Deux points à ne pas manquer.
1004
+
1005
+ - **Visez `_all`, pas un vrai slot.** `_all` existe dès le démarrage et répond
1006
+ lui-même à la poignée de main, donc l'hôte se connecte même si aucun
1007
+ fournisseur n'est encore arrivé, et il annonce les nouveaux outils au fur et à
1008
+ mesure. Pointé sur un vrai slot, l'hôte démarre avant le fournisseur, reçoit
1009
+ « non connecté » dès son premier message et abandonne. Pour un fournisseur
1010
+ hébergé dans une page web, c'est garanti : la page ne peut pas être ouverte
1011
+ avant le lancement de l'hôte. `_broker` répond toujours lui aussi, mais
1012
+ n'offrira jamais que les cinq outils d'introspection. Le broker prévient au
1013
+ démarrage quand `stdioProvider` nomme un slot qu'il n'héberge pas.
1014
+ - **Gardez cela dans un fichier séparé.** Avec `stdioProvider`, la sortie
1015
+ standard appartient au flux JSON-RPC et toutes les traces passent sur la sortie
1016
+ d'erreur : un broker lancé ainsi dans un terminal a l'air de ne rien faire.
1017
+
1018
+ `MCP_BROKER_STDIO_PROVIDER` règle la même chose depuis l'environnement.
762
1019
 
763
- ## Lignes 119 à 128 : bundle MCP local signé
1020
+ ## Bundle MCP local signé
764
1021
 
765
1022
  ```json
766
1023
  "mcpbBundles": [
@@ -849,17 +1106,23 @@ Si Alice tente `start_motor` sur le four critique :
849
1106
 
850
1107
  ## Checklist avant un déploiement
851
1108
 
1109
+ - Passer `auth.enabled` à `true`. Le modèle le livre désactivé pour qu'une copie
1110
+ fraîche démarre ; le laisser ainsi en production signifie que tous les clients
1111
+ atteignent tous les slots.
852
1112
  - Remplacer tous les domaines `.local` par les adresses réelles.
853
1113
  - Vérifier que `publicBaseUrl` est exactement l'adresse publique du broker.
854
1114
  - Vérifier que les JWT utilisent cette ressource dans leur audience.
855
1115
  - Vérifier l'URL JWKS et l'émetteur attendu.
856
- - Ne jamais conserver `change-me`.
1116
+ - Si vous ajoutez `providerSecret`, ne jamais conserver `change-me`.
857
1117
  - Ne jamais publier la clé TLS privée.
858
1118
  - Ne jamais publier les clés API de `userConfig`.
859
1119
  - Utiliser `127.0.0.1` au lieu de `0.0.0.0` si aucun accès réseau n'est requis.
860
1120
  - Tester chaque rôle avec un compte représentatif.
861
1121
  - Tester les denies sur les actifs critiques.
862
1122
  - Vérifier que `_all` ne révèle pas les providers non autorisés.
1123
+ - Lister dans `allowedOrigins` exactement les origines navigateur qui doivent
1124
+ accéder au broker, avec le bon schéma et le bon port, et aucune autre. Une page
1125
+ servie par ce broker compte aussi comme une origine navigateur.
863
1126
  - Laisser `audit.logAllowed` à `false` après le diagnostic.
864
1127
  - Redémarrer le broker après toute modification, car les politiques sont
865
1128
  chargées une seule fois au démarrage.
@@ -867,5 +1130,11 @@ Si Alice tente `start_motor` sur le four critique :
867
1130
  ## Pour aller plus loin
868
1131
 
869
1132
  - [Référence complète de configuration](../docs/config.md)
870
- - [Guide OAuth du broker](../../docs/authorization.md)
871
- - [Autorisation hiérarchique](../../docs/hierarchical-authorization.md)
1133
+ - [Guide OAuth du broker](../../../../docs/authorization.md)
1134
+ - [Autorisation hiérarchique](../../../../docs/hierarchical-authorization.md)
1135
+ - [Points d'entrée et transports](../../../../docs/endpoints.md)
1136
+
1137
+ Ou demandez au broker lui-même : le slot réservé `_broker` expose un outil
1138
+ `broker_guide` (guides d'intégration, écrits depuis le code source) et un outil
1139
+ `broker_diagnose` (état en direct et problèmes prouvés, chacun avec sa
1140
+ correction).
@@ -7,16 +7,79 @@ broker, then adapt to your needs:
7
7
  cp -r .mcp-broker.example .mcp-broker
8
8
  ```
9
9
 
10
+ **Read this before you copy.** `config.json` is a *production reference*: it
11
+ shows every key the broker understands, filled in with values from an imagined
12
+ factory deployment. Two consequences.
13
+
14
+ 1. **Authorization ships off** (`auth.enabled: false`), so a fresh copy starts
15
+ and answers. The whole `auth` block is still there as the reference for when
16
+ you turn it on: flip `enabled` to `true`, then replace
17
+ `identity.factory.local` with your own authorization server and
18
+ `providerSecret: "change-me"` with a real secret. See
19
+ [`docs/authorization.md`](../../../../docs/authorization.md).
20
+ 2. **Other values point at hosts and files that do not exist here**: the TLS
21
+ material under `certs/`, the `www/` directory, the `/data` filesystem
22
+ upstream, the `.mcpb` bundle under `bundles/`. Delete the sections you do not
23
+ use. A missing directory is skipped with a warning; a missing TLS file stops
24
+ startup, with a message naming the two files and the three ways out.
25
+
26
+ So a straight `cp -r` does not run yet. Do one of these first:
27
+
28
+ ```sh
29
+ # either: run without TLS, which ignores the tls block entirely
30
+ MCP_BROKER_PROTOCOL=http npx @cyanmycelium/mcp-broker
31
+
32
+ # or: put a self-signed pair where the config says
33
+ mkdir -p .mcp-broker/certs
34
+ openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj "/CN=localhost" \
35
+ -keyout .mcp-broker/certs/key.pem -out .mcp-broker/certs/cert.pem
36
+ ```
37
+
38
+ (Working inside this repository, `npm run gen-cert -w @cyanmycelium/mcp-broker`
39
+ does the same thing without openssl. That script is not usable from an installed
40
+ package: it needs a development dependency the published package does not
41
+ carry.)
42
+
43
+ If you take the second route, remember the `allowedOrigins` entries below become
44
+ `http://`, since the scheme is part of the comparison.
45
+
10
46
  Property-by-property educational guides:
11
47
 
12
48
  - [English](CONFIGURATION-EN.md)
13
49
  - [Français](CONFIGURATION-FR.md)
14
50
 
51
+ ## Bridging an MCP host over stdio
52
+
53
+ `config.stdio-bridge.json` is the second, minimal file in this folder: it turns
54
+ the broker into a stdio MCP server for a host such as Claude Desktop, bridged to
55
+ the reserved `_all` slot.
56
+
57
+ ```json
58
+ {
59
+ "command": "npx",
60
+ "args": ["-y", "@cyanmycelium/mcp-broker"],
61
+ "env": { "MCP_BROKER_CONFIG": "/abs/path/to/.mcp-broker/config.stdio-bridge.json" }
62
+ }
63
+ ```
64
+
65
+ Pin the bridge to `_all`, not to a real slot. `_all` exists from startup and
66
+ answers the handshake itself, so the host connects even though no provider has
67
+ arrived yet, and it pushes `notifications/tools/list_changed` as providers join,
68
+ so a browser page opened later shows up live. Pinned to a real slot, the host
69
+ starts before the provider does and fails the handshake every time. Providers
70
+ join `_all` by opting in: `aggregate: true` on an upstream, or
71
+ `{ aggregate: true }` on `DirectTransport` / `MultiplexTransport.create`.
72
+
73
+ Keep that file separate from `config.json`: with `stdioProvider` set, stdout
74
+ carries JSON-RPC and every log line moves to stderr, so a broker started that
75
+ way in a terminal looks like it is doing nothing.
76
+
15
77
  ## Layout
16
78
 
17
79
  ```
18
80
  .mcp-broker/
19
81
  ├── config.json ← broker configuration (port, locale, TLS, mounts, ...)
82
+ ├── config.stdio-bridge.json ← minimal config for an MCP host over stdio (optional)
20
83
  ├── certs/ ← TLS material (optional, gitignore this)
21
84
  │ ├── cert.pem
22
85
  │ └── key.pem
@@ -31,8 +94,16 @@ Property-by-property educational guides:
31
94
  └── index.html
32
95
  ```
33
96
 
34
- A ready-made instance UI lives at [`node/web/`](../web/), point a `www`
35
- mount at it (`"dir": "../web"`) to serve it. See [`node/web/README.md`](../web/README.md).
97
+ A ready-made instance UI lives at
98
+ [`node/packages/broker/web/`](../web/), point a `www` mount at it
99
+ (`"dir": "../web"`) to serve it. See [its README](../web/README.md).
100
+
101
+ The page is served by the broker but is still a *browser* origin, so it has to
102
+ be listed in `allowedOrigins` before it may call `/<slot>/mcp`, `/<slot>/sse` or
103
+ `/<slot>/messages`. Being served by the same broker exempts nothing. With
104
+ `port: 3001` and no TLS, that is `"http://localhost:3001"`; the template ships
105
+ `https://` entries because it also sets `tls.cert`/`tls.key`, which makes the
106
+ broker speak HTTPS.
36
107
 
37
108
  ## Path resolution
38
109