@cyanmycelium/mcp-broker 0.4.0 → 1.2.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 (151) hide show
  1. package/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
  2. package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
  3. package/.mcp-broker.example/README.md +8 -3
  4. package/.mcp-broker.example/config.json +86 -0
  5. package/README.md +122 -3
  6. package/dist/bin.d.ts +0 -1
  7. package/dist/bin.js +180 -208
  8. package/dist/bin.js.map +1 -1
  9. package/dist/chunk-FTDKH2C4.js +3670 -0
  10. package/dist/chunk-FTDKH2C4.js.map +1 -0
  11. package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
  12. package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
  13. package/dist/index.d.ts +1790 -18
  14. package/dist/index.js +2 -14
  15. package/dist/index.js.map +1 -1
  16. package/package.json +14 -8
  17. package/scripts/copy-assets.mjs +16 -10
  18. package/scripts/gen-cert.mjs +5 -5
  19. package/scripts/pack-mcpb.mjs +5 -5
  20. package/scripts/sign-bundle.mjs +6 -6
  21. package/src/auth/auth.config.ts +98 -0
  22. package/src/auth/auth.types.ts +120 -0
  23. package/src/auth/http.auth.ts +128 -0
  24. package/src/auth/index.ts +34 -0
  25. package/src/auth/jwt.validator.ts +63 -0
  26. package/src/auth/provider.auth.ts +114 -0
  27. package/src/auth/resource.metadata.ts +34 -0
  28. package/src/authorization/audit.ts +35 -0
  29. package/src/authorization/capability.classifier.ts +114 -0
  30. package/src/authorization/index.ts +37 -0
  31. package/src/authorization/policy.engine.ts +212 -0
  32. package/src/authorization/policy.types.ts +105 -0
  33. package/src/authorization/resource.path.ts +126 -0
  34. package/src/authorization/runtime.ts +56 -0
  35. package/src/authorization/slot.resource.ts +50 -0
  36. package/src/authorization/subject.mapper.ts +81 -0
  37. package/src/bin.ts +90 -7
  38. package/src/broker/adapters/broker.adapter.info.ts +3 -3
  39. package/src/broker/adapters/broker.adapter.providers.ts +3 -3
  40. package/src/broker/aggregate/aggregate.catalog.ts +49 -21
  41. package/src/broker/aggregate/aggregate.server.ts +149 -19
  42. package/src/broker/aggregate/provider.client.session.ts +22 -22
  43. package/src/broker/behaviors/broker.behavior.info.ts +4 -4
  44. package/src/broker/behaviors/broker.behavior.providers.ts +6 -6
  45. package/src/broker/broker.context.ts +10 -4
  46. package/src/broker/broker.grammars.ts +16 -13
  47. package/src/broker/broker.server.ts +12 -9
  48. package/src/broker/grammars/claude/en.json +1 -1
  49. package/src/broker/grammars/claude/fr.json +1 -1
  50. package/src/broker/index.ts +9 -9
  51. package/src/config.ts +81 -8
  52. package/src/index.ts +121 -20
  53. package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +24 -21
  54. package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
  55. package/src/remote.transports.ts +11 -8
  56. package/src/remote.upstream.ts +11 -8
  57. package/src/stdio.upstream.ts +9 -6
  58. package/src/upstream.ts +6 -3
  59. package/src/ws/ws.interfaces.ts +357 -0
  60. package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +112 -9
  61. package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +646 -457
  62. package/web/README.md +94 -0
  63. package/web/assets/logo.png +0 -0
  64. package/web/broker-self-mcp.html +338 -0
  65. package/web/css/styles.css +580 -0
  66. package/web/demos/DemoPlaceholder.html +258 -0
  67. package/web/demos/broker-explorer/css/app.css +393 -0
  68. package/web/demos/broker-explorer/index.html +94 -0
  69. package/web/demos/broker-explorer/js/app.js +271 -0
  70. package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
  71. package/web/demos/oauth-lab/README.md +94 -0
  72. package/web/demos/oauth-lab/config.json +117 -0
  73. package/web/demos/oauth-lab/css/app.css +1097 -0
  74. package/web/demos/oauth-lab/index.html +323 -0
  75. package/web/demos/oauth-lab/js/app.js +654 -0
  76. package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
  77. package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
  78. package/web/demos/oauth-lab/server/smoke-test.mjs +308 -0
  79. package/web/demos/oauth-lab/server/start.mjs +106 -0
  80. package/web/demos/provider-tunnel/css/app.css +384 -0
  81. package/web/demos/provider-tunnel/index.html +99 -0
  82. package/web/demos/provider-tunnel/js/app.js +226 -0
  83. package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
  84. package/web/index.html +558 -0
  85. package/web/js/lib/broker-tunnel.js +173 -0
  86. package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
  87. package/dist/broker/adapters/broker.adapter.info.js +0 -43
  88. package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
  89. package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
  90. package/dist/broker/adapters/broker.adapter.providers.js +0 -61
  91. package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
  92. package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -54
  93. package/dist/broker/aggregate/aggregate.catalog.js +0 -105
  94. package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
  95. package/dist/broker/aggregate/aggregate.server.d.ts +0 -47
  96. package/dist/broker/aggregate/aggregate.server.js +0 -151
  97. package/dist/broker/aggregate/aggregate.server.js.map +0 -1
  98. package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
  99. package/dist/broker/aggregate/provider.client.session.js +0 -140
  100. package/dist/broker/aggregate/provider.client.session.js.map +0 -1
  101. package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
  102. package/dist/broker/behaviors/broker.behavior.info.js +0 -41
  103. package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
  104. package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
  105. package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
  106. package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
  107. package/dist/broker/broker.context.d.ts +0 -59
  108. package/dist/broker/broker.context.js +0 -2
  109. package/dist/broker/broker.context.js.map +0 -1
  110. package/dist/broker/broker.grammars.d.ts +0 -130
  111. package/dist/broker/broker.grammars.js +0 -229
  112. package/dist/broker/broker.grammars.js.map +0 -1
  113. package/dist/broker/broker.server.d.ts +0 -66
  114. package/dist/broker/broker.server.js +0 -73
  115. package/dist/broker/broker.server.js.map +0 -1
  116. package/dist/broker/index.d.ts +0 -9
  117. package/dist/broker/index.js +0 -7
  118. package/dist/broker/index.js.map +0 -1
  119. package/dist/config.d.ts +0 -136
  120. package/dist/config.js +0 -61
  121. package/dist/config.js.map +0 -1
  122. package/dist/mcpb.loader.d.ts +0 -24
  123. package/dist/mcpb.loader.js +0 -161
  124. package/dist/mcpb.loader.js.map +0 -1
  125. package/dist/mcpb.unzip.d.ts +0 -6
  126. package/dist/mcpb.unzip.js +0 -95
  127. package/dist/mcpb.unzip.js.map +0 -1
  128. package/dist/remote.transports.d.ts +0 -16
  129. package/dist/remote.transports.js +0 -297
  130. package/dist/remote.transports.js.map +0 -1
  131. package/dist/remote.upstream.d.ts +0 -36
  132. package/dist/remote.upstream.js +0 -52
  133. package/dist/remote.upstream.js.map +0 -1
  134. package/dist/stdio.upstream.d.ts +0 -45
  135. package/dist/stdio.upstream.js +0 -85
  136. package/dist/stdio.upstream.js.map +0 -1
  137. package/dist/upstream.d.ts +0 -33
  138. package/dist/upstream.js +0 -2
  139. package/dist/upstream.js.map +0 -1
  140. package/dist/version.d.ts +0 -2
  141. package/dist/version.js +0 -9
  142. package/dist/version.js.map +0 -1
  143. package/dist/ws.tunnel.builder.d.ts +0 -139
  144. package/dist/ws.tunnel.builder.js +0 -205
  145. package/dist/ws.tunnel.builder.js.map +0 -1
  146. package/dist/ws.tunnel.d.ts +0 -373
  147. package/dist/ws.tunnel.js +0 -1090
  148. package/dist/ws.tunnel.js.map +0 -1
  149. /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
  150. /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
  151. /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
@@ -0,0 +1,861 @@
1
+ # Educational guide to `config.json`
2
+
3
+ This document explains the [`config.json`](config.json) file property by
4
+ property. It is intended for developers who are not yet familiar with OAuth,
5
+ JWTs, or permission models.
6
+
7
+ ## Before you begin
8
+
9
+ The real configuration file uses strict JSON. JSON does not support comments.
10
+ Do not add lines beginning with `//` to `config.json`.
11
+
12
+ Comments and partial examples in this guide are for explanation only. They
13
+ must not be copied directly into the JSON file.
14
+
15
+ Basic reading rules:
16
+
17
+ - `{` opens an object, which is a collection of properties.
18
+ - `}` closes an object.
19
+ - `[` opens a list.
20
+ - `]` closes a list.
21
+ - `,` separates properties or list items.
22
+ - Spaces used to align values do not change behavior.
23
+ - Relative file paths are resolved from the `.mcp-broker/` directory.
24
+
25
+ ## The mental model
26
+
27
+ The file answers five questions:
28
+
29
+ 1. Where does the broker listen?
30
+ 2. How are network connections encrypted?
31
+ 3. How does the broker identify clients and providers?
32
+ 4. What may each client do, and on which resources?
33
+ 5. Which local or packaged MCP servers should be loaded?
34
+
35
+ The `auth` block is the most security-sensitive part. Read it this way:
36
+
37
+ ```text
38
+ JWT subjects Roles and capabilities Resource paths
39
+ who? what? where?
40
+ \ | /
41
+ \ | /
42
+ allow or deny decision
43
+ ```
44
+
45
+ ## OAuth and authorization vocabulary
46
+
47
+ | Term | Plain-language explanation |
48
+ |---|---|
49
+ | OAuth 2.1 | Protocol that lets a client present a token to the broker. The broker does not issue this token |
50
+ | Authorization Server | External server that authenticates the user and issues the token |
51
+ | JWT | Common token format. It contains properties called claims |
52
+ | Claim | Property inside a JWT, such as `sub`, `groups`, or `client_id` |
53
+ | JWKS | Public endpoint containing the keys used to verify JWT signatures |
54
+ | OAuth scope | Coarse permission carried by the JWT and checked before detailed policy evaluation |
55
+ | Subject | Identity derived from the JWT, such as `user:alice` or `group:energy-team` |
56
+ | Capability | Stable functional action, such as `mcp.tools.diagnose` |
57
+ | Role | Reusable collection of capabilities |
58
+ | Resource | Stable location in the hierarchy, such as `/enterprise/site/area/asset` |
59
+ | Assignment | Grant of a role to a subject on a resource |
60
+ | Deny | Explicit prohibition that always overrides an allow |
61
+ | Slot | Technical name used to reach an MCP provider |
62
+ | Provider | MCP server that publishes tools, resources, or prompts through a slot |
63
+
64
+ ## Authorization decision order
65
+
66
+ For each protected request, the broker follows these steps:
67
+
68
+ 1. It reads the bearer token from the HTTP `Authorization` header.
69
+ 2. It verifies the JWT signature, issuer, audience, and expiration.
70
+ 3. It checks `requiredScopes` or the slot-specific `perSlotScopes` rule.
71
+ 4. It converts JWT claims into subjects.
72
+ 5. It converts the MCP operation into a capability.
73
+ 6. It converts the slot name into a resource path.
74
+ 7. It finds roles assigned to the subjects on that path.
75
+ 8. It applies matching `denies`.
76
+ 9. A matching deny always rejects the request.
77
+ 10. Without a deny, at least one matching role must grant the capability.
78
+ 11. Without an explicit matching grant, the request is rejected.
79
+
80
+ This separation is important:
81
+
82
+ - OAuth scopes provide the first coarse security gate.
83
+ - Roles describe what is allowed.
84
+ - Resources describe where it is allowed.
85
+ - Subjects describe who receives the permission.
86
+
87
+ ## Lines 1 to 5: general settings
88
+
89
+ ```json
90
+ {
91
+ "port": 3001,
92
+ "host": "0.0.0.0",
93
+ "locale": "fr",
94
+ "brokerName": "broker-eu-west"
95
+ }
96
+ ```
97
+
98
+ ### `port`
99
+
100
+ TCP port on which the broker listens.
101
+
102
+ - `3001` means clients may use an address such as
103
+ `https://server-name:3001`.
104
+ - The `MCP_BROKER_PORT` environment variable can override this value.
105
+
106
+ ### `host`
107
+
108
+ Network interface on which the broker accepts connections.
109
+
110
+ - `0.0.0.0` means every network interface on the machine.
111
+ - For local development only, prefer `127.0.0.1`.
112
+ - Never expose `0.0.0.0` to an untrusted network without TLS and
113
+ authentication.
114
+
115
+ ### `locale`
116
+
117
+ Language used for descriptions exposed by the internal `_broker` provider.
118
+
119
+ - `fr` selects French.
120
+ - This value does not change capability names or resource paths.
121
+
122
+ ### `brokerName`
123
+
124
+ Logical name displayed by the broker introspection tools.
125
+
126
+ - It helps distinguish multiple broker instances.
127
+ - It has no effect on authorization.
128
+
129
+ ## Lines 7 to 11: HTTP and WebSocket paths
130
+
131
+ ```json
132
+ "paths": {
133
+ "provider": "/provider",
134
+ "client": "/",
135
+ "mcp": "/mcp"
136
+ }
137
+ ```
138
+
139
+ ### `paths.provider`
140
+
141
+ WebSocket prefix used by a provider connecting to the broker.
142
+
143
+ Example:
144
+
145
+ ```text
146
+ wss://mcp.factory.local/provider/spoony-00452
147
+ ```
148
+
149
+ The provider requests the `spoony-00452` slot.
150
+
151
+ ### `paths.client`
152
+
153
+ Prefix used by raw MCP WebSocket clients. The value `/` preserves the
154
+ historical URL form:
155
+
156
+ ```text
157
+ wss://mcp.factory.local/spoony-00452
158
+ ```
159
+
160
+ ### `paths.mcp`
161
+
162
+ Suffix used by the MCP Streamable HTTP transport.
163
+
164
+ For the `spoony-00452` slot, the URL becomes:
165
+
166
+ ```text
167
+ https://mcp.factory.local/spoony-00452/mcp
168
+ ```
169
+
170
+ The `providers`, `sse`, and `messages` paths are not overridden in this
171
+ example, so the broker uses their default values.
172
+
173
+ ## Lines 13 to 16: TLS
174
+
175
+ ```json
176
+ "tls": {
177
+ "cert": "certs/cert.pem",
178
+ "key": "certs/key.pem"
179
+ }
180
+ ```
181
+
182
+ TLS encrypts network traffic and enables HTTPS/WSS.
183
+
184
+ ### `tls.cert`
185
+
186
+ Path to the public certificate in PEM format.
187
+
188
+ In this example, the broker looks for:
189
+
190
+ ```text
191
+ .mcp-broker/certs/cert.pem
192
+ ```
193
+
194
+ ### `tls.key`
195
+
196
+ Path to the private key associated with the certificate.
197
+
198
+ This key is secret. It must never be committed to the Git repository.
199
+
200
+ The broker must be able to read both files. A mismatched certificate and key
201
+ pair prevents HTTPS startup.
202
+
203
+ ## Lines 18 to 23: static web files
204
+
205
+ ```json
206
+ "www": {
207
+ "open": false,
208
+ "mounts": [
209
+ { "urlPrefix": "/", "dir": "www" }
210
+ ]
211
+ }
212
+ ```
213
+
214
+ ### `www.open`
215
+
216
+ Controls whether the broker automatically opens a web browser.
217
+
218
+ - `false` is suitable for servers, containers, and headless environments.
219
+ - `true` is convenient during local development.
220
+
221
+ ### `www.mounts`
222
+
223
+ List of static directories served by the broker.
224
+
225
+ ### `urlPrefix`
226
+
227
+ URL prefix associated with the directory. Here, `/` represents the web root.
228
+
229
+ ### `dir`
230
+
231
+ Local directory containing the web files. Here, `www` resolves to:
232
+
233
+ ```text
234
+ .mcp-broker/www/
235
+ ```
236
+
237
+ This block does not automatically secure a web application. MCP routes are
238
+ protected by `auth`, but a static web application must also be designed not to
239
+ expose secrets.
240
+
241
+ ## Lines 25 to 35: enabling OAuth
242
+
243
+ ```json
244
+ "auth": {
245
+ "enabled": true,
246
+ "publicBaseUrl": "https://mcp.factory.local",
247
+ "authorizationServers": [
248
+ "https://identity.factory.local"
249
+ ],
250
+ "jwks": "https://identity.factory.local/.well-known/jwks.json",
251
+ "requiredScopes": ["mcp:call"],
252
+ "perSlotScopes": {
253
+ "_broker": ["broker:admin"]
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### `auth.enabled`
259
+
260
+ Enables OAuth authentication for MCP clients.
261
+
262
+ - `true` requires a valid bearer token.
263
+ - `false` preserves the historical unauthenticated mode.
264
+ - A detailed policy is useful only when clients have an authenticated
265
+ identity.
266
+
267
+ ### `auth.publicBaseUrl`
268
+
269
+ Public address through which clients reach the broker.
270
+
271
+ This value must match the address visible to clients, which may differ from the
272
+ internal process address.
273
+
274
+ It is also used to calculate the expected JWT audience. For the
275
+ `spoony-00452` slot, the expected audience is:
276
+
277
+ ```text
278
+ https://mcp.factory.local/spoony-00452/mcp
279
+ ```
280
+
281
+ A common mistake is to use `http://localhost:3001` while clients actually use a
282
+ public HTTPS reverse proxy.
283
+
284
+ ### `auth.authorizationServers`
285
+
286
+ List of external authorization servers advertised to clients.
287
+
288
+ In this example, `https://identity.factory.local`:
289
+
290
+ - authenticates users or applications;
291
+ - issues access tokens;
292
+ - remains external to the broker.
293
+
294
+ The broker does not become an identity provider.
295
+
296
+ ### `auth.jwks`
297
+
298
+ URL of the authorization server's JWKS document.
299
+
300
+ The broker downloads public keys from this endpoint to verify JWT signatures.
301
+ A public key can verify a token, but it cannot issue one.
302
+
303
+ Do not put a private key or OAuth client secret here.
304
+
305
+ ### `auth.requiredScopes`
306
+
307
+ OAuth scopes required by default before a client can reach a slot.
308
+
309
+ ```json
310
+ ["mcp:call"]
311
+ ```
312
+
313
+ means the JWT must contain the `mcp:call` scope.
314
+
315
+ This scope is not sufficient by itself when hierarchical policies are enabled.
316
+ It only opens the first gate. Roles, resources, and denies are evaluated next.
317
+
318
+ ### `auth.perSlotScopes`
319
+
320
+ Replaces `requiredScopes` for specific slots.
321
+
322
+ ```json
323
+ "_broker": ["broker:admin"]
324
+ ```
325
+
326
+ means the internal `_broker` slot requires `broker:admin` instead of
327
+ `mcp:call`.
328
+
329
+ This rule protects network access to `_broker`. Hierarchical policy then checks
330
+ the `broker.providers.read` capability on the reserved
331
+ `/_system/broker` resource.
332
+
333
+ The example file intentionally contains no assignment for
334
+ `/_system/broker`. By default, nobody can use `_broker` tools, even with the
335
+ `broker:admin` scope.
336
+
337
+ To grant this access, add an assignment such as:
338
+
339
+ ```json
340
+ {
341
+ "id": "broker-administrators",
342
+ "subject": "group:broker-administrators",
343
+ "role": "administrator",
344
+ "resource": "/_system/broker"
345
+ }
346
+ ```
347
+
348
+ The JWT must then contain both the `broker:admin` scope and the
349
+ `broker-administrators` group.
350
+
351
+ ## Lines 36 to 40: converting JWT claims into subjects
352
+
353
+ ```json
354
+ "subjectMapping": {
355
+ "userClaim": "sub",
356
+ "groupClaims": ["groups"],
357
+ "clientClaim": "client_id"
358
+ }
359
+ ```
360
+
361
+ The broker trusts only claims from an already validated JWT.
362
+
363
+ ### `userClaim`
364
+
365
+ Name of the claim containing the user identifier.
366
+
367
+ With:
368
+
369
+ ```json
370
+ { "sub": "alice" }
371
+ ```
372
+
373
+ the broker produces:
374
+
375
+ ```text
376
+ user:alice
377
+ ```
378
+
379
+ ### `groupClaims`
380
+
381
+ Claims containing the user's groups.
382
+
383
+ With:
384
+
385
+ ```json
386
+ { "groups": ["maintenance-area-a", "employees"] }
387
+ ```
388
+
389
+ the broker produces:
390
+
391
+ ```text
392
+ group:maintenance-area-a
393
+ group:employees
394
+ ```
395
+
396
+ The claim may be a single string or a list of strings. An invalid type causes
397
+ authorization to fail safely.
398
+
399
+ ### `clientClaim`
400
+
401
+ Claim containing the client application identifier.
402
+
403
+ With:
404
+
405
+ ```json
406
+ { "client_id": "local-ai-assistant" }
407
+ ```
408
+
409
+ the broker produces:
410
+
411
+ ```text
412
+ client:local-ai-assistant
413
+ ```
414
+
415
+ A single call may therefore have multiple identities at the same time, such as
416
+ one user, two groups, and one client application.
417
+
418
+ ## Lines 41 to 64: roles and capabilities
419
+
420
+ A role answers only the question "what may be done?" It never contains a
421
+ resource path.
422
+
423
+ ### `viewer` role
424
+
425
+ ```json
426
+ "viewer": {
427
+ "capabilities": [
428
+ "mcp.resources.read",
429
+ "mcp.tools.list",
430
+ "mcp.prompts.read"
431
+ ]
432
+ }
433
+ ```
434
+
435
+ This role allows:
436
+
437
+ - `mcp.resources.read`: list and read MCP resources;
438
+ - `mcp.tools.list`: view the tool catalog;
439
+ - `mcp.prompts.read`: list and read prompts.
440
+
441
+ It does not allow tool calls.
442
+
443
+ ### `maintenance` role
444
+
445
+ ```json
446
+ "maintenance": {
447
+ "inherits": ["viewer"],
448
+ "capabilities": [
449
+ "mcp.tools.call",
450
+ "mcp.tools.diagnose",
451
+ "mcp.tools.configure-analysis"
452
+ ]
453
+ }
454
+ ```
455
+
456
+ `inherits: ["viewer"]` means that `maintenance` also receives every capability
457
+ from `viewer`.
458
+
459
+ Its additional capabilities are:
460
+
461
+ - `mcp.tools.call`: call a tool without a more specific mapping;
462
+ - `mcp.tools.diagnose`: run a diagnostic;
463
+ - `mcp.tools.configure-analysis`: modify an analysis configuration.
464
+
465
+ ### `operator` role
466
+
467
+ ```json
468
+ "operator": {
469
+ "inherits": ["viewer"],
470
+ "capabilities": ["mcp.tools.operate"]
471
+ }
472
+ ```
473
+
474
+ This role can view resources, tools, and prompts through `viewer`, then perform
475
+ operations classified as `mcp.tools.operate`.
476
+
477
+ ### `administrator` role
478
+
479
+ ```json
480
+ "administrator": {
481
+ "capabilities": ["*"]
482
+ }
483
+ ```
484
+
485
+ `*` means every capability, but only on resources covered by an assignment.
486
+
487
+ Declaring a role does not grant it to anyone. The example file contains no
488
+ assignment for `administrator`, so nobody becomes an administrator from this
489
+ block alone.
490
+
491
+ ## Lines 65 to 78: assignments
492
+
493
+ An assignment expresses this sentence:
494
+
495
+ ```text
496
+ This subject receives this role on this resource.
497
+ ```
498
+
499
+ ### `maintenance-area-a` assignment
500
+
501
+ ```json
502
+ {
503
+ "id": "maintenance-area-a",
504
+ "subject": "group:maintenance-area-a",
505
+ "role": "maintenance",
506
+ "resource": "/enterprise-a/site-paris/area-a/**"
507
+ }
508
+ ```
509
+
510
+ #### `id`
511
+
512
+ Unique identifier used in validation and audit logs.
513
+
514
+ #### `subject`
515
+
516
+ Subject receiving the role. Here, it applies to every JWT containing the
517
+ `maintenance-area-a` group.
518
+
519
+ #### `role`
520
+
521
+ Exact name of a role declared in the `roles` block.
522
+
523
+ #### `resource`
524
+
525
+ Industrial subtree on which the role is valid.
526
+
527
+ The `/**` suffix means:
528
+
529
+ - the `/enterprise-a/site-paris/area-a` resource itself;
530
+ - every descendant, regardless of depth.
531
+
532
+ A provider added later under this area is automatically covered by the
533
+ assignment.
534
+
535
+ ### `energy-team` assignment
536
+
537
+ ```json
538
+ {
539
+ "id": "energy-team",
540
+ "subject": "group:energy-team",
541
+ "role": "viewer",
542
+ "resource": "/enterprise-a/site-paris/**"
543
+ }
544
+ ```
545
+
546
+ The `energy-team` group can view resources, tools, and prompts across the Paris
547
+ site, but it cannot call tools.
548
+
549
+ ### Wildcard meanings
550
+
551
+ | Form | Meaning |
552
+ |---|---|
553
+ | `/enterprise/site/asset` | This exact path only |
554
+ | `/enterprise/site/*` | One direct level below the site |
555
+ | `/enterprise/site/**` | The site and every descendant |
556
+
557
+ Regular expressions are not supported.
558
+
559
+ ## Lines 79 to 89: explicit deny
560
+
561
+ ```json
562
+ "denies": [
563
+ {
564
+ "id": "protect-critical-furnace",
565
+ "subject": "group:maintenance-area-a",
566
+ "capabilities": [
567
+ "mcp.tools.configure-analysis",
568
+ "mcp.tools.operate"
569
+ ],
570
+ "resource": "/enterprise-a/site-paris/area-a/line-2/cell-4/critical-furnace"
571
+ }
572
+ ]
573
+ ```
574
+
575
+ This rule prevents the maintenance group from:
576
+
577
+ - modifying analysis configuration;
578
+ - running an operational action;
579
+ - only on the specified critical furnace.
580
+
581
+ The group keeps its other permissions everywhere else in `area-a`.
582
+
583
+ A matching deny always overrides an allow assignment, regardless of rule order
584
+ in the file.
585
+
586
+ Use `"capabilities": ["*"]` to deny every capability on a specific resource.
587
+
588
+ ## Lines 90 to 93: technical names and stable resources
589
+
590
+ ```json
591
+ "slotResources": {
592
+ "spoony-00452": "/enterprise-a/site-paris/area-a/line-3/cell-2/motor-7",
593
+ "site-energy": "/enterprise-a/site-paris"
594
+ }
595
+ ```
596
+
597
+ The key on the left is the technical slot name. The value on the right is its
598
+ stable identity in the hierarchy.
599
+
600
+ ### `spoony-00452`
601
+
602
+ A client uses the technical slot:
603
+
604
+ ```text
605
+ /spoony-00452/mcp
606
+ ```
607
+
608
+ but the policy engine evaluates:
609
+
610
+ ```text
611
+ /enterprise-a/site-paris/area-a/line-3/cell-2/motor-7
612
+ ```
613
+
614
+ The provider may reconnect or change IP address without changing this
615
+ identity.
616
+
617
+ ### `site-energy`
618
+
619
+ This slot represents the Paris site itself. A resource does not need to be a
620
+ leaf such as a motor.
621
+
622
+ An undeclared slot normally becomes `/<slot-name>`. In an industrial
623
+ environment, explicit mappings are preferable because they preserve stable
624
+ identities.
625
+
626
+ ## Lines 94 to 99: global tool classification
627
+
628
+ ```json
629
+ "toolCapabilities": {
630
+ "get_electrical_state": "mcp.resources.read",
631
+ "diagnose_motor": "mcp.tools.diagnose",
632
+ "reset_baseline": "mcp.tools.configure-analysis",
633
+ "start_motor": "mcp.tools.operate"
634
+ }
635
+ ```
636
+
637
+ The broker never guesses permission from a tool name. This block explicitly
638
+ maps each tool to a capability.
639
+
640
+ | Tool | Required capability |
641
+ |---|---|
642
+ | `get_electrical_state` | Resource read |
643
+ | `diagnose_motor` | Diagnostic |
644
+ | `reset_baseline` | Analysis configuration change |
645
+ | `start_motor` | Equipment operation |
646
+
647
+ If a tool is absent from every mapping, the broker uses the generic
648
+ `mcp.tools.call` capability.
649
+
650
+ This fallback explains why the `maintenance` role also contains
651
+ `mcp.tools.call`.
652
+
653
+ ## Lines 100 to 104: resource-specific tool classification
654
+
655
+ ```json
656
+ "providerToolCapabilities": {
657
+ "/enterprise-a/site-paris/area-a/**": {
658
+ "start_motor": "mcp.tools.operate"
659
+ }
660
+ }
661
+ ```
662
+
663
+ This block can change a tool's classification for one resource or subtree.
664
+
665
+ Resolution order:
666
+
667
+ 1. resource-specific mapping in `providerToolCapabilities`;
668
+ 2. global mapping in `toolCapabilities`;
669
+ 3. generic `mcp.tools.call` capability.
670
+
671
+ In this example, the specific `start_motor` value is the same as the global
672
+ value. This duplication is intentionally educational. In a real deployment,
673
+ this block is useful when the same tool name has a different risk level for a
674
+ particular provider or area.
675
+
676
+ ## Lines 105 to 107: audit logging
677
+
678
+ ```json
679
+ "audit": {
680
+ "logAllowed": false
681
+ }
682
+ ```
683
+
684
+ Denied decisions are always logged.
685
+
686
+ `logAllowed: false` means successful decisions are not logged. This is the
687
+ recommended setting because it avoids excessive log volume.
688
+
689
+ Temporarily set it to `true` when learning the policy or diagnosing a problem.
690
+ Audit records contain the decision and matching policy identifiers, but never
691
+ the bearer token or provider secret.
692
+
693
+ ## Line 108: shared provider secret
694
+
695
+ ```json
696
+ "providerSecret": "change-me"
697
+ ```
698
+
699
+ This secret authenticates MCP servers connecting to `/provider/<slot>` or
700
+ `/providers`.
701
+
702
+ It is independent from client bearer tokens.
703
+
704
+ The `change-me` value is only a placeholder. In production:
705
+
706
+ - generate a long, random value;
707
+ - preferably provide it through `MCP_BROKER_PROVIDER_SECRET`;
708
+ - never commit it to Git;
709
+ - never share it with MCP clients.
710
+
711
+ The shared secret preserves historical compatibility and allows every resource
712
+ path. To restrict each device to its own subtree, use a custom
713
+ `IProviderAuthenticator` that returns `IProviderPrincipal.allowedResources`.
714
+
715
+ ## Lines 111 to 117: local MCP server started by the broker
716
+
717
+ ```json
718
+ "stdioUpstreams": [
719
+ {
720
+ "name": "fs",
721
+ "command": "npx",
722
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
723
+ }
724
+ ]
725
+ ```
726
+
727
+ ### `name`
728
+
729
+ Slot name exposed by the broker. A client uses:
730
+
731
+ ```text
732
+ /fs/mcp
733
+ ```
734
+
735
+ ### `command`
736
+
737
+ Program started by the broker. Here, it is `npx`.
738
+
739
+ ### `args`
740
+
741
+ Arguments passed to the program:
742
+
743
+ - `-y` automatically accepts the installation requested by `npx`;
744
+ - `@modelcontextprotocol/server-filesystem` is the package to run;
745
+ - `/data` is the directory exposed to the server.
746
+
747
+ Filesystem access is sensitive. Restrict `/data` to the smallest required
748
+ directory.
749
+
750
+ Add `"aggregate": true` if this provider should also appear in `_all`.
751
+ Without this property, the stdio upstream remains available through its direct
752
+ slot only.
753
+
754
+ ## Lines 119 to 128: signed local MCP bundle
755
+
756
+ ```json
757
+ "mcpbBundles": [
758
+ {
759
+ "name": "weather",
760
+ "path": "bundles/weather.mcpb",
761
+ "publicKey": "bundles/mcpb-signing.pub.pem",
762
+ "signature": "bundles/weather.mcpb.sig",
763
+ "userConfig": { "apiKey": "your-key-here" },
764
+ "aggregate": true
765
+ }
766
+ ]
767
+ ```
768
+
769
+ ### `name`
770
+
771
+ Exposed slot name, here `weather`.
772
+
773
+ ### `path`
774
+
775
+ Path to the `.mcpb` bundle.
776
+
777
+ ### `publicKey`
778
+
779
+ Public key used to verify that the bundle was signed by a trusted source.
780
+
781
+ ### `signature`
782
+
783
+ Detached signature file corresponding to the bundle.
784
+
785
+ The broker refuses to start the bundle if the signature is missing or invalid.
786
+
787
+ ### `userConfig`
788
+
789
+ Values injected into the configuration declared by the bundle.
790
+
791
+ `apiKey` is an example secret. Never store a real API key in a public or shared
792
+ version of this file.
793
+
794
+ ### `aggregate`
795
+
796
+ `true` adds the `weather` provider to the `_all` aggregate slot.
797
+
798
+ Even inside `_all`, visibility and calls remain filtered by authorization
799
+ policy.
800
+
801
+ ## Complete decision example
802
+
803
+ Assume a validated JWT contains:
804
+
805
+ ```json
806
+ {
807
+ "sub": "alice",
808
+ "groups": ["maintenance-area-a"],
809
+ "client_id": "local-ai-assistant",
810
+ "scope": "mcp:call"
811
+ }
812
+ ```
813
+
814
+ Alice calls:
815
+
816
+ ```text
817
+ tool: diagnose_motor
818
+ slot: spoony-00452
819
+ ```
820
+
821
+ The broker calculates:
822
+
823
+ 1. The `mcp:call` scope passes the OAuth gate.
824
+ 2. The `groups` claim produces `group:maintenance-area-a`.
825
+ 3. `diagnose_motor` produces the `mcp.tools.diagnose` capability.
826
+ 4. `spoony-00452` produces the
827
+ `/enterprise-a/site-paris/area-a/line-3/cell-2/motor-7` resource.
828
+ 5. The `maintenance-area-a` assignment matches the subject and resource.
829
+ 6. The `maintenance` role contains `mcp.tools.diagnose`.
830
+ 7. No deny matches this motor.
831
+ 8. The final decision is allow.
832
+
833
+ If Alice attempts `start_motor` on the critical furnace:
834
+
835
+ 1. `start_motor` produces `mcp.tools.operate`.
836
+ 2. The `protect-critical-furnace` deny matches the resource.
837
+ 3. The deny has priority.
838
+ 4. The final decision is deny.
839
+
840
+ ## Pre-deployment checklist
841
+
842
+ - Replace every `.local` domain with the real address.
843
+ - Confirm that `publicBaseUrl` exactly matches the broker's public address.
844
+ - Confirm that JWTs use this resource as their audience.
845
+ - Verify the JWKS URL and expected issuer.
846
+ - Never keep `change-me`.
847
+ - Never publish the private TLS key.
848
+ - Never publish API keys from `userConfig`.
849
+ - Use `127.0.0.1` instead of `0.0.0.0` when network access is unnecessary.
850
+ - Test every role with a representative account.
851
+ - Test denies against critical assets.
852
+ - Confirm that `_all` does not reveal unauthorized providers.
853
+ - Return `audit.logAllowed` to `false` after troubleshooting.
854
+ - Restart the broker after each policy change because policies are loaded only
855
+ once at startup.
856
+
857
+ ## Further reading
858
+
859
+ - [Complete configuration reference](../docs/config.md)
860
+ - [Broker OAuth guide](../../docs/authorization.md)
861
+ - [Hierarchical authorization](../../docs/hierarchical-authorization.md)