@cyanmycelium/mcp-broker 1.4.0 → 1.5.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 (41) hide show
  1. package/.mcp-broker.example/README.md +8 -0
  2. package/.mcp-broker.example/security.example.json +42 -0
  3. package/README.md +110 -0
  4. package/dist/bin.js +128 -22
  5. package/dist/bin.js.map +1 -1
  6. package/dist/chunk-BTWLF6KI.js +378 -0
  7. package/dist/chunk-BTWLF6KI.js.map +1 -0
  8. package/dist/{chunk-YTRVLPHP.js → chunk-K2EQP4RQ.js} +2828 -1388
  9. package/dist/chunk-K2EQP4RQ.js.map +1 -0
  10. package/dist/index.d.ts +319 -2264
  11. package/dist/index.js +2 -1
  12. package/dist/testing/index.d.ts +93 -0
  13. package/dist/testing/index.js +108 -0
  14. package/dist/testing/index.js.map +1 -0
  15. package/dist/ws.tunnel.builder-BYCkbtA7.d.ts +2691 -0
  16. package/package.json +7 -2
  17. package/src/auth/index.ts +2 -1
  18. package/src/auth/provider.auth.ts +91 -0
  19. package/src/authority/broker.authority.ts +641 -0
  20. package/src/authority/declaration.ts +414 -0
  21. package/src/authorization/policy.types.ts +47 -1
  22. package/src/authorization/runtime.ts +8 -0
  23. package/src/bin.ts +155 -22
  24. package/src/broker/adapters/broker.adapter.info.ts +3 -0
  25. package/src/broker/aggregate/aggregate.server.ts +14 -1
  26. package/src/broker/aggregate/provider.client.session.ts +16 -8
  27. package/src/broker/broker.context.ts +12 -0
  28. package/src/broker/broker.diagnostics.ts +103 -1
  29. package/src/broker/broker.guides.ts +93 -0
  30. package/src/config.ts +233 -11
  31. package/src/index.ts +61 -3
  32. package/src/telemetry/index.ts +15 -0
  33. package/src/telemetry/otlp.http.exporter.ts +117 -0
  34. package/src/telemetry/telemetry.dispatcher.ts +231 -0
  35. package/src/telemetry/telemetry.types.ts +79 -0
  36. package/src/telemetry/trace.context.ts +50 -0
  37. package/src/testing/index.ts +230 -0
  38. package/src/ws/ws.interfaces.ts +70 -2
  39. package/src/ws/ws.tunnel.builder.ts +87 -2
  40. package/src/ws/ws.tunnel.ts +555 -41
  41. package/dist/chunk-YTRVLPHP.js.map +0 -1
@@ -23,6 +23,14 @@ factory deployment. Two consequences.
23
23
  use. A missing directory is skipped with a warning; a missing TLS file stops
24
24
  startup, with a message naming the two files and the three ways out.
25
25
 
26
+ 3. **`security.example.json` is the other way to hold `auth`** (broker 1.5.0):
27
+ a separate security file with one identity and secret per provider and the
28
+ protected slots, apart from the topology. To use it, rename it
29
+ `security.json`, add `"securityFile": "security.json"` to `config.json`, and
30
+ **delete the `auth` block from `config.json`**: the broker refuses to start
31
+ with `auth` in both files. Each provider's secret comes from the environment
32
+ variable its entry names, never from the file.
33
+
26
34
  So a straight `cp -r` does not run yet. Do one of these first:
27
35
 
28
36
  ```sh
@@ -0,0 +1,42 @@
1
+ {
2
+ "description": "Security settings, kept apart from config.json. Name this file from config.json with \"securityFile\": \"security.json\", or with MCP_BROKER_SECURITY_FILE. No secret is written here: each provider names the environment variable that holds its own.",
3
+ "auth": {
4
+ "enabled": true,
5
+ "publicBaseUrl": "https://broker.example.com",
6
+ "authorizationServers": ["https://login.example.com"],
7
+ "jwks": "https://login.example.com/.well-known/jwks.json",
8
+ "requiredScopes": ["mcp:call"],
9
+ "subjectMapping": { "userClaim": "sub", "groupClaims": ["groups"], "serviceClaims": ["service"] },
10
+ "slotResources": {
11
+ "scada": "/production/site1/scada",
12
+ "bench-motor01": "/production/site1/bench/motor01"
13
+ },
14
+ "roles": {
15
+ "mcp-caller": { "capabilities": ["mcp.tools.list", "mcp.tools.call"] },
16
+ "scada-observer": { "inherits": ["mcp-caller"], "capabilities": ["scada.observe"] },
17
+ "scada-operator": { "inherits": ["scada-observer"], "capabilities": ["scada.acquire", "scada.control"] }
18
+ },
19
+ "assignments": [
20
+ { "id": "line1-operators", "subject": "group:operators-line1", "role": "scada-operator", "resource": "/production/site1/line1/**" },
21
+ { "id": "scada-service", "subject": "service:mcp-scada", "role": "mcp-caller", "resource": "/production/site1/**" }
22
+ ]
23
+ },
24
+ "providers": [
25
+ {
26
+ "id": "mcp-scada",
27
+ "secretEnv": "SCADA_PROVIDER_SECRET",
28
+ "subjects": ["service:mcp-scada"],
29
+ "allowedResources": ["/production/site1/**"]
30
+ },
31
+ {
32
+ "id": "modbus-bench",
33
+ "secretEnv": "MODBUS_PROVIDER_SECRET",
34
+ "allowedResources": ["/production/site1/bench/**"]
35
+ }
36
+ ],
37
+ "authorization": {
38
+ "protectedSlots": {
39
+ "bench-motor01": { "declaredBy": "mcp-scada", "publishedBy": "modbus-bench" }
40
+ }
41
+ }
42
+ }
package/README.md CHANGED
@@ -79,6 +79,10 @@ the observable signatures are worth knowing:
79
79
 
80
80
  `broker_diagnose` reports the first as `transport-path-mismatch`.
81
81
 
82
+ > **Testing?** `startTestBroker()` from `@cyanmycelium/mcp-broker/testing` runs a
83
+ > real broker in one call, with callers that need no authorization server. See
84
+ > [docs/testing.md](docs/testing.md).
85
+
82
86
  ## Configuration
83
87
 
84
88
  Two sources, env vars **always win** over the file. The file is the static baseline you ship with the broker; env vars are deploy-specific overrides.
@@ -159,6 +163,8 @@ This table is complete: it lists every `MCP_BROKER_*` variable the CLI reads.
159
163
  | `MCP_BROKER_PROVIDER_HEARTBEAT_MS` | `30000` | ws-level ping interval on provider sockets. `0` disables |
160
164
  | `MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS` | `60000` | How long a provider has to answer one request before it is failed. `0` disables |
161
165
  | `MCP_BROKER_PROVIDER_TAKEOVER` | `liveness` | `reject`, `liveness` or `always` when a second provider claims an occupied slot |
166
+ | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | (unset) | Standard OpenTelemetry full traces endpoint. Enables provider telemetry export |
167
+ | `OTEL_EXPORTER_OTLP_HEADERS` | (unset) | Standard comma-separated, percent-encoded OTLP headers |
162
168
  | `MCP_BROKER_WWW_DIR` | (unset) | If set, serve this directory at `/` |
163
169
  | `MCP_BROKER_BUNDLE_DIR` | (unset) | If set, serve this directory at `/bundle` |
164
170
  | `MCP_BROKER_OPEN` | (unset) | `1` opens the broker root on startup; a `/path` or a same-origin absolute URL opens that page. Opens only when a static mount actually covers the resolved path |
@@ -361,6 +367,110 @@ class GaugeAdapter extends McpAdapterBase {
361
367
  }
362
368
  ```
363
369
 
370
+ ## Provider telemetry and OTLP
371
+
372
+ The complete protocol and deployment contract is in
373
+ [docs/telemetry.md](https://github.com/pandaGaume/mcp-broker/blob/main/docs/telemetry.md).
374
+
375
+ The optional telemetry extension accepts `broker/telemetry` from any
376
+ provider slot. The broker consumes these notifications itself. It never sends
377
+ them to MCP clients, and it never waits for the exporter while routing requests
378
+ or responses.
379
+
380
+ The pipeline has explicit bounds. By default it accepts frames up to 64 KiB,
381
+ queues at most 256 spans, exports batches of up to 32, and drops new telemetry
382
+ when the queue is full. `getTelemetryStats()` reports every accepted, exported,
383
+ and dropped span. A trace failure therefore cannot block MCP control traffic.
384
+
385
+ For the CLI, set the standard OpenTelemetry environment variable:
386
+
387
+ ```sh
388
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4318/v1/traces mcp-broker
389
+ ```
390
+
391
+ `OTEL_EXPORTER_OTLP_HEADERS` carries optional comma-separated, percent-encoded
392
+ headers. The same settings can be kept in `config.json` under `telemetry`:
393
+
394
+ ```json
395
+ {
396
+ "telemetry": {
397
+ "otlpHttpEndpoint": "http://otel-collector:4318/v1/traces",
398
+ "timeoutMs": 5000,
399
+ "queueCapacity": 256,
400
+ "batchSize": 32
401
+ }
402
+ }
403
+ ```
404
+
405
+ Configure the built-in dependency-free OTLP/HTTP JSON exporter:
406
+
407
+ ```ts
408
+ import { WsTunnelBuilder } from "@cyanmycelium/mcp-broker";
409
+
410
+ const broker = new WsTunnelBuilder()
411
+ .withPort(3000)
412
+ .withOtlpHttpTelemetry(
413
+ {
414
+ endpoint: "http://otel-collector:4318/v1/traces",
415
+ headers: { Authorization: `Bearer ${process.env.OTLP_TOKEN}` },
416
+ },
417
+ {
418
+ queueCapacity: 512,
419
+ batchSize: 32,
420
+ onExportError: (error) => console.error("telemetry export failed", error),
421
+ }
422
+ )
423
+ .build();
424
+ ```
425
+
426
+ Or pass any sink with `withTelemetry({ exporter })`. This is useful for a file,
427
+ Kafka, an existing OpenTelemetry SDK, or a deterministic test collector.
428
+
429
+ Provider wire format:
430
+
431
+ ```json
432
+ {
433
+ "jsonrpc": "2.0",
434
+ "method": "broker/telemetry",
435
+ "params": {
436
+ "version": 1,
437
+ "signal": "traces",
438
+ "span": {
439
+ "traceId": "0123456789abcdef0123456789abcdef",
440
+ "spanId": "0123456789abcdef",
441
+ "parentSpanId": "fedcba9876543210",
442
+ "name": "modbus.read",
443
+ "kind": 3,
444
+ "startTimeUnixNano": "1720000000000000000",
445
+ "endTimeUnixNano": "1720000000001000000",
446
+ "attributes": {
447
+ "modbus.function_code": 3,
448
+ "network.transport": "tcp"
449
+ },
450
+ "events": [
451
+ {
452
+ "name": "pdu.rx",
453
+ "timeUnixNano": "1720000000000900000",
454
+ "attributes": { "bytes": 9 }
455
+ }
456
+ ],
457
+ "status": { "code": 1 }
458
+ }
459
+ }
460
+ }
461
+ ```
462
+
463
+ Trace IDs and parent relationships follow W3C Trace Context representation.
464
+ Every routed request carries the context in `params._meta.traceparent`.
465
+ TypeScript providers can continue it with `traceparentOf()`,
466
+ `childTraceparent()` and `withTraceparent()`, then emit with
467
+ `transport.broker.span(span)`. The exporter produces an OTLP
468
+ `ExportTraceServiceRequest`. `service.name` is the authenticated provider
469
+ principal when available, otherwise the slot. `mcp.provider.slot` always keeps
470
+ the slot, and `mcp.provider.principal` records the authenticated identity. Raw
471
+ protocol payloads are not required by the schema. A provider should emit them
472
+ only when its own runtime trace policy explicitly enables that level of detail.
473
+
364
474
  ## Authorization (OAuth 2.1)
365
475
 
366
476
  By default the broker performs **no** authentication. That is fine behind a
package/dist/bin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { PACKAGE_NAME, VERSION, loadBrokerConfig, WsTunnelBuilder, loadMcpbBundle, BROKER_AGGREGATE_NAME, BROKER_PROVIDER_NAME, resolveOpenTarget } from './chunk-YTRVLPHP.js';
2
+ import { loadBrokerConfig, loadSecurityConfig, BrokerConfigError, loadMcpbBundle, resolveOpenTarget } from './chunk-BTWLF6KI.js';
3
+ import { PACKAGE_NAME, VERSION, WsTunnelBuilder, BROKER_AGGREGATE_NAME, BROKER_PROVIDER_NAME } from './chunk-K2EQP4RQ.js';
3
4
  import * as fs from 'fs';
4
5
  import * as path from 'path';
5
6
  import open from 'open';
@@ -59,6 +60,10 @@ PROVIDER LIVENESS
59
60
  MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS Deadline for one provider answer (default 60000, 0 off)
60
61
  MCP_BROKER_PROVIDER_TAKEOVER reject | liveness | always (default liveness)
61
62
 
63
+ PROVIDER TELEMETRY (OpenTelemetry, opt-in)
64
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT Full OTLP/HTTP endpoint, enables trace export
65
+ OTEL_EXPORTER_OTLP_HEADERS Comma-separated percent-encoded key=value headers
66
+
62
67
  AUTHORIZATION (OAuth 2.1, opt-in)
63
68
  MCP_BROKER_AUTH_ENABLED "1" to require bearer tokens on client endpoints
64
69
  MCP_BROKER_PUBLIC_BASE_URL Public origin, e.g. https://mcp.example.com
@@ -87,8 +92,20 @@ Full reference: https://github.com/pandaGaume/mcp-broker/tree/main/node
87
92
  `
88
93
  );
89
94
  }
90
- var { config, baseDir } = loadBrokerConfig();
95
+ function loadConfigOrExit() {
96
+ try {
97
+ const loaded = loadBrokerConfig();
98
+ return { ...loaded, security: loadSecurityConfig(loaded) };
99
+ } catch (error) {
100
+ if (!(error instanceof BrokerConfigError)) throw error;
101
+ process.stderr.write(`${error.message}
102
+ `);
103
+ process.exit(1);
104
+ }
105
+ }
106
+ var { config, baseDir, security } = loadConfigOrExit();
91
107
  var cwd = process.cwd();
108
+ var authConfig = security?.security.auth ?? config.auth;
92
109
  function envFromConfig(envName, configValue) {
93
110
  if (configValue === void 0 || configValue === null) return;
94
111
  if (process.env[envName] !== void 0 && process.env[envName] !== "") return;
@@ -112,11 +129,11 @@ envFromConfig("MCP_BROKER_MAX_SUBSCRIPTIONS_PER_SLOT", config.resourceSubscripti
112
129
  envFromConfig("MCP_BROKER_MAX_RESOURCE_URI_LENGTH", config.resourceSubscriptions?.maxResourceUriLength);
113
130
  envFromConfig("MCP_BROKER_PROVIDER_TAKEOVER", config.providerTakeover);
114
131
  envFromConfig("MCP_BROKER_OPEN", config.www?.open === true ? "1" : typeof config.www?.open === "string" ? config.www.open : void 0);
115
- envFromConfig("MCP_BROKER_AUTH_ENABLED", config.auth?.enabled === true ? "1" : void 0);
116
- envFromConfig("MCP_BROKER_PUBLIC_BASE_URL", config.auth?.publicBaseUrl);
117
- envFromConfig("MCP_BROKER_JWKS", config.auth?.jwks);
118
- envFromConfig("MCP_BROKER_ISSUER", config.auth?.issuer);
119
- envFromConfig("MCP_BROKER_PROVIDER_SECRET", config.auth?.providerSecret);
132
+ envFromConfig("MCP_BROKER_AUTH_ENABLED", authConfig?.enabled === true ? "1" : void 0);
133
+ envFromConfig("MCP_BROKER_PUBLIC_BASE_URL", authConfig?.publicBaseUrl);
134
+ envFromConfig("MCP_BROKER_JWKS", authConfig?.jwks);
135
+ envFromConfig("MCP_BROKER_ISSUER", authConfig?.issuer);
136
+ envFromConfig("MCP_BROKER_PROVIDER_SECRET", authConfig?.providerSecret);
120
137
  var stdioProvider = process.env["MCP_BROKER_STDIO_PROVIDER"];
121
138
  if (stdioProvider) {
122
139
  const toStderr = (...args) => process.stderr.write(args.join(" ") + "\n");
@@ -133,6 +150,27 @@ var clientPath = process.env["MCP_BROKER_CLIENT_PATH"] ?? "/";
133
150
  var mcpPath = process.env["MCP_BROKER_MCP_PATH"] ?? "/mcp";
134
151
  var ssePath = process.env["MCP_BROKER_SSE_PATH"] ?? "/sse";
135
152
  var messagesPath = process.env["MCP_BROKER_MESSAGES_PATH"] ?? "/messages";
153
+ var otlpTracesEndpoint = process.env["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"] ?? config.telemetry?.otlpHttpEndpoint;
154
+ function otlpHeadersFromEnv(raw) {
155
+ const headers = {};
156
+ if (!raw) return headers;
157
+ for (const field of raw.split(",")) {
158
+ const separator = field.indexOf("=");
159
+ if (separator <= 0) {
160
+ console.warn(`[mcp-broker] Ignoring malformed OTEL_EXPORTER_OTLP_HEADERS field: ${JSON.stringify(field)}.`);
161
+ continue;
162
+ }
163
+ try {
164
+ const key = decodeURIComponent(field.slice(0, separator).trim());
165
+ const value = decodeURIComponent(field.slice(separator + 1).trim());
166
+ if (key) headers[key] = value;
167
+ } catch {
168
+ console.warn(`[mcp-broker] Ignoring malformed percent encoding in OTEL_EXPORTER_OTLP_HEADERS field: ${JSON.stringify(field)}.`);
169
+ }
170
+ }
171
+ return headers;
172
+ }
173
+ var otlpHeaders = { ...config.telemetry?.headers ?? {}, ...otlpHeadersFromEnv(process.env["OTEL_EXPORTER_OTLP_HEADERS"]) };
136
174
  var tlsCertPath = process.env["MCP_BROKER_TLS_CERT"] ? path.resolve(cwd, process.env["MCP_BROKER_TLS_CERT"]) : config.tls?.cert ? path.resolve(baseDir, config.tls.cert) : null;
137
175
  var tlsKeyPath = process.env["MCP_BROKER_TLS_KEY"] ? path.resolve(cwd, process.env["MCP_BROKER_TLS_KEY"]) : config.tls?.key ? path.resolve(baseDir, config.tls.key) : null;
138
176
  var protocolOverride = process.env["MCP_BROKER_PROTOCOL"]?.toLowerCase();
@@ -215,6 +253,34 @@ function warnIfStdioTargetUnknown(target) {
215
253
  `[mcp-broker] MCP_BROKER_STDIO_PROVIDER="${target}" names a slot this broker does not host. Nothing answers on it until a WebSocket provider connects to ${providerPath}/${target} (or announces "${target}" on ${providersPath}), and until then every request from the MCP host, including the initial handshake, fails with 'Provider "${target}" not connected'. An MCP host that starts before that provider is up, which is always the case for a provider hosted in a browser page, will therefore never connect. Prefer MCP_BROKER_STDIO_PROVIDER="${BROKER_AGGREGATE_NAME}": it exists from startup, answers the handshake itself, unions every opted-in provider, and pushes notifications/tools/list_changed as providers join, so a page opened later appears live. Slots this broker hosts right now: ${hosted.length > 0 ? hosted.map((n) => `"${n}"`).join(", ") + `, plus the reserved "${BROKER_AGGREGATE_NAME}" and "${BROKER_PROVIDER_NAME}"` : `the reserved "${BROKER_AGGREGATE_NAME}" and "${BROKER_PROVIDER_NAME}" only`}.`
216
254
  );
217
255
  }
256
+ function telemetryExportLogger(intervalMs = 6e4) {
257
+ let failures = 0;
258
+ let suppressed = 0;
259
+ let lastReport = 0;
260
+ return {
261
+ error(error) {
262
+ failures++;
263
+ const now = Date.now();
264
+ if (lastReport === 0 || now - lastReport >= intervalMs) {
265
+ const suffix = suppressed > 0 ? ` (${suppressed} repeated failure(s) suppressed)` : "";
266
+ console.error(`[mcp-broker] telemetry export failed: ${error.message}${suffix}`);
267
+ lastReport = now;
268
+ suppressed = 0;
269
+ } else {
270
+ suppressed++;
271
+ }
272
+ },
273
+ success(recordCount) {
274
+ if (failures === 0) return;
275
+ console.info(
276
+ `[mcp-broker] telemetry export recovered after ${failures} failed batch(es)${suppressed > 0 ? `, ${suppressed} repeated log line(s) suppressed` : ""}; exported ${recordCount} span(s).`
277
+ );
278
+ failures = 0;
279
+ suppressed = 0;
280
+ lastReport = 0;
281
+ }
282
+ };
283
+ }
218
284
  async function main() {
219
285
  const localGrammarsDir = path.join(baseDir, "grammars");
220
286
  const hasLocalGrammars = fs.existsSync(localGrammarsDir);
@@ -235,6 +301,26 @@ async function main() {
235
301
  if (providerTakeover) {
236
302
  builder.withProviderTakeover(providerTakeover);
237
303
  }
304
+ if (otlpTracesEndpoint) {
305
+ const exportLog = telemetryExportLogger();
306
+ builder.withOtlpHttpTelemetry(
307
+ {
308
+ endpoint: otlpTracesEndpoint,
309
+ headers: otlpHeaders,
310
+ timeoutMs: config.telemetry?.timeoutMs,
311
+ serviceNamespace: config.telemetry?.serviceNamespace
312
+ },
313
+ {
314
+ maxFrameBytes: config.telemetry?.maxFrameBytes,
315
+ queueCapacity: config.telemetry?.queueCapacity,
316
+ batchSize: config.telemetry?.batchSize,
317
+ maxAttributes: config.telemetry?.maxAttributes,
318
+ maxEvents: config.telemetry?.maxEvents,
319
+ onExportError: exportLog.error,
320
+ onExportSuccess: exportLog.success
321
+ }
322
+ );
323
+ }
238
324
  if (useTls) {
239
325
  try {
240
326
  builder.withTlsFiles(tlsCertPath, tlsKeyPath);
@@ -306,7 +392,7 @@ async function main() {
306
392
  const publicBaseUrl = process.env["MCP_BROKER_PUBLIC_BASE_URL"];
307
393
  const jwks = process.env["MCP_BROKER_JWKS"];
308
394
  const issuer = process.env["MCP_BROKER_ISSUER"];
309
- const authorizationServers = config.auth?.authorizationServers ?? (issuer ? [issuer] : []);
395
+ const authorizationServers = authConfig?.authorizationServers ?? (issuer ? [issuer] : []);
310
396
  if (!publicBaseUrl || !jwks || authorizationServers.length === 0) {
311
397
  console.error(
312
398
  "[mcp-broker] auth.enabled requires publicBaseUrl, jwks, and at least one authorizationServers entry (or issuer). Set them via config.auth or MCP_BROKER_PUBLIC_BASE_URL / MCP_BROKER_JWKS / MCP_BROKER_ISSUER."
@@ -318,25 +404,37 @@ async function main() {
318
404
  authorizationServers,
319
405
  jwksUri: jwks,
320
406
  issuer,
321
- scopesSupported: config.auth?.scopesSupported,
322
- requiredScopes: config.auth?.requiredScopes,
323
- perSlotScopes: config.auth?.perSlotScopes,
324
- providerScopes: config.auth?.providerScopes,
325
- subjectMapping: config.auth?.subjectMapping,
326
- roles: config.auth?.roles,
327
- assignments: config.auth?.assignments,
328
- denies: config.auth?.denies,
329
- slotResources: config.auth?.slotResources,
330
- toolCapabilities: config.auth?.toolCapabilities,
331
- providerToolCapabilities: config.auth?.providerToolCapabilities,
332
- audit: config.auth?.audit
407
+ scopesSupported: authConfig?.scopesSupported,
408
+ requiredScopes: authConfig?.requiredScopes,
409
+ perSlotScopes: authConfig?.perSlotScopes,
410
+ providerScopes: authConfig?.providerScopes,
411
+ subjectMapping: authConfig?.subjectMapping,
412
+ roles: authConfig?.roles,
413
+ assignments: authConfig?.assignments,
414
+ denies: authConfig?.denies,
415
+ slotResources: authConfig?.slotResources,
416
+ toolCapabilities: authConfig?.toolCapabilities,
417
+ providerToolCapabilities: authConfig?.providerToolCapabilities,
418
+ audit: authConfig?.audit
333
419
  });
334
420
  }
335
421
  const providerSecret = process.env["MCP_BROKER_PROVIDER_SECRET"];
336
422
  if (providerSecret) {
337
423
  builder.withProviderSecret(providerSecret);
338
424
  }
339
- const tunnel = builder.build();
425
+ let tunnel;
426
+ try {
427
+ if (security) {
428
+ if (security.credentials.length > 0) builder.withProviderPrincipals(security.credentials);
429
+ const protectedSlots = security.security.authorization?.protectedSlots;
430
+ if (protectedSlots && Object.keys(protectedSlots).length > 0) builder.withProtectedSlots(protectedSlots);
431
+ builder.withSecurityVersion(security.version);
432
+ }
433
+ tunnel = builder.build();
434
+ } catch (error) {
435
+ console.error(`[mcp-broker] ${error.message}`);
436
+ process.exit(1);
437
+ }
340
438
  await tunnel.start();
341
439
  const httpScheme = useTls ? "https" : "http";
342
440
  const wsScheme = useTls ? "wss" : "ws";
@@ -365,12 +463,20 @@ async function main() {
365
463
  console.log(`\u{1F310} Local grammars ${localGrammarsDir}`);
366
464
  }
367
465
  console.log(`\u{1F510} Authorization ${authEnabled ? "OAuth 2.1 (Bearer required)" : "disabled (trusted network only)"}`);
368
- console.log(`\u{1F6E1}\uFE0F Provider auth ${providerSecret ? "shared secret required" : "disabled"}`);
466
+ const providerIdentities = security?.credentials.length ?? 0;
467
+ console.log(
468
+ `\u{1F6E1}\uFE0F Provider auth ${providerIdentities > 0 ? `${providerIdentities} provider identit${providerIdentities === 1 ? "y" : "ies"}${providerSecret ? " + shared secret" : ""}` : providerSecret ? "shared secret required" : "disabled"}`
469
+ );
470
+ if (security) {
471
+ const protectedCount = Object.keys(security.security.authorization?.protectedSlots ?? {}).length;
472
+ console.log(`\u{1F50F} Security file ${security.sourcePath} (version ${security.version}${protectedCount > 0 ? `, ${protectedCount} protected slot(s)` : ""})`);
473
+ }
369
474
  console.log(`\u{1F30D} Browser origins ${describeAllowedOrigins()}`);
370
475
  console.log(` enforced on /<name>/${mcpSuffix}, /<name>/${sseSuffix} and /<name>/${messagesSuffix}`);
371
476
  if (!allowedOrigins) {
372
477
  console.log(` a page this broker serves is refused too, list its origin to admit it`);
373
478
  }
479
+ console.log(`\u{1F4C8} Provider telemetry ${otlpTracesEndpoint ? `OTLP/HTTP to ${otlpTracesEndpoint}` : "disabled"}`);
374
480
  console.log(hr);
375
481
  console.log(` New here? Call broker_guide on the ${BROKER_PROVIDER_NAME} slot: ${localhost}/${BROKER_PROVIDER_NAME}/${mcpSuffix}`);
376
482
  console.log(` Press Ctrl+C to stop.`);