@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.
Files changed (49) hide show
  1. package/.mcp-broker.example/CONFIGURATION-EN.md +300 -44
  2. package/.mcp-broker.example/CONFIGURATION-FR.md +313 -44
  3. package/.mcp-broker.example/README.md +73 -2
  4. package/.mcp-broker.example/config.json +18 -9
  5. package/.mcp-broker.example/config.stdio-bridge.json +16 -0
  6. package/README.md +407 -27
  7. package/dist/bin.js +215 -20
  8. package/dist/bin.js.map +1 -1
  9. package/dist/chunk-BZUZYXVA.js +5955 -0
  10. package/dist/chunk-BZUZYXVA.js.map +1 -0
  11. package/dist/grammars/claude/en.json +12 -0
  12. package/dist/grammars/claude/fr.json +12 -0
  13. package/dist/grammars/default/en.json +40 -0
  14. package/dist/grammars/default/fr.json +40 -0
  15. package/dist/grammars/default/zh.json +40 -0
  16. package/dist/index.d.ts +991 -25
  17. package/dist/index.js +1 -1
  18. package/package.json +3 -3
  19. package/src/auth/index.ts +3 -1
  20. package/src/auth/provider.auth.ts +126 -8
  21. package/src/authorization/policy.engine.ts +11 -2
  22. package/src/authorization/policy.types.ts +25 -1
  23. package/src/bin.ts +325 -28
  24. package/src/broker/adapters/broker.adapter.diagnose.ts +45 -0
  25. package/src/broker/adapters/broker.adapter.guide.ts +108 -0
  26. package/src/broker/aggregate/aggregate.server.ts +82 -15
  27. package/src/broker/aggregate/provider.client.session.ts +85 -11
  28. package/src/broker/behaviors/broker.behavior.diagnose.ts +47 -0
  29. package/src/broker/behaviors/broker.behavior.guide.ts +79 -0
  30. package/src/broker/broker.context.ts +65 -0
  31. package/src/broker/broker.diagnostics.ts +495 -0
  32. package/src/broker/broker.guides.ts +1029 -0
  33. package/src/broker/broker.server.ts +23 -7
  34. package/src/broker/broker.slots.ts +36 -0
  35. package/src/broker/grammars/claude/en.json +12 -0
  36. package/src/broker/grammars/claude/fr.json +12 -0
  37. package/src/broker/grammars/default/en.json +40 -0
  38. package/src/broker/grammars/default/fr.json +40 -0
  39. package/src/broker/grammars/default/zh.json +40 -0
  40. package/src/config.ts +191 -4
  41. package/src/index.ts +38 -3
  42. package/src/remote.transports.ts +127 -10
  43. package/src/remote.upstream.ts +4 -1
  44. package/src/ws/ws.interfaces.ts +148 -3
  45. package/src/ws/ws.tunnel.builder.ts +63 -1
  46. package/src/ws/ws.tunnel.ts +1150 -173
  47. package/web/README.md +31 -4
  48. package/dist/chunk-FTDKH2C4.js +0 -3670
  49. package/dist/chunk-FTDKH2C4.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _cyanmycelium_mcp_core from '@cyanmycelium/mcp-core';
2
- import { McpBehavior, McpResource, McpTool, McpResourceTemplate, GrammarResolverOptions, IMcpServer, IMessageTransport, McpGrammar, IAccessTokenClaims, IMcpPrincipal, McpAuthError, ITokenValidator, IProtectedResourceMetadata } from '@cyanmycelium/mcp-core';
2
+ import { McpBehavior, McpResource, McpTool, McpResourceTemplate, GrammarResolverOptions, IMcpServer, IMessageTransport, McpGrammar, IAccessTokenClaims, IMcpPrincipal, McpAuthError, ITokenValidator, IProtectedResourceMetadata, McpAdapterBase, McpResourceContent, McpToolResult } from '@cyanmycelium/mcp-core';
3
3
  export { IAccessTokenClaims, IProtectedResourceMetadata, ITokenValidator } from '@cyanmycelium/mcp-core';
4
4
  import { IncomingMessage, ServerResponse } from 'http';
5
5
 
@@ -37,6 +37,49 @@ interface IBrokerContext {
37
37
  getProvidersInfo(): IBrokerProviderInfo[];
38
38
  /** Snapshot of a single provider slot, or `undefined` if the name is unknown. */
39
39
  getProviderInfo(name: string): IBrokerProviderInfo | undefined;
40
+ /**
41
+ * Membership of the reserved `_all` aggregate slot, or `undefined` when the
42
+ * host cannot report it. Lets a diagnosis distinguish "the aggregate is
43
+ * empty because nobody opted in" from "the aggregate is disabled".
44
+ */
45
+ getAggregateInfo?(): IBrokerAggregateInfo | undefined;
46
+ /**
47
+ * Security posture of the listening surface, or `undefined` when the host
48
+ * cannot report it. Lets a diagnosis catch the combination that produces a
49
+ * `403 invalid_origin` on a page the broker itself serves.
50
+ */
51
+ getSecurityInfo?(): IBrokerSecurityInfo | undefined;
52
+ /**
53
+ * Slot the stdio bridge is pinned to (`MCP_BROKER_STDIO_PROVIDER` /
54
+ * `withStdioClient`), `null` when no bridge is configured, or `undefined`
55
+ * when the host cannot report it. Lets a diagnosis catch a bridge pinned to
56
+ * a slot that cannot exist at host start.
57
+ */
58
+ getStdioBridgeTarget?(): string | null | undefined;
59
+ }
60
+ /**
61
+ * Membership snapshot of the reserved `_all` aggregate slot.
62
+ *
63
+ * `providers` lists the slot names currently contributing tools and prompts,
64
+ * which normally includes `_broker`. It is not the same set as "connected
65
+ * slots": a provider is in `_all` only if it opted in.
66
+ */
67
+ interface IBrokerAggregateInfo {
68
+ /** `false` when the aggregate slot was disabled at construction. */
69
+ enabled: boolean;
70
+ /** Slot names currently contributing to the aggregate. */
71
+ providers: readonly string[];
72
+ }
73
+ /** What the broker enforces on its listening surface right now. */
74
+ interface IBrokerSecurityInfo {
75
+ /** `true` when at least one browser origin is allowed on `/<slot>/mcp`. */
76
+ allowedOriginsConfigured: boolean;
77
+ /** `true` when clients must present an OAuth 2.1 bearer token. */
78
+ clientAuthEnabled: boolean;
79
+ /** `true` when providers must authenticate at the WebSocket upgrade. */
80
+ providerAuthEnabled: boolean;
81
+ /** URL prefixes served as static files, e.g. `["/bundle", "/"]`. */
82
+ staticMountPrefixes: readonly string[];
40
83
  }
41
84
  /**
42
85
  * Transport kind currently feeding a provider slot.
@@ -97,14 +140,37 @@ declare class BrokerProvidersBehavior extends McpBehavior {
97
140
  protected _buildTools(): McpTool[];
98
141
  }
99
142
 
143
+ /**
144
+ * The broker's reserved slot names, in one dependency-free module.
145
+ *
146
+ * They live here rather than next to the code that registers them because
147
+ * three unrelated layers need them: `broker.server.ts` registers `_broker`,
148
+ * `AggregateServer` owns `_all`, and the diagnostics engine has to tell a
149
+ * reserved slot from a user slot without importing either. Importing the
150
+ * registrars would close a module cycle; importing this does not.
151
+ */
100
152
  /**
101
153
  * Reserved provider slot name under which the broker exposes itself as an MCP
102
- * server. Clients reach it via `<host>/_broker/mcp` (or any other client transport).
154
+ * server. Clients reach it via `<host>/_broker/mcp` (or any other client
155
+ * transport).
103
156
  *
104
157
  * Prefixed with `_` to make it unambiguously a system slot, and to reduce the
105
158
  * chance of collision with user-supplied provider names.
106
159
  */
107
160
  declare const BROKER_PROVIDER_NAME = "_broker";
161
+ /**
162
+ * Reserved provider slot name under which the broker exposes the aggregate of
163
+ * every opted-in provider. Mirrors `AggregateServer.SLOT`.
164
+ */
165
+ declare const BROKER_AGGREGATE_NAME = "_all";
166
+ /** Every slot name the broker reserves for itself. */
167
+ declare const BROKER_RESERVED_SLOTS: readonly string[];
168
+ /**
169
+ * `true` when `name` is one of the broker's own slots rather than a slot a
170
+ * provider may claim. A provider connecting to a reserved name is refused.
171
+ */
172
+ declare function isReservedBrokerSlot(name: string): boolean;
173
+
108
174
  /**
109
175
  * Optional knobs passed to {@link startBrokerServer}. Lets the embedder
110
176
  * replace either resolver with custom logic without touching mcp-broker
@@ -142,9 +208,20 @@ interface IStartBrokerServerOptions {
142
208
  localGrammarsDir?: string;
143
209
  }
144
210
  /**
145
- * Constructs the broker's own MCP server (the Tier-1 introspection behaviors)
146
- * and returns the running server plus the loopback transport that must be
147
- * registered against the {@link WsTunnel} as the `_broker` provider slot.
211
+ * Constructs the broker's own MCP server and returns the running server plus
212
+ * the loopback transport that must be registered against the {@link WsTunnel}
213
+ * as the `_broker` provider slot.
214
+ *
215
+ * Four behaviors are registered, in the order an agent meets them:
216
+ *
217
+ * - {@link BrokerInfoBehavior}: `broker_info`, `broker://info`.
218
+ * - {@link BrokerProvidersBehavior}: `providers_list`, `provider_status`,
219
+ * `broker://providers`, `broker://providers/{name}`.
220
+ * - {@link BrokerGuideBehavior}: `broker_guide`, `broker://guide/*`. The
221
+ * broker's own integration documentation, so an agent wiring something to
222
+ * this broker never has to go find a README.
223
+ * - {@link BrokerDiagnoseBehavior}: `broker_diagnose`. Live state plus the
224
+ * problems the broker can prove about its own wiring, each with a fix.
148
225
  *
149
226
  * Usage from {@link WsTunnel.start}:
150
227
  * ```ts
@@ -434,7 +511,24 @@ interface IAuthorizationRequest {
434
511
  readonly provider?: string;
435
512
  readonly tool?: string;
436
513
  }
437
- type AuthorizationDecisionReason = "explicit-deny" | "role-grant" | "no-matching-grant" | "invalid-resource" | "unknown-resource";
514
+ /**
515
+ * Why a decision came out the way it did, as written to the audit log.
516
+ *
517
+ * The distinction between a policy outcome and a fault is load-bearing: the
518
+ * audit log is what an operator actually reads, and reporting a fault as
519
+ * `"no-matching-grant"` sends them off writing grants that can never help.
520
+ *
521
+ * - `explicit-deny` a deny policy matched.
522
+ * - `role-grant` an assignment matched; the only allowing reason.
523
+ * - `no-matching-grant` no policy matched. A genuine policy outcome.
524
+ * - `invalid-resource` the request carried no resource at all.
525
+ * - `unknown-resource` the slot maps to no configured resource path.
526
+ * - `invalid-capability` the requested capability string is malformed, so no
527
+ * grant could ever match it. A configuration fault.
528
+ * - `evaluation-error` evaluation threw. Nothing was decided; the request is
529
+ * denied because that is the safe answer. A fault.
530
+ */
531
+ type AuthorizationDecisionReason = "explicit-deny" | "role-grant" | "no-matching-grant" | "invalid-resource" | "unknown-resource" | "invalid-capability" | "evaluation-error";
438
532
  interface IAuthorizationDecision {
439
533
  readonly allowed: boolean;
440
534
  readonly reason: AuthorizationDecisionReason;
@@ -853,6 +947,44 @@ interface IProviderAuthenticator {
853
947
  authenticate(req: IncomingMessage, slot: string | undefined): ProviderAuthenticatorReturn | Promise<ProviderAuthenticatorReturn>;
854
948
  }
855
949
  declare function normalizeProviderAuthentication(result: ProviderAuthenticatorReturn, fallbackId?: string): ProviderAuthenticationResult;
950
+ /** Why a provider was refused a slot. `undefined` when it was allowed. */
951
+ type ProviderPublishDenialReason = "no-allowed-resources" | "out-of-namespace" | "malformed-pattern";
952
+ /** The outcome of a provider namespace check, with the cause when it denies. */
953
+ interface IProviderPublishDecision {
954
+ readonly allowed: boolean;
955
+ /** Machine-readable cause, present only when `allowed` is `false`. */
956
+ readonly reason?: ProviderPublishDenialReason;
957
+ /** Operator-facing explanation that already names the fix. */
958
+ readonly detail?: string;
959
+ }
960
+ /**
961
+ * Compiles a principal's `allowedResources`, throwing on the first malformed
962
+ * pattern with the offending pattern quoted in the message.
963
+ *
964
+ * Call this wherever a provider principal is *built* (config load, a custom
965
+ * authenticator's constructor) so a typo fails the broker at startup instead of
966
+ * silently 403-ing every provider on that principal forever.
967
+ * {@link providerPublishDecision} deliberately does not throw: it runs inside the
968
+ * WebSocket upgrade, where the only safe answer to "is this pattern valid?" is
969
+ * to deny loudly.
970
+ */
971
+ declare function compileProviderAllowedResources(allowed: readonly string[], label?: string): readonly ResourcePathPattern[];
972
+ /**
973
+ * Decides whether a provider principal may claim `resource`, and says why when
974
+ * it may not.
975
+ *
976
+ * Every denial here surfaces to the provider as a bare 403 at the WebSocket
977
+ * handshake, which a browser reports as a contentless error event. The reason is
978
+ * therefore the only thing an operator (or an agent wiring this up) has to work
979
+ * from, so it is returned to the caller for the registration log and, for the
980
+ * configuration-fault case, logged here as well.
981
+ */
982
+ declare function providerPublishDecision(principal: IProviderPrincipal, resource: ResourcePath): IProviderPublishDecision;
983
+ /**
984
+ * Boolean form of {@link providerPublishDecision}, kept for callers that only
985
+ * need the verdict. Prefer the decision form so the refusal can be logged with
986
+ * its cause.
987
+ */
856
988
  declare function providerMayPublish(principal: IProviderPrincipal, resource: ResourcePath): boolean;
857
989
  /**
858
990
  * The default {@link IProviderAuthenticator}: every provider connection must
@@ -871,6 +1003,24 @@ type ProviderPrincipal = IProviderPrincipal;
871
1003
  /** @deprecated Use {@link IProviderAuthenticator}. */
872
1004
  type ProviderAuthenticator = IProviderAuthenticator;
873
1005
 
1006
+ /**
1007
+ * What the broker does when a provider connects to a slot another socket
1008
+ * already holds.
1009
+ *
1010
+ * - `reject`: refuse the newcomer whenever the incumbent socket is OPEN. The
1011
+ * pre-1.3 behavior, and the only mode that never disconnects a live provider,
1012
+ * at the cost of a slot wedged by a half-open socket until the OS gives up on
1013
+ * it (roughly two hours).
1014
+ * - `liveness` (default): refuse only while the incumbent still answers the
1015
+ * heartbeat. A socket that missed its last ping is terminated and the
1016
+ * newcomer takes the slot.
1017
+ * - `always`: the newcomer always wins. Honored **only** when
1018
+ * {@link IWsTunnelOptions.providerAuth} is configured and the newcomer
1019
+ * authenticated as the same principal as the incumbent; otherwise the broker
1020
+ * falls back to `liveness` and says so, because with provider auth off anyone
1021
+ * who can reach the URL could evict the real provider at will.
1022
+ */
1023
+ type ProviderTakeoverMode = "reject" | "liveness" | "always";
874
1024
  /**
875
1025
  * How `/<slot>/mcp` decides whether a browser origin may reach it.
876
1026
  *
@@ -968,7 +1118,8 @@ interface IWsTunnelOptions {
968
1118
  */
969
1119
  samplesIndexPath?: string;
970
1120
  /**
971
- * Browser origins allowed to reach `/<slot>/mcp`.
1121
+ * Browser origins allowed to reach a slot's client endpoints: `/<slot>/mcp`
1122
+ * (Streamable HTTP) and the legacy SSE pair `/<slot>/sse` + `/<slot>/messages`.
972
1123
  *
973
1124
  * The MCP specification requires the `Origin` header to be validated,
974
1125
  * because without it any web page the operator's browser happens to load
@@ -982,6 +1133,53 @@ interface IWsTunnelOptions {
982
1133
  * @default undefined, no browser origin is allowed
983
1134
  */
984
1135
  allowedOrigins?: AllowedOrigins;
1136
+ /**
1137
+ * How often, in milliseconds, the broker pings every connected provider
1138
+ * socket to check it is still there. `0` disables the heartbeat entirely.
1139
+ *
1140
+ * A provider that does not answer within one full interval is terminated,
1141
+ * which frees its slot for the reconnect that is usually already being
1142
+ * refused. Without this the only evidence of occupancy is the socket's
1143
+ * `readyState`, and a half-open socket (a killed browser tab, a laptop that
1144
+ * slept, a severed VPN) stays `OPEN` until the OS gives up on the TCP
1145
+ * connection, roughly two hours, during which the broker cheerfully reports
1146
+ * the zombie as connected, routes client frames into it, and refuses every
1147
+ * reconnect attempt with a `1008`.
1148
+ *
1149
+ * **What a pong actually proves.** An RFC 6455 pong is answered by the
1150
+ * peer's network stack, which in a browser is not the page's JavaScript
1151
+ * thread. So this detects a dead process, a dead machine and a dead network
1152
+ * path; it does **not** detect a page whose event loop is blocked or whose
1153
+ * MCP server stopped serving. For that failure use
1154
+ * {@link providerRequestTimeoutMs}, which measures the answer rather than
1155
+ * the socket.
1156
+ *
1157
+ * @default 30000
1158
+ */
1159
+ providerHeartbeatIntervalMs?: number;
1160
+ /**
1161
+ * What happens when a provider connects to a slot another socket holds.
1162
+ *
1163
+ * @default "liveness", the incumbent keeps the slot only while it still
1164
+ * answers the heartbeat
1165
+ */
1166
+ providerTakeover?: ProviderTakeoverMode;
1167
+ /**
1168
+ * How long, in milliseconds, the broker waits for a provider to answer one
1169
+ * request before failing it. `0` disables the timeout.
1170
+ *
1171
+ * On expiry the waiting client receives a JSON-RPC error naming the slot and
1172
+ * the elapsed time, addressed to the id it used. Without it, a provider that
1173
+ * simply never answers (a browser tab throttled in the background is the
1174
+ * normal case, not the exotic one) leaves the client's request open forever
1175
+ * with nothing to release it and no diagnostic anywhere.
1176
+ *
1177
+ * Raise it if you host genuinely long-running tools; a slow answer is worth
1178
+ * waiting for, but a hang with no error is not.
1179
+ *
1180
+ * @default 60000
1181
+ */
1182
+ providerRequestTimeoutMs?: number;
985
1183
  /**
986
1184
  * Optional static-file mounts served over plain HTTP.
987
1185
  * Matched by longest URL prefix; directory requests fall back to `index.html`.
@@ -1145,6 +1343,46 @@ declare class WsTunnel implements IBrokerContext {
1145
1343
  private readonly _providers;
1146
1344
  /** Maps a multiplexed WebSocket to the set of provider names it feeds. */
1147
1345
  private readonly _multiplexSockets;
1346
+ /**
1347
+ * Every inbound provider socket the heartbeat watches, dedicated and
1348
+ * multiplexed alike.
1349
+ *
1350
+ * An explicit collection is needed because there was none: `_providers`
1351
+ * holds only the socket that currently owns a slot (so it misses a
1352
+ * multiplex socket that has not announced a name yet), and
1353
+ * `_providerPrincipals` only has entries when provider auth is configured.
1354
+ */
1355
+ private readonly _providerSockets;
1356
+ /**
1357
+ * Liveness of each provider socket: `true` when it answered the last ping,
1358
+ * `false` while a ping is outstanding.
1359
+ *
1360
+ * A `WeakMap` so a terminated socket needs no cleanup here, and so a socket
1361
+ * the heartbeat never saw (heartbeat disabled) reads back `undefined`,
1362
+ * which the admission check treats as "no evidence either way" and refuses
1363
+ * the takeover.
1364
+ */
1365
+ private readonly _alive;
1366
+ /** Heartbeat sweep timer; `null` while stopped or disabled. */
1367
+ private _heartbeatTimer;
1368
+ /** Pending-request deadline sweep timer; `null` while stopped or disabled. */
1369
+ private _requestTimeoutTimer;
1370
+ /**
1371
+ * Counter behind every broker-assigned request id. Monotonic per process,
1372
+ * which is all the uniqueness the pending maps need.
1373
+ */
1374
+ private _nextRequestId;
1375
+ /**
1376
+ * Slots already warned about for answering with an id the broker never
1377
+ * issued, so a provider that does this on every frame warns once.
1378
+ */
1379
+ private readonly _unmatchedIdWarnedProviders;
1380
+ /**
1381
+ * Slots already warned about for emitting a frame that is not JSON-RPC.
1382
+ * A provider that speaks a non-JSON dialect emits one on every message, so
1383
+ * the warning fires once per slot instead of flooding the log.
1384
+ */
1385
+ private readonly _nonJsonWarnedProviders;
1148
1386
  /** Upstream providers (stdio child processes and remote URL servers), keyed by name. */
1149
1387
  private readonly _upstreams;
1150
1388
  /**
@@ -1191,6 +1429,18 @@ declare class WsTunnel implements IBrokerContext {
1191
1429
  get port(): number;
1192
1430
  get tls(): boolean;
1193
1431
  get paths(): IBrokerContext["paths"];
1432
+ /**
1433
+ * What the broker enforces on its listening surface right now, for
1434
+ * `broker_diagnose`.
1435
+ *
1436
+ * The rule it feeds is the one that catches a page the broker serves itself
1437
+ * being refused by its own origin check: static files mounted, no browser
1438
+ * origin allowed. That combination now costs more than it did, because the
1439
+ * check reaches the legacy SSE endpoints too.
1440
+ */
1441
+ getSecurityInfo(): IBrokerSecurityInfo;
1442
+ /** Slot the stdio bridge is pinned to, or `null` when there is no bridge. */
1443
+ getStdioBridgeTarget(): string | null;
1194
1444
  getProvidersInfo(): IBrokerProviderInfo[];
1195
1445
  getProviderInfo(name: string): IBrokerProviderInfo | undefined;
1196
1446
  private _buildProviderInfo;
@@ -1220,14 +1470,32 @@ declare class WsTunnel implements IBrokerContext {
1220
1470
  /** @deprecated Check `providerNames.length > 0` instead. */
1221
1471
  get hasProvider(): boolean;
1222
1472
  /**
1223
- * Starts the broker. Resolves once the HTTP server is listening.
1473
+ * Starts the broker. Resolves once the HTTP server is listening, and
1474
+ * **rejects** when the listen fails.
1475
+ *
1476
+ * The rejection is the point: until the port is bound, a listen failure has
1477
+ * nowhere else to go. `ws` mirrors the HTTP server's `'error'` onto the
1478
+ * `WebSocketServer` from a listener it installs inside its own constructor,
1479
+ * so an unhandled one is rethrown by Node as an uncaught exception, outside
1480
+ * any caller's `await` and outside any `catch` the caller wrote.
1224
1481
  */
1225
1482
  start(): Promise<void>;
1483
+ /**
1484
+ * Turns a listen failure into an error whose message says what to do next.
1485
+ *
1486
+ * `EADDRINUSE` is the one that happens in practice, and the reflex it
1487
+ * triggers is usually the wrong one: another broker gets spawned on another
1488
+ * port. Two brokers do not share anything, a slot is held by exactly one
1489
+ * process, so the second instance's providers are invisible to the first
1490
+ * instance's clients. Hence the message points at attaching to the running
1491
+ * broker first, and only then at changing the port.
1492
+ */
1493
+ private _listenError;
1226
1494
  /**
1227
1495
  * Starts the in-process MCP server that exposes the broker's own behaviors
1228
- * (`broker_info`, `providers_list`, `provider_status`) under the reserved
1229
- * provider slot `_broker`. No-op when {@link IWsTunnelOptions.enableBrokerProvider}
1230
- * is `false`.
1496
+ * (`broker_info`, `providers_list`, `provider_status`, `broker_guide`,
1497
+ * `broker_diagnose`) under the reserved provider slot `_broker`. No-op when
1498
+ * {@link IWsTunnelOptions.enableBrokerProvider} is `false`.
1231
1499
  */
1232
1500
  private _maybeStartBrokerServer;
1233
1501
  /**
@@ -1239,6 +1507,112 @@ declare class WsTunnel implements IBrokerContext {
1239
1507
  * Gracefully closes all connections and stops the HTTP server.
1240
1508
  */
1241
1509
  stop(): Promise<void>;
1510
+ /** Effective heartbeat period in ms; `0` means the heartbeat is off. */
1511
+ private _heartbeatIntervalMs;
1512
+ /** Effective per-request deadline in ms; `0` means requests never expire. */
1513
+ private _requestTimeoutMs;
1514
+ /**
1515
+ * Starts watching every provider socket for a missed pong.
1516
+ *
1517
+ * `unref()` is not cosmetic: without it this interval alone keeps the Node
1518
+ * event loop alive, so every test file that starts a tunnel would hang on
1519
+ * teardown, and an embedder's process would refuse to exit.
1520
+ */
1521
+ private _startHeartbeat;
1522
+ private _stopHeartbeat;
1523
+ /**
1524
+ * One heartbeat round: terminate whoever did not answer the previous ping,
1525
+ * then ping everybody still standing.
1526
+ *
1527
+ * The two-phase shape is what gives a provider a full interval to reply,
1528
+ * and it is why {@link _alive} means "answered since the last sweep" rather
1529
+ * than "is connected".
1530
+ */
1531
+ private _sweepHeartbeats;
1532
+ /**
1533
+ * Puts one accepted provider socket under the heartbeat.
1534
+ *
1535
+ * No-op when the heartbeat is disabled, which is what keeps
1536
+ * {@link _alive} empty and makes the admission check fall back to refusing
1537
+ * every takeover: with no liveness evidence, evicting the incumbent would
1538
+ * be a guess.
1539
+ */
1540
+ private _watchProviderSocket;
1541
+ /** Human-readable list of the slots one provider socket currently serves. */
1542
+ private _slotsOfSocket;
1543
+ /**
1544
+ * Decides whether a newly connected provider socket may take the slot
1545
+ * `name`, evicting the incumbent when it is allowed to.
1546
+ *
1547
+ * Returns `null` when the newcomer is admitted, or the refusal otherwise:
1548
+ * `detail` is the full diagnosis for the log and for the envelope the
1549
+ * multiplexed path can carry, `closeReason` the same thing squeezed into the
1550
+ * 123 bytes RFC 6455 allows on a close frame, with the actionable part
1551
+ * first so truncation eats the slot name (which the peer already knows)
1552
+ * rather than the instruction.
1553
+ *
1554
+ * See {@link ProviderTakeoverMode} for what each mode means and why
1555
+ * `"always"` is gated on provider authentication.
1556
+ */
1557
+ private _claimSlot;
1558
+ /**
1559
+ * `true` when two provider sockets authenticated as the same principal.
1560
+ *
1561
+ * Deliberately `false` when provider auth is off: with no authenticator
1562
+ * there are no principals to compare, and treating "both anonymous" as
1563
+ * "the same provider" is exactly the spoofing primitive the gate exists to
1564
+ * prevent.
1565
+ */
1566
+ private _sameProviderPrincipal;
1567
+ /** Closes a socket with a reason RFC 6455 actually allows on the wire. */
1568
+ private _closeWs;
1569
+ /**
1570
+ * Registers one outbound request against a slot and returns the frame to
1571
+ * put on the wire, with the client's JSON-RPC id replaced by a
1572
+ * broker-assigned one.
1573
+ *
1574
+ * The rewrite is the fix for a cross-client response leak. `state.pending`
1575
+ * is one map per **slot**, written from five different ingresses (raw WS,
1576
+ * legacy SSE, Streamable HTTP, the stdio bridge, in-process clients), and
1577
+ * it used to be keyed by the id the client chose. Two clients on one slot
1578
+ * that both start numbering at 1 (MCP Inspector plus Claude, the pairing
1579
+ * the architecture doc explicitly advertises) therefore shared one entry:
1580
+ * the second write replaced the first, one client received the other's
1581
+ * result, and the overwritten request hung forever with no error. Rewriting
1582
+ * also stops the provider from seeing two concurrent requests with the same
1583
+ * id, which it has no way to answer correctly either.
1584
+ *
1585
+ * Invisible to conforming clients: {@link _routeFromProvider} restores the
1586
+ * original id before the answer is delivered.
1587
+ *
1588
+ * Frames with no usable id (notifications) and JSON-RPC batches (an array,
1589
+ * which has no top-level id) are returned untouched and untracked, exactly
1590
+ * as before.
1591
+ */
1592
+ private _trackRequest;
1593
+ /** Delivers one already-addressed frame to the sink that is waiting for it. */
1594
+ private _deliverToSink;
1595
+ /**
1596
+ * Starts the sweep that fails requests a provider never answered.
1597
+ *
1598
+ * Same `unref()` requirement as the heartbeat. The period is derived from
1599
+ * the deadline rather than fixed, so a short timeout (a test, or a
1600
+ * latency-sensitive deployment) is still honored roughly on time instead of
1601
+ * being rounded up to the next sweep minutes later.
1602
+ */
1603
+ private _startRequestTimeoutSweep;
1604
+ private _stopRequestTimeoutSweep;
1605
+ /**
1606
+ * Fails every request whose deadline has passed.
1607
+ *
1608
+ * Until this existed a pending entry was released only by a matching
1609
+ * response, a provider disconnect, or the client's own close, so a provider
1610
+ * that stayed connected and simply never answered (a browser tab throttled
1611
+ * in the background is the ordinary case) left the caller waiting with
1612
+ * nothing to release it and no trace anywhere. A named error is strictly
1613
+ * better than a hang.
1614
+ */
1615
+ private _sweepPendingTimeouts;
1242
1616
  private _authorizationSubject;
1243
1617
  private _auditDecision;
1244
1618
  /**
@@ -1250,6 +1624,26 @@ declare class WsTunnel implements IBrokerContext {
1250
1624
  /** The JSON-RPC error returned when the slot has nobody behind it. */
1251
1625
  private _notConnectedPayload;
1252
1626
  private _handleHttp;
1627
+ /**
1628
+ * Applies the browser-origin check to the legacy SSE pair, answering `403`
1629
+ * and returning `false` when the origin is refused.
1630
+ *
1631
+ * This closes a hole, and it is the one behavior change in this release a
1632
+ * browser page can notice. `allowedOrigins` reached exactly one place, the
1633
+ * Streamable HTTP endpoint, so `/<slot>/sse` and `/<slot>/messages` were
1634
+ * open to every page on the machine: `Access-Control-Allow-Origin: *` is
1635
+ * set unconditionally a few lines above, and a POST of JSON to
1636
+ * `/<slot>/messages` is a CORS *simple* request, so any page could open an
1637
+ * `EventSource`, read the session id off the `endpoint` event, and drive the
1638
+ * broker. That is verbatim the attack the origin check exists to stop.
1639
+ *
1640
+ * The contract of the check is preserved exactly: a request carrying **no**
1641
+ * `Origin` always passes, which is every non-browser client (Claude Desktop,
1642
+ * Inspector, the server-side SDKs), and an `Origin` passes only if
1643
+ * `allowedOrigins` names it. The same predicate object is used as for
1644
+ * `/<slot>/mcp`, so the two endpoints cannot drift apart.
1645
+ */
1646
+ private _sseOriginAllowed;
1253
1647
  /**
1254
1648
  * Parses `/<providerName>/<endpoint>` from a URL path.
1255
1649
  * Returns `null` if the URL does not match this two-segment pattern.
@@ -1266,17 +1660,40 @@ declare class WsTunnel implements IBrokerContext {
1266
1660
  /** Turns a rejected {@link HttpAuthGuard.authorize} into an HTTP response. */
1267
1661
  private _handleAuthFailure;
1268
1662
  /**
1269
- * Builds the `ws` verifyClient hook that authenticates every WebSocket
1270
- * upgrade before the connection is accepted, returning a real `401`/`403`
1271
- * during the handshake rather than a post-handshake close:
1663
+ * Decides, from the URL alone, what one WebSocket upgrade is asking for.
1272
1664
  *
1665
+ * One function rather than the two `startsWith` chains this used to be, one
1666
+ * in the connection handler and one in `verifyClient`. They agreed by
1667
+ * coincidence, and a disagreement would mean a socket authenticated as one
1668
+ * role and then served as another.
1669
+ *
1670
+ * The three refusals are paths that cannot work whatever is behind them:
1671
+ * a provider URL with no slot name (which used to mint a slot literally
1672
+ * named `(unnamed)` that nothing could ever address) and a slot-scoped
1673
+ * provider URL with more than one segment (`/provider/a/b`, which aliases
1674
+ * the `%2F`-encoded spelling of the same slot, so refusing it removes
1675
+ * nothing and forces one spelling).
1676
+ *
1677
+ * What is deliberately **not** refused: a slot name that matches nothing
1678
+ * configured. Claiming a free slot by connecting to it is how a provider
1679
+ * registers, so an unknown name is the normal case, not an error.
1680
+ */
1681
+ private _classifyWsRoute;
1682
+ /**
1683
+ * Builds the `ws` verifyClient hook, which decides every WebSocket upgrade
1684
+ * before the connection is accepted, returning a real HTTP status during
1685
+ * the handshake rather than a post-handshake close:
1686
+ *
1687
+ * - Paths that cannot work are refused with `400` and the full reason as the
1688
+ * response body (see {@link _classifyWsRoute}).
1273
1689
  * - **Raw MCP clients** (`/<slot>`) are gated by the OAuth 2.1 resource
1274
1690
  * server ({@link _authGuard}) with the RFC 9728 `WWW-Authenticate` challenge.
1275
1691
  * - **Providers** (`/provider/<slot>`, `/providers`) are gated by the
1276
1692
  * {@link _providerAuth} shared-secret / custom authenticator.
1277
1693
  *
1278
- * Each side is independent: a branch with no authenticator configured is let
1279
- * through unchanged. Returns `undefined` (no hook) when neither is set.
1694
+ * Each authentication side is independent: a branch with no authenticator
1695
+ * configured is let through unchanged. The hook is always installed now,
1696
+ * because the path check applies whether or not anything is authenticated.
1280
1697
  */
1281
1698
  private _makeVerifyClient;
1282
1699
  /**
@@ -1315,16 +1732,88 @@ declare class WsTunnel implements IBrokerContext {
1315
1732
  private _fromHttpSession;
1316
1733
  /** Writes one JSON-RPC message as an SSE `message` event. */
1317
1734
  private _sendSseEvent;
1735
+ /**
1736
+ * Prints one line for every accepted WebSocket upgrade: the path asked for,
1737
+ * the role the router gave it, and the slot it landed on.
1738
+ *
1739
+ * Without it a successful connect produces no output whatsoever, so a
1740
+ * mistyped provider URL looks exactly like a working one until nothing ever
1741
+ * answers. The last router branch accepts **any** unmatched path as a client
1742
+ * slot, so `/providers/foo` (neither the multiplex endpoint `/providers` nor
1743
+ * a dedicated `/provider/foo`) becomes a client on a slot literally named
1744
+ * `providers/foo`; that case gets the fix spelled out in the same line.
1745
+ *
1746
+ * `console.log` is safe here: in stdio mode `bin.ts` rebinds the console to
1747
+ * stderr before the tunnel starts, so this never reaches the JSON-RPC stream.
1748
+ */
1749
+ private _logWsConnection;
1318
1750
  private _logProviderRegistration;
1319
1751
  private _onProviderConnect;
1320
1752
  /**
1321
- * Inspects a provider's first WebSocket message for an optional registration
1322
- * control frame `{ "type": "register", "aggregate": boolean }`. Returns
1323
- * `true` when the message was a registration frame: and thus consumed, not
1324
- * routed as MCP traffic. A normal MCP frame always carries `jsonrpc`, so it
1753
+ * Inspects a provider's first WebSocket message for a registration frame,
1754
+ * and consumes it when that is what it is. Returns `true` when the message
1755
+ * was a registration, and thus must not be routed as MCP traffic.
1756
+ *
1757
+ * Two shapes are accepted, both notifications a peer that does not know them
1758
+ * ignores:
1759
+ *
1760
+ * - `{"jsonrpc":"2.0","method":"notifications/register","params":{"aggregate":true}}`,
1761
+ * what `@cyanmycelium/mcp-broker-provider` sends on **both** paths. This
1762
+ * is the shape to write new providers against: the multiplexed socket has
1763
+ * always used it (wrapped in an envelope), so there is now one
1764
+ * registration to learn instead of one per path.
1765
+ * - `{"type":"register","aggregate":true}`, the legacy control frame, kept
1766
+ * working indefinitely for hand-written providers already sending it.
1767
+ *
1768
+ * `aggregate` is opt-in and stays that way. Joining `_all` publishes a
1769
+ * provider's tools and prompts to every client of the aggregate slot, which
1770
+ * is a confidentiality boundary: a provider that never asks is reachable
1771
+ * only on its own slot.
1772
+ *
1773
+ * A normal MCP frame carries `jsonrpc` with some other `method`, so it
1325
1774
  * returns `false` and the provider stays non-aggregated.
1326
1775
  */
1327
1776
  private _tryHandleRegistration;
1777
+ /**
1778
+ * Detects a `MultiplexTransport` connected to a slot-scoped provider URL,
1779
+ * the single most reported way to wire this broker up wrong, and refuses it
1780
+ * instead of letting it look like it worked.
1781
+ *
1782
+ * What the mistake looks like without this check: the socket connects, the
1783
+ * broker registers the slot from the URL before any frame exists, so
1784
+ * `provider_status` reports the provider connected; then every frame the
1785
+ * broker sends is a plain JSON-RPC one that the peer's envelope decoder
1786
+ * drops on the floor, and every frame the peer sends is an envelope with no
1787
+ * top-level `id`, so it matches no pending request. Both sides believe they
1788
+ * are connected and no request is ever answered. Nothing is logged anywhere.
1789
+ *
1790
+ * Only an **unambiguous** envelope closes the socket: a JSON object with no
1791
+ * `jsonrpc` member that decodes as `{provider, payload}`. A malformed frame
1792
+ * is left alone and routed as before, since a provider is entitled to speak
1793
+ * a dialect the broker does not recognize. The legacy
1794
+ * `{"type":"register","aggregate":true}` frame has no `provider`/`payload`
1795
+ * pair and so is never mistaken for one.
1796
+ *
1797
+ * The diagnosis goes out three ways because each reaches a different reader:
1798
+ * the broker log, an error **envelope** (the only framing this particular
1799
+ * peer can decode), and the close reason, which the provider SDK surfaces
1800
+ * through `onError` and prints to the browser console.
1801
+ */
1802
+ private _refuseEnvelopeOnSlotPath;
1803
+ /**
1804
+ * The mirror image: a `DirectTransport` connected to the shared multiplexed
1805
+ * base. Refuses it instead of dropping its frames one by one in silence.
1806
+ *
1807
+ * Signature of the mistake: the socket opens, the broker never learns a slot
1808
+ * name (they only arrive inside envelopes), so no slot is ever claimed and
1809
+ * every client is told the provider is not connected, while the provider
1810
+ * believes it published successfully.
1811
+ *
1812
+ * Only a frame that is unambiguously plain JSON-RPC (a JSON object carrying
1813
+ * `jsonrpc`, that did not decode as an envelope) closes the socket. Anything
1814
+ * else keeps the old behavior of dropping the frame, now with one warning.
1815
+ */
1816
+ private _refusePlainFrameOnMultiplexPath;
1328
1817
  private _onClientConnect;
1329
1818
  /**
1330
1819
  * Handles a multiplexed provider WebSocket (`/providers`).
@@ -1341,6 +1830,21 @@ declare class WsTunnel implements IBrokerContext {
1341
1830
  private _routeFromStdioClient;
1342
1831
  private _routeFromClient;
1343
1832
  private _routeFromProvider;
1833
+ /**
1834
+ * Reports a frame carrying an id nothing is waiting for.
1835
+ *
1836
+ * Two very different causes, so the message names both. Either the provider
1837
+ * did not echo the id it was given (the frame is then unroutable and the
1838
+ * real client hangs until the request deadline), or the provider is opening
1839
+ * a **server-to-client** request of its own (`sampling/createMessage`,
1840
+ * `roots/list`, `elicitation/create`), which this broker does not relay: the
1841
+ * frame is dropped and the provider waits for an answer that never comes.
1842
+ * Both used to be silent, which is what made the second one impossible to
1843
+ * diagnose from either end.
1844
+ *
1845
+ * Once per slot: a provider doing this does it on every frame.
1846
+ */
1847
+ private _warnUnmatchedResponseId;
1344
1848
  /** Sends a message to all clients connected to one provider. */
1345
1849
  private _broadcast;
1346
1850
  /**
@@ -1388,6 +1892,9 @@ declare class WsTunnelBuilder {
1388
1892
  private _mcpPath;
1389
1893
  private _samplesIndexPath;
1390
1894
  private _allowedOrigins;
1895
+ private _providerHeartbeatIntervalMs;
1896
+ private _providerTakeover;
1897
+ private _providerRequestTimeoutMs;
1391
1898
  private _staticMounts;
1392
1899
  private _stdioUpstreams;
1393
1900
  private _remoteUpstreams;
@@ -1459,6 +1966,50 @@ declare class WsTunnelBuilder {
1459
1966
  * ```
1460
1967
  */
1461
1968
  withAllowedOrigins(allowed: AllowedOrigins): this;
1969
+ /**
1970
+ * Sets how often the broker pings each connected provider socket to check
1971
+ * it is still there. Pass `0` to disable the heartbeat.
1972
+ *
1973
+ * A provider that misses a full interval is terminated and its slot freed,
1974
+ * which is what stops a half-open socket (a killed tab, a slept laptop, a
1975
+ * dropped VPN) from holding a slot for the ~2 hours it takes the OS to give
1976
+ * up on the TCP connection, refusing every reconnect in the meantime.
1977
+ *
1978
+ * Honest about what it proves: a pong is answered by the peer's network
1979
+ * stack, not by the page's JavaScript. It detects a dead process, machine or
1980
+ * network path, not a provider that is connected and simply not serving.
1981
+ * For that, see {@link withProviderRequestTimeout}.
1982
+ *
1983
+ * @default 30000
1984
+ */
1985
+ withProviderHeartbeat(intervalMs: number): this;
1986
+ /**
1987
+ * Sets what happens when a provider connects to a slot another socket
1988
+ * already holds.
1989
+ *
1990
+ * - `"reject"`: the incumbent always keeps the slot.
1991
+ * - `"liveness"` (default): the incumbent keeps it only while it answers the
1992
+ * heartbeat; a socket that missed its last ping is terminated.
1993
+ * - `"always"`: the newcomer wins, but **only** when
1994
+ * {@link withProviderSecret} / {@link withProviderAuth} is configured and
1995
+ * it authenticated as the same principal as the incumbent. Without that,
1996
+ * the broker falls back to `"liveness"` and logs why: with provider auth
1997
+ * off, unconditional takeover would let anyone who can reach the URL evict
1998
+ * the real provider.
1999
+ */
2000
+ withProviderTakeover(mode: ProviderTakeoverMode): this;
2001
+ /**
2002
+ * Sets how long the broker waits for a provider to answer one request before
2003
+ * failing it with a JSON-RPC error naming the slot. Pass `0` to disable.
2004
+ *
2005
+ * Without a deadline, a provider that stays connected and never answers (a
2006
+ * browser tab throttled in the background is the ordinary case) leaves the
2007
+ * caller waiting forever with nothing to release it. Raise it if you host
2008
+ * genuinely long-running tools.
2009
+ *
2010
+ * @default 60000
2011
+ */
2012
+ withProviderRequestTimeout(timeoutMs: number): this;
1462
2013
  /**
1463
2014
  * Adds a static-file mount served over plain HTTP.
1464
2015
  * Can be called multiple times; longest-prefix match wins at runtime.
@@ -1582,6 +2133,290 @@ type McpbBundleConfig = IMcpbBundleConfig;
1582
2133
  */
1583
2134
  declare function unzipMcpb(mcpbPath: string, destDir: string): void;
1584
2135
 
2136
+ /**
2137
+ * Teaches an agent how to integrate with this broker, from inside the broker.
2138
+ *
2139
+ * Every page is exposed twice: as a resource (`broker://guide/<topic>`, plus
2140
+ * the `broker://guide/{topic}` template) and through the `broker_guide` tool,
2141
+ * because a fair number of MCP clients implement tools and ignore resources
2142
+ * entirely. Both return the same text.
2143
+ *
2144
+ * This is the answer to "an agent asked to embed mcp-broker has to find and
2145
+ * read several READMEs across two packages": it does not, it reads this.
2146
+ */
2147
+ declare class BrokerGuideBehavior extends McpBehavior {
2148
+ static readonly NAMESPACE = "broker_guide";
2149
+ constructor(context: IBrokerContext);
2150
+ protected _buildResources(): McpResource[];
2151
+ protected _buildTemplate(): McpResourceTemplate[];
2152
+ protected _buildTools(): McpTool[];
2153
+ }
2154
+
2155
+ /**
2156
+ * Exposes `broker_diagnose({ slot? })`: the live state of the broker plus the
2157
+ * problems it can prove about its own wiring, each with a symptom, the
2158
+ * evidence, and a fix.
2159
+ *
2160
+ * It exists because the raw counters were not enough. `provider_status`
2161
+ * already reported `pendingCount`, and that number is what located a
2162
+ * transport/path mismatch in the field, but only after someone thought to
2163
+ * correlate it with the transport kind. That correlation is mechanical, so
2164
+ * the broker does it here instead of leaving it to the reader.
2165
+ *
2166
+ * Tool-only, no resource: a diagnosis must never be served from a cache.
2167
+ */
2168
+ declare class BrokerDiagnoseBehavior extends McpBehavior {
2169
+ static readonly NAMESPACE = "broker_diagnostics";
2170
+ constructor(context: IBrokerContext);
2171
+ protected _buildTools(): McpTool[];
2172
+ }
2173
+
2174
+ /**
2175
+ * The broker's self-documentation, as plain Markdown constants.
2176
+ *
2177
+ * This module is the **source of truth** for how to integrate with an
2178
+ * mcp-broker instance. It is served live over MCP by
2179
+ * {@link BrokerGuideBehavior} (resources `broker://guide/*` and the
2180
+ * `broker_guide` tool), so an agent that can reach the reserved `_broker`
2181
+ * slot, or `_all` which aggregates it, learns the integration rules without
2182
+ * ever opening a README.
2183
+ *
2184
+ * Every statement here is derived from the broker's own source, not from the
2185
+ * repository's prose documentation. When the two disagree, this file is right
2186
+ * and the prose is stale. The markdown docs and the samples import these
2187
+ * constants rather than restating them, so there is exactly one copy of the
2188
+ * rules to keep correct.
2189
+ *
2190
+ * Content is written for a machine reader with no prior knowledge of this
2191
+ * project: rules are stated as rules, failure modes name their observable
2192
+ * signature, and every diagnosis ends in an action.
2193
+ */
2194
+ /**
2195
+ * Identifier of one guide page. Also the last URI segment of the matching
2196
+ * `broker://guide/<topic>` resource, and the value accepted by the
2197
+ * `broker_guide` tool's `topic` argument.
2198
+ */
2199
+ type BrokerGuideTopic = "index" | "publish-provider" | "connect-client" | "host-config" | "deploy" | "troubleshooting";
2200
+ /** Scheme + path prefix shared by every guide resource URI. */
2201
+ declare const BROKER_GUIDE_URI_PREFIX = "broker://guide/";
2202
+ /** RFC 6570 URI template covering every guide page. */
2203
+ declare const BROKER_GUIDE_URI_TEMPLATE = "broker://guide/{topic}";
2204
+ /** One guide page: its identity, its one-line pitch, and its Markdown body. */
2205
+ interface IBrokerGuide {
2206
+ /** Stable topic id, used by the tool argument and the resource URI. */
2207
+ topic: BrokerGuideTopic;
2208
+ /** Canonical resource URI, `broker://guide/<topic>`. */
2209
+ uri: string;
2210
+ /** Short human title, e.g. "Publish a provider". */
2211
+ title: string;
2212
+ /** One line describing when to read this page. Shown in the index. */
2213
+ summary: string;
2214
+ /** The page body, Markdown, self-contained. */
2215
+ content: string;
2216
+ }
2217
+ /**
2218
+ * Every guide page, in the order an agent should meet them: the index first,
2219
+ * then the two integration directions, then deployment, then the failure
2220
+ * reference.
2221
+ */
2222
+ declare const BROKER_GUIDES: readonly IBrokerGuide[];
2223
+ /** Every topic id, in catalog order. */
2224
+ declare const BROKER_GUIDE_TOPICS: readonly BrokerGuideTopic[];
2225
+ /** MIME type every guide page is served with. */
2226
+ declare const BROKER_GUIDE_MIME_TYPE = "text/markdown";
2227
+ /** Builds the canonical resource URI for a topic. */
2228
+ declare function brokerGuideUri(topic: BrokerGuideTopic): string;
2229
+ /** Looks a guide page up by topic id. `undefined` when the topic is unknown. */
2230
+ declare function brokerGuide(topic: string): IBrokerGuide | undefined;
2231
+ /**
2232
+ * Extracts the topic from a `broker://guide/<topic>` URI, or `undefined` when
2233
+ * the URI is not a guide URI or names a topic that does not exist.
2234
+ *
2235
+ * Percent-encoding is decoded so `broker://guide/publish%2Dprovider` resolves,
2236
+ * and a decode failure is treated as "not a guide URI" rather than throwing:
2237
+ * the URI comes off the wire.
2238
+ */
2239
+ declare function brokerGuideTopicFromUri(uri: string): BrokerGuideTopic | undefined;
2240
+ /**
2241
+ * The index rendered as data rather than prose: one entry per page, for a
2242
+ * client that would rather plan its reads than parse a Markdown table.
2243
+ */
2244
+ declare function brokerGuideIndex(): Array<{
2245
+ topic: BrokerGuideTopic;
2246
+ uri: string;
2247
+ title: string;
2248
+ summary: string;
2249
+ }>;
2250
+
2251
+ /**
2252
+ * Serves the broker's own integration documentation, so an agent that can
2253
+ * reach this slot never has to find a README.
2254
+ *
2255
+ * The prose lives in `broker.guides.ts` and is static, which is what makes it
2256
+ * safe to cache and to re-export to the markdown docs. What is NOT static is
2257
+ * the deployment it describes: port, scheme and URL paths are per-instance.
2258
+ * Rather than templating the guides, the adapter appends one short block of
2259
+ * effective values to every page, so a reader never has to guess whether the
2260
+ * defaults in the prose are the values in force here.
2261
+ */
2262
+ declare class BrokerGuideAdapter extends McpAdapterBase {
2263
+ private readonly _context;
2264
+ constructor(_context: IBrokerContext);
2265
+ readResourceAsync(uri: string): Promise<McpResourceContent | undefined>;
2266
+ executeToolAsync(_uri: string, toolName: string, args: Record<string, unknown>): Promise<McpToolResult>;
2267
+ /**
2268
+ * Machine-readable form of the index, for a caller that wants to plan its
2269
+ * reads without parsing a Markdown table. Exposed for the behavior's tool
2270
+ * schema documentation and for tests.
2271
+ */
2272
+ index(): ReturnType<typeof brokerGuideIndex>;
2273
+ /**
2274
+ * Appends the effective configuration of THIS broker to a guide page.
2275
+ *
2276
+ * Everything in the block is read live from the context, so a deployment
2277
+ * that moved a path or enabled TLS does not silently contradict the prose
2278
+ * above it.
2279
+ */
2280
+ private _render;
2281
+ }
2282
+
2283
+ /**
2284
+ * Runs the broker's self-diagnosis on demand.
2285
+ *
2286
+ * Deliberately tool-only, with no backing resource: the answer is a snapshot
2287
+ * of live state, and a resource read can be served from the behavior's content
2288
+ * cache, which would hand a caller a stale diagnosis at exactly the moment it
2289
+ * matters. `broker://providers` is the resource for state you want cached;
2290
+ * this is the one you want fresh.
2291
+ */
2292
+ declare class BrokerDiagnoseAdapter extends McpAdapterBase {
2293
+ private readonly _context;
2294
+ constructor(_context: IBrokerContext);
2295
+ readResourceAsync(_uri: string): Promise<McpResourceContent | undefined>;
2296
+ executeToolAsync(_uri: string, toolName: string, args: Record<string, unknown>): Promise<McpToolResult>;
2297
+ }
2298
+
2299
+ /**
2300
+ * The broker's self-diagnosis: live state plus the problems the broker can
2301
+ * *prove* about its own wiring, each carrying the fix.
2302
+ *
2303
+ * The point is to do the correlation an integrator would otherwise have to do
2304
+ * by hand. `provider_status` already exposes the raw counters, and the field
2305
+ * evidence is that `pendingCount` is what finally located a transport/path
2306
+ * mismatch. That inference is mechanical, so the broker should make it instead
2307
+ * of making a caller notice a number.
2308
+ *
2309
+ * Two rules govern what lands in {@link IBrokerDiagnosisProblem}:
2310
+ *
2311
+ * 1. **Evidence or nothing.** A problem is reported only when the state
2312
+ * actually observed implies it. A check whose input is not reachable on this
2313
+ * host is reported as skipped (see {@link IBrokerDiagnosisSkippedCheck}),
2314
+ * never guessed at.
2315
+ * 2. **Every problem ends in an action.** `fix` must be executable by a reader
2316
+ * that has read nothing else, so it names paths, slots and settings in full
2317
+ * rather than pointing at a document.
2318
+ *
2319
+ * The engine is a pure function of {@link IBrokerContext}, so it is testable
2320
+ * without a socket and reusable by anything holding a context.
2321
+ */
2322
+
2323
+ /**
2324
+ * How much a problem matters.
2325
+ *
2326
+ * - `error`: something is broken right now and a caller is being hurt by it.
2327
+ * - `warning`: a configuration that will break under a foreseeable condition.
2328
+ * - `info`: worth knowing, nothing is wrong.
2329
+ */
2330
+ type BrokerDiagnosisSeverity = "error" | "warning" | "info";
2331
+ /** Stable identifier of a diagnostic rule. Safe to branch on programmatically. */
2332
+ type BrokerDiagnosisRuleId = "broker-not-started" | "transport-path-mismatch" | "provider-not-responding" | "upstream-not-responding" | "sessions-without-clients" | "no-providers" | "slot-never-connected" | "aggregate-disabled" | "aggregate-empty" | "aggregate-missing-live-slots" | "stdio-bridge-target-unreachable" | "self-served-page-blocked";
2333
+ /** One detected problem: what is wrong, what proves it, and what to do. */
2334
+ interface IBrokerDiagnosisProblem {
2335
+ /** Stable rule id. */
2336
+ id: BrokerDiagnosisRuleId;
2337
+ severity: BrokerDiagnosisSeverity;
2338
+ /** Slot the problem is about, when it is about one. */
2339
+ slot?: string;
2340
+ /** What a caller of this broker actually observes. */
2341
+ symptom: string;
2342
+ /** The state that proves the symptom, verbatim from the live snapshot. */
2343
+ evidence: Record<string, unknown>;
2344
+ /** A concrete action, executable without reading anything else. */
2345
+ fix: string;
2346
+ }
2347
+ /** A rule that could not run, and why. Never a silent omission. */
2348
+ interface IBrokerDiagnosisSkippedCheck {
2349
+ id: BrokerDiagnosisRuleId;
2350
+ reason: string;
2351
+ }
2352
+ /** One slot as the diagnosis sees it: the live counters plus its role. */
2353
+ interface IBrokerDiagnosisSlot extends IBrokerProviderInfo {
2354
+ /** `true` for `_broker` and `_all`, which no provider may claim. */
2355
+ reserved: boolean;
2356
+ /** `true` when this slot currently contributes to `_all`. `undefined` when unknown. */
2357
+ aggregated?: boolean;
2358
+ }
2359
+ /** The whole answer of `broker_diagnose`. */
2360
+ interface IBrokerDiagnosis {
2361
+ /** Identity and listening configuration, enough to build a URL. */
2362
+ broker: {
2363
+ name: string;
2364
+ version: string;
2365
+ startedAt: string | null;
2366
+ uptimeSeconds: number;
2367
+ listening: string;
2368
+ tls: boolean;
2369
+ paths: IBrokerContext["paths"];
2370
+ endpoints: {
2371
+ provider: string;
2372
+ providers: string;
2373
+ client: string;
2374
+ streamableHttp: string;
2375
+ };
2376
+ };
2377
+ /** Slots in scope: every slot, or just the one the caller asked about. */
2378
+ slots: IBrokerDiagnosisSlot[];
2379
+ /** Counts, so a caller does not have to reduce the array itself. */
2380
+ summary: {
2381
+ slots: number;
2382
+ connected: number;
2383
+ providerSlots: number;
2384
+ connectedProviderSlots: number;
2385
+ pendingRequests: number;
2386
+ clients: number;
2387
+ sessions: number;
2388
+ };
2389
+ /** Membership of `_all`, when the host can report it. */
2390
+ aggregate?: {
2391
+ enabled: boolean;
2392
+ providers: readonly string[];
2393
+ };
2394
+ /** Detected problems, most severe first. Empty means nothing was provable. */
2395
+ problems: IBrokerDiagnosisProblem[];
2396
+ /** Rules that could not run here. */
2397
+ checksSkipped: IBrokerDiagnosisSkippedCheck[];
2398
+ /**
2399
+ * Statements that are true and worth acting on but are not faults, e.g. a
2400
+ * configuration that only breaks a topology this broker cannot see.
2401
+ */
2402
+ notes: string[];
2403
+ /** Where to read the rule behind a problem. */
2404
+ seeAlso: string[];
2405
+ }
2406
+ /**
2407
+ * Runs every rule that has the evidence to run, against a live broker context.
2408
+ *
2409
+ * @param context Read-only view of the broker.
2410
+ * @param slot Narrows the report to one slot. Problems that are not about a
2411
+ * slot (no providers at all, a blocked self-served page) are
2412
+ * dropped in that mode, because they are not answers to the
2413
+ * question that was asked.
2414
+ * @returns The full diagnosis. `undefined` only when `slot` names a slot the
2415
+ * broker has never heard of, which the caller should report as an
2416
+ * error naming `providers_list`.
2417
+ */
2418
+ declare function diagnoseBroker(context: IBrokerContext, slot?: string): IBrokerDiagnosis | undefined;
2419
+
1585
2420
  declare const VERSION: string;
1586
2421
  declare const PACKAGE_NAME: string;
1587
2422
 
@@ -1641,8 +2476,57 @@ interface IBrokerConfig {
1641
2476
  locale?: string;
1642
2477
  /** Bridge stdin/stdout for a Claude-Desktop-style client. Maps to `MCP_BROKER_STDIO_PROVIDER`. */
1643
2478
  stdioProvider?: string;
1644
- /** Logical broker name reported by `broker_info`. */
2479
+ /**
2480
+ * Logical broker name reported by `broker_info`.
2481
+ *
2482
+ * **Library-only today.** `WsTunnel` honors it
2483
+ * (`IWsTunnelOptions.brokerName`), but `WsTunnelBuilder` has no
2484
+ * `withBrokerName()`, so the CLI cannot forward it and setting it in
2485
+ * `config.json` has no effect. Set it through the programmatic API until
2486
+ * the setter exists.
2487
+ */
1645
2488
  brokerName?: string;
2489
+ /**
2490
+ * How often (ms) the broker pings each connected provider socket to check
2491
+ * it is still there. `0` disables the heartbeat. Maps to
2492
+ * `MCP_BROKER_PROVIDER_HEARTBEAT_MS`.
2493
+ *
2494
+ * A provider that misses a full interval is terminated and its slot freed,
2495
+ * which is what stops a half-open socket (a killed browser tab, a slept
2496
+ * laptop, a dropped VPN) from holding a slot for the ~2 hours the OS takes
2497
+ * to give up on the TCP connection, refusing every reconnect meanwhile.
2498
+ *
2499
+ * @default 30000
2500
+ */
2501
+ providerHeartbeatIntervalMs?: number;
2502
+ /**
2503
+ * How long (ms) the broker waits for a provider to answer one request
2504
+ * before failing it with a JSON-RPC error naming the slot. `0` disables the
2505
+ * deadline. Maps to `MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS`.
2506
+ *
2507
+ * Raise it if you host genuinely long-running tools; without it a provider
2508
+ * that stays connected and never answers (a throttled background browser
2509
+ * tab is the ordinary case) leaves the caller waiting forever.
2510
+ *
2511
+ * @default 60000
2512
+ */
2513
+ providerRequestTimeoutMs?: number;
2514
+ /**
2515
+ * What happens when a provider connects to a slot another socket already
2516
+ * holds. Maps to `MCP_BROKER_PROVIDER_TAKEOVER`.
2517
+ *
2518
+ * - `"reject"`: the incumbent always keeps the slot.
2519
+ * - `"liveness"` (default): the incumbent keeps it only while it answers
2520
+ * the heartbeat.
2521
+ * - `"always"`: the newcomer wins, but only when provider authentication is
2522
+ * configured and it authenticated as the same principal as the incumbent.
2523
+ * Without provider auth the broker falls back to `"liveness"` and says so,
2524
+ * because unconditional takeover would let anyone who can reach the URL
2525
+ * evict the real provider.
2526
+ *
2527
+ * @default "liveness"
2528
+ */
2529
+ providerTakeover?: "reject" | "liveness" | "always";
1646
2530
  /**
1647
2531
  * Browser origins allowed to reach `/<slot>/mcp`.
1648
2532
  *
@@ -1678,13 +2562,29 @@ interface IBrokerConfig {
1678
2562
  * `MCP_BROKER_PUBLIC_BASE_URL`, `MCP_BROKER_JWKS`, `MCP_BROKER_ISSUER`.
1679
2563
  */
1680
2564
  auth?: IBrokerAuthConfig;
1681
- /** URL paths (override the defaults). */
2565
+ /**
2566
+ * URL paths (override the defaults). Every key is also settable through an
2567
+ * environment variable, which wins.
2568
+ *
2569
+ * Changing one moves an endpoint for **every** peer: a provider SDK, a
2570
+ * client, and the startup banner all have to agree. The two provider paths
2571
+ * are not interchangeable, they carry different framing:
2572
+ * `provider` is a prefix (`<provider>/<slot>`) speaking plain JSON-RPC
2573
+ * frames (`DirectTransport`), `providers` is matched exactly and speaks
2574
+ * multiplex envelopes (`MultiplexTransport`).
2575
+ */
1682
2576
  paths?: {
2577
+ /** Prefix for one-slot-per-socket provider connections. Maps to `MCP_BROKER_PROVIDER_PATH`. @default "/provider" */
1683
2578
  provider?: string;
2579
+ /** Exact path for multiplexed provider connections. Maps to `MCP_BROKER_PROVIDERS_PATH`. @default "/providers" */
1684
2580
  providers?: string;
2581
+ /** Prefix raw-WebSocket clients connect to. Maps to `MCP_BROKER_CLIENT_PATH`. @default "/" */
1685
2582
  client?: string;
2583
+ /** Per-slot suffix for the Streamable HTTP transport. Maps to `MCP_BROKER_MCP_PATH`. @default "/mcp" */
1686
2584
  mcp?: string;
2585
+ /** Per-slot suffix for the legacy SSE stream (GET). Maps to `MCP_BROKER_SSE_PATH`. @default "/sse" */
1687
2586
  sse?: string;
2587
+ /** Per-slot suffix for legacy SSE JSON-RPC posts. Maps to `MCP_BROKER_MESSAGES_PATH`. @default "/messages" */
1688
2588
  messages?: string;
1689
2589
  };
1690
2590
  /** TLS material as paths on disk. Resolved against the config file's directory. */
@@ -1697,8 +2597,22 @@ interface IBrokerConfig {
1697
2597
  * always take precedence.
1698
2598
  */
1699
2599
  www?: {
1700
- /** Auto-launch the default browser at the root URL on startup. */
1701
- open?: boolean;
2600
+ /**
2601
+ * Auto-launch the default browser on startup. Maps to `MCP_BROKER_OPEN`.
2602
+ *
2603
+ * - `false` / absent / `""` / `"0"`: do not open anything.
2604
+ * - `true` / `"1"`: open the broker root, `<scheme>://localhost:<port>/`.
2605
+ * - a path (`"/app/index.html"`): open that page on this broker.
2606
+ * - an absolute URL on this broker's own origin: opened as given.
2607
+ *
2608
+ * A URL on any other origin is refused with a message, and so is any
2609
+ * other string. See {@link resolveOpenTarget} for why.
2610
+ *
2611
+ * The browser opens only when a static mount actually covers the
2612
+ * resolved path; otherwise the broker says which mounts exist instead of
2613
+ * launching a browser onto a 404.
2614
+ */
2615
+ open?: boolean | string;
1702
2616
  /** URL-prefix → directory mappings. Longest-prefix match wins. */
1703
2617
  mounts?: Array<{
1704
2618
  urlPrefix: string;
@@ -1780,6 +2694,58 @@ declare const DEFAULT_CONFIG_FILENAME = "config.json";
1780
2694
  * {@link ILoadedBrokerConfig.baseDir} by the consumer.
1781
2695
  */
1782
2696
  declare function loadBrokerConfig(path?: string): ILoadedBrokerConfig;
2697
+ /**
2698
+ * Outcome of {@link resolveOpenTarget}. Exactly one of the three states holds:
2699
+ *
2700
+ * - `{ url: null, path: null }` and no `error`: nothing should be opened.
2701
+ * - `{ url, path }`: open `url`; `path` is what a static mount has to cover.
2702
+ * - `{ url: null, path: null, error }`: the value was refused, `error` is a
2703
+ * sentence to print verbatim that names both the fault and the fix.
2704
+ */
2705
+ interface IOpenTargetResolution {
2706
+ /** Absolute URL to hand to the platform opener, or `null` to open nothing. */
2707
+ url: string | null;
2708
+ /** Path portion of {@link url} (always starts with `/`), or `null`. */
2709
+ path: string | null;
2710
+ /** Set when the raw value was refused. Human-readable, names the fix. */
2711
+ error?: string;
2712
+ }
2713
+ /**
2714
+ * Resolves `www.open` / `MCP_BROKER_OPEN` into an absolute URL to launch.
2715
+ *
2716
+ * Lives here rather than in `bin.ts` because `bin.ts` starts a server the
2717
+ * moment it is imported, so nothing in it can be unit-tested.
2718
+ *
2719
+ * Accepted forms (leading/trailing whitespace is trimmed):
2720
+ *
2721
+ * | `raw` | result |
2722
+ * |----------------------------|-------------------------------------------|
2723
+ * | `undefined`, `false`, `""`, `"0"`, `"false"` | open nothing |
2724
+ * | `true`, `"1"`, `"true"` | `<baseUrl>/` |
2725
+ * | `"/app/"`, `"/index.html"` | resolved against `baseUrl` |
2726
+ * | `"http://localhost:3000/x"`| passed through when the origin is `baseUrl`'s |
2727
+ * | anything else | refused, with `error` explaining why |
2728
+ *
2729
+ * Two refusals are security-load-bearing rather than pedantic:
2730
+ *
2731
+ * - **A foreign origin is refused.** Auto-opening is a startup convenience, and
2732
+ * a config file (or an env var inherited from a parent process) that can make
2733
+ * the broker launch a browser at an arbitrary site is a phishing primitive
2734
+ * with no upside: nothing about starting a broker requires visiting another
2735
+ * host. Open it yourself, or point `open` at a page this broker serves.
2736
+ * - **`//host/path` is refused** even though it starts with `/`: it is a
2737
+ * protocol-relative URL, so `new URL("//evil.example/x", base)` resolves to
2738
+ * `evil.example` and a naive "starts with a slash so it is local" test lets
2739
+ * it through.
2740
+ *
2741
+ * Anything that is neither is refused rather than forwarded, because the
2742
+ * platform opener performs no validation of its own: a typo'd relative path is
2743
+ * handed to the shell and can launch a local file or a registered application.
2744
+ *
2745
+ * @param raw The configured value (`config.www.open`) or the raw env string.
2746
+ * @param baseUrl Origin this broker is reachable at, e.g. `http://localhost:3000`.
2747
+ */
2748
+ declare function resolveOpenTarget(raw: boolean | string | undefined | null, baseUrl: string): IOpenTargetResolution;
1783
2749
  /** @deprecated Use {@link IBrokerAuthConfig}. */
1784
2750
  type BrokerAuthConfig = IBrokerAuthConfig;
1785
2751
  /** @deprecated Use {@link IBrokerConfig}. */
@@ -1787,4 +2753,4 @@ type BrokerConfig = IBrokerConfig;
1787
2753
  /** @deprecated Use {@link ILoadedBrokerConfig}. */
1788
2754
  type LoadedBrokerConfig = ILoadedBrokerConfig;
1789
2755
 
1790
- export { type AccessTokenClaims, type AggregateScopeFilter, type AllowedOrigins, type AuditContext, AuthError, type AuthErrorCode, type AuthorizationAuditConfig, type AuthorizationAuditEvent, type AuthorizationDecision, type AuthorizationDecisionReason, type AuthorizationPolicyConfig, type AuthorizationRequest, type AuthorizationSubject, BROKER_PROVIDER_NAME, type BrokerAuthConfig, type BrokerConfig, type BrokerContext, type BrokerGrammarEntry, BrokerInfoBehavior, type BrokerLocale, type BrokerProviderInfo, type BrokerProviderTransport, BrokerProvidersBehavior, type BrokerUserAgent, type CapabilityClassifier, type ClassifiedCapability, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, type DenyPolicy, HttpAuthGuard, type IAuditContext, type IAuthorizationAuditConfig, type IAuthorizationAuditEvent, type IAuthorizationDecision, type IAuthorizationPolicyConfig, type IAuthorizationRequest, type IAuthorizationSubject, type IBrokerAuthConfig, type IBrokerConfig, type IBrokerContext, type IBrokerGrammarEntry, type IBrokerProviderInfo, type ICapabilityClassifier, type IClassifiedCapability, type IDenyPolicy, type IInternalClient, type IJwtAuthOptions, type IJwtValidatorOptions, type ILoadedBrokerConfig, type IMcpOperation, type IMcpbBundleConfig, type IPolicyAssignment, type IPolicyAuthorization, type IPolicyEngine, type IPrincipal, type IProviderAuthenticator, type IProviderPrincipal, type IRemoteUpstreamConfig, type IResolvedAuth, type IRoleDefinition, type ISlotResourceResolver, type IStartBrokerServerOptions, type IStaticMount, type IStdioUpstreamConfig, type ISubjectMapper, type ISubjectMappingConfig, type IUpstream, type IWsTunnelOptions, type InternalClient, type JwtAuthOptions, JwtSubjectMapper, JwtTokenValidator, type JwtValidatorOptions, type LoadedBrokerConfig, type McpOperation, type McpbBundleConfig, PACKAGE_NAME, type PolicyAssignment, type PolicyAuthorization, type PolicyEngine, type Principal, type ProtectedResourceMetadata, type ProviderAuthenticationResult, type ProviderAuthenticator, type ProviderAuthenticatorReturn, type ProviderPrincipal, RemoteUpstream, type RemoteUpstreamConfig, type ResolvedAuth, ResourcePath, ResourcePathPattern, type RoleDefinition, SharedSecretProviderAuthenticator, type SlotResourceResolver, type StartBrokerServerOptions, type StaticMount, StdioUpstream, type StdioUpstreamConfig, type SubjectMapper, type SubjectMappingConfig, SubjectMappingError, type TokenValidator, type Upstream, VERSION, WsTunnel, WsTunnelBuilder, type WsTunnelOptions, authorizationWithEngine, brokerGrammarKey, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, hasAuthorizationPolicies, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, scopesOf, startBrokerServer, unzipMcpb, validateCapability };
2756
+ export { type AccessTokenClaims, type AggregateScopeFilter, type AllowedOrigins, type AuditContext, AuthError, type AuthErrorCode, type AuthorizationAuditConfig, type AuthorizationAuditEvent, type AuthorizationDecision, type AuthorizationDecisionReason, type AuthorizationPolicyConfig, type AuthorizationRequest, type AuthorizationSubject, BROKER_AGGREGATE_NAME, BROKER_GUIDES, BROKER_GUIDE_MIME_TYPE, BROKER_GUIDE_TOPICS, BROKER_GUIDE_URI_PREFIX, BROKER_GUIDE_URI_TEMPLATE, BROKER_PROVIDER_NAME, BROKER_RESERVED_SLOTS, type BrokerAuthConfig, type BrokerConfig, type BrokerContext, BrokerDiagnoseAdapter, BrokerDiagnoseBehavior, type BrokerDiagnosisRuleId, type BrokerDiagnosisSeverity, type BrokerGrammarEntry, BrokerGuideAdapter, BrokerGuideBehavior, type BrokerGuideTopic, BrokerInfoBehavior, type BrokerLocale, type BrokerProviderInfo, type BrokerProviderTransport, BrokerProvidersBehavior, type BrokerUserAgent, type CapabilityClassifier, type ClassifiedCapability, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, type DenyPolicy, HttpAuthGuard, type IAuditContext, type IAuthorizationAuditConfig, type IAuthorizationAuditEvent, type IAuthorizationDecision, type IAuthorizationPolicyConfig, type IAuthorizationRequest, type IAuthorizationSubject, type IBrokerAggregateInfo, type IBrokerAuthConfig, type IBrokerConfig, type IBrokerContext, type IBrokerDiagnosis, type IBrokerDiagnosisProblem, type IBrokerDiagnosisSkippedCheck, type IBrokerDiagnosisSlot, type IBrokerGrammarEntry, type IBrokerGuide, type IBrokerProviderInfo, type IBrokerSecurityInfo, type ICapabilityClassifier, type IClassifiedCapability, type IDenyPolicy, type IInternalClient, type IJwtAuthOptions, type IJwtValidatorOptions, type ILoadedBrokerConfig, type IMcpOperation, type IMcpbBundleConfig, type IOpenTargetResolution, type IPolicyAssignment, type IPolicyAuthorization, type IPolicyEngine, type IPrincipal, type IProviderAuthenticator, type IProviderPrincipal, type IProviderPublishDecision, type IRemoteUpstreamConfig, type IResolvedAuth, type IRoleDefinition, type ISlotResourceResolver, type IStartBrokerServerOptions, type IStaticMount, type IStdioUpstreamConfig, type ISubjectMapper, type ISubjectMappingConfig, type IUpstream, type IWsTunnelOptions, type InternalClient, type JwtAuthOptions, JwtSubjectMapper, JwtTokenValidator, type JwtValidatorOptions, type LoadedBrokerConfig, type McpOperation, type McpbBundleConfig, PACKAGE_NAME, type PolicyAssignment, type PolicyAuthorization, type PolicyEngine, type Principal, type ProtectedResourceMetadata, type ProviderAuthenticationResult, type ProviderAuthenticator, type ProviderAuthenticatorReturn, type ProviderPrincipal, type ProviderPublishDenialReason, type ProviderTakeoverMode, RemoteUpstream, type RemoteUpstreamConfig, type ResolvedAuth, ResourcePath, ResourcePathPattern, type RoleDefinition, SharedSecretProviderAuthenticator, type SlotResourceResolver, type StartBrokerServerOptions, type StaticMount, StdioUpstream, type StdioUpstreamConfig, type SubjectMapper, type SubjectMappingConfig, SubjectMappingError, type TokenValidator, type Upstream, VERSION, WsTunnel, WsTunnelBuilder, type WsTunnelOptions, authorizationWithEngine, brokerGrammarKey, brokerGuide, brokerGuideIndex, brokerGuideTopicFromUri, brokerGuideUri, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, compileProviderAllowedResources, diagnoseBroker, hasAuthorizationPolicies, isReservedBrokerSlot, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, providerPublishDecision, resolveOpenTarget, scopesOf, startBrokerServer, unzipMcpb, validateCapability };