@cyanmycelium/mcp-broker 0.3.0 → 1.0.1
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.
- package/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
- package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
- package/.mcp-broker.example/README.md +5 -0
- package/.mcp-broker.example/config.json +86 -0
- package/README.md +116 -1
- package/dist/auth/auth.config.d.ts +40 -0
- package/dist/auth/auth.config.js +62 -0
- package/dist/auth/auth.config.js.map +1 -0
- package/dist/auth/auth.types.d.ts +119 -0
- package/dist/auth/auth.types.js +38 -0
- package/dist/auth/auth.types.js.map +1 -0
- package/dist/auth/http.auth.d.ts +51 -0
- package/dist/auth/http.auth.js +121 -0
- package/dist/auth/http.auth.js.map +1 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.js +12 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/jwt.validator.d.ts +35 -0
- package/dist/auth/jwt.validator.js +46 -0
- package/dist/auth/jwt.validator.js.map +1 -0
- package/dist/auth/provider.auth.d.ts +51 -0
- package/dist/auth/provider.auth.js +80 -0
- package/dist/auth/provider.auth.js.map +1 -0
- package/dist/auth/resource.metadata.d.ts +32 -0
- package/dist/auth/resource.metadata.js +26 -0
- package/dist/auth/resource.metadata.js.map +1 -0
- package/dist/authorization/audit.d.ts +14 -0
- package/dist/authorization/audit.js +20 -0
- package/dist/authorization/audit.js.map +1 -0
- package/dist/authorization/capability.classifier.d.ts +30 -0
- package/dist/authorization/capability.classifier.js +85 -0
- package/dist/authorization/capability.classifier.js.map +1 -0
- package/dist/authorization/index.d.ts +13 -0
- package/dist/authorization/index.js +8 -0
- package/dist/authorization/index.js.map +1 -0
- package/dist/authorization/policy.engine.d.ts +11 -0
- package/dist/authorization/policy.engine.js +192 -0
- package/dist/authorization/policy.engine.js.map +1 -0
- package/dist/authorization/policy.types.d.ts +92 -0
- package/dist/authorization/policy.types.js +2 -0
- package/dist/authorization/policy.types.js.map +1 -0
- package/dist/authorization/resource.path.d.ts +27 -0
- package/dist/authorization/resource.path.js +122 -0
- package/dist/authorization/resource.path.js.map +1 -0
- package/dist/authorization/runtime.d.ts +22 -0
- package/dist/authorization/runtime.js +34 -0
- package/dist/authorization/runtime.js.map +1 -0
- package/dist/authorization/slot.resource.d.ts +12 -0
- package/dist/authorization/slot.resource.js +41 -0
- package/dist/authorization/slot.resource.js.map +1 -0
- package/dist/authorization/subject.mapper.d.ts +15 -0
- package/dist/authorization/subject.mapper.js +67 -0
- package/dist/authorization/subject.mapper.js.map +1 -0
- package/dist/bin.js +49 -0
- package/dist/bin.js.map +1 -1
- package/dist/broker/adapters/broker.adapter.info.d.ts +3 -3
- package/dist/broker/adapters/broker.adapter.info.js +1 -1
- package/dist/broker/adapters/broker.adapter.info.js.map +1 -1
- package/dist/broker/adapters/broker.adapter.providers.d.ts +2 -2
- package/dist/broker/adapters/broker.adapter.providers.js.map +1 -1
- package/dist/broker/aggregate/aggregate.catalog.d.ts +23 -11
- package/dist/broker/aggregate/aggregate.catalog.js +14 -0
- package/dist/broker/aggregate/aggregate.catalog.js.map +1 -1
- package/dist/broker/aggregate/aggregate.server.d.ts +22 -3
- package/dist/broker/aggregate/aggregate.server.js +115 -9
- package/dist/broker/aggregate/aggregate.server.js.map +1 -1
- package/dist/broker/aggregate/provider.client.session.d.ts +9 -9
- package/dist/broker/aggregate/provider.client.session.js +1 -1
- package/dist/broker/aggregate/provider.client.session.js.map +1 -1
- package/dist/broker/behaviors/broker.behavior.info.d.ts +2 -2
- package/dist/broker/behaviors/broker.behavior.info.js.map +1 -1
- package/dist/broker/behaviors/broker.behavior.providers.d.ts +2 -2
- package/dist/broker/behaviors/broker.behavior.providers.js.map +1 -1
- package/dist/broker/broker.context.d.ts +8 -4
- package/dist/broker/broker.grammars.d.ts +52 -86
- package/dist/broker/broker.grammars.js +55 -84
- package/dist/broker/broker.grammars.js.map +1 -1
- package/dist/broker/broker.server.d.ts +28 -24
- package/dist/broker/broker.server.js +21 -70
- package/dist/broker/broker.server.js.map +1 -1
- package/dist/broker/index.d.ts +4 -4
- package/dist/broker/index.js +1 -1
- package/dist/broker/index.js.map +1 -1
- package/dist/config.d.ts +51 -5
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mcpb.loader.d.ts +6 -4
- package/dist/mcpb.loader.js +3 -3
- package/dist/mcpb.loader.js.map +1 -1
- package/dist/remote.transports.d.ts +4 -2
- package/dist/remote.transports.js.map +1 -1
- package/dist/remote.upstream.d.ts +6 -4
- package/dist/remote.upstream.js.map +1 -1
- package/dist/stdio.upstream.d.ts +6 -4
- package/dist/stdio.upstream.js.map +1 -1
- package/dist/upstream.d.ts +3 -1
- package/dist/ws.tunnel.builder.d.ts +41 -4
- package/dist/ws.tunnel.builder.js +66 -0
- package/dist/ws.tunnel.builder.js.map +1 -1
- package/dist/ws.tunnel.d.ts +116 -36
- package/dist/ws.tunnel.js +384 -51
- package/dist/ws.tunnel.js.map +1 -1
- package/package.json +7 -3
- package/src/auth/auth.config.ts +98 -0
- package/src/auth/auth.types.ts +145 -0
- package/src/auth/http.auth.ts +132 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/jwt.validator.ts +63 -0
- package/src/auth/provider.auth.ts +114 -0
- package/src/auth/resource.metadata.ts +41 -0
- package/src/authorization/audit.ts +35 -0
- package/src/authorization/capability.classifier.ts +114 -0
- package/src/authorization/index.ts +37 -0
- package/src/authorization/policy.engine.ts +212 -0
- package/src/authorization/policy.types.ts +105 -0
- package/src/authorization/resource.path.ts +126 -0
- package/src/authorization/runtime.ts +56 -0
- package/src/authorization/slot.resource.ts +50 -0
- package/src/authorization/subject.mapper.ts +81 -0
- package/src/bin.ts +53 -0
- package/src/broker/adapters/broker.adapter.info.ts +3 -3
- package/src/broker/adapters/broker.adapter.providers.ts +3 -3
- package/src/broker/aggregate/aggregate.catalog.ts +48 -20
- package/src/broker/aggregate/aggregate.server.ts +146 -16
- package/src/broker/aggregate/provider.client.session.ts +22 -22
- package/src/broker/behaviors/broker.behavior.info.ts +2 -2
- package/src/broker/behaviors/broker.behavior.providers.ts +2 -2
- package/src/broker/broker.context.ts +10 -4
- package/src/broker/broker.grammars.ts +77 -122
- package/src/broker/broker.server.ts +51 -101
- package/src/broker/index.ts +5 -7
- package/src/config.ts +57 -6
- package/src/index.ts +109 -16
- package/src/mcpb.loader.ts +14 -11
- package/src/remote.transports.ts +8 -5
- package/src/remote.upstream.ts +10 -7
- package/src/stdio.upstream.ts +8 -5
- package/src/upstream.ts +4 -1
- package/src/ws.tunnel.builder.ts +89 -9
- package/src/ws.tunnel.ts +521 -102
- package/web/README.md +94 -0
- package/web/assets/logo.png +0 -0
- package/web/broker-self-mcp.html +338 -0
- package/web/css/styles.css +580 -0
- package/web/demos/DemoPlaceholder.html +258 -0
- package/web/demos/broker-explorer/css/app.css +393 -0
- package/web/demos/broker-explorer/index.html +94 -0
- package/web/demos/broker-explorer/js/app.js +271 -0
- package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
- package/web/demos/oauth-lab/README.md +68 -0
- package/web/demos/oauth-lab/config.json +116 -0
- package/web/demos/oauth-lab/css/app.css +1097 -0
- package/web/demos/oauth-lab/index.html +323 -0
- package/web/demos/oauth-lab/js/app.js +630 -0
- package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
- package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
- package/web/demos/oauth-lab/server/smoke-test.mjs +261 -0
- package/web/demos/oauth-lab/server/start.mjs +106 -0
- package/web/demos/provider-tunnel/css/app.css +384 -0
- package/web/demos/provider-tunnel/index.html +99 -0
- package/web/demos/provider-tunnel/js/app.js +226 -0
- package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
- package/web/index.html +558 -0
- package/web/js/lib/broker-tunnel.js +173 -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)
|