@cyanmycelium/mcp-broker 1.4.0 → 1.6.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/README.md +8 -0
- package/.mcp-broker.example/security.example.json +42 -0
- package/README.md +117 -2
- package/dist/bin.js +129 -22
- package/dist/bin.js.map +1 -1
- package/dist/chunk-364F4ET6.js +386 -0
- package/dist/chunk-364F4ET6.js.map +1 -0
- package/dist/{chunk-YTRVLPHP.js → chunk-PC4TLZGY.js} +3348 -1474
- package/dist/chunk-PC4TLZGY.js.map +1 -0
- package/dist/index.d.ts +320 -2264
- package/dist/index.js +2 -1
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.js +108 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ws.tunnel.builder-DwxT5zve.d.ts +2816 -0
- package/package.json +8 -3
- package/src/auth/index.ts +2 -1
- package/src/auth/provider.auth.ts +91 -0
- package/src/authority/broker.authority.ts +705 -0
- package/src/authority/declaration.ts +420 -0
- package/src/authorization/policy.types.ts +47 -1
- package/src/authorization/runtime.ts +8 -0
- package/src/bin.ts +156 -22
- package/src/broker/adapters/broker.adapter.info.ts +3 -0
- package/src/broker/aggregate/aggregate.server.ts +14 -1
- package/src/broker/aggregate/provider.client.session.ts +16 -8
- package/src/broker/broker.context.ts +12 -0
- package/src/broker/broker.diagnostics.ts +132 -1
- package/src/broker/broker.guides.ts +97 -0
- package/src/config.ts +243 -11
- package/src/index.ts +72 -3
- package/src/limits/controller.ts +423 -0
- package/src/telemetry/index.ts +15 -0
- package/src/telemetry/otlp.http.exporter.ts +117 -0
- package/src/telemetry/telemetry.dispatcher.ts +231 -0
- package/src/telemetry/telemetry.types.ts +79 -0
- package/src/telemetry/trace.context.ts +50 -0
- package/src/testing/index.ts +230 -0
- package/src/ws/ws.interfaces.ts +74 -2
- package/src/ws/ws.tunnel.builder.ts +95 -2
- package/src/ws/ws.tunnel.ts +631 -42
- 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
|
|
@@ -818,8 +928,9 @@ The package is published to npm by [`.github/workflows/release-node.yml`](https:
|
|
|
818
928
|
|
|
819
929
|
```sh
|
|
820
930
|
# from the node/ directory:
|
|
821
|
-
npm
|
|
822
|
-
git push
|
|
931
|
+
npm run bump:minor # updates the broker, commits, and creates node-v<version>
|
|
932
|
+
git push # pushes the release commit
|
|
933
|
+
git push origin node-v1.6.0 # pushes the tag explicitly and starts the workflow
|
|
823
934
|
```
|
|
824
935
|
|
|
825
936
|
The workflow runs lint, build, test, then `npm publish --access public --provenance` and creates a GitHub Release with auto-generated notes.
|
|
@@ -827,3 +938,7 @@ The workflow runs lint, build, test, then `npm publish --access public --provena
|
|
|
827
938
|
## License
|
|
828
939
|
|
|
829
940
|
Apache-2.0. See [LICENSE](LICENSE).
|
|
941
|
+
|
|
942
|
+
## Execution limits
|
|
943
|
+
|
|
944
|
+
Operator-owned call quotas and provider operation budgets: [configuration, SDK and recovery](docs/execution-limits.md).
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { loadBrokerConfig, loadSecurityConfig, BrokerConfigError, loadMcpbBundle, resolveOpenTarget } from './chunk-364F4ET6.js';
|
|
3
|
+
import { PACKAGE_NAME, VERSION, WsTunnelBuilder, BROKER_AGGREGATE_NAME, BROKER_PROVIDER_NAME } from './chunk-PC4TLZGY.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
|
-
|
|
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",
|
|
116
|
-
envFromConfig("MCP_BROKER_PUBLIC_BASE_URL",
|
|
117
|
-
envFromConfig("MCP_BROKER_JWKS",
|
|
118
|
-
envFromConfig("MCP_BROKER_ISSUER",
|
|
119
|
-
envFromConfig("MCP_BROKER_PROVIDER_SECRET",
|
|
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 =
|
|
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,38 @@ async function main() {
|
|
|
318
404
|
authorizationServers,
|
|
319
405
|
jwksUri: jwks,
|
|
320
406
|
issuer,
|
|
321
|
-
scopesSupported:
|
|
322
|
-
requiredScopes:
|
|
323
|
-
perSlotScopes:
|
|
324
|
-
providerScopes:
|
|
325
|
-
subjectMapping:
|
|
326
|
-
roles:
|
|
327
|
-
assignments:
|
|
328
|
-
denies:
|
|
329
|
-
slotResources:
|
|
330
|
-
toolCapabilities:
|
|
331
|
-
providerToolCapabilities:
|
|
332
|
-
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
|
-
|
|
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
|
+
if (security.security.limits) builder.withLimits(security.security.limits);
|
|
432
|
+
builder.withSecurityVersion(security.version);
|
|
433
|
+
}
|
|
434
|
+
tunnel = builder.build();
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(`[mcp-broker] ${error.message}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
340
439
|
await tunnel.start();
|
|
341
440
|
const httpScheme = useTls ? "https" : "http";
|
|
342
441
|
const wsScheme = useTls ? "wss" : "ws";
|
|
@@ -365,12 +464,20 @@ async function main() {
|
|
|
365
464
|
console.log(`\u{1F310} Local grammars ${localGrammarsDir}`);
|
|
366
465
|
}
|
|
367
466
|
console.log(`\u{1F510} Authorization ${authEnabled ? "OAuth 2.1 (Bearer required)" : "disabled (trusted network only)"}`);
|
|
368
|
-
|
|
467
|
+
const providerIdentities = security?.credentials.length ?? 0;
|
|
468
|
+
console.log(
|
|
469
|
+
`\u{1F6E1}\uFE0F Provider auth ${providerIdentities > 0 ? `${providerIdentities} provider identit${providerIdentities === 1 ? "y" : "ies"}${providerSecret ? " + shared secret" : ""}` : providerSecret ? "shared secret required" : "disabled"}`
|
|
470
|
+
);
|
|
471
|
+
if (security) {
|
|
472
|
+
const protectedCount = Object.keys(security.security.authorization?.protectedSlots ?? {}).length;
|
|
473
|
+
console.log(`\u{1F50F} Security file ${security.sourcePath} (version ${security.version}${protectedCount > 0 ? `, ${protectedCount} protected slot(s)` : ""})`);
|
|
474
|
+
}
|
|
369
475
|
console.log(`\u{1F30D} Browser origins ${describeAllowedOrigins()}`);
|
|
370
476
|
console.log(` enforced on /<name>/${mcpSuffix}, /<name>/${sseSuffix} and /<name>/${messagesSuffix}`);
|
|
371
477
|
if (!allowedOrigins) {
|
|
372
478
|
console.log(` a page this broker serves is refused too, list its origin to admit it`);
|
|
373
479
|
}
|
|
480
|
+
console.log(`\u{1F4C8} Provider telemetry ${otlpTracesEndpoint ? `OTLP/HTTP to ${otlpTracesEndpoint}` : "disabled"}`);
|
|
374
481
|
console.log(hr);
|
|
375
482
|
console.log(` New here? Call broker_guide on the ${BROKER_PROVIDER_NAME} slot: ${localhost}/${BROKER_PROVIDER_NAME}/${mcpSuffix}`);
|
|
376
483
|
console.log(` Press Ctrl+C to stop.`);
|