@cyanmycelium/mcp-broker 1.3.3 → 1.4.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 +34 -0
- package/.mcp-broker.example/CONFIGURATION-FR.md +35 -0
- package/.mcp-broker.example/config.json +6 -0
- package/README.md +59 -1
- package/dist/bin.js +23 -1
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-J5TN5RYU.js → chunk-YTRVLPHP.js} +642 -9
- package/dist/chunk-YTRVLPHP.js.map +1 -0
- package/dist/index.d.ts +293 -2
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/authorization/capability.classifier.ts +14 -1
- package/src/bin.ts +26 -0
- package/src/broker/adapters/broker.adapter.providers.ts +18 -0
- package/src/broker/aggregate/aggregate.server.ts +8 -0
- package/src/broker/behaviors/broker.behavior.info.ts +10 -1
- package/src/broker/behaviors/broker.behavior.providers.ts +10 -1
- package/src/broker/broker.context.ts +23 -0
- package/src/broker/broker.guides.ts +56 -2
- package/src/config.ts +12 -0
- package/src/index.ts +9 -0
- package/src/subscriptions/resource.subscription.registry.ts +370 -0
- package/src/ws/ws.interfaces.ts +25 -1
- package/src/ws/ws.tunnel.builder.ts +13 -0
- package/src/ws/ws.tunnel.ts +324 -3
- package/dist/chunk-J5TN5RYU.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, McpAdapterBase,
|
|
2
|
+
import { IEventSource, McpBehavior, McpResourceContent, McpResource, McpTool, McpResourceTemplate, GrammarResolverOptions, IMcpServer, IMessageTransport, McpGrammar, IAccessTokenClaims, IMcpPrincipal, McpAuthError, ITokenValidator, IProtectedResourceMetadata, McpAdapterBase, McpToolResult } from '@cyanmycelium/mcp-core';
|
|
3
3
|
export { IAccessTokenClaims, IProtectedResourceMetadata, ITokenValidator } from '@cyanmycelium/mcp-core';
|
|
4
4
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
5
5
|
|
|
@@ -56,6 +56,18 @@ interface IBrokerContext {
|
|
|
56
56
|
* a slot that cannot exist at host start.
|
|
57
57
|
*/
|
|
58
58
|
getStdioBridgeTarget?(): string | null | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Fires with the names of the slots whose *state* changed: a slot
|
|
61
|
+
* appeared, a provider attached or detached, a slot joined or left `_all`.
|
|
62
|
+
* Changes in the same tick arrive as one batch.
|
|
63
|
+
*
|
|
64
|
+
* Counters (`pendingCount`, `clientCount`, `sessionCount`) deliberately do
|
|
65
|
+
* not fire it: reading a resource moves them, so notifying on them would
|
|
66
|
+
* make every read trigger the next notification. They stay readable on
|
|
67
|
+
* demand. `_broker` turns this into `notifications/resources/updated` on
|
|
68
|
+
* `broker://providers` and `broker://providers/<name>`.
|
|
69
|
+
*/
|
|
70
|
+
readonly onProvidersChanged?: IEventSource<readonly string[]>;
|
|
59
71
|
}
|
|
60
72
|
/**
|
|
61
73
|
* Membership snapshot of the reserved `_all` aggregate slot.
|
|
@@ -123,6 +135,13 @@ interface IBrokerProviderInfo {
|
|
|
123
135
|
sessionCount: number;
|
|
124
136
|
/** Number of in-flight JSON-RPC requests awaiting a response. */
|
|
125
137
|
pendingCount: number;
|
|
138
|
+
/**
|
|
139
|
+
* Client/URI pairs held by `resources/subscribe` on this slot. A number
|
|
140
|
+
* that only grows is the signature of Streamable HTTP clients leaving
|
|
141
|
+
* without `DELETE`: their sessions, and so their subscriptions, never
|
|
142
|
+
* expire. Optional so hand-written contexts keep compiling.
|
|
143
|
+
*/
|
|
144
|
+
resourceSubscriptionCount?: number;
|
|
126
145
|
}
|
|
127
146
|
/** @deprecated Use {@link IBrokerContext}. */
|
|
128
147
|
type BrokerContext = IBrokerContext;
|
|
@@ -138,6 +157,12 @@ type BrokerProviderInfo = IBrokerProviderInfo;
|
|
|
138
157
|
declare class BrokerInfoBehavior extends McpBehavior {
|
|
139
158
|
static readonly NAMESPACE = "broker";
|
|
140
159
|
constructor(context: IBrokerContext);
|
|
160
|
+
/**
|
|
161
|
+
* Always reads live. `McpBehavior` caches the content of its root
|
|
162
|
+
* resource on first read and never refreshes it, which suits static
|
|
163
|
+
* content but froze this snapshot at whatever the first reader saw.
|
|
164
|
+
*/
|
|
165
|
+
readResourceAsync(uri: string): Promise<McpResourceContent | undefined>;
|
|
141
166
|
protected _buildResources(): McpResource[];
|
|
142
167
|
protected _buildTools(): McpTool[];
|
|
143
168
|
}
|
|
@@ -154,6 +179,12 @@ declare class BrokerInfoBehavior extends McpBehavior {
|
|
|
154
179
|
declare class BrokerProvidersBehavior extends McpBehavior {
|
|
155
180
|
static readonly NAMESPACE = "broker_providers";
|
|
156
181
|
constructor(context: IBrokerContext);
|
|
182
|
+
/**
|
|
183
|
+
* Always reads live. `McpBehavior` caches the content of its root
|
|
184
|
+
* resource on first read and never refreshes it, which suits static
|
|
185
|
+
* content but froze this snapshot at whatever the first reader saw.
|
|
186
|
+
*/
|
|
187
|
+
readResourceAsync(uri: string): Promise<McpResourceContent | undefined>;
|
|
157
188
|
protected _buildResources(): McpResource[];
|
|
158
189
|
protected _buildTemplate(): McpResourceTemplate[];
|
|
159
190
|
protected _buildTools(): McpTool[];
|
|
@@ -1022,6 +1053,151 @@ type ProviderPrincipal = IProviderPrincipal;
|
|
|
1022
1053
|
/** @deprecated Use {@link IProviderAuthenticator}. */
|
|
1023
1054
|
type ProviderAuthenticator = IProviderAuthenticator;
|
|
1024
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Per-slot bookkeeping for `resources/subscribe`.
|
|
1058
|
+
*
|
|
1059
|
+
* A slot is one MCP server shared by every client attached to it, so the
|
|
1060
|
+
* provider behind it must not see one subscription per browser tab: it sees
|
|
1061
|
+
* one per URI. This registry owns that reference count. The first subscriber
|
|
1062
|
+
* of a URI triggers the upstream `resources/subscribe`, the last one to leave
|
|
1063
|
+
* triggers the upstream `resources/unsubscribe`, and everybody in between is
|
|
1064
|
+
* answered locally.
|
|
1065
|
+
*
|
|
1066
|
+
* Every operation on one `(slot, uri)` pair runs through a queue, so two
|
|
1067
|
+
* clients subscribing at the same instant cannot both send upstream, and an
|
|
1068
|
+
* unsubscribe cannot overtake the subscribe it undoes. A second subscriber that
|
|
1069
|
+
* arrives while the first upstream round trip is in flight waits for it: on
|
|
1070
|
+
* success it is added locally, with no second upstream call; on failure it
|
|
1071
|
+
* asks the provider itself, since the first refusal may have been transient.
|
|
1072
|
+
*
|
|
1073
|
+
* The registry does no I/O. The tunnel supplies the upstream calls through
|
|
1074
|
+
* {@link IResourceSubscriptionUpstream} and decides what a subscriber is: `S`
|
|
1075
|
+
* is whatever it needs to deliver a notification later.
|
|
1076
|
+
*/
|
|
1077
|
+
/** Identifies one consumer across transports, e.g. `ws:7`, `http:<session>`, `stdio`. */
|
|
1078
|
+
type ClientKey = string;
|
|
1079
|
+
/** An upstream answer, reduced to what the registry needs. */
|
|
1080
|
+
type SubscriptionOutcome = {
|
|
1081
|
+
readonly ok: true;
|
|
1082
|
+
} | {
|
|
1083
|
+
readonly ok: false;
|
|
1084
|
+
readonly error: {
|
|
1085
|
+
readonly code: number;
|
|
1086
|
+
readonly message: string;
|
|
1087
|
+
};
|
|
1088
|
+
};
|
|
1089
|
+
/** What the tunnel provides so the registry can talk to a provider. */
|
|
1090
|
+
interface IResourceSubscriptionUpstream {
|
|
1091
|
+
/** Sends one request to the provider behind `slot` and resolves with its answer, never rejects. */
|
|
1092
|
+
request(slot: string, method: "resources/subscribe" | "resources/unsubscribe", uri: string): Promise<SubscriptionOutcome>;
|
|
1093
|
+
/** `true` when a provider currently serves `slot`. */
|
|
1094
|
+
isConnected(slot: string): boolean;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Bounds on what clients can make the broker hold. Without them one client, or
|
|
1098
|
+
* many sessions a client never closes, could grow the registry without limit
|
|
1099
|
+
* and pin upstream subscriptions forever.
|
|
1100
|
+
*/
|
|
1101
|
+
interface IResourceSubscriptionLimits {
|
|
1102
|
+
/** Distinct URIs one client may be subscribed to at once. */
|
|
1103
|
+
readonly maxSubscriptionsPerClient: number;
|
|
1104
|
+
/** Client/URI pairs one slot may hold at once. */
|
|
1105
|
+
readonly maxSubscriptionsPerSlot: number;
|
|
1106
|
+
/** Longest URI accepted, in UTF-16 code units. */
|
|
1107
|
+
readonly maxResourceUriLength: number;
|
|
1108
|
+
}
|
|
1109
|
+
declare const DEFAULT_RESOURCE_SUBSCRIPTION_LIMITS: IResourceSubscriptionLimits;
|
|
1110
|
+
/**
|
|
1111
|
+
* Where a URI stands with the provider.
|
|
1112
|
+
*
|
|
1113
|
+
* - `inactive`: not subscribed upstream. Either nothing was ever sent, or the
|
|
1114
|
+
* provider disconnected and the subscription waits for {@link ResourceSubscriptionRegistry.replay}.
|
|
1115
|
+
* - `subscribing` / `unsubscribing`: an upstream round trip is in flight.
|
|
1116
|
+
* - `active`: the provider confirmed the subscription.
|
|
1117
|
+
*/
|
|
1118
|
+
type SubscriptionState = "inactive" | "subscribing" | "active" | "unsubscribing";
|
|
1119
|
+
/** Error code for a subscription refused by a broker limit. JSON-RPC reserves -32000..-32099 for servers. */
|
|
1120
|
+
declare const SUBSCRIPTION_LIMIT_ERROR_CODE = -32000;
|
|
1121
|
+
/** One URI whose upstream subscription was re-sent after a provider reconnected. */
|
|
1122
|
+
interface IReplayResult<S> {
|
|
1123
|
+
readonly uri: string;
|
|
1124
|
+
readonly outcome: SubscriptionOutcome;
|
|
1125
|
+
/** Who was subscribed at the time; they are dropped when the outcome is a failure. */
|
|
1126
|
+
readonly subscribers: ReadonlyArray<{
|
|
1127
|
+
readonly client: ClientKey;
|
|
1128
|
+
readonly sink: S;
|
|
1129
|
+
}>;
|
|
1130
|
+
}
|
|
1131
|
+
declare class ResourceSubscriptionRegistry<S> {
|
|
1132
|
+
private readonly _upstream;
|
|
1133
|
+
private readonly _limits;
|
|
1134
|
+
/** slot → uri → entry. */
|
|
1135
|
+
private readonly _slots;
|
|
1136
|
+
/**
|
|
1137
|
+
* client → slot → URIs, counting subscriptions still being confirmed.
|
|
1138
|
+
* Counting those is what makes the per-client limit hold under concurrent
|
|
1139
|
+
* requests: a reservation is taken before the upstream call, not after.
|
|
1140
|
+
*/
|
|
1141
|
+
private readonly _clients;
|
|
1142
|
+
/** slot → number of client/URI pairs, reserved ones included. */
|
|
1143
|
+
private readonly _slotCounts;
|
|
1144
|
+
constructor(upstream: IResourceSubscriptionUpstream, limits?: Partial<IResourceSubscriptionLimits>);
|
|
1145
|
+
get limits(): IResourceSubscriptionLimits;
|
|
1146
|
+
/**
|
|
1147
|
+
* Subscribes `client` to `uri` on `slot`. Idempotent: subscribing twice
|
|
1148
|
+
* keeps one entry and answers success.
|
|
1149
|
+
*/
|
|
1150
|
+
subscribe(slot: string, client: ClientKey, sink: S, uri: string): Promise<SubscriptionOutcome>;
|
|
1151
|
+
/**
|
|
1152
|
+
* Unsubscribes `client` from `uri`. Always succeeds, including for a URI
|
|
1153
|
+
* the client never subscribed to: the client asked not to be subscribed,
|
|
1154
|
+
* and it is not. The provider is told only when the last subscriber leaves,
|
|
1155
|
+
* and only when it is connected; its answer does not change ours.
|
|
1156
|
+
*/
|
|
1157
|
+
unsubscribe(slot: string, client: ClientKey, uri: string): Promise<SubscriptionOutcome>;
|
|
1158
|
+
/** Confirmed subscribers of `uri` on `slot`, the only ones a notification may reach. */
|
|
1159
|
+
subscribers(slot: string, uri: string): ReadonlyArray<{
|
|
1160
|
+
readonly client: ClientKey;
|
|
1161
|
+
readonly sink: S;
|
|
1162
|
+
}>;
|
|
1163
|
+
/** Where `uri` stands with the provider behind `slot`. */
|
|
1164
|
+
stateOf(slot: string, uri: string): SubscriptionState;
|
|
1165
|
+
/** Number of client/URI pairs on `slot`, subscriptions being confirmed included. */
|
|
1166
|
+
countFor(slot: string): number;
|
|
1167
|
+
/** `true` when `slot` has at least one URI someone is subscribed to. */
|
|
1168
|
+
hasSubscriptions(slot: string): boolean;
|
|
1169
|
+
/**
|
|
1170
|
+
* Drops every subscription `client` holds, on every slot. Safe to call
|
|
1171
|
+
* repeatedly and for a client that holds nothing.
|
|
1172
|
+
*/
|
|
1173
|
+
removeClient(client: ClientKey): Promise<void>;
|
|
1174
|
+
/**
|
|
1175
|
+
* Records that the provider behind `slot` went away: whatever it had
|
|
1176
|
+
* subscribed is gone with it. Subscribers are kept, so {@link replay} can
|
|
1177
|
+
* restore them when a provider comes back.
|
|
1178
|
+
*/
|
|
1179
|
+
providerDisconnected(slot: string): void;
|
|
1180
|
+
/**
|
|
1181
|
+
* Re-sends one upstream `resources/subscribe` per URI that still has
|
|
1182
|
+
* subscribers, after a provider (re)attached to `slot`. A URI the new
|
|
1183
|
+
* provider refuses is dropped along with its subscribers, since nothing
|
|
1184
|
+
* will ever notify them; the result lists them so the caller can tell them.
|
|
1185
|
+
*/
|
|
1186
|
+
replay(slot: string): Promise<IReplayResult<S>[]>;
|
|
1187
|
+
/** Forgets everything without calling upstream. Used when the broker stops. */
|
|
1188
|
+
clear(): void;
|
|
1189
|
+
private _enqueue;
|
|
1190
|
+
private _entry;
|
|
1191
|
+
/** Removes an entry nobody holds or waits for, once its queue is idle. */
|
|
1192
|
+
private _dropIfUnused;
|
|
1193
|
+
private _reserve;
|
|
1194
|
+
private _release;
|
|
1195
|
+
private _isReserved;
|
|
1196
|
+
/** `true` when some client reserved `uri` on `slot`, confirmed or not. */
|
|
1197
|
+
private _hasReservation;
|
|
1198
|
+
private _clientTotal;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1025
1201
|
/**
|
|
1026
1202
|
* What the broker does when a provider connects to a slot another socket
|
|
1027
1203
|
* already holds.
|
|
@@ -1199,6 +1375,18 @@ interface IWsTunnelOptions {
|
|
|
1199
1375
|
* @default 60000
|
|
1200
1376
|
*/
|
|
1201
1377
|
providerRequestTimeoutMs?: number;
|
|
1378
|
+
/**
|
|
1379
|
+
* Bounds on `resources/subscribe` bookkeeping. Every field is optional and
|
|
1380
|
+
* falls back to {@link DEFAULT_RESOURCE_SUBSCRIPTION_LIMITS}: 64 URIs per
|
|
1381
|
+
* client, 1024 client subscriptions per slot, URIs of at most 2048
|
|
1382
|
+
* characters. A subscription past a limit is refused with `-32000`, an
|
|
1383
|
+
* overlong URI with `-32602`.
|
|
1384
|
+
*
|
|
1385
|
+
* The per-slot bound is what caps a Streamable HTTP client that closes its
|
|
1386
|
+
* tab without `DELETE`: its session never expires, so neither do its
|
|
1387
|
+
* subscriptions.
|
|
1388
|
+
*/
|
|
1389
|
+
resourceSubscriptions?: Partial<IResourceSubscriptionLimits>;
|
|
1202
1390
|
/**
|
|
1203
1391
|
* Optional static-file mounts served over plain HTTP.
|
|
1204
1392
|
* Matched by longest URL prefix; directory requests fall back to `index.html`.
|
|
@@ -1439,6 +1627,24 @@ declare class WsTunnel implements IBrokerContext {
|
|
|
1439
1627
|
/** Provider principals captured during successful WebSocket upgrades. */
|
|
1440
1628
|
private readonly _pendingProviderPrincipals;
|
|
1441
1629
|
private readonly _providerPrincipals;
|
|
1630
|
+
/**
|
|
1631
|
+
* `resources/subscribe` bookkeeping for every slot: who is subscribed to
|
|
1632
|
+
* what, and the one upstream subscription per URI that stands for them.
|
|
1633
|
+
*/
|
|
1634
|
+
private readonly _subscriptions;
|
|
1635
|
+
/** Stable id per raw WS client socket, the WS part of a {@link ClientKey}. */
|
|
1636
|
+
private readonly _wsClientIds;
|
|
1637
|
+
private _nextWsClientId;
|
|
1638
|
+
/** Stable id per in-process client, the internal part of a {@link ClientKey}. */
|
|
1639
|
+
private readonly _internalClientIds;
|
|
1640
|
+
private _nextInternalClientId;
|
|
1641
|
+
/** Emitter behind {@link onProvidersChanged}. */
|
|
1642
|
+
private readonly _providersChanged;
|
|
1643
|
+
/** Slots changed since the last {@link _providersChanged} batch went out. */
|
|
1644
|
+
private readonly _changedSlots;
|
|
1645
|
+
private _changedSlotsTimer;
|
|
1646
|
+
/** Slots already warned about for a malformed `notifications/resources/updated`. */
|
|
1647
|
+
private readonly _invalidUpdateWarnedProviders;
|
|
1442
1648
|
constructor(options: IWsTunnelOptions);
|
|
1443
1649
|
get version(): string;
|
|
1444
1650
|
get name(): string;
|
|
@@ -1471,6 +1677,8 @@ declare class WsTunnel implements IBrokerContext {
|
|
|
1471
1677
|
* `_all` by hand.
|
|
1472
1678
|
*/
|
|
1473
1679
|
getAggregateInfo(): IBrokerAggregateInfo;
|
|
1680
|
+
/** See {@link IBrokerContext.onProvidersChanged}. */
|
|
1681
|
+
get onProvidersChanged(): IEventSource<readonly string[]>;
|
|
1474
1682
|
getProvidersInfo(): IBrokerProviderInfo[];
|
|
1475
1683
|
getProviderInfo(name: string): IBrokerProviderInfo | undefined;
|
|
1476
1684
|
private _buildProviderInfo;
|
|
@@ -1890,6 +2098,71 @@ declare class WsTunnel implements IBrokerContext {
|
|
|
1890
2098
|
private _isProviderConnected;
|
|
1891
2099
|
/** Returns the state for `name`, creating it lazily if it doesn't exist yet. */
|
|
1892
2100
|
private _getOrCreateProviderState;
|
|
2101
|
+
/** The {@link ClientKey} of a raw WS client socket. */
|
|
2102
|
+
private _wsClientKey;
|
|
2103
|
+
/** The {@link ClientKey} of a sink that is a client, `null` for the broker's own and internal sinks. */
|
|
2104
|
+
private _clientKeyOf;
|
|
2105
|
+
/** The {@link ClientKey} of an in-process client from {@link openInternalClient}. */
|
|
2106
|
+
private _internalClientKey;
|
|
2107
|
+
/** The principal behind a client sink, for the per-notification policy check. */
|
|
2108
|
+
private _principalOfSink;
|
|
2109
|
+
/**
|
|
2110
|
+
* Answers `resources/subscribe` and `resources/unsubscribe` from the
|
|
2111
|
+
* broker's own registry instead of relaying them. Returns `true` when the
|
|
2112
|
+
* frame was one of those and has been (or will be) answered.
|
|
2113
|
+
*
|
|
2114
|
+
* Also remembers the last `initialize` of the slot, for {@link _replaySubscriptions}.
|
|
2115
|
+
*
|
|
2116
|
+
* Runs after the policy check. `resources/unsubscribe` is not classified,
|
|
2117
|
+
* so it is never refused: a client whose read grant was revoked must
|
|
2118
|
+
* still be able to drop what it holds. `_all` is skipped: it serves tools
|
|
2119
|
+
* and prompts only, and answers resources methods with `-32601` itself.
|
|
2120
|
+
*/
|
|
2121
|
+
private _interceptClientFrame;
|
|
2122
|
+
/**
|
|
2123
|
+
* Sends one aggregated `resources/subscribe` or `resources/unsubscribe` to
|
|
2124
|
+
* the provider behind `slot` and resolves with its answer. Never rejects:
|
|
2125
|
+
* a disconnect or a timeout arrives as an error answer through the same
|
|
2126
|
+
* pending-request machinery every client request uses.
|
|
2127
|
+
*/
|
|
2128
|
+
private _subscriptionRequest;
|
|
2129
|
+
/** Sends a request of the broker's own to a provider and resolves with the parsed answer. */
|
|
2130
|
+
private _brokerRequest;
|
|
2131
|
+
/**
|
|
2132
|
+
* Delivers a provider's `notifications/resources/updated` to the sessions
|
|
2133
|
+
* subscribed to its URI, and to nobody else.
|
|
2134
|
+
*
|
|
2135
|
+
* A frame without a usable URI is dropped, never broadcast: there is no
|
|
2136
|
+
* safe audience for it. Each recipient is re-checked against the policy,
|
|
2137
|
+
* because a grant can be revoked after the subscription was accepted; a
|
|
2138
|
+
* recipient that fails the check is unsubscribed as well as skipped.
|
|
2139
|
+
*/
|
|
2140
|
+
private _routeResourceUpdated;
|
|
2141
|
+
private _deliverResourceUpdated;
|
|
2142
|
+
/** A provider now serves `name`: announce it, and restore the subscriptions it should hold. */
|
|
2143
|
+
private _onProviderAttached;
|
|
2144
|
+
/**
|
|
2145
|
+
* Re-subscribes a provider that (re)attached to URIs clients still hold,
|
|
2146
|
+
* once per URI.
|
|
2147
|
+
*
|
|
2148
|
+
* The provider is handshaken first, with the last `initialize` a client
|
|
2149
|
+
* sent on this slot, because a fresh provider has no session: requests
|
|
2150
|
+
* before `initialize` break the MCP lifecycle, and an mcp-core server
|
|
2151
|
+
* suppresses its list_changed notifications until it sees
|
|
2152
|
+
* `notifications/initialized`.
|
|
2153
|
+
*
|
|
2154
|
+
* Every subscriber then gets one `notifications/resources/updated`: the
|
|
2155
|
+
* content may have changed while nobody was watching, so a re-read is due.
|
|
2156
|
+
* Where the new provider refuses a URI, that is also what tells its
|
|
2157
|
+
* subscribers, since their re-read fails; their subscription is dropped.
|
|
2158
|
+
*/
|
|
2159
|
+
private _replaySubscriptions;
|
|
2160
|
+
/**
|
|
2161
|
+
* Queues `name` for the next {@link onProvidersChanged} batch. Batched per
|
|
2162
|
+
* tick, so a multiplexed socket announcing ten slots, or a slot attaching
|
|
2163
|
+
* and joining `_all` in the same breath, produces one notification.
|
|
2164
|
+
*/
|
|
2165
|
+
private _emitProviderChanged;
|
|
1893
2166
|
private _handleSamplesIndex;
|
|
1894
2167
|
private _serveStatic;
|
|
1895
2168
|
}
|
|
@@ -1925,6 +2198,7 @@ declare class WsTunnelBuilder {
|
|
|
1925
2198
|
private _providerHeartbeatIntervalMs;
|
|
1926
2199
|
private _providerTakeover;
|
|
1927
2200
|
private _providerRequestTimeoutMs;
|
|
2201
|
+
private _resourceSubscriptions;
|
|
1928
2202
|
private _staticMounts;
|
|
1929
2203
|
private _stdioUpstreams;
|
|
1930
2204
|
private _remoteUpstreams;
|
|
@@ -2040,6 +2314,12 @@ declare class WsTunnelBuilder {
|
|
|
2040
2314
|
* @default 60000
|
|
2041
2315
|
*/
|
|
2042
2316
|
withProviderRequestTimeout(timeoutMs: number): this;
|
|
2317
|
+
/**
|
|
2318
|
+
* Bounds what `resources/subscribe` can make the broker hold. Fields left
|
|
2319
|
+
* out keep their default: 64 URIs per client, 1024 client subscriptions per
|
|
2320
|
+
* slot, URIs of at most 2048 characters.
|
|
2321
|
+
*/
|
|
2322
|
+
withResourceSubscriptionLimits(limits: Partial<IResourceSubscriptionLimits>): this;
|
|
2043
2323
|
/**
|
|
2044
2324
|
* Adds a static-file mount served over plain HTTP.
|
|
2045
2325
|
* Can be called multiple times; longest-prefix match wins at runtime.
|
|
@@ -2541,6 +2821,17 @@ interface IBrokerConfig {
|
|
|
2541
2821
|
* @default 60000
|
|
2542
2822
|
*/
|
|
2543
2823
|
providerRequestTimeoutMs?: number;
|
|
2824
|
+
/**
|
|
2825
|
+
* Bounds on `resources/subscribe` bookkeeping. Each field maps to an env
|
|
2826
|
+
* var: `MCP_BROKER_MAX_SUBSCRIPTIONS_PER_CLIENT` (default 64),
|
|
2827
|
+
* `MCP_BROKER_MAX_SUBSCRIPTIONS_PER_SLOT` (default 1024),
|
|
2828
|
+
* `MCP_BROKER_MAX_RESOURCE_URI_LENGTH` (default 2048).
|
|
2829
|
+
*/
|
|
2830
|
+
resourceSubscriptions?: {
|
|
2831
|
+
maxSubscriptionsPerClient?: number;
|
|
2832
|
+
maxSubscriptionsPerSlot?: number;
|
|
2833
|
+
maxResourceUriLength?: number;
|
|
2834
|
+
};
|
|
2544
2835
|
/**
|
|
2545
2836
|
* What happens when a provider connects to a slot another socket already
|
|
2546
2837
|
* holds. Maps to `MCP_BROKER_PROVIDER_TAKEOVER`.
|
|
@@ -2783,4 +3074,4 @@ type BrokerConfig = IBrokerConfig;
|
|
|
2783
3074
|
/** @deprecated Use {@link ILoadedBrokerConfig}. */
|
|
2784
3075
|
type LoadedBrokerConfig = ILoadedBrokerConfig;
|
|
2785
3076
|
|
|
2786
|
-
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 };
|
|
3077
|
+
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, type ClientKey, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DEFAULT_RESOURCE_SUBSCRIPTION_LIMITS, 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 IReplayResult, type IResolvedAuth, type IResourceSubscriptionLimits, type IResourceSubscriptionUpstream, 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, ResourceSubscriptionRegistry, type RoleDefinition, SUBSCRIPTION_LIMIT_ERROR_CODE, SharedSecretProviderAuthenticator, type SlotResourceResolver, type StartBrokerServerOptions, type StaticMount, StdioUpstream, type StdioUpstreamConfig, type SubjectMapper, type SubjectMappingConfig, SubjectMappingError, type SubscriptionOutcome, type SubscriptionState, 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 };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { AuthError, 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, BrokerDiagnoseAdapter, BrokerDiagnoseBehavior, BrokerGuideAdapter, BrokerGuideBehavior, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, 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 } from './chunk-
|
|
1
|
+
export { AuthError, 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, BrokerDiagnoseAdapter, BrokerDiagnoseBehavior, BrokerGuideAdapter, BrokerGuideBehavior, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DEFAULT_RESOURCE_SUBSCRIPTION_LIMITS, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, ResourceSubscriptionRegistry, SUBSCRIPTION_LIMIT_ERROR_CODE, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, 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 } from './chunk-YTRVLPHP.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanmycelium/mcp-broker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "WebSocket-based Model Context Protocol broker. Aggregates multiple MCP providers behind a single endpoint with stdio, SSE, and Streamable HTTP client transports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"prepublishOnly": "npm run lint && npm run build && npm test"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@cyanmycelium/mcp-core": "^1.
|
|
72
|
+
"@cyanmycelium/mcp-core": "^1.3.0",
|
|
73
73
|
"jose": "^6.2.3",
|
|
74
74
|
"open": "^11.0.0",
|
|
75
75
|
"ws": "^8.18.0"
|
|
@@ -22,6 +22,11 @@ interface IProviderMapping {
|
|
|
22
22
|
const STATIC_CAPABILITIES: Readonly<Record<string, string>> = {
|
|
23
23
|
"resources/list": "mcp.resources.read",
|
|
24
24
|
"resources/read": "mcp.resources.read",
|
|
25
|
+
"resources/templates/list": "mcp.resources.read",
|
|
26
|
+
// Subscribing is reading over time. Unsubscribing is deliberately absent:
|
|
27
|
+
// an unclassified method is not refused, and a client whose grant was
|
|
28
|
+
// revoked must still be able to drop what it holds.
|
|
29
|
+
"resources/subscribe": "mcp.resources.read",
|
|
25
30
|
"tools/list": "mcp.tools.list",
|
|
26
31
|
"prompts/list": "mcp.prompts.read",
|
|
27
32
|
"prompts/get": "mcp.prompts.read",
|
|
@@ -34,6 +39,14 @@ const STATIC_CAPABILITIES: Readonly<Record<string, string>> = {
|
|
|
34
39
|
|
|
35
40
|
const BROKER_READ_TOOLS = new Set(["broker_info", "providers_list", "provider_status"]);
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Methods that only read broker state. On `_broker` they all need
|
|
44
|
+
* `broker.providers.read`, the capability its tools need, rather than the
|
|
45
|
+
* generic `mcp.resources.read`: otherwise a caller could read the list and
|
|
46
|
+
* not subscribe to it, or the reverse.
|
|
47
|
+
*/
|
|
48
|
+
const BROKER_READ_METHODS = new Set(["tools/list", "resources/list", "resources/read", "resources/templates/list", "resources/subscribe", "notifications/resources/updated"]);
|
|
49
|
+
|
|
37
50
|
function toolNameFrom(params: unknown): string | undefined {
|
|
38
51
|
if (typeof params !== "object" || params === null || Array.isArray(params)) return undefined;
|
|
39
52
|
const name = (params as Readonly<Record<string, unknown>>)["name"];
|
|
@@ -80,7 +93,7 @@ export class ConfiguredCapabilityClassifier implements ICapabilityClassifier {
|
|
|
80
93
|
if (!method) return undefined;
|
|
81
94
|
|
|
82
95
|
if (provider === "_broker") {
|
|
83
|
-
if (method
|
|
96
|
+
if (BROKER_READ_METHODS.has(method)) {
|
|
84
97
|
return { capability: "broker.providers.read" };
|
|
85
98
|
}
|
|
86
99
|
if (method === "tools/call") {
|
package/src/bin.ts
CHANGED
|
@@ -176,6 +176,9 @@ envFromConfig("MCP_BROKER_SSE_PATH", config.paths?.sse);
|
|
|
176
176
|
envFromConfig("MCP_BROKER_MESSAGES_PATH", config.paths?.messages);
|
|
177
177
|
envFromConfig("MCP_BROKER_PROVIDER_HEARTBEAT_MS", config.providerHeartbeatIntervalMs);
|
|
178
178
|
envFromConfig("MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS", config.providerRequestTimeoutMs);
|
|
179
|
+
envFromConfig("MCP_BROKER_MAX_SUBSCRIPTIONS_PER_CLIENT", config.resourceSubscriptions?.maxSubscriptionsPerClient);
|
|
180
|
+
envFromConfig("MCP_BROKER_MAX_SUBSCRIPTIONS_PER_SLOT", config.resourceSubscriptions?.maxSubscriptionsPerSlot);
|
|
181
|
+
envFromConfig("MCP_BROKER_MAX_RESOURCE_URI_LENGTH", config.resourceSubscriptions?.maxResourceUriLength);
|
|
179
182
|
envFromConfig("MCP_BROKER_PROVIDER_TAKEOVER", config.providerTakeover);
|
|
180
183
|
// `www.open` is `boolean | string`: `true` means the root, a string is a path
|
|
181
184
|
// or a same-origin URL. Both travel as the env string and are resolved once,
|
|
@@ -253,6 +256,24 @@ function millisFromEnv(envName: string): number | undefined {
|
|
|
253
256
|
const providerHeartbeatIntervalMs = millisFromEnv("MCP_BROKER_PROVIDER_HEARTBEAT_MS");
|
|
254
257
|
const providerRequestTimeoutMs = millisFromEnv("MCP_BROKER_PROVIDER_REQUEST_TIMEOUT_MS");
|
|
255
258
|
|
|
259
|
+
/** Reads a positive whole number from the environment, or `undefined` (with a warning) for anything else. */
|
|
260
|
+
function countFromEnv(envName: string): number | undefined {
|
|
261
|
+
const raw = process.env[envName];
|
|
262
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
263
|
+
const value = Number(raw);
|
|
264
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
265
|
+
console.warn(`[mcp-broker] Ignoring ${envName}="${raw}": expected a whole number of at least 1. Using the default.`);
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const resourceSubscriptionLimits = {
|
|
272
|
+
maxSubscriptionsPerClient: countFromEnv("MCP_BROKER_MAX_SUBSCRIPTIONS_PER_CLIENT"),
|
|
273
|
+
maxSubscriptionsPerSlot: countFromEnv("MCP_BROKER_MAX_SUBSCRIPTIONS_PER_SLOT"),
|
|
274
|
+
maxResourceUriLength: countFromEnv("MCP_BROKER_MAX_RESOURCE_URI_LENGTH"),
|
|
275
|
+
};
|
|
276
|
+
|
|
256
277
|
const takeoverRaw = process.env["MCP_BROKER_PROVIDER_TAKEOVER"]?.trim().toLowerCase();
|
|
257
278
|
let providerTakeover: ProviderTakeoverMode | undefined;
|
|
258
279
|
if (takeoverRaw) {
|
|
@@ -367,6 +388,11 @@ async function main(): Promise<void> {
|
|
|
367
388
|
if (providerRequestTimeoutMs !== undefined) {
|
|
368
389
|
builder.withProviderRequestTimeout(providerRequestTimeoutMs);
|
|
369
390
|
}
|
|
391
|
+
// Only the limits actually set: an `undefined` field would override the default.
|
|
392
|
+
const limits = Object.fromEntries(Object.entries(resourceSubscriptionLimits).filter(([, v]) => v !== undefined));
|
|
393
|
+
if (Object.keys(limits).length > 0) {
|
|
394
|
+
builder.withResourceSubscriptionLimits(limits);
|
|
395
|
+
}
|
|
370
396
|
if (providerTakeover) {
|
|
371
397
|
builder.withProviderTakeover(providerTakeover);
|
|
372
398
|
}
|
|
@@ -8,6 +8,15 @@ export const PROVIDERS_URI = "broker://providers";
|
|
|
8
8
|
/** RFC 6570 URI template for one specific provider slot. */
|
|
9
9
|
export const PROVIDER_URI_TEMPLATE = "broker://providers/{name}";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The URI of one slot's resource, as the `broker://providers/{name}` template
|
|
13
|
+
* expands it. The name is percent-encoded, so a slot such as `a/b` is
|
|
14
|
+
* `broker://providers/a%2Fb`; a subscription must use that exact string.
|
|
15
|
+
*/
|
|
16
|
+
export function providerUri(name: string): string {
|
|
17
|
+
return `broker://providers/${encodeURIComponent(name)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* Adapter that exposes the broker's current provider slots, both as a list
|
|
13
22
|
* (read of `broker://providers`) and individually (`broker://providers/<name>`).
|
|
@@ -15,6 +24,15 @@ export const PROVIDER_URI_TEMPLATE = "broker://providers/{name}";
|
|
|
15
24
|
export class BrokerProvidersAdapter extends McpAdapterBase {
|
|
16
25
|
constructor(private readonly _context: IBrokerContext) {
|
|
17
26
|
super("broker");
|
|
27
|
+
|
|
28
|
+
// Each batch of slot changes updates the list once, and each slot's
|
|
29
|
+
// own resource once. The server delivers them only to a session that
|
|
30
|
+
// subscribed to that exact URI.
|
|
31
|
+
_context.onProvidersChanged?.subscribe((names) => {
|
|
32
|
+
if (names.length === 0) return;
|
|
33
|
+
this._forwardResourceContentChanged(PROVIDERS_URI);
|
|
34
|
+
for (const name of new Set(names)) this._forwardResourceContentChanged(providerUri(name));
|
|
35
|
+
});
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
public async readResourceAsync(uri: string): Promise<McpResourceContent | undefined> {
|
|
@@ -68,6 +68,12 @@ export class AggregateServer implements IMessageTransport {
|
|
|
68
68
|
onClose: (() => void) | null = null;
|
|
69
69
|
onError: ((error: Error) => void) | null = null;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Called with a slot name whenever it joins or leaves the aggregate, so the
|
|
73
|
+
* broker can announce the change on `broker://providers`.
|
|
74
|
+
*/
|
|
75
|
+
onMembershipChanged: ((name: string) => void) | null = null;
|
|
76
|
+
|
|
71
77
|
constructor(openClient: InternalClientFactory) {
|
|
72
78
|
this._openClient = openClient;
|
|
73
79
|
}
|
|
@@ -139,6 +145,7 @@ export class AggregateServer implements IMessageTransport {
|
|
|
139
145
|
|
|
140
146
|
const session = new ProviderClientSession(name, this._openClient(name));
|
|
141
147
|
this._sessions.set(name, session);
|
|
148
|
+
this.onMembershipChanged?.(name);
|
|
142
149
|
|
|
143
150
|
session.onCatalogChanged = (): void => {
|
|
144
151
|
this._catalog.setProvider(name, { tools: session.tools, prompts: session.prompts });
|
|
@@ -181,6 +188,7 @@ export class AggregateServer implements IMessageTransport {
|
|
|
181
188
|
session.close();
|
|
182
189
|
this._catalog.removeProvider(name);
|
|
183
190
|
this._emitListChanged();
|
|
191
|
+
this.onMembershipChanged?.(name);
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
private _subjectFor(principal: IPrincipal | null): IAuthorizationSubject {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpBehavior } from "@cyanmycelium/mcp-core";
|
|
2
|
-
import type { McpResource, McpTool } from "@cyanmycelium/mcp-core";
|
|
2
|
+
import type { McpResource, McpResourceContent, McpTool } from "@cyanmycelium/mcp-core";
|
|
3
3
|
import { BROKER_INFO_URI, BrokerInfoAdapter } from "../adapters/broker.adapter.info";
|
|
4
4
|
import { brokerBaselineResourceDescription, brokerBaselineResourceName, brokerBaselineToolDescription } from "../broker.grammars";
|
|
5
5
|
import type { IBrokerContext } from "../broker.context";
|
|
@@ -19,6 +19,15 @@ export class BrokerInfoBehavior extends McpBehavior {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Always reads live. `McpBehavior` caches the content of its root
|
|
24
|
+
* resource on first read and never refreshes it, which suits static
|
|
25
|
+
* content but froze this snapshot at whatever the first reader saw.
|
|
26
|
+
*/
|
|
27
|
+
public override readResourceAsync(uri: string): Promise<McpResourceContent | undefined> {
|
|
28
|
+
return this.adapter.readResourceAsync(uri);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
protected override _buildResources(): McpResource[] {
|
|
23
32
|
return [
|
|
24
33
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpBehavior } from "@cyanmycelium/mcp-core";
|
|
2
|
-
import type { McpResource, McpResourceTemplate, McpTool } from "@cyanmycelium/mcp-core";
|
|
2
|
+
import type { McpResource, McpResourceContent, McpResourceTemplate, McpTool } from "@cyanmycelium/mcp-core";
|
|
3
3
|
import { BrokerProvidersAdapter, PROVIDERS_URI, PROVIDER_URI_TEMPLATE } from "../adapters/broker.adapter.providers";
|
|
4
4
|
import {
|
|
5
5
|
brokerBaselinePropertyDescription,
|
|
@@ -29,6 +29,15 @@ export class BrokerProvidersBehavior extends McpBehavior {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Always reads live. `McpBehavior` caches the content of its root
|
|
34
|
+
* resource on first read and never refreshes it, which suits static
|
|
35
|
+
* content but froze this snapshot at whatever the first reader saw.
|
|
36
|
+
*/
|
|
37
|
+
public override readResourceAsync(uri: string): Promise<McpResourceContent | undefined> {
|
|
38
|
+
return this.adapter.readResourceAsync(uri);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
protected override _buildResources(): McpResource[] {
|
|
33
42
|
return [
|
|
34
43
|
{
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { IEventSource } from "@cyanmycelium/mcp-core";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Read-only view of the broker's runtime state, exposed to broker behaviors.
|
|
3
5
|
*
|
|
@@ -76,6 +78,19 @@ export interface IBrokerContext {
|
|
|
76
78
|
* a slot that cannot exist at host start.
|
|
77
79
|
*/
|
|
78
80
|
getStdioBridgeTarget?(): string | null | undefined;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fires with the names of the slots whose *state* changed: a slot
|
|
84
|
+
* appeared, a provider attached or detached, a slot joined or left `_all`.
|
|
85
|
+
* Changes in the same tick arrive as one batch.
|
|
86
|
+
*
|
|
87
|
+
* Counters (`pendingCount`, `clientCount`, `sessionCount`) deliberately do
|
|
88
|
+
* not fire it: reading a resource moves them, so notifying on them would
|
|
89
|
+
* make every read trigger the next notification. They stay readable on
|
|
90
|
+
* demand. `_broker` turns this into `notifications/resources/updated` on
|
|
91
|
+
* `broker://providers` and `broker://providers/<name>`.
|
|
92
|
+
*/
|
|
93
|
+
readonly onProvidersChanged?: IEventSource<readonly string[]>;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
/**
|
|
@@ -159,6 +174,14 @@ export interface IBrokerProviderInfo {
|
|
|
159
174
|
|
|
160
175
|
/** Number of in-flight JSON-RPC requests awaiting a response. */
|
|
161
176
|
pendingCount: number;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Client/URI pairs held by `resources/subscribe` on this slot. A number
|
|
180
|
+
* that only grows is the signature of Streamable HTTP clients leaving
|
|
181
|
+
* without `DELETE`: their sessions, and so their subscriptions, never
|
|
182
|
+
* expire. Optional so hand-written contexts keep compiling.
|
|
183
|
+
*/
|
|
184
|
+
resourceSubscriptionCount?: number;
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
/** @deprecated Use {@link IBrokerContext}. */
|