@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.
- package/.mcp-broker.example/CONFIGURATION-EN.md +300 -44
- package/.mcp-broker.example/CONFIGURATION-FR.md +313 -44
- package/.mcp-broker.example/README.md +73 -2
- package/.mcp-broker.example/config.json +18 -9
- package/.mcp-broker.example/config.stdio-bridge.json +16 -0
- package/README.md +407 -27
- package/dist/bin.js +215 -20
- package/dist/bin.js.map +1 -1
- package/dist/chunk-BZUZYXVA.js +5955 -0
- package/dist/chunk-BZUZYXVA.js.map +1 -0
- package/dist/grammars/claude/en.json +12 -0
- package/dist/grammars/claude/fr.json +12 -0
- package/dist/grammars/default/en.json +40 -0
- package/dist/grammars/default/fr.json +40 -0
- package/dist/grammars/default/zh.json +40 -0
- package/dist/index.d.ts +991 -25
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/auth/index.ts +3 -1
- package/src/auth/provider.auth.ts +126 -8
- package/src/authorization/policy.engine.ts +11 -2
- package/src/authorization/policy.types.ts +25 -1
- package/src/bin.ts +325 -28
- package/src/broker/adapters/broker.adapter.diagnose.ts +45 -0
- package/src/broker/adapters/broker.adapter.guide.ts +108 -0
- package/src/broker/aggregate/aggregate.server.ts +82 -15
- package/src/broker/aggregate/provider.client.session.ts +85 -11
- package/src/broker/behaviors/broker.behavior.diagnose.ts +47 -0
- package/src/broker/behaviors/broker.behavior.guide.ts +79 -0
- package/src/broker/broker.context.ts +65 -0
- package/src/broker/broker.diagnostics.ts +495 -0
- package/src/broker/broker.guides.ts +1029 -0
- package/src/broker/broker.server.ts +23 -7
- package/src/broker/broker.slots.ts +36 -0
- package/src/broker/grammars/claude/en.json +12 -0
- package/src/broker/grammars/claude/fr.json +12 -0
- package/src/broker/grammars/default/en.json +40 -0
- package/src/broker/grammars/default/fr.json +40 -0
- package/src/broker/grammars/default/zh.json +40 -0
- package/src/config.ts +191 -4
- package/src/index.ts +38 -3
- package/src/remote.transports.ts +127 -10
- package/src/remote.upstream.ts +4 -1
- package/src/ws/ws.interfaces.ts +148 -3
- package/src/ws/ws.tunnel.builder.ts +63 -1
- package/src/ws/ws.tunnel.ts +1150 -173
- package/web/README.md +31 -4
- package/dist/chunk-FTDKH2C4.js +0 -3670
- package/dist/chunk-FTDKH2C4.js.map +0 -1
|
@@ -22,15 +22,25 @@ Basic reading rules:
|
|
|
22
22
|
- Spaces used to align values do not change behavior.
|
|
23
23
|
- Relative file paths are resolved from the `.mcp-broker/` directory.
|
|
24
24
|
|
|
25
|
+
Each section below is named after the key it explains, not after a line number,
|
|
26
|
+
so the guide stays correct when the example file grows.
|
|
27
|
+
|
|
28
|
+
One thing to know before anything else: **the template ships with
|
|
29
|
+
`auth.enabled: false`**. A fresh copy starts and answers every client, which is
|
|
30
|
+
what you want while you are finding your way. The whole `auth` block is present
|
|
31
|
+
as the reference for the day you turn it on. Read
|
|
32
|
+
[`docs/authorization.md`](../../../../docs/authorization.md) before you do.
|
|
33
|
+
|
|
25
34
|
## The mental model
|
|
26
35
|
|
|
27
|
-
The file answers
|
|
36
|
+
The file answers six questions:
|
|
28
37
|
|
|
29
38
|
1. Where does the broker listen?
|
|
30
39
|
2. How are network connections encrypted?
|
|
31
|
-
3.
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
40
|
+
3. Which browser pages, if any, may call it?
|
|
41
|
+
4. How does the broker identify clients and providers?
|
|
42
|
+
5. What may each client do, and on which resources?
|
|
43
|
+
6. Which local or packaged MCP servers should be loaded?
|
|
34
44
|
|
|
35
45
|
The `auth` block is the most security-sensitive part. Read it this way:
|
|
36
46
|
|
|
@@ -84,7 +94,7 @@ This separation is important:
|
|
|
84
94
|
- Resources describe where it is allowed.
|
|
85
95
|
- Subjects describe who receives the permission.
|
|
86
96
|
|
|
87
|
-
##
|
|
97
|
+
## General settings
|
|
88
98
|
|
|
89
99
|
```json
|
|
90
100
|
{
|
|
@@ -125,17 +135,69 @@ Logical name displayed by the broker introspection tools.
|
|
|
125
135
|
|
|
126
136
|
- It helps distinguish multiple broker instances.
|
|
127
137
|
- It has no effect on authorization.
|
|
138
|
+
- **Library-only today.** The tunnel honors it, but the command-line broker has
|
|
139
|
+
no way to forward it yet, so setting it here changes nothing. Set it through
|
|
140
|
+
the programmatic API (`IWsTunnelOptions.brokerName`) if you need it now.
|
|
141
|
+
|
|
142
|
+
## Browser origins
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
"allowedOrigins": ["https://app.factory.local", "https://mcp.factory.local"]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The list of web-page origins allowed to call this broker over HTTP. It is
|
|
149
|
+
checked on `/<slot>/mcp`, `/<slot>/sse` and `/<slot>/messages`.
|
|
150
|
+
|
|
151
|
+
Three rules worth memorizing, because each one surprises somebody:
|
|
152
|
+
|
|
153
|
+
1. **Absent means closed.** With no `allowedOrigins`, every request carrying an
|
|
154
|
+
`Origin` header is refused with `403`. That is deliberate: without the check,
|
|
155
|
+
any page the user happens to have open could drive your broker.
|
|
156
|
+
2. **A request with no `Origin` header always passes.** Claude Desktop, the MCP
|
|
157
|
+
Inspector and every server-side SDK send none, which is why the broker
|
|
158
|
+
appears to work perfectly until the first browser tries.
|
|
159
|
+
3. **Being served by this broker exempts nothing.** A page loaded from the `www`
|
|
160
|
+
mount is still a browser origin and must be listed.
|
|
161
|
+
|
|
162
|
+
Origins are compared verbatim, so the scheme and the port are part of the value:
|
|
163
|
+
`https://app.factory.local` does not match `http://app.factory.local` or
|
|
164
|
+
`https://app.factory.local:8443`, and a trailing slash never matches. This
|
|
165
|
+
template sets `tls.cert`/`tls.key`, so the broker speaks HTTPS and the entries
|
|
166
|
+
use `https://`. Drop the TLS block and they must become `http://...:3001`.
|
|
128
167
|
|
|
129
|
-
|
|
168
|
+
A regular expression is accepted instead of a list when the origins are not
|
|
169
|
+
known in advance:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
"allowedOrigins": { "pattern": "^https://[a-z0-9-]+\\.factory\\.local$" }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`MCP_BROKER_ALLOWED_ORIGINS` overrides the file with a comma-separated list. It
|
|
176
|
+
cannot carry the pattern form, since a regular expression does not survive
|
|
177
|
+
comma-splitting.
|
|
178
|
+
|
|
179
|
+
## HTTP and WebSocket paths
|
|
130
180
|
|
|
131
181
|
```json
|
|
132
182
|
"paths": {
|
|
133
|
-
"provider":
|
|
134
|
-
"
|
|
135
|
-
"
|
|
183
|
+
"provider": "/provider",
|
|
184
|
+
"providers": "/providers",
|
|
185
|
+
"client": "/",
|
|
186
|
+
"mcp": "/mcp",
|
|
187
|
+
"sse": "/sse",
|
|
188
|
+
"messages": "/messages"
|
|
136
189
|
}
|
|
137
190
|
```
|
|
138
191
|
|
|
192
|
+
All six keys are honored, and each is also settable through an environment
|
|
193
|
+
variable, which wins: `MCP_BROKER_PROVIDER_PATH`, `MCP_BROKER_PROVIDERS_PATH`,
|
|
194
|
+
`MCP_BROKER_CLIENT_PATH`, `MCP_BROKER_MCP_PATH`, `MCP_BROKER_SSE_PATH`,
|
|
195
|
+
`MCP_BROKER_MESSAGES_PATH`.
|
|
196
|
+
|
|
197
|
+
Change one and you move the endpoint for everybody: the provider SDK, the
|
|
198
|
+
clients, and the URLs printed in the startup banner. Leave them alone unless you
|
|
199
|
+
have a reason.
|
|
200
|
+
|
|
139
201
|
### `paths.provider`
|
|
140
202
|
|
|
141
203
|
WebSocket prefix used by a provider connecting to the broker.
|
|
@@ -146,7 +208,34 @@ Example:
|
|
|
146
208
|
wss://mcp.factory.local/provider/spoony-00452
|
|
147
209
|
```
|
|
148
210
|
|
|
149
|
-
The provider requests the `spoony-00452` slot.
|
|
211
|
+
The provider requests the `spoony-00452` slot. The socket carries plain
|
|
212
|
+
JSON-RPC frames, one provider per socket. In
|
|
213
|
+
`@cyanmycelium/mcp-broker-provider` this is `DirectTransport`.
|
|
214
|
+
|
|
215
|
+
### `paths.providers`
|
|
216
|
+
|
|
217
|
+
Exact WebSocket path (no slot appended) used by a provider that carries several
|
|
218
|
+
slots over a single socket, wrapping each frame in an envelope that names the
|
|
219
|
+
slot. In `@cyanmycelium/mcp-broker-provider` this is `MultiplexTransport`.
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
wss://mcp.factory.local/providers
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`paths.provider` and `paths.providers` are one letter apart and are **not
|
|
226
|
+
interchangeable**: they differ by framing, not just by URL. Pointing a
|
|
227
|
+
`MultiplexTransport` at `/provider/<name>`, or a `DirectTransport` at
|
|
228
|
+
`/providers`, is the single most common integration failure. The broker now
|
|
229
|
+
names the mismatch and refuses the socket rather than hanging, but the pairing
|
|
230
|
+
is worth getting right the first time:
|
|
231
|
+
|
|
232
|
+
| Endpoint | Framing | Transport |
|
|
233
|
+
|---------------------|--------------------|----------------------|
|
|
234
|
+
| `/provider/<name>` | plain JSON-RPC | `DirectTransport` |
|
|
235
|
+
| `/providers` | envelopes | `MultiplexTransport` |
|
|
236
|
+
|
|
237
|
+
`/providers/<name>` is neither, and is taken as a *client* connection on a slot
|
|
238
|
+
literally named `providers/<name>`.
|
|
150
239
|
|
|
151
240
|
### `paths.client`
|
|
152
241
|
|
|
@@ -167,10 +256,84 @@ For the `spoony-00452` slot, the URL becomes:
|
|
|
167
256
|
https://mcp.factory.local/spoony-00452/mcp
|
|
168
257
|
```
|
|
169
258
|
|
|
170
|
-
|
|
171
|
-
|
|
259
|
+
This is the transport to reach for. The two below it are the legacy pair.
|
|
260
|
+
|
|
261
|
+
### `paths.sse`
|
|
262
|
+
|
|
263
|
+
Suffix of the legacy SSE stream, opened with `GET`. The broker answers it with
|
|
264
|
+
an `endpoint` event carrying the URL to post to.
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
https://mcp.factory.local/spoony-00452/sse
|
|
268
|
+
```
|
|
172
269
|
|
|
173
|
-
|
|
270
|
+
### `paths.messages`
|
|
271
|
+
|
|
272
|
+
Suffix the legacy SSE client `POST`s its JSON-RPC requests to, paired with the
|
|
273
|
+
stream above.
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
https://mcp.factory.local/spoony-00452/messages
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Both legacy endpoints are subject to the same `allowedOrigins` check as
|
|
280
|
+
`/<slot>/mcp`.
|
|
281
|
+
|
|
282
|
+
## Provider liveness
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
"providerHeartbeatIntervalMs": 30000,
|
|
286
|
+
"providerRequestTimeoutMs": 60000,
|
|
287
|
+
"providerTakeover": "liveness"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Three optional keys. The defaults shown are the built-in ones, so you can delete
|
|
291
|
+
the block entirely; they are spelled out here because when a provider misbehaves
|
|
292
|
+
these are what you tune.
|
|
293
|
+
|
|
294
|
+
### `providerHeartbeatIntervalMs`
|
|
295
|
+
|
|
296
|
+
How often the broker pings each connected provider socket. A provider that
|
|
297
|
+
misses a full interval is disconnected and its slot freed. `0` disables the
|
|
298
|
+
heartbeat. Also `MCP_BROKER_PROVIDER_HEARTBEAT_MS`.
|
|
299
|
+
|
|
300
|
+
Without it, a socket whose peer vanished without closing (a killed browser tab,
|
|
301
|
+
a slept laptop, a dropped VPN) stays open as far as the operating system is
|
|
302
|
+
concerned for around two hours, during which the broker reports the slot as
|
|
303
|
+
connected and refuses every reconnection attempt.
|
|
304
|
+
|
|
305
|
+
Be honest about what it proves: a pong is answered by the peer's network stack,
|
|
306
|
+
not by the page's JavaScript. It detects a dead process, machine or network
|
|
307
|
+
path, not a provider that is connected and simply not answering. For that, see
|
|
308
|
+
the next key.
|
|
309
|
+
|
|
310
|
+
### `providerRequestTimeoutMs`
|
|
311
|
+
|
|
312
|
+
How long the broker waits for a provider to answer one request before failing it
|
|
313
|
+
with a JSON-RPC error naming the slot. `0` disables the deadline. Also
|
|
314
|
+
`MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS`.
|
|
315
|
+
|
|
316
|
+
Raise it if you host genuinely long-running tools. Lower it if you would rather
|
|
317
|
+
see an error than a client that waits forever, which is the alternative: a
|
|
318
|
+
browser tab throttled in the background is a connected provider that answers
|
|
319
|
+
nothing.
|
|
320
|
+
|
|
321
|
+
### `providerTakeover`
|
|
322
|
+
|
|
323
|
+
What happens when a provider connects to a slot another socket already holds.
|
|
324
|
+
|
|
325
|
+
- `"reject"`: the incumbent always keeps the slot.
|
|
326
|
+
- `"liveness"` (default): the incumbent keeps it only while it answers the
|
|
327
|
+
heartbeat.
|
|
328
|
+
- `"always"`: the newcomer wins, but only when provider authentication is
|
|
329
|
+
configured and it authenticated as the same principal as the incumbent.
|
|
330
|
+
Without provider authentication the broker falls back to `"liveness"` and says
|
|
331
|
+
so, because unconditional takeover would let anyone who can reach the URL
|
|
332
|
+
evict the real provider.
|
|
333
|
+
|
|
334
|
+
Also `MCP_BROKER_PROVIDER_TAKEOVER`.
|
|
335
|
+
|
|
336
|
+
## TLS
|
|
174
337
|
|
|
175
338
|
```json
|
|
176
339
|
"tls": {
|
|
@@ -200,7 +363,7 @@ This key is secret. It must never be committed to the Git repository.
|
|
|
200
363
|
The broker must be able to read both files. A mismatched certificate and key
|
|
201
364
|
pair prevents HTTPS startup.
|
|
202
365
|
|
|
203
|
-
##
|
|
366
|
+
## Static web files
|
|
204
367
|
|
|
205
368
|
```json
|
|
206
369
|
"www": {
|
|
@@ -213,10 +376,24 @@ pair prevents HTTPS startup.
|
|
|
213
376
|
|
|
214
377
|
### `www.open`
|
|
215
378
|
|
|
216
|
-
Controls whether the broker automatically opens a web browser.
|
|
379
|
+
Controls whether the broker automatically opens a web browser at startup.
|
|
380
|
+
|
|
381
|
+
- `false` (or absent) is suitable for servers, containers, and headless
|
|
382
|
+
environments.
|
|
383
|
+
- `true` opens the broker root, `https://localhost:3001/`.
|
|
384
|
+
- A string opens a specific page: `"/app/index.html"`, or an absolute URL on
|
|
385
|
+
this broker's own origin.
|
|
386
|
+
|
|
387
|
+
A URL on any other origin is refused with a message on stderr, and so is any
|
|
388
|
+
other string: a value that reaches the platform's "open this" command unchecked
|
|
389
|
+
can launch a local file or a registered application, and nothing about starting
|
|
390
|
+
a broker requires visiting another host. Open it yourself instead.
|
|
217
391
|
|
|
218
|
-
|
|
219
|
-
|
|
392
|
+
The browser opens only when a `www.mounts` entry actually covers the resolved
|
|
393
|
+
path. If none does, the broker says which prefixes are mounted rather than
|
|
394
|
+
launching a browser onto a `404`.
|
|
395
|
+
|
|
396
|
+
`MCP_BROKER_OPEN` carries the same values (`"1"` for the root).
|
|
220
397
|
|
|
221
398
|
### `www.mounts`
|
|
222
399
|
|
|
@@ -238,11 +415,11 @@ This block does not automatically secure a web application. MCP routes are
|
|
|
238
415
|
protected by `auth`, but a static web application must also be designed not to
|
|
239
416
|
expose secrets.
|
|
240
417
|
|
|
241
|
-
##
|
|
418
|
+
## Enabling OAuth
|
|
242
419
|
|
|
243
420
|
```json
|
|
244
421
|
"auth": {
|
|
245
|
-
"enabled":
|
|
422
|
+
"enabled": false,
|
|
246
423
|
"publicBaseUrl": "https://mcp.factory.local",
|
|
247
424
|
"authorizationServers": [
|
|
248
425
|
"https://identity.factory.local"
|
|
@@ -257,13 +434,23 @@ expose secrets.
|
|
|
257
434
|
|
|
258
435
|
### `auth.enabled`
|
|
259
436
|
|
|
260
|
-
Enables OAuth authentication for MCP clients.
|
|
437
|
+
Enables OAuth authentication for MCP clients. **This template ships it off.**
|
|
261
438
|
|
|
262
|
-
- `
|
|
263
|
-
|
|
439
|
+
- `false` (the shipped value) preserves the historical unauthenticated mode:
|
|
440
|
+
every client reaches every slot. Use it on a trusted network, and while you
|
|
441
|
+
are getting the rest working.
|
|
442
|
+
- `true` requires a valid bearer token on every client request. The rest of this
|
|
443
|
+
block then has to describe a real authorization server: with `enabled: true`
|
|
444
|
+
and the placeholder `identity.factory.local` values still in place, the broker
|
|
445
|
+
answers every client with a `401` whose challenge points at a host that does
|
|
446
|
+
not resolve, which is a confusing way to spend an afternoon.
|
|
264
447
|
- A detailed policy is useful only when clients have an authenticated
|
|
265
448
|
identity.
|
|
266
449
|
|
|
450
|
+
Everything below this point (`roles`, `assignments`, `denies`, `slotResources`,
|
|
451
|
+
`toolCapabilities`) is inert while `enabled` is `false`. It is kept in the
|
|
452
|
+
template as a worked example, not because it is doing anything.
|
|
453
|
+
|
|
267
454
|
### `auth.publicBaseUrl`
|
|
268
455
|
|
|
269
456
|
Public address through which clients reach the broker.
|
|
@@ -348,7 +535,7 @@ To grant this access, add an assignment such as:
|
|
|
348
535
|
The JWT must then contain both the `broker:admin` scope and the
|
|
349
536
|
`broker-administrators` group.
|
|
350
537
|
|
|
351
|
-
##
|
|
538
|
+
## Converting JWT claims into subjects
|
|
352
539
|
|
|
353
540
|
```json
|
|
354
541
|
"subjectMapping": {
|
|
@@ -415,7 +602,7 @@ client:local-ai-assistant
|
|
|
415
602
|
A single call may therefore have multiple identities at the same time, such as
|
|
416
603
|
one user, two groups, and one client application.
|
|
417
604
|
|
|
418
|
-
##
|
|
605
|
+
## Roles and capabilities
|
|
419
606
|
|
|
420
607
|
A role answers only the question "what may be done?" It never contains a
|
|
421
608
|
resource path.
|
|
@@ -488,7 +675,7 @@ Declaring a role does not grant it to anyone. The example file contains no
|
|
|
488
675
|
assignment for `administrator`, so nobody becomes an administrator from this
|
|
489
676
|
block alone.
|
|
490
677
|
|
|
491
|
-
##
|
|
678
|
+
## Assignments
|
|
492
679
|
|
|
493
680
|
An assignment expresses this sentence:
|
|
494
681
|
|
|
@@ -556,7 +743,7 @@ site, but it cannot call tools.
|
|
|
556
743
|
|
|
557
744
|
Regular expressions are not supported.
|
|
558
745
|
|
|
559
|
-
##
|
|
746
|
+
## Explicit deny
|
|
560
747
|
|
|
561
748
|
```json
|
|
562
749
|
"denies": [
|
|
@@ -585,7 +772,7 @@ in the file.
|
|
|
585
772
|
|
|
586
773
|
Use `"capabilities": ["*"]` to deny every capability on a specific resource.
|
|
587
774
|
|
|
588
|
-
##
|
|
775
|
+
## Technical names and stable resources
|
|
589
776
|
|
|
590
777
|
```json
|
|
591
778
|
"slotResources": {
|
|
@@ -623,7 +810,7 @@ An undeclared slot normally becomes `/<slot-name>`. In an industrial
|
|
|
623
810
|
environment, explicit mappings are preferable because they preserve stable
|
|
624
811
|
identities.
|
|
625
812
|
|
|
626
|
-
##
|
|
813
|
+
## Global tool classification
|
|
627
814
|
|
|
628
815
|
```json
|
|
629
816
|
"toolCapabilities": {
|
|
@@ -650,7 +837,7 @@ If a tool is absent from every mapping, the broker uses the generic
|
|
|
650
837
|
This fallback explains why the `maintenance` role also contains
|
|
651
838
|
`mcp.tools.call`.
|
|
652
839
|
|
|
653
|
-
##
|
|
840
|
+
## Resource-specific tool classification
|
|
654
841
|
|
|
655
842
|
```json
|
|
656
843
|
"providerToolCapabilities": {
|
|
@@ -673,7 +860,7 @@ value. This duplication is intentionally educational. In a real deployment,
|
|
|
673
860
|
this block is useful when the same tool name has a different risk level for a
|
|
674
861
|
particular provider or area.
|
|
675
862
|
|
|
676
|
-
##
|
|
863
|
+
## Audit logging
|
|
677
864
|
|
|
678
865
|
```json
|
|
679
866
|
"audit": {
|
|
@@ -690,16 +877,30 @@ Temporarily set it to `true` when learning the policy or diagnosing a problem.
|
|
|
690
877
|
Audit records contain the decision and matching policy identifiers, but never
|
|
691
878
|
the bearer token or provider secret.
|
|
692
879
|
|
|
693
|
-
##
|
|
880
|
+
## Shared provider secret
|
|
694
881
|
|
|
695
882
|
```json
|
|
696
883
|
"providerSecret": "change-me"
|
|
697
884
|
```
|
|
698
885
|
|
|
886
|
+
**Deliberately absent from the shipped template.** Add the key inside `auth` to
|
|
887
|
+
turn provider authentication on.
|
|
888
|
+
|
|
699
889
|
This secret authenticates MCP servers connecting to `/provider/<slot>` or
|
|
700
|
-
`/providers`.
|
|
890
|
+
`/providers`. Every provider must then present it as `X-Provider-Token` or
|
|
891
|
+
`Authorization: Bearer`, and one that does not is refused at the WebSocket
|
|
892
|
+
handshake.
|
|
893
|
+
|
|
894
|
+
It is independent from client bearer tokens, and in particular it is **not**
|
|
895
|
+
governed by `auth.enabled`: set it and provider authentication is on, even with
|
|
896
|
+
OAuth off. That is why the template does not ship it. Left in place with the
|
|
897
|
+
placeholder value, it would refuse every provider on a broker the reader
|
|
898
|
+
believes is running wide open.
|
|
701
899
|
|
|
702
|
-
|
|
900
|
+
One consequence to plan around: the browser `WebSocket` constructor cannot set
|
|
901
|
+
request headers, so a provider hosted in a web page cannot present the secret at
|
|
902
|
+
all. With `providerSecret` set, browser-hosted providers are locked out; they
|
|
903
|
+
need provider authentication off, or an authenticating reverse proxy in front.
|
|
703
904
|
|
|
704
905
|
The `change-me` value is only a placeholder. In production:
|
|
705
906
|
|
|
@@ -712,14 +913,15 @@ The shared secret preserves historical compatibility and allows every resource
|
|
|
712
913
|
path. To restrict each device to its own subtree, use a custom
|
|
713
914
|
`IProviderAuthenticator` that returns `IProviderPrincipal.allowedResources`.
|
|
714
915
|
|
|
715
|
-
##
|
|
916
|
+
## Local MCP server started by the broker
|
|
716
917
|
|
|
717
918
|
```json
|
|
718
919
|
"stdioUpstreams": [
|
|
719
920
|
{
|
|
720
|
-
"name":
|
|
721
|
-
"command":
|
|
722
|
-
"args":
|
|
921
|
+
"name": "fs",
|
|
922
|
+
"command": "npx",
|
|
923
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
|
|
924
|
+
"aggregate": true
|
|
723
925
|
}
|
|
724
926
|
]
|
|
725
927
|
```
|
|
@@ -747,11 +949,55 @@ Arguments passed to the program:
|
|
|
747
949
|
Filesystem access is sensitive. Restrict `/data` to the smallest required
|
|
748
950
|
directory.
|
|
749
951
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
slot
|
|
952
|
+
### `aggregate`
|
|
953
|
+
|
|
954
|
+
`true` makes this provider part of the reserved `_all` slot, in addition to its
|
|
955
|
+
own `/fs/mcp`. Without it the stdio upstream is reachable through its own slot
|
|
956
|
+
only.
|
|
957
|
+
|
|
958
|
+
Note the asymmetry: `stdioUpstreams` entries do **not** join `_all` by default,
|
|
959
|
+
while `mcpServers` and `mcpbBundles` entries do. Set it explicitly either way
|
|
960
|
+
and you never have to remember which is which.
|
|
961
|
+
|
|
962
|
+
## Bridging an MCP host over stdio
|
|
753
963
|
|
|
754
|
-
|
|
964
|
+
Not in `config.json`, but the reason most people set `aggregate` in the first
|
|
965
|
+
place. A second file in this folder, `config.stdio-bridge.json`, adds one key:
|
|
966
|
+
|
|
967
|
+
```json
|
|
968
|
+
"stdioProvider": "_all"
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
With it, the broker also behaves as a stdio MCP server: it reads JSON-RPC from
|
|
972
|
+
its standard input and writes answers to its standard output, bridging a host
|
|
973
|
+
such as Claude Desktop to one slot. Point the host's config at that file:
|
|
974
|
+
|
|
975
|
+
```json
|
|
976
|
+
{
|
|
977
|
+
"command": "npx",
|
|
978
|
+
"args": ["-y", "@cyanmycelium/mcp-broker"],
|
|
979
|
+
"env": { "MCP_BROKER_CONFIG": "/abs/path/to/.mcp-broker/config.stdio-bridge.json" }
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
Two things to get right.
|
|
984
|
+
|
|
985
|
+
- **Pin it to `_all`, not to a real slot.** `_all` exists from startup and
|
|
986
|
+
answers the handshake itself, so the host connects even though no provider has
|
|
987
|
+
arrived yet, and it announces new tools as providers join. Pinned to a real
|
|
988
|
+
slot, the host starts before the provider does, gets "not connected" for its
|
|
989
|
+
very first message, and gives up. For a provider hosted in a browser page that
|
|
990
|
+
is guaranteed: the page cannot possibly be open before the host launches.
|
|
991
|
+
`_broker` also always answers, but it only ever offers the five introspection
|
|
992
|
+
tools. The broker warns at startup when `stdioProvider` names a slot it does
|
|
993
|
+
not host.
|
|
994
|
+
- **Keep it in its own file.** With `stdioProvider` set, standard output belongs
|
|
995
|
+
to the JSON-RPC stream and every log line moves to standard error, so a broker
|
|
996
|
+
started that way in a terminal looks like it is doing nothing.
|
|
997
|
+
|
|
998
|
+
`MCP_BROKER_STDIO_PROVIDER` sets the same thing from the environment.
|
|
999
|
+
|
|
1000
|
+
## Signed local MCP bundle
|
|
755
1001
|
|
|
756
1002
|
```json
|
|
757
1003
|
"mcpbBundles": [
|
|
@@ -839,17 +1085,22 @@ If Alice attempts `start_motor` on the critical furnace:
|
|
|
839
1085
|
|
|
840
1086
|
## Pre-deployment checklist
|
|
841
1087
|
|
|
1088
|
+
- Set `auth.enabled` to `true`. The template ships it off so that a fresh copy
|
|
1089
|
+
runs; leaving it off in production means every client reaches every slot.
|
|
842
1090
|
- Replace every `.local` domain with the real address.
|
|
843
1091
|
- Confirm that `publicBaseUrl` exactly matches the broker's public address.
|
|
844
1092
|
- Confirm that JWTs use this resource as their audience.
|
|
845
1093
|
- Verify the JWKS URL and expected issuer.
|
|
846
|
-
-
|
|
1094
|
+
- If you add `providerSecret`, never keep `change-me`.
|
|
847
1095
|
- Never publish the private TLS key.
|
|
848
1096
|
- Never publish API keys from `userConfig`.
|
|
849
1097
|
- Use `127.0.0.1` instead of `0.0.0.0` when network access is unnecessary.
|
|
850
1098
|
- Test every role with a representative account.
|
|
851
1099
|
- Test denies against critical assets.
|
|
852
1100
|
- Confirm that `_all` does not reveal unauthorized providers.
|
|
1101
|
+
- List in `allowedOrigins` exactly the browser origins that need access, with
|
|
1102
|
+
the right scheme and port, and no others. Remember that a page served by this
|
|
1103
|
+
broker still counts as a browser origin.
|
|
853
1104
|
- Return `audit.logAllowed` to `false` after troubleshooting.
|
|
854
1105
|
- Restart the broker after each policy change because policies are loaded only
|
|
855
1106
|
once at startup.
|
|
@@ -857,5 +1108,10 @@ If Alice attempts `start_motor` on the critical furnace:
|
|
|
857
1108
|
## Further reading
|
|
858
1109
|
|
|
859
1110
|
- [Complete configuration reference](../docs/config.md)
|
|
860
|
-
- [Broker OAuth guide](
|
|
861
|
-
- [Hierarchical authorization](
|
|
1111
|
+
- [Broker OAuth guide](../../../../docs/authorization.md)
|
|
1112
|
+
- [Hierarchical authorization](../../../../docs/hierarchical-authorization.md)
|
|
1113
|
+
- [Endpoints and transports](../../../../docs/endpoints.md)
|
|
1114
|
+
|
|
1115
|
+
Or ask the broker itself: the reserved `_broker` slot exposes a `broker_guide`
|
|
1116
|
+
tool (integration walkthroughs, written from the source) and a `broker_diagnose`
|
|
1117
|
+
tool (live state plus the problems it can prove, each with a fix).
|