@cyanmycelium/mcp-broker 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp-broker.example/CONFIGURATION-EN.md +300 -44
- package/.mcp-broker.example/CONFIGURATION-FR.md +313 -44
- package/.mcp-broker.example/README.md +73 -2
- package/.mcp-broker.example/config.json +18 -9
- package/.mcp-broker.example/config.stdio-bridge.json +16 -0
- package/README.md +407 -27
- package/dist/bin.js +215 -20
- package/dist/bin.js.map +1 -1
- package/dist/chunk-BZUZYXVA.js +5955 -0
- package/dist/chunk-BZUZYXVA.js.map +1 -0
- package/dist/grammars/claude/en.json +12 -0
- package/dist/grammars/claude/fr.json +12 -0
- package/dist/grammars/default/en.json +40 -0
- package/dist/grammars/default/fr.json +40 -0
- package/dist/grammars/default/zh.json +40 -0
- package/dist/index.d.ts +991 -25
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/auth/index.ts +3 -1
- package/src/auth/provider.auth.ts +126 -8
- package/src/authorization/policy.engine.ts +11 -2
- package/src/authorization/policy.types.ts +25 -1
- package/src/bin.ts +325 -28
- package/src/broker/adapters/broker.adapter.diagnose.ts +45 -0
- package/src/broker/adapters/broker.adapter.guide.ts +108 -0
- package/src/broker/aggregate/aggregate.server.ts +82 -15
- package/src/broker/aggregate/provider.client.session.ts +85 -11
- package/src/broker/behaviors/broker.behavior.diagnose.ts +47 -0
- package/src/broker/behaviors/broker.behavior.guide.ts +79 -0
- package/src/broker/broker.context.ts +65 -0
- package/src/broker/broker.diagnostics.ts +495 -0
- package/src/broker/broker.guides.ts +1029 -0
- package/src/broker/broker.server.ts +23 -7
- package/src/broker/broker.slots.ts +36 -0
- package/src/broker/grammars/claude/en.json +12 -0
- package/src/broker/grammars/claude/fr.json +12 -0
- package/src/broker/grammars/default/en.json +40 -0
- package/src/broker/grammars/default/fr.json +40 -0
- package/src/broker/grammars/default/zh.json +40 -0
- package/src/config.ts +191 -4
- package/src/index.ts +38 -3
- package/src/remote.transports.ts +127 -10
- package/src/remote.upstream.ts +4 -1
- package/src/ws/ws.interfaces.ts +148 -3
- package/src/ws/ws.tunnel.builder.ts +63 -1
- package/src/ws/ws.tunnel.ts +1150 -173
- package/web/README.md +31 -4
- package/dist/chunk-FTDKH2C4.js +0 -3670
- package/dist/chunk-FTDKH2C4.js.map +0 -1
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
|
|
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
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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
|
-
|
|
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`
|
|
1229
|
-
* provider slot `_broker`. No-op when
|
|
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
|
-
*
|
|
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
|
|
1279
|
-
* through unchanged.
|
|
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
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
1701
|
-
|
|
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 };
|