@cyanmycelium/mcp-broker 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.mcp-broker.example/README.md +3 -3
  2. package/README.md +6 -2
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +176 -253
  5. package/dist/bin.js.map +1 -1
  6. package/dist/chunk-FTDKH2C4.js +3670 -0
  7. package/dist/chunk-FTDKH2C4.js.map +1 -0
  8. package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
  9. package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
  10. package/dist/index.d.ts +1790 -22
  11. package/dist/index.js +2 -18
  12. package/dist/index.js.map +1 -1
  13. package/package.json +9 -7
  14. package/scripts/copy-assets.mjs +16 -10
  15. package/scripts/gen-cert.mjs +5 -5
  16. package/scripts/pack-mcpb.mjs +5 -5
  17. package/scripts/sign-bundle.mjs +6 -6
  18. package/src/auth/auth.config.ts +3 -3
  19. package/src/auth/auth.types.ts +34 -59
  20. package/src/auth/http.auth.ts +18 -22
  21. package/src/auth/index.ts +12 -12
  22. package/src/auth/jwt.validator.ts +3 -3
  23. package/src/auth/provider.auth.ts +1 -1
  24. package/src/auth/resource.metadata.ts +14 -21
  25. package/src/authorization/audit.ts +2 -2
  26. package/src/authorization/capability.classifier.ts +1 -1
  27. package/src/authorization/index.ts +13 -13
  28. package/src/authorization/policy.engine.ts +3 -3
  29. package/src/authorization/policy.types.ts +1 -1
  30. package/src/authorization/runtime.ts +5 -5
  31. package/src/authorization/slot.resource.ts +1 -1
  32. package/src/authorization/subject.mapper.ts +1 -1
  33. package/src/bin.ts +38 -8
  34. package/src/broker/adapters/broker.adapter.info.ts +1 -1
  35. package/src/broker/adapters/broker.adapter.providers.ts +1 -1
  36. package/src/broker/aggregate/aggregate.catalog.ts +1 -1
  37. package/src/broker/aggregate/aggregate.server.ts +7 -7
  38. package/src/broker/aggregate/provider.client.session.ts +2 -2
  39. package/src/broker/behaviors/broker.behavior.info.ts +3 -3
  40. package/src/broker/behaviors/broker.behavior.providers.ts +5 -5
  41. package/src/broker/broker.grammars.ts +9 -9
  42. package/src/broker/broker.server.ts +6 -6
  43. package/src/broker/grammars/claude/en.json +1 -1
  44. package/src/broker/grammars/claude/fr.json +1 -1
  45. package/src/broker/index.ts +9 -9
  46. package/src/config.ts +25 -3
  47. package/src/index.ts +24 -24
  48. package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +11 -11
  49. package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
  50. package/src/remote.transports.ts +3 -3
  51. package/src/remote.upstream.ts +3 -3
  52. package/src/stdio.upstream.ts +2 -2
  53. package/src/upstream.ts +2 -2
  54. package/src/ws/ws.interfaces.ts +357 -0
  55. package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +28 -5
  56. package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +231 -467
  57. package/web/README.md +3 -3
  58. package/web/broker-self-mcp.html +9 -9
  59. package/web/css/styles.css +1 -1
  60. package/web/demos/DemoPlaceholder.html +6 -6
  61. package/web/demos/broker-explorer/css/app.css +1 -1
  62. package/web/demos/broker-explorer/index.html +2 -2
  63. package/web/demos/broker-explorer/js/app.js +4 -4
  64. package/web/demos/broker-explorer/js/mcp-ws-client.js +2 -2
  65. package/web/demos/oauth-lab/README.md +26 -0
  66. package/web/demos/oauth-lab/config.json +1 -0
  67. package/web/demos/oauth-lab/js/app.js +40 -16
  68. package/web/demos/oauth-lab/server/smoke-test.mjs +57 -10
  69. package/web/demos/provider-tunnel/css/app.css +1 -1
  70. package/web/demos/provider-tunnel/index.html +3 -3
  71. package/web/demos/provider-tunnel/js/app.js +10 -10
  72. package/web/demos/provider-tunnel/js/toolbox-server.js +5 -5
  73. package/web/index.html +11 -11
  74. package/web/js/lib/broker-tunnel.js +12 -12
  75. package/dist/auth/auth.config.d.ts +0 -40
  76. package/dist/auth/auth.config.js +0 -62
  77. package/dist/auth/auth.config.js.map +0 -1
  78. package/dist/auth/auth.types.d.ts +0 -119
  79. package/dist/auth/auth.types.js +0 -38
  80. package/dist/auth/auth.types.js.map +0 -1
  81. package/dist/auth/http.auth.d.ts +0 -51
  82. package/dist/auth/http.auth.js +0 -121
  83. package/dist/auth/http.auth.js.map +0 -1
  84. package/dist/auth/index.d.ts +0 -16
  85. package/dist/auth/index.js +0 -12
  86. package/dist/auth/index.js.map +0 -1
  87. package/dist/auth/jwt.validator.d.ts +0 -35
  88. package/dist/auth/jwt.validator.js +0 -46
  89. package/dist/auth/jwt.validator.js.map +0 -1
  90. package/dist/auth/provider.auth.d.ts +0 -51
  91. package/dist/auth/provider.auth.js +0 -80
  92. package/dist/auth/provider.auth.js.map +0 -1
  93. package/dist/auth/resource.metadata.d.ts +0 -32
  94. package/dist/auth/resource.metadata.js +0 -26
  95. package/dist/auth/resource.metadata.js.map +0 -1
  96. package/dist/authorization/audit.d.ts +0 -14
  97. package/dist/authorization/audit.js +0 -20
  98. package/dist/authorization/audit.js.map +0 -1
  99. package/dist/authorization/capability.classifier.d.ts +0 -30
  100. package/dist/authorization/capability.classifier.js +0 -85
  101. package/dist/authorization/capability.classifier.js.map +0 -1
  102. package/dist/authorization/index.d.ts +0 -13
  103. package/dist/authorization/index.js +0 -8
  104. package/dist/authorization/index.js.map +0 -1
  105. package/dist/authorization/policy.engine.d.ts +0 -11
  106. package/dist/authorization/policy.engine.js +0 -192
  107. package/dist/authorization/policy.engine.js.map +0 -1
  108. package/dist/authorization/policy.types.d.ts +0 -92
  109. package/dist/authorization/policy.types.js +0 -2
  110. package/dist/authorization/policy.types.js.map +0 -1
  111. package/dist/authorization/resource.path.d.ts +0 -27
  112. package/dist/authorization/resource.path.js +0 -122
  113. package/dist/authorization/resource.path.js.map +0 -1
  114. package/dist/authorization/runtime.d.ts +0 -22
  115. package/dist/authorization/runtime.js +0 -34
  116. package/dist/authorization/runtime.js.map +0 -1
  117. package/dist/authorization/slot.resource.d.ts +0 -12
  118. package/dist/authorization/slot.resource.js +0 -41
  119. package/dist/authorization/slot.resource.js.map +0 -1
  120. package/dist/authorization/subject.mapper.d.ts +0 -15
  121. package/dist/authorization/subject.mapper.js +0 -67
  122. package/dist/authorization/subject.mapper.js.map +0 -1
  123. package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
  124. package/dist/broker/adapters/broker.adapter.info.js +0 -43
  125. package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
  126. package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
  127. package/dist/broker/adapters/broker.adapter.providers.js +0 -61
  128. package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
  129. package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -66
  130. package/dist/broker/aggregate/aggregate.catalog.js +0 -119
  131. package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
  132. package/dist/broker/aggregate/aggregate.server.d.ts +0 -66
  133. package/dist/broker/aggregate/aggregate.server.js +0 -257
  134. package/dist/broker/aggregate/aggregate.server.js.map +0 -1
  135. package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
  136. package/dist/broker/aggregate/provider.client.session.js +0 -140
  137. package/dist/broker/aggregate/provider.client.session.js.map +0 -1
  138. package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
  139. package/dist/broker/behaviors/broker.behavior.info.js +0 -41
  140. package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
  141. package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
  142. package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
  143. package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
  144. package/dist/broker/broker.context.d.ts +0 -63
  145. package/dist/broker/broker.context.js +0 -2
  146. package/dist/broker/broker.context.js.map +0 -1
  147. package/dist/broker/broker.grammars.d.ts +0 -132
  148. package/dist/broker/broker.grammars.js +0 -229
  149. package/dist/broker/broker.grammars.js.map +0 -1
  150. package/dist/broker/broker.server.d.ts +0 -68
  151. package/dist/broker/broker.server.js +0 -73
  152. package/dist/broker/broker.server.js.map +0 -1
  153. package/dist/broker/index.d.ts +0 -9
  154. package/dist/broker/index.js +0 -7
  155. package/dist/broker/index.js.map +0 -1
  156. package/dist/config.d.ts +0 -182
  157. package/dist/config.js +0 -61
  158. package/dist/config.js.map +0 -1
  159. package/dist/mcpb.loader.d.ts +0 -26
  160. package/dist/mcpb.loader.js +0 -161
  161. package/dist/mcpb.loader.js.map +0 -1
  162. package/dist/mcpb.unzip.d.ts +0 -6
  163. package/dist/mcpb.unzip.js +0 -95
  164. package/dist/mcpb.unzip.js.map +0 -1
  165. package/dist/remote.transports.d.ts +0 -18
  166. package/dist/remote.transports.js +0 -297
  167. package/dist/remote.transports.js.map +0 -1
  168. package/dist/remote.upstream.d.ts +0 -38
  169. package/dist/remote.upstream.js +0 -52
  170. package/dist/remote.upstream.js.map +0 -1
  171. package/dist/stdio.upstream.d.ts +0 -47
  172. package/dist/stdio.upstream.js +0 -85
  173. package/dist/stdio.upstream.js.map +0 -1
  174. package/dist/upstream.d.ts +0 -35
  175. package/dist/upstream.js +0 -2
  176. package/dist/upstream.js.map +0 -1
  177. package/dist/version.d.ts +0 -2
  178. package/dist/version.js +0 -9
  179. package/dist/version.js.map +0 -1
  180. package/dist/ws.tunnel.builder.d.ts +0 -176
  181. package/dist/ws.tunnel.builder.js +0 -271
  182. package/dist/ws.tunnel.builder.js.map +0 -1
  183. package/dist/ws.tunnel.d.ts +0 -455
  184. package/dist/ws.tunnel.js +0 -1425
  185. package/dist/ws.tunnel.js.map +0 -1
  186. /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
  187. /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
  188. /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
@@ -0,0 +1,3670 @@
1
+ import * as fs from 'fs';
2
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, rmSync } from 'fs';
3
+ import * as http from 'http';
4
+ import * as https from 'https';
5
+ import * as nodePath from 'path';
6
+ import { join, dirname, resolve, relative, isAbsolute, sep } from 'path';
7
+ import { randomUUID, timingSafeEqual, X509Certificate, createPublicKey, verify } from 'crypto';
8
+ import { WebSocket, WebSocketServer } from 'ws';
9
+ import { StdioTransport, StreamableHttpEndpoint } from '@cyanmycelium/mcp-core/node';
10
+ import { spawn } from 'child_process';
11
+ import { McpAuthError, McpGrammar, McpBehavior, LoopbackTransport, McpServerBuilder, scopesOf as scopesOf$1, buildProtectedResourceMetadata, PROTECTED_RESOURCE_METADATA_PREFIX, bearerToken, buildChallengeHeader, McpAdapterBase, McpToolResults } from '@cyanmycelium/mcp-core';
12
+ import { fileURLToPath } from 'url';
13
+ import { createRemoteJWKSet, jwtVerify, errors } from 'jose';
14
+ import { createRequire } from 'module';
15
+ import { homedir } from 'os';
16
+ import { inflateRawSync } from 'zlib';
17
+
18
+ // src/ws/ws.tunnel.ts
19
+
20
+ // ../provider/dist/protocol/index.js
21
+ var TunnelErrorCodes = {
22
+ /** The slot is taken by another upstream, or the provider is not connected. */
23
+ ProviderUnavailable: -32e3,
24
+ /** The provider's credentials do not allow publishing on this slot. */
25
+ RegistrationForbidden: -32001
26
+ };
27
+ function encodeEnvelope(provider, frame) {
28
+ return encodeEnvelopeMessage(provider, JSON.parse(frame));
29
+ }
30
+ function encodeEnvelopeMessage(provider, payload) {
31
+ const envelope = { provider, payload };
32
+ return JSON.stringify(envelope);
33
+ }
34
+ function decodeEnvelope(raw) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ } catch {
39
+ return void 0;
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
42
+ const { provider, payload } = parsed;
43
+ if (typeof provider !== "string" || provider.length === 0 || payload === void 0) return void 0;
44
+ return { provider, payload };
45
+ }
46
+ function envelopeFrame(envelope) {
47
+ return JSON.stringify(envelope.payload);
48
+ }
49
+ function encodeErrorEnvelope(provider, code, message) {
50
+ return encodeEnvelopeMessage(provider, { jsonrpc: "2.0", id: null, error: { code, message } });
51
+ }
52
+ var StdioUpstream = class {
53
+ name;
54
+ _config;
55
+ _proc = null;
56
+ _buffer = "";
57
+ _open = false;
58
+ _stopped = false;
59
+ /** Called when a complete JSON-RPC line arrives from the process stdout. */
60
+ onMessage = null;
61
+ /** Called when the process has started and stdin is writable. */
62
+ onOpen = null;
63
+ /** Called when the process exits (cleanly or otherwise). */
64
+ onClose = null;
65
+ /** Called on spawn or runtime errors. */
66
+ onError = null;
67
+ constructor(config) {
68
+ this.name = config.name;
69
+ this._config = config;
70
+ }
71
+ get isOpen() {
72
+ return this._open;
73
+ }
74
+ /** Spawns the child process and wires up stdio listeners. */
75
+ connect() {
76
+ this._stopped = false;
77
+ const { command, args = [], env } = this._config;
78
+ this._proc = spawn(command, args, {
79
+ env: { ...process.env, ...env },
80
+ stdio: ["pipe", "pipe", "inherit"]
81
+ });
82
+ this._proc.on("error", (err) => {
83
+ this._open = false;
84
+ this.onError?.(new Error(`StdioUpstream "${this.name}": process error, ${err.message}`));
85
+ });
86
+ this._proc.on("spawn", () => {
87
+ this._open = true;
88
+ this.onOpen?.();
89
+ });
90
+ this._proc.stdout.on("data", (chunk) => {
91
+ this._buffer += chunk.toString("utf8");
92
+ let nl;
93
+ while ((nl = this._buffer.indexOf("\n")) !== -1) {
94
+ const line = this._buffer.slice(0, nl).trim();
95
+ this._buffer = this._buffer.slice(nl + 1);
96
+ if (line) this.onMessage?.(line);
97
+ }
98
+ });
99
+ this._proc.on("close", (code) => {
100
+ this._open = false;
101
+ this._proc = null;
102
+ this.onClose?.();
103
+ if (!this._stopped) {
104
+ this.onError?.(new Error(`StdioUpstream "${this.name}": process exited with code ${code ?? "null"}`));
105
+ }
106
+ });
107
+ }
108
+ /** Sends a JSON-RPC message to the process stdin (appends newline). */
109
+ send(data) {
110
+ if (!this._open || !this._proc?.stdin?.writable) return;
111
+ this._proc.stdin.write(data + "\n", "utf8");
112
+ }
113
+ /** Kills the child process and prevents further reconnection attempts. */
114
+ close() {
115
+ this._stopped = true;
116
+ this._open = false;
117
+ this._proc?.stdin?.end();
118
+ this._proc?.kill();
119
+ this._proc = null;
120
+ }
121
+ };
122
+ function makeRequest(url, options, onResponse) {
123
+ return url.protocol === "https:" ? https.request(url, options, onResponse) : http.request(url, options, onResponse);
124
+ }
125
+ var SseDecoder = class {
126
+ _buf = "";
127
+ feed(chunk, emit) {
128
+ this._buf += chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
129
+ let sep3;
130
+ while ((sep3 = this._buf.indexOf("\n\n")) !== -1) {
131
+ const block = this._buf.slice(0, sep3);
132
+ this._buf = this._buf.slice(sep3 + 2);
133
+ let event = "message";
134
+ const data = [];
135
+ for (const line of block.split("\n")) {
136
+ if (line.startsWith(":")) continue;
137
+ const colon = line.indexOf(":");
138
+ const field = colon === -1 ? line : line.slice(0, colon);
139
+ let value = colon === -1 ? "" : line.slice(colon + 1);
140
+ if (value.startsWith(" ")) value = value.slice(1);
141
+ if (field === "event") event = value;
142
+ else if (field === "data") data.push(value);
143
+ }
144
+ if (data.length > 0) emit(event, data.join("\n"));
145
+ }
146
+ }
147
+ };
148
+ var WebSocketRemoteTransport = class {
149
+ constructor(_url, _headers) {
150
+ this._url = _url;
151
+ this._headers = _headers;
152
+ }
153
+ _url;
154
+ _headers;
155
+ onMessage = null;
156
+ onOpen = null;
157
+ onClose = null;
158
+ onError = null;
159
+ _ws = null;
160
+ connect() {
161
+ const ws = new WebSocket(this._url, { headers: this._headers });
162
+ this._ws = ws;
163
+ ws.on("open", () => this.onOpen?.());
164
+ ws.on("message", (data) => this.onMessage?.(data.toString("utf8")));
165
+ ws.on("close", () => this.onClose?.());
166
+ ws.on("error", (err) => this.onError?.(err));
167
+ }
168
+ send(data) {
169
+ if (this._ws && this._ws.readyState === WebSocket.OPEN) this._ws.send(data);
170
+ }
171
+ close() {
172
+ this._ws?.close();
173
+ this._ws = null;
174
+ }
175
+ };
176
+ var SseRemoteTransport = class {
177
+ constructor(_url, _headers) {
178
+ this._url = _url;
179
+ this._headers = _headers;
180
+ }
181
+ _url;
182
+ _headers;
183
+ onMessage = null;
184
+ onOpen = null;
185
+ onClose = null;
186
+ onError = null;
187
+ _decoder = new SseDecoder();
188
+ _streamReq = null;
189
+ _postUrl = null;
190
+ _closed = false;
191
+ connect() {
192
+ const req = makeRequest(new URL(this._url), { method: "GET", headers: { ...this._headers, Accept: "text/event-stream" } }, (res) => {
193
+ const status = res.statusCode ?? 0;
194
+ if (status >= 400) {
195
+ this.onError?.(new Error(`SSE GET responded ${status}`));
196
+ res.resume();
197
+ return;
198
+ }
199
+ res.setEncoding("utf8");
200
+ res.on("data", (chunk) => this._decoder.feed(chunk, (ev, data) => this._onEvent(ev, data)));
201
+ res.on("end", () => {
202
+ if (!this._closed) this.onClose?.();
203
+ });
204
+ });
205
+ this._streamReq = req;
206
+ req.on("error", (err) => {
207
+ if (!this._closed) this.onError?.(err);
208
+ });
209
+ req.end();
210
+ }
211
+ _onEvent(event, data) {
212
+ if (event === "endpoint") {
213
+ this._postUrl = new URL(data, this._url).toString();
214
+ this.onOpen?.();
215
+ } else if (event === "message") {
216
+ this.onMessage?.(data);
217
+ }
218
+ }
219
+ send(data) {
220
+ if (!this._postUrl) return;
221
+ const req = makeRequest(new URL(this._postUrl), { method: "POST", headers: { ...this._headers, "Content-Type": "application/json" } }, (res) => res.resume());
222
+ req.on("error", (err) => this.onError?.(err));
223
+ req.end(data);
224
+ }
225
+ close() {
226
+ this._closed = true;
227
+ this._streamReq?.destroy();
228
+ this._streamReq = null;
229
+ }
230
+ };
231
+ var StreamableHttpRemoteTransport = class {
232
+ constructor(_url, _headers) {
233
+ this._url = _url;
234
+ this._headers = _headers;
235
+ }
236
+ _url;
237
+ _headers;
238
+ onMessage = null;
239
+ onOpen = null;
240
+ onClose = null;
241
+ onError = null;
242
+ _sessionId = null;
243
+ _streamReq = null;
244
+ _closed = false;
245
+ connect() {
246
+ this._closed = false;
247
+ this.onOpen?.();
248
+ }
249
+ send(data) {
250
+ if (this._closed) return;
251
+ const headers = {
252
+ ...this._headers,
253
+ "Content-Type": "application/json",
254
+ Accept: "application/json, text/event-stream"
255
+ };
256
+ if (this._sessionId) headers["Mcp-Session-Id"] = this._sessionId;
257
+ const req = makeRequest(new URL(this._url), { method: "POST", headers }, (res) => this._handleResponse(res));
258
+ req.on("error", (err) => {
259
+ if (!this._closed) this.onError?.(err);
260
+ });
261
+ req.end(data);
262
+ }
263
+ _handleResponse(res) {
264
+ const sid = res.headers["mcp-session-id"];
265
+ if (typeof sid === "string" && sid.length > 0 && sid !== this._sessionId) {
266
+ this._sessionId = sid;
267
+ this._openServerStream();
268
+ }
269
+ const status = res.statusCode ?? 0;
270
+ if (status === 202) {
271
+ res.resume();
272
+ return;
273
+ }
274
+ if (status >= 400) {
275
+ this.onError?.(new Error(`Streamable HTTP POST responded ${status}`));
276
+ res.resume();
277
+ return;
278
+ }
279
+ res.setEncoding("utf8");
280
+ if (String(res.headers["content-type"] ?? "").includes("text/event-stream")) {
281
+ const decoder = new SseDecoder();
282
+ res.on(
283
+ "data",
284
+ (chunk) => decoder.feed(chunk, (ev, data) => {
285
+ if (ev === "message") this.onMessage?.(data);
286
+ })
287
+ );
288
+ } else {
289
+ let body = "";
290
+ res.on("data", (chunk) => {
291
+ body += chunk;
292
+ });
293
+ res.on("end", () => this._emitJsonBody(body));
294
+ }
295
+ }
296
+ _emitJsonBody(body) {
297
+ const trimmed = body.trim();
298
+ if (!trimmed) return;
299
+ try {
300
+ const parsed = JSON.parse(trimmed);
301
+ if (Array.isArray(parsed)) {
302
+ for (const message of parsed) this.onMessage?.(JSON.stringify(message));
303
+ return;
304
+ }
305
+ } catch {
306
+ }
307
+ this.onMessage?.(trimmed);
308
+ }
309
+ /** Opens the optional standalone GET stream for server-initiated messages. */
310
+ _openServerStream() {
311
+ if (this._closed || this._streamReq) return;
312
+ const headers = { ...this._headers, Accept: "text/event-stream" };
313
+ if (this._sessionId) headers["Mcp-Session-Id"] = this._sessionId;
314
+ const req = makeRequest(new URL(this._url), { method: "GET", headers }, (res) => {
315
+ if ((res.statusCode ?? 0) >= 400) {
316
+ res.resume();
317
+ this._streamReq = null;
318
+ return;
319
+ }
320
+ const decoder = new SseDecoder();
321
+ res.setEncoding("utf8");
322
+ res.on(
323
+ "data",
324
+ (chunk) => decoder.feed(chunk, (ev, data) => {
325
+ if (ev === "message") this.onMessage?.(data);
326
+ })
327
+ );
328
+ res.on("end", () => {
329
+ this._streamReq = null;
330
+ });
331
+ });
332
+ this._streamReq = req;
333
+ req.on("error", () => {
334
+ this._streamReq = null;
335
+ });
336
+ req.end();
337
+ }
338
+ close() {
339
+ this._closed = true;
340
+ this._streamReq?.destroy();
341
+ this._streamReq = null;
342
+ }
343
+ };
344
+ function createRemoteTransport(url, kind, headers) {
345
+ switch (kind) {
346
+ case "websocket":
347
+ return new WebSocketRemoteTransport(url, headers);
348
+ case "sse":
349
+ return new SseRemoteTransport(url, headers);
350
+ case "streamable-http":
351
+ return new StreamableHttpRemoteTransport(url, headers);
352
+ }
353
+ }
354
+ function detectTransport(url) {
355
+ let parsed;
356
+ try {
357
+ parsed = new URL(url);
358
+ } catch {
359
+ return "streamable-http";
360
+ }
361
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") return "websocket";
362
+ if (parsed.pathname.replace(/\/+$/, "").endsWith("/sse")) return "sse";
363
+ return "streamable-http";
364
+ }
365
+
366
+ // src/remote.upstream.ts
367
+ var RemoteUpstream = class {
368
+ name;
369
+ onMessage = null;
370
+ onOpen = null;
371
+ onClose = null;
372
+ onError = null;
373
+ _config;
374
+ _transport = null;
375
+ _open = false;
376
+ constructor(config) {
377
+ this.name = config.name;
378
+ this._config = config;
379
+ }
380
+ get isOpen() {
381
+ return this._open;
382
+ }
383
+ connect() {
384
+ const kind = this._config.transport ?? detectTransport(this._config.url);
385
+ const transport = createRemoteTransport(this._config.url, kind, this._config.headers ?? {});
386
+ this._transport = transport;
387
+ transport.onOpen = () => {
388
+ this._open = true;
389
+ this.onOpen?.();
390
+ };
391
+ transport.onMessage = (data) => this.onMessage?.(data);
392
+ transport.onClose = () => {
393
+ this._open = false;
394
+ this.onClose?.();
395
+ };
396
+ transport.onError = (err) => {
397
+ this.onError?.(new Error(`RemoteUpstream "${this.name}": ${err.message}`));
398
+ };
399
+ transport.connect();
400
+ }
401
+ send(data) {
402
+ this._transport?.send(data);
403
+ }
404
+ close() {
405
+ this._open = false;
406
+ this._transport?.close();
407
+ this._transport = null;
408
+ }
409
+ };
410
+ var BROKER_INFO_URI = "broker://info";
411
+ var BrokerInfoAdapter = class extends McpAdapterBase {
412
+ constructor(_context) {
413
+ super("broker");
414
+ this._context = _context;
415
+ }
416
+ _context;
417
+ async readResourceAsync(uri) {
418
+ if (uri !== BROKER_INFO_URI) return void 0;
419
+ return {
420
+ uri,
421
+ mimeType: "application/json",
422
+ text: JSON.stringify(this._snapshot())
423
+ };
424
+ }
425
+ async executeToolAsync(_uri, toolName, _args) {
426
+ if (toolName !== "broker_info") {
427
+ return McpToolResults.error(`Unknown tool: ${toolName}`);
428
+ }
429
+ return McpToolResults.json(this._snapshot());
430
+ }
431
+ _snapshot() {
432
+ const c = this._context;
433
+ return {
434
+ name: c.name,
435
+ version: c.version,
436
+ startedAt: c.startedAt?.toISOString() ?? null,
437
+ uptimeSeconds: c.uptimeSeconds,
438
+ host: c.host ?? "0.0.0.0",
439
+ port: c.port,
440
+ tls: c.tls,
441
+ paths: c.paths
442
+ };
443
+ }
444
+ };
445
+ function brokerGrammarKey(userAgent, locale, version) {
446
+ const base = `${userAgent}:${locale}`;
447
+ return version ? `${base}@${version}` : base;
448
+ }
449
+ function parseBrokerGrammarStem(stem) {
450
+ const at = stem.indexOf("@");
451
+ if (at < 0) return stem.length > 0 ? { locale: stem } : null;
452
+ const locale = stem.slice(0, at);
453
+ const version = stem.slice(at + 1);
454
+ if (locale.length === 0 || version.length === 0) return null;
455
+ return { locale, version };
456
+ }
457
+ var GRAMMARS_DIR = join(dirname(fileURLToPath(import.meta.url)), "grammars");
458
+ var _cache = /* @__PURE__ */ new Map();
459
+ function loadBrokerGrammar(userAgent, locale, version) {
460
+ const key = brokerGrammarKey(userAgent, locale, version);
461
+ const cached = _cache.get(key);
462
+ if (cached) return cached;
463
+ const filename = version ? `${locale}@${version}.json` : `${locale}.json`;
464
+ const path = join(GRAMMARS_DIR, userAgent, filename);
465
+ if (!existsSync(path)) return void 0;
466
+ const raw = readFileSync(path, "utf-8");
467
+ const data = JSON.parse(raw);
468
+ const grammar = McpGrammar.fromJSON(data);
469
+ _cache.set(key, grammar);
470
+ return grammar;
471
+ }
472
+ function* iterBrokerGrammarsFrom(grammarsDir) {
473
+ if (!existsSync(grammarsDir)) return;
474
+ const userAgents = readdirSync(grammarsDir).sort();
475
+ for (const userAgent of userAgents) {
476
+ const uaDir = join(grammarsDir, userAgent);
477
+ if (!statSync(uaDir).isDirectory()) continue;
478
+ const files = readdirSync(uaDir).sort();
479
+ for (const file of files) {
480
+ if (!file.endsWith(".json")) continue;
481
+ const stem = file.slice(0, -".json".length);
482
+ const parsed = parseBrokerGrammarStem(stem);
483
+ if (!parsed) {
484
+ process.stderr.write(`[mcp-broker] Skipping unparseable grammar filename ${file} in ${uaDir}
485
+ `);
486
+ continue;
487
+ }
488
+ const { locale, version } = parsed;
489
+ const path = join(uaDir, file);
490
+ try {
491
+ const raw = readFileSync(path, "utf-8");
492
+ const data = JSON.parse(raw);
493
+ const grammar = McpGrammar.fromJSON(data);
494
+ yield {
495
+ userAgent,
496
+ locale,
497
+ version,
498
+ key: brokerGrammarKey(userAgent, locale, version),
499
+ grammar
500
+ };
501
+ } catch (err) {
502
+ process.stderr.write(`[mcp-broker] Failed to load grammar ${path}: ${err.message}
503
+ `);
504
+ }
505
+ }
506
+ }
507
+ }
508
+ function* iterAvailableBrokerGrammars() {
509
+ yield* iterBrokerGrammarsFrom(GRAMMARS_DIR);
510
+ }
511
+ function brokerBaselineGrammar() {
512
+ const g = loadBrokerGrammar("default", "en");
513
+ if (!g) {
514
+ throw new Error(`Required baseline broker grammar "default:en" is missing, expected at ${join(GRAMMARS_DIR, "default", "en.json")}.`);
515
+ }
516
+ return g;
517
+ }
518
+ function brokerBaselineToolDescription(toolName) {
519
+ const desc = brokerBaselineGrammar().getToolDescription(toolName);
520
+ if (!desc) {
521
+ throw new Error(`Missing baseline description for tool "${toolName}" in default/en.json.`);
522
+ }
523
+ return desc;
524
+ }
525
+ function brokerBaselinePropertyDescription(toolName, propertyName) {
526
+ const desc = brokerBaselineGrammar().getPropertyDescription(toolName, propertyName);
527
+ if (!desc) {
528
+ throw new Error(`Missing baseline description for property "${propertyName}" of tool "${toolName}" in default/en.json.`);
529
+ }
530
+ return desc;
531
+ }
532
+ function brokerBaselineResourceName(uri) {
533
+ const name = brokerBaselineGrammar().getResourceName(uri);
534
+ if (!name) {
535
+ throw new Error(`Missing baseline name for resource "${uri}" in default/en.json.`);
536
+ }
537
+ return name;
538
+ }
539
+ function brokerBaselineResourceDescription(uri) {
540
+ const desc = brokerBaselineGrammar().getResourceDescription(uri);
541
+ if (!desc) {
542
+ throw new Error(`Missing baseline description for resource "${uri}" in default/en.json.`);
543
+ }
544
+ return desc;
545
+ }
546
+ function brokerBaselineResourceTemplateName(uriTemplate) {
547
+ const name = brokerBaselineGrammar().getResourceTemplateName(uriTemplate);
548
+ if (!name) {
549
+ throw new Error(`Missing baseline name for resource template "${uriTemplate}" in default/en.json.`);
550
+ }
551
+ return name;
552
+ }
553
+ function brokerBaselineResourceTemplateDescription(uriTemplate) {
554
+ const desc = brokerBaselineGrammar().getResourceTemplateDescription(uriTemplate);
555
+ if (!desc) {
556
+ throw new Error(`Missing baseline description for resource template "${uriTemplate}" in default/en.json.`);
557
+ }
558
+ return desc;
559
+ }
560
+
561
+ // src/broker/behaviors/broker.behavior.info.ts
562
+ var BrokerInfoBehavior = class _BrokerInfoBehavior extends McpBehavior {
563
+ static NAMESPACE = "broker";
564
+ constructor(context) {
565
+ super(new BrokerInfoAdapter(context), {
566
+ namespace: _BrokerInfoBehavior.NAMESPACE
567
+ });
568
+ }
569
+ _buildResources() {
570
+ return [
571
+ {
572
+ uri: BROKER_INFO_URI,
573
+ name: brokerBaselineResourceName(BROKER_INFO_URI),
574
+ mimeType: "application/json",
575
+ description: brokerBaselineResourceDescription(BROKER_INFO_URI)
576
+ }
577
+ ];
578
+ }
579
+ _buildTools() {
580
+ return [
581
+ {
582
+ name: "broker_info",
583
+ description: brokerBaselineToolDescription("broker_info"),
584
+ inputSchema: {
585
+ type: "object",
586
+ properties: {},
587
+ additionalProperties: false
588
+ }
589
+ }
590
+ ];
591
+ }
592
+ };
593
+ var PROVIDERS_URI = "broker://providers";
594
+ var PROVIDER_URI_TEMPLATE = "broker://providers/{name}";
595
+ var BrokerProvidersAdapter = class extends McpAdapterBase {
596
+ constructor(_context) {
597
+ super("broker");
598
+ this._context = _context;
599
+ }
600
+ _context;
601
+ async readResourceAsync(uri) {
602
+ if (uri === PROVIDERS_URI) {
603
+ return {
604
+ uri,
605
+ mimeType: "application/json",
606
+ text: JSON.stringify(this._listSnapshot())
607
+ };
608
+ }
609
+ const match = /^broker:\/\/providers\/([^/]+)$/.exec(uri);
610
+ if (match) {
611
+ const name = decodeURIComponent(match[1]);
612
+ const info = this._context.getProviderInfo(name);
613
+ if (!info) return void 0;
614
+ return {
615
+ uri,
616
+ mimeType: "application/json",
617
+ text: JSON.stringify(info)
618
+ };
619
+ }
620
+ return void 0;
621
+ }
622
+ async executeToolAsync(_uri, toolName, args) {
623
+ switch (toolName) {
624
+ case "providers_list":
625
+ return McpToolResults.json(this._listSnapshot());
626
+ case "provider_status": {
627
+ const name = typeof args.name === "string" ? args.name : "";
628
+ if (!name) return McpToolResults.error('Missing required argument: "name" (string).');
629
+ const info = this._context.getProviderInfo(name);
630
+ if (!info) return McpToolResults.error(`Unknown provider: "${name}".`);
631
+ return McpToolResults.json(info);
632
+ }
633
+ default:
634
+ return McpToolResults.error(`Unknown tool: ${toolName}`);
635
+ }
636
+ }
637
+ _listSnapshot() {
638
+ const providers = this._context.getProvidersInfo();
639
+ return { count: providers.length, providers };
640
+ }
641
+ };
642
+
643
+ // src/broker/behaviors/broker.behavior.providers.ts
644
+ var BrokerProvidersBehavior = class _BrokerProvidersBehavior extends McpBehavior {
645
+ static NAMESPACE = "broker_providers";
646
+ constructor(context) {
647
+ super(new BrokerProvidersAdapter(context), {
648
+ namespace: _BrokerProvidersBehavior.NAMESPACE
649
+ });
650
+ }
651
+ _buildResources() {
652
+ return [
653
+ {
654
+ uri: PROVIDERS_URI,
655
+ name: brokerBaselineResourceName(PROVIDERS_URI),
656
+ mimeType: "application/json",
657
+ description: brokerBaselineResourceDescription(PROVIDERS_URI)
658
+ }
659
+ ];
660
+ }
661
+ _buildTemplate() {
662
+ return [
663
+ {
664
+ uriTemplate: PROVIDER_URI_TEMPLATE,
665
+ name: brokerBaselineResourceTemplateName(PROVIDER_URI_TEMPLATE),
666
+ mimeType: "application/json",
667
+ description: brokerBaselineResourceTemplateDescription(PROVIDER_URI_TEMPLATE)
668
+ }
669
+ ];
670
+ }
671
+ _buildTools() {
672
+ return [
673
+ {
674
+ name: "providers_list",
675
+ description: brokerBaselineToolDescription("providers_list"),
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: {},
679
+ additionalProperties: false
680
+ }
681
+ },
682
+ {
683
+ name: "provider_status",
684
+ description: brokerBaselineToolDescription("provider_status"),
685
+ inputSchema: {
686
+ type: "object",
687
+ properties: {
688
+ name: {
689
+ type: "string",
690
+ description: brokerBaselinePropertyDescription("provider_status", "name")
691
+ }
692
+ },
693
+ required: ["name"],
694
+ additionalProperties: false
695
+ }
696
+ }
697
+ ];
698
+ }
699
+ };
700
+ var BROKER_PROVIDER_NAME = "_broker";
701
+ async function startBrokerServer(context, options = {}) {
702
+ const [serverEnd, clientEnd] = LoopbackTransport.createPair();
703
+ const builder = new McpServerBuilder().withName(BROKER_PROVIDER_NAME).withTransport(serverEnd).withInitializer({
704
+ initialize: () => ({
705
+ protocolVersion: "2024-11-05",
706
+ serverInfo: { name: BROKER_PROVIDER_NAME, version: context.version }
707
+ })
708
+ }).register(new BrokerInfoBehavior(context), new BrokerProvidersBehavior(context));
709
+ for (const entry of iterAvailableBrokerGrammars()) {
710
+ builder.withGrammar(entry.key, entry.grammar);
711
+ }
712
+ if (options.localGrammarsDir) {
713
+ for (const entry of iterBrokerGrammarsFrom(options.localGrammarsDir)) {
714
+ builder.withGrammar(entry.key, entry.grammar);
715
+ }
716
+ }
717
+ builder.withGrammarResolver({
718
+ localeSource: () => process.env["MCP_BROKER_LOCALE"],
719
+ ...options.grammarResolverOptions ?? {}
720
+ });
721
+ const server = builder.build();
722
+ await server.start();
723
+ return { server, clientTransport: clientEnd };
724
+ }
725
+
726
+ // src/authorization/resource.path.ts
727
+ var WILDCARD = "*";
728
+ var RECURSIVE_WILDCARD = "**";
729
+ function assertString(value, label) {
730
+ if (typeof value !== "string" || value.length === 0) {
731
+ throw new Error(`${label}: a non-empty string is required.`);
732
+ }
733
+ }
734
+ function splitAbsolutePath(value, label) {
735
+ assertString(value, label);
736
+ if (!value.startsWith("/")) {
737
+ throw new Error(`${label}: "${value}" must start with "/".`);
738
+ }
739
+ if (value === "/") return [];
740
+ const withoutTrailingSlash = value.endsWith("/") ? value.slice(0, -1) : value;
741
+ const segments = withoutTrailingSlash.slice(1).split("/");
742
+ for (const segment of segments) {
743
+ if (!segment) {
744
+ throw new Error(`${label}: "${value}" contains an empty path segment.`);
745
+ }
746
+ if (segment === "." || segment === "..") {
747
+ throw new Error(`${label}: "${value}" contains a forbidden "${segment}" segment.`);
748
+ }
749
+ }
750
+ return segments;
751
+ }
752
+ function normalizedValue(segments) {
753
+ return segments.length === 0 ? "/" : `/${segments.join("/")}`;
754
+ }
755
+ var ResourcePath = class _ResourcePath {
756
+ value;
757
+ segments;
758
+ constructor(segments) {
759
+ this.segments = Object.freeze([...segments]);
760
+ this.value = normalizedValue(this.segments);
761
+ }
762
+ static parse(value) {
763
+ const segments = splitAbsolutePath(value, "resource path");
764
+ for (const segment of segments) {
765
+ if (segment === WILDCARD || segment === RECURSIVE_WILDCARD) {
766
+ throw new Error(`resource path: wildcards are not allowed in "${value}".`);
767
+ }
768
+ }
769
+ return new _ResourcePath(segments);
770
+ }
771
+ static tryParse(value) {
772
+ try {
773
+ return _ResourcePath.parse(value);
774
+ } catch {
775
+ return void 0;
776
+ }
777
+ }
778
+ toString() {
779
+ return this.value;
780
+ }
781
+ };
782
+ var ResourcePathPattern = class _ResourcePathPattern {
783
+ value;
784
+ segments;
785
+ specificity;
786
+ constructor(segments) {
787
+ this.segments = Object.freeze([...segments]);
788
+ this.value = normalizedValue(this.segments);
789
+ this.specificity = this.segments.reduce((score, segment) => {
790
+ if (segment === RECURSIVE_WILDCARD) return score;
791
+ if (segment === WILDCARD) return score + 1;
792
+ return score + 4;
793
+ }, this.segments.length);
794
+ }
795
+ static parse(value) {
796
+ const canonical = value === RECURSIVE_WILDCARD ? `/${RECURSIVE_WILDCARD}` : value;
797
+ const segments = splitAbsolutePath(canonical, "resource pattern");
798
+ let recursiveSeen = false;
799
+ for (let index = 0; index < segments.length; index += 1) {
800
+ const segment = segments[index];
801
+ if (segment === RECURSIVE_WILDCARD) {
802
+ if (index !== segments.length - 1) {
803
+ throw new Error(`resource pattern: "**" must be the final segment in "${value}".`);
804
+ }
805
+ recursiveSeen = true;
806
+ } else if (recursiveSeen) {
807
+ throw new Error(`resource pattern: no segment may follow "**" in "${value}".`);
808
+ } else if (segment !== WILDCARD && segment.includes(WILDCARD)) {
809
+ throw new Error(`resource pattern: wildcards must occupy a complete segment in "${value}".`);
810
+ }
811
+ }
812
+ return new _ResourcePathPattern(segments);
813
+ }
814
+ matches(path) {
815
+ const pattern = this.segments;
816
+ const target = path.segments;
817
+ for (let index = 0; index < pattern.length; index += 1) {
818
+ const segment = pattern[index];
819
+ if (segment === RECURSIVE_WILDCARD) return true;
820
+ if (index >= target.length) return false;
821
+ if (segment !== WILDCARD && segment !== target[index]) return false;
822
+ }
823
+ return target.length === pattern.length;
824
+ }
825
+ toString() {
826
+ return this.value;
827
+ }
828
+ };
829
+
830
+ // src/authorization/capability.classifier.ts
831
+ var STATIC_CAPABILITIES = {
832
+ "resources/list": "mcp.resources.read",
833
+ "resources/read": "mcp.resources.read",
834
+ "tools/list": "mcp.tools.list",
835
+ "prompts/list": "mcp.prompts.read",
836
+ "prompts/get": "mcp.prompts.read",
837
+ "completion/complete": "mcp.completion.use",
838
+ "notifications/resources/list_changed": "mcp.resources.read",
839
+ "notifications/resources/updated": "mcp.resources.read",
840
+ "notifications/tools/list_changed": "mcp.tools.list",
841
+ "notifications/prompts/list_changed": "mcp.prompts.read"
842
+ };
843
+ var BROKER_READ_TOOLS = /* @__PURE__ */ new Set(["broker_info", "providers_list", "provider_status"]);
844
+ function toolNameFrom(params) {
845
+ if (typeof params !== "object" || params === null || Array.isArray(params)) return void 0;
846
+ const name = params["name"];
847
+ return typeof name === "string" && name ? name : void 0;
848
+ }
849
+ function validateCapability(capability, label = "capability") {
850
+ if (typeof capability !== "string" || capability !== "*" && !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(capability)) {
851
+ throw new Error(`authorization ${label}: "${capability}" is malformed.`);
852
+ }
853
+ }
854
+ var ConfiguredCapabilityClassifier = class {
855
+ _globalTools;
856
+ _providerMappings;
857
+ constructor(toolCapabilities = {}, providerToolCapabilities = {}) {
858
+ for (const [tool, capability] of Object.entries(toolCapabilities)) {
859
+ if (!tool) throw new Error("authorization toolCapabilities: tool names must not be empty.");
860
+ validateCapability(capability, `toolCapabilities["${tool}"]`);
861
+ }
862
+ this._globalTools = Object.freeze({ ...toolCapabilities });
863
+ const mappings = [];
864
+ for (const [patternValue, tools] of Object.entries(providerToolCapabilities)) {
865
+ const pattern = ResourcePathPattern.parse(patternValue);
866
+ for (const [tool, capability] of Object.entries(tools)) {
867
+ if (!tool) throw new Error(`authorization providerToolCapabilities["${patternValue}"]: tool names must not be empty.`);
868
+ validateCapability(capability, `providerToolCapabilities["${patternValue}"]["${tool}"]`);
869
+ }
870
+ mappings.push({ pattern, tools: Object.freeze({ ...tools }) });
871
+ }
872
+ mappings.sort((left, right) => right.pattern.specificity - left.pattern.specificity || left.pattern.value.localeCompare(right.pattern.value));
873
+ this._providerMappings = Object.freeze(mappings);
874
+ }
875
+ classify(operation, resource, provider) {
876
+ const method = operation.method;
877
+ if (!method) return void 0;
878
+ if (provider === "_broker") {
879
+ if (method === "tools/list" || method === "resources/list" || method === "resources/read") {
880
+ return { capability: "broker.providers.read" };
881
+ }
882
+ if (method === "tools/call") {
883
+ const tool2 = toolNameFrom(operation.params);
884
+ if (tool2 && BROKER_READ_TOOLS.has(tool2)) return { capability: "broker.providers.read", tool: tool2 };
885
+ }
886
+ }
887
+ if (method !== "tools/call") {
888
+ const capability = STATIC_CAPABILITIES[method];
889
+ return capability ? { capability } : void 0;
890
+ }
891
+ const tool = toolNameFrom(operation.params);
892
+ if (!tool) return { capability: "mcp.tools.call" };
893
+ for (const mapping of this._providerMappings) {
894
+ if (mapping.pattern.matches(resource)) {
895
+ const capability = mapping.tools[tool];
896
+ if (capability) return { capability, tool };
897
+ }
898
+ }
899
+ return { capability: this._globalTools[tool] ?? "mcp.tools.call", tool };
900
+ }
901
+ };
902
+
903
+ // src/authorization/policy.engine.ts
904
+ function validateName(value, label) {
905
+ if (typeof value !== "string" || !value || value.trim() !== value || value.includes("/")) {
906
+ throw new Error(`authorization ${label}: "${value}" is invalid.`);
907
+ }
908
+ }
909
+ function validateSubject(subject, label) {
910
+ if (typeof subject !== "string" || !/^[A-Za-z][A-Za-z0-9_-]*:.+$/.test(subject)) {
911
+ throw new Error(`authorization ${label}: subject "${subject}" must use a non-empty extensible prefix such as "user:alice".`);
912
+ }
913
+ }
914
+ function validatePolicyId(id, label) {
915
+ if (typeof id !== "string" || !id || id.trim() !== id) {
916
+ throw new Error(`authorization ${label}: policy ids must be non-empty and must not contain surrounding whitespace.`);
917
+ }
918
+ }
919
+ function expandRoles(roles) {
920
+ const expanded = /* @__PURE__ */ new Map();
921
+ const visiting = [];
922
+ for (const [name, role] of Object.entries(roles)) {
923
+ validateName(name, "role");
924
+ for (const key of Object.keys(role)) {
925
+ if (key !== "inherits" && key !== "capabilities") {
926
+ throw new Error(`authorization role "${name}": unsupported field "${key}"; roles contain capabilities, not resource paths.`);
927
+ }
928
+ }
929
+ if (!Array.isArray(role.capabilities)) {
930
+ throw new Error(`authorization role "${name}": capabilities must be an array.`);
931
+ }
932
+ if (role.inherits !== void 0 && !Array.isArray(role.inherits)) {
933
+ throw new Error(`authorization role "${name}": inherits must be an array.`);
934
+ }
935
+ for (const capability of role.capabilities) validateCapability(capability, `role "${name}" capability`);
936
+ for (const inherited of role.inherits ?? []) {
937
+ validateName(inherited, `role "${name}" inheritance`);
938
+ if (!roles[inherited]) {
939
+ throw new Error(`authorization role "${name}": inherited role "${inherited}" is not defined.`);
940
+ }
941
+ }
942
+ }
943
+ const visit = (name) => {
944
+ const cached = expanded.get(name);
945
+ if (cached) return cached;
946
+ const cycleStart = visiting.indexOf(name);
947
+ if (cycleStart >= 0) {
948
+ const cycle = [...visiting.slice(cycleStart), name].join(" -> ");
949
+ throw new Error(`authorization roles: inheritance cycle detected: ${cycle}.`);
950
+ }
951
+ const role = roles[name];
952
+ if (!role) throw new Error(`authorization roles: role "${name}" is not defined.`);
953
+ visiting.push(name);
954
+ const capabilities = /* @__PURE__ */ new Set();
955
+ for (const inherited of role.inherits ?? []) {
956
+ for (const capability of visit(inherited)) capabilities.add(capability);
957
+ }
958
+ for (const capability of role.capabilities) capabilities.add(capability);
959
+ visiting.pop();
960
+ const frozen = Object.freeze(capabilities);
961
+ expanded.set(name, frozen);
962
+ return frozen;
963
+ };
964
+ for (const name of Object.keys(roles)) visit(name);
965
+ return expanded;
966
+ }
967
+ function indexBySubject(entries) {
968
+ const mutable = /* @__PURE__ */ new Map();
969
+ for (const entry of entries) {
970
+ const bucket = mutable.get(entry.subject);
971
+ if (bucket) bucket.push(entry);
972
+ else mutable.set(entry.subject, [entry]);
973
+ }
974
+ const result = /* @__PURE__ */ new Map();
975
+ for (const [subject, bucket] of mutable) result.set(subject, Object.freeze(bucket));
976
+ return result;
977
+ }
978
+ function grants(capabilities, requested) {
979
+ return capabilities.has("*") || capabilities.has(requested);
980
+ }
981
+ var ConfigPolicyEngine = class {
982
+ _assignmentsBySubject;
983
+ _deniesBySubject;
984
+ constructor(roles, assignments, denies) {
985
+ const expandedRoles = expandRoles(roles);
986
+ const policyIds = /* @__PURE__ */ new Set();
987
+ const reserveId = (id, label) => {
988
+ validatePolicyId(id, label);
989
+ if (policyIds.has(id)) throw new Error(`authorization policies: duplicate policy id "${id}".`);
990
+ policyIds.add(id);
991
+ };
992
+ const compiledAssignments = assignments.map((assignment, index) => {
993
+ const id = assignment.id ?? `assignment:${index}`;
994
+ reserveId(id, `assignment ${index}`);
995
+ validateSubject(assignment.subject, `assignment "${id}"`);
996
+ validateName(assignment.role, `assignment "${id}" role`);
997
+ const capabilities = expandedRoles.get(assignment.role);
998
+ if (!capabilities) {
999
+ throw new Error(`authorization assignment "${id}": role "${assignment.role}" is not defined.`);
1000
+ }
1001
+ if (!assignment.resource) throw new Error(`authorization assignment "${id}": resource must not be empty.`);
1002
+ return {
1003
+ id,
1004
+ subject: assignment.subject,
1005
+ capabilities,
1006
+ resource: ResourcePathPattern.parse(assignment.resource)
1007
+ };
1008
+ });
1009
+ const compiledDenies = denies.map((deny, index) => {
1010
+ const id = deny.id ?? `deny:${index}`;
1011
+ reserveId(id, `deny ${index}`);
1012
+ validateSubject(deny.subject, `deny "${id}"`);
1013
+ if (deny.effect !== void 0 && deny.effect !== "deny") {
1014
+ throw new Error(`authorization deny "${id}": effect must be "deny".`);
1015
+ }
1016
+ if (!Array.isArray(deny.capabilities) || deny.capabilities.length === 0) {
1017
+ throw new Error(`authorization deny "${id}": at least one capability is required.`);
1018
+ }
1019
+ const capabilities = /* @__PURE__ */ new Set();
1020
+ for (const capability of deny.capabilities) {
1021
+ validateCapability(capability, `deny "${id}" capability`);
1022
+ capabilities.add(capability);
1023
+ }
1024
+ if (!deny.resource) throw new Error(`authorization deny "${id}": resource must not be empty.`);
1025
+ return {
1026
+ id,
1027
+ subject: deny.subject,
1028
+ capabilities: Object.freeze(capabilities),
1029
+ resource: ResourcePathPattern.parse(deny.resource)
1030
+ };
1031
+ });
1032
+ this._assignmentsBySubject = indexBySubject(compiledAssignments);
1033
+ this._deniesBySubject = indexBySubject(compiledDenies);
1034
+ }
1035
+ authorize(request3) {
1036
+ if (!request3.resource) return { allowed: false, reason: "invalid-resource" };
1037
+ try {
1038
+ validateCapability(request3.capability, "request capability");
1039
+ } catch {
1040
+ return { allowed: false, reason: "no-matching-grant" };
1041
+ }
1042
+ const matchingDenies = /* @__PURE__ */ new Set();
1043
+ for (const subject of request3.subject.ids) {
1044
+ for (const deny of this._deniesBySubject.get(subject) ?? []) {
1045
+ if (deny.resource.matches(request3.resource) && grants(deny.capabilities, request3.capability)) {
1046
+ matchingDenies.add(deny.id);
1047
+ }
1048
+ }
1049
+ }
1050
+ if (matchingDenies.size > 0) {
1051
+ return {
1052
+ allowed: false,
1053
+ reason: "explicit-deny",
1054
+ matchedPolicies: Object.freeze([...matchingDenies].sort())
1055
+ };
1056
+ }
1057
+ const matchingGrants = /* @__PURE__ */ new Set();
1058
+ for (const subject of request3.subject.ids) {
1059
+ for (const assignment of this._assignmentsBySubject.get(subject) ?? []) {
1060
+ if (assignment.resource.matches(request3.resource) && grants(assignment.capabilities, request3.capability)) {
1061
+ matchingGrants.add(assignment.id);
1062
+ }
1063
+ }
1064
+ }
1065
+ if (matchingGrants.size > 0) {
1066
+ return {
1067
+ allowed: true,
1068
+ reason: "role-grant",
1069
+ matchedPolicies: Object.freeze([...matchingGrants].sort())
1070
+ };
1071
+ }
1072
+ return { allowed: false, reason: "no-matching-grant" };
1073
+ }
1074
+ };
1075
+
1076
+ // src/authorization/subject.mapper.ts
1077
+ var SubjectMappingError = class extends Error {
1078
+ constructor(message) {
1079
+ super(message);
1080
+ this.name = "SubjectMappingError";
1081
+ }
1082
+ };
1083
+ function claimAt(claims, claimName) {
1084
+ const parts = claimName.split(".");
1085
+ let current = claims;
1086
+ for (const part of parts) {
1087
+ if (!part || typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
1088
+ current = current[part];
1089
+ }
1090
+ return current;
1091
+ }
1092
+ function valuesOf(value, claimName) {
1093
+ if (value === void 0 || value === null) return [];
1094
+ if (typeof value === "string") return value ? [value] : [];
1095
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
1096
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
1097
+ }
1098
+ throw new SubjectMappingError(`JWT subject claim "${claimName}" must be a string or an array of strings.`);
1099
+ }
1100
+ function validateClaimName(value, label) {
1101
+ if (!value || value.split(".").some((part) => !part)) {
1102
+ throw new Error(`authorization subject mapping: ${label} contains an invalid claim name.`);
1103
+ }
1104
+ }
1105
+ var JwtSubjectMapper = class {
1106
+ _rules;
1107
+ constructor(config = {}) {
1108
+ const userClaim = config.userClaim ?? "sub";
1109
+ const groupClaims = config.groupClaims ?? ["groups", "roles"];
1110
+ const clientClaim = config.clientClaim ?? "client_id";
1111
+ const serviceClaims = config.serviceClaims ?? [];
1112
+ if (!Array.isArray(groupClaims)) throw new Error("authorization subject mapping: groupClaims must be an array.");
1113
+ if (!Array.isArray(serviceClaims)) throw new Error("authorization subject mapping: serviceClaims must be an array.");
1114
+ validateClaimName(userClaim, "userClaim");
1115
+ validateClaimName(clientClaim, "clientClaim");
1116
+ for (const claim of groupClaims) validateClaimName(claim, "groupClaims");
1117
+ for (const claim of serviceClaims) validateClaimName(claim, "serviceClaims");
1118
+ this._rules = Object.freeze([
1119
+ { prefix: "user", claim: userClaim },
1120
+ ...groupClaims.map((claim) => ({ prefix: "group", claim })),
1121
+ { prefix: "client", claim: clientClaim },
1122
+ ...serviceClaims.map((claim) => ({ prefix: "service", claim }))
1123
+ ]);
1124
+ }
1125
+ map(claims) {
1126
+ const ids = /* @__PURE__ */ new Set();
1127
+ for (const rule of this._rules) {
1128
+ for (const value of valuesOf(claimAt(claims, rule.claim), rule.claim)) {
1129
+ ids.add(`${rule.prefix}:${value}`);
1130
+ }
1131
+ }
1132
+ return { ids: Object.freeze([...ids]), claims };
1133
+ }
1134
+ };
1135
+
1136
+ // src/authorization/slot.resource.ts
1137
+ var RESERVED_RESOURCES = {
1138
+ _broker: "/_system/broker",
1139
+ _all: "/_system/all"
1140
+ };
1141
+ function isReservedNamespace(resource) {
1142
+ return resource.value === "/_system" || resource.value.startsWith("/_system/");
1143
+ }
1144
+ var DefaultSlotResourceResolver = class {
1145
+ _explicit = /* @__PURE__ */ new Map();
1146
+ constructor(slotResources = {}) {
1147
+ for (const [slot, configuredPath] of Object.entries(slotResources)) {
1148
+ if (!slot) throw new Error("authorization slotResources: slot names must not be empty.");
1149
+ const resource = ResourcePath.parse(configuredPath);
1150
+ const reserved = RESERVED_RESOURCES[slot];
1151
+ if (isReservedNamespace(resource) && reserved !== resource.value) {
1152
+ throw new Error(`authorization slotResources: slot "${slot}" may not use reserved resource "${resource.value}".`);
1153
+ }
1154
+ if (reserved && resource.value !== reserved) {
1155
+ throw new Error(`authorization slotResources: reserved slot "${slot}" must resolve to "${reserved}".`);
1156
+ }
1157
+ this._explicit.set(slot, resource);
1158
+ }
1159
+ }
1160
+ resolve(slot) {
1161
+ const explicit = this._explicit.get(slot);
1162
+ if (explicit) return explicit;
1163
+ const reserved = RESERVED_RESOURCES[slot];
1164
+ if (reserved) return ResourcePath.parse(reserved);
1165
+ if (!slot) return void 0;
1166
+ const candidate = slot.startsWith("/") ? slot : `/${slot}`;
1167
+ const resource = ResourcePath.tryParse(candidate);
1168
+ return resource && !isReservedNamespace(resource) ? resource : void 0;
1169
+ }
1170
+ };
1171
+
1172
+ // src/authorization/runtime.ts
1173
+ function hasAuthorizationPolicies(config) {
1174
+ return Object.keys(config.roles ?? {}).length > 0 || (config.assignments?.length ?? 0) > 0 || (config.denies?.length ?? 0) > 0;
1175
+ }
1176
+ function compileAuthorizationPolicy(config) {
1177
+ if (config.audit?.logAllowed !== void 0 && typeof config.audit.logAllowed !== "boolean") {
1178
+ throw new Error("authorization audit.logAllowed must be a boolean.");
1179
+ }
1180
+ const engine = new ConfigPolicyEngine(config.roles ?? {}, config.assignments ?? [], config.denies ?? []);
1181
+ const subjectMapper = new JwtSubjectMapper(config.subjectMapping);
1182
+ const slotResourceResolver = new DefaultSlotResourceResolver(config.slotResources);
1183
+ const capabilityClassifier = new ConfiguredCapabilityClassifier(config.toolCapabilities, config.providerToolCapabilities);
1184
+ return {
1185
+ engine,
1186
+ subjectMapper,
1187
+ slotResourceResolver,
1188
+ capabilityClassifier,
1189
+ audit: Object.freeze({ logAllowed: config.audit?.logAllowed ?? false })
1190
+ };
1191
+ }
1192
+ function authorizationWithEngine(engine, overrides = {}) {
1193
+ return {
1194
+ engine,
1195
+ subjectMapper: overrides.subjectMapper ?? new JwtSubjectMapper(),
1196
+ slotResourceResolver: overrides.slotResourceResolver ?? new DefaultSlotResourceResolver(),
1197
+ capabilityClassifier: overrides.capabilityClassifier ?? new ConfiguredCapabilityClassifier(),
1198
+ audit: Object.freeze({ logAllowed: overrides.audit?.logAllowed ?? false })
1199
+ };
1200
+ }
1201
+
1202
+ // src/authorization/audit.ts
1203
+ function makeAuthorizationAuditEvent(context, decision) {
1204
+ const clientId = context.subject.ids.find((id) => id.startsWith("client:"))?.slice("client:".length);
1205
+ return {
1206
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1207
+ allowed: decision.allowed,
1208
+ subjectIds: context.subject.ids,
1209
+ clientId,
1210
+ slot: context.slot,
1211
+ resource: context.resource?.value,
1212
+ capability: context.capability,
1213
+ provider: context.provider,
1214
+ tool: context.tool,
1215
+ reason: decision.reason,
1216
+ matchedPolicies: decision.matchedPolicies
1217
+ };
1218
+ }
1219
+ function writeAuthorizationAuditEvent(event) {
1220
+ console.error(`[broker] authorization ${JSON.stringify(event)}`);
1221
+ }
1222
+
1223
+ // src/broker/aggregate/aggregate.catalog.ts
1224
+ var SEPARATOR = "-";
1225
+ var MAX_TOOL_NAME_LENGTH = 64;
1226
+ var AggregateCatalog = class {
1227
+ _providers = /* @__PURE__ */ new Map();
1228
+ _tools = [];
1229
+ _prompts = [];
1230
+ _toolRoutes = /* @__PURE__ */ new Map();
1231
+ _promptRoutes = /* @__PURE__ */ new Map();
1232
+ /** Number of providers currently contributing to the aggregate. */
1233
+ get providerCount() {
1234
+ return this._providers.size;
1235
+ }
1236
+ /** The merged, namespaced tool list. */
1237
+ get tools() {
1238
+ return this._tools;
1239
+ }
1240
+ /** The merged, namespaced prompt list. */
1241
+ get prompts() {
1242
+ return this._prompts;
1243
+ }
1244
+ /** Adds or replaces a provider's contribution, then rebuilds the merged view. */
1245
+ setProvider(provider, entry) {
1246
+ this._providers.set(provider, entry);
1247
+ this._rebuild();
1248
+ }
1249
+ /** Drops a provider from the aggregate, then rebuilds. */
1250
+ removeProvider(provider) {
1251
+ if (this._providers.delete(provider)) this._rebuild();
1252
+ }
1253
+ /** The merged tool list restricted to providers `allow` returns `true` for. */
1254
+ toolsFor(allow) {
1255
+ return this._tools.filter((tool) => {
1256
+ const route = this._toolRoutes.get(tool.name);
1257
+ return route ? allow(route.provider) : false;
1258
+ });
1259
+ }
1260
+ /** The merged prompt list restricted to providers `allow` returns `true` for. */
1261
+ promptsFor(allow) {
1262
+ return this._prompts.filter((prompt) => {
1263
+ const route = this._promptRoutes.get(prompt.name);
1264
+ return route ? allow(route.provider) : false;
1265
+ });
1266
+ }
1267
+ /** Resolves an aggregated tool name to its origin, or `undefined` if unknown. */
1268
+ resolveTool(name) {
1269
+ return this._toolRoutes.get(name);
1270
+ }
1271
+ /** Resolves an aggregated prompt name to its origin, or `undefined` if unknown. */
1272
+ resolvePrompt(name) {
1273
+ return this._promptRoutes.get(name);
1274
+ }
1275
+ _rebuild() {
1276
+ const tools = [];
1277
+ const prompts = [];
1278
+ const toolRoutes = /* @__PURE__ */ new Map();
1279
+ const promptRoutes = /* @__PURE__ */ new Map();
1280
+ for (const [provider, entry] of this._providers) {
1281
+ for (const tool of entry.tools) {
1282
+ const name = unique(capToolName(prefixed(provider, tool.name)), toolRoutes);
1283
+ toolRoutes.set(name, { provider, original: tool.name });
1284
+ tools.push({ ...tool, name, description: tag(provider, tool.description) });
1285
+ }
1286
+ for (const prompt of entry.prompts) {
1287
+ const name = unique(prefixed(provider, prompt.name), promptRoutes);
1288
+ promptRoutes.set(name, { provider, original: prompt.name });
1289
+ prompts.push({ ...prompt, name, description: tag(provider, prompt.description) });
1290
+ }
1291
+ }
1292
+ this._tools = tools;
1293
+ this._prompts = prompts;
1294
+ this._toolRoutes = toolRoutes;
1295
+ this._promptRoutes = promptRoutes;
1296
+ }
1297
+ };
1298
+ function prefixed(provider, original) {
1299
+ return `${provider}${SEPARATOR}${original}`;
1300
+ }
1301
+ function tag(provider, description) {
1302
+ const label = `[${provider}]`;
1303
+ return description ? `${label} ${description}` : label;
1304
+ }
1305
+ function capToolName(name) {
1306
+ if (name.length <= MAX_TOOL_NAME_LENGTH) return name;
1307
+ const hash = djb2(name);
1308
+ return name.slice(0, MAX_TOOL_NAME_LENGTH - hash.length - 1) + SEPARATOR + hash;
1309
+ }
1310
+ function unique(candidate, taken) {
1311
+ if (!taken.has(candidate)) return candidate;
1312
+ let n = 2;
1313
+ while (taken.has(`${candidate}${SEPARATOR}${n}`)) n++;
1314
+ return `${candidate}${SEPARATOR}${n}`;
1315
+ }
1316
+ function djb2(input) {
1317
+ let h = 5381;
1318
+ for (let i = 0; i < input.length; i++) {
1319
+ h = (h << 5) + h + input.charCodeAt(i) >>> 0;
1320
+ }
1321
+ return h.toString(36).padStart(8, "0").slice(-8);
1322
+ }
1323
+
1324
+ // src/broker/aggregate/provider.client.session.ts
1325
+ var PROTOCOL_VERSION = "2024-11-05";
1326
+ var REQUEST_TIMEOUT_MS = 3e4;
1327
+ var ProviderClientSession = class {
1328
+ provider;
1329
+ _client;
1330
+ _idPrefix;
1331
+ _pending = /* @__PURE__ */ new Map();
1332
+ _nextId = 0;
1333
+ _tools = [];
1334
+ _prompts = [];
1335
+ _closed = false;
1336
+ /** Fires after the cached catalog changes (initial load or `list_changed`). */
1337
+ onCatalogChanged = null;
1338
+ /** Fires when the underlying provider slot disconnects. */
1339
+ onClosed = null;
1340
+ constructor(provider, client) {
1341
+ this.provider = provider;
1342
+ this._client = client;
1343
+ this._idPrefix = `agg-${provider}-`;
1344
+ client.onMessage = (data) => this._handleMessage(data);
1345
+ client.onClose = () => {
1346
+ if (this._closed) return;
1347
+ this._rejectAll("provider disconnected");
1348
+ this.onClosed?.();
1349
+ };
1350
+ }
1351
+ get tools() {
1352
+ return this._tools;
1353
+ }
1354
+ get prompts() {
1355
+ return this._prompts;
1356
+ }
1357
+ /** Runs the `initialize` handshake and the first catalog fetch. */
1358
+ async initialize() {
1359
+ await this._request("initialize", {
1360
+ protocolVersion: PROTOCOL_VERSION,
1361
+ capabilities: {},
1362
+ clientInfo: { name: "mcp-broker-aggregate", version: "0" }
1363
+ });
1364
+ this._client.send(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
1365
+ await this._refresh();
1366
+ }
1367
+ /** Forwards a `tools/call` to the provider, relaying the raw outcome. */
1368
+ callTool(name, args) {
1369
+ return this._request("tools/call", { name, arguments: args });
1370
+ }
1371
+ /** Forwards a `prompts/get` to the provider, relaying the raw outcome. */
1372
+ getPrompt(name, args) {
1373
+ return this._request("prompts/get", { name, arguments: args });
1374
+ }
1375
+ /** Detaches the session and its internal client. */
1376
+ close() {
1377
+ if (this._closed) return;
1378
+ this._closed = true;
1379
+ this._rejectAll("session closed");
1380
+ this._client.close();
1381
+ }
1382
+ async _refresh() {
1383
+ this._tools = await this._listAll("tools/list", "tools");
1384
+ this._prompts = await this._listAll("prompts/list", "prompts");
1385
+ this.onCatalogChanged?.();
1386
+ }
1387
+ /**
1388
+ * Calls a list method, following `nextCursor` pagination. A provider that
1389
+ * does not implement the primitive answers with an error, which is treated
1390
+ * as an empty list.
1391
+ */
1392
+ async _listAll(method, key) {
1393
+ const items = [];
1394
+ let cursor;
1395
+ do {
1396
+ const { result, error } = await this._request(method, cursor ? { cursor } : {});
1397
+ if (error) return [];
1398
+ const page = result ?? {};
1399
+ const list = page[key];
1400
+ if (Array.isArray(list)) items.push(...list);
1401
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : void 0;
1402
+ } while (cursor);
1403
+ return items;
1404
+ }
1405
+ _request(method, params) {
1406
+ return new Promise((resolve5) => {
1407
+ if (this._closed) {
1408
+ resolve5({ error: { code: -32e3, message: "session closed" } });
1409
+ return;
1410
+ }
1411
+ const id = this._idPrefix + String(++this._nextId);
1412
+ const timer = setTimeout(() => {
1413
+ this._pending.delete(id);
1414
+ resolve5({ error: { code: -32e3, message: `request "${method}" timed out` } });
1415
+ }, REQUEST_TIMEOUT_MS);
1416
+ this._pending.set(id, { resolve: resolve5, timer });
1417
+ this._client.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
1418
+ });
1419
+ }
1420
+ _handleMessage(data) {
1421
+ let msg;
1422
+ try {
1423
+ msg = JSON.parse(data);
1424
+ } catch {
1425
+ return;
1426
+ }
1427
+ if (typeof msg.id === "string") {
1428
+ const pending = this._pending.get(msg.id);
1429
+ if (pending) {
1430
+ this._pending.delete(msg.id);
1431
+ clearTimeout(pending.timer);
1432
+ pending.resolve({ result: msg.result, error: msg.error });
1433
+ }
1434
+ return;
1435
+ }
1436
+ if (msg.id == null && (msg.method === "notifications/tools/list_changed" || msg.method === "notifications/prompts/list_changed")) {
1437
+ void this._refresh();
1438
+ }
1439
+ }
1440
+ _rejectAll(reason) {
1441
+ for (const pending of this._pending.values()) {
1442
+ clearTimeout(pending.timer);
1443
+ pending.resolve({ error: { code: -32e3, message: reason } });
1444
+ }
1445
+ this._pending.clear();
1446
+ }
1447
+ };
1448
+
1449
+ // src/broker/aggregate/aggregate.server.ts
1450
+ var PROTOCOL_VERSION2 = "2024-11-05";
1451
+ var AggregateServer = class _AggregateServer {
1452
+ /** Reserved provider slot the aggregate is published on. */
1453
+ static SLOT = "_all";
1454
+ _catalog = new AggregateCatalog();
1455
+ _sessions = /* @__PURE__ */ new Map();
1456
+ _openClient;
1457
+ _running = false;
1458
+ /** Per-caller provider visibility filter; `null` ⇒ every caller sees all. */
1459
+ _scopeFilter = null;
1460
+ /** Provider-aware hierarchical authorization, absent in legacy mode. */
1461
+ _authorization = null;
1462
+ onMessage = null;
1463
+ onOpen = null;
1464
+ onClose = null;
1465
+ onError = null;
1466
+ constructor(openClient) {
1467
+ this._openClient = openClient;
1468
+ }
1469
+ get isOpen() {
1470
+ return this._running;
1471
+ }
1472
+ /** Number of providers currently in the aggregate. */
1473
+ get providerCount() {
1474
+ return this._sessions.size;
1475
+ }
1476
+ /** Marks the aggregate transport open. Call before registering it. */
1477
+ start() {
1478
+ this._running = true;
1479
+ }
1480
+ /** Sets the per-caller provider visibility filter. `null` disables filtering. */
1481
+ setScopeFilter(filter) {
1482
+ this._scopeFilter = filter;
1483
+ }
1484
+ /** Sets the compiled hierarchical policy runtime. */
1485
+ setPolicyAuthorization(authorization) {
1486
+ this._authorization = authorization;
1487
+ }
1488
+ /** WsTunnel hands a client request for the `_all` slot here (no principal). */
1489
+ send(data) {
1490
+ void this._handleClientMessage(data, null);
1491
+ }
1492
+ /**
1493
+ * Like {@link send}, but carries the authenticated caller so the aggregate
1494
+ * narrows the catalog and routing to the providers the caller may see.
1495
+ */
1496
+ sendAs(data, principal) {
1497
+ void this._handleClientMessage(data, principal);
1498
+ }
1499
+ /** Closes every provider session and the aggregate transport. */
1500
+ close() {
1501
+ if (!this._running) return;
1502
+ this._running = false;
1503
+ for (const session of this._sessions.values()) session.close();
1504
+ this._sessions.clear();
1505
+ this.onClose?.();
1506
+ }
1507
+ /**
1508
+ * Adds a provider to the aggregate: opens an internal client, runs the
1509
+ * session handshake, and merges the provider's catalog. A no-op for an
1510
+ * already-aggregated provider or the reserved `_all` slot itself.
1511
+ */
1512
+ async addProvider(name) {
1513
+ if (name === _AggregateServer.SLOT || this._sessions.has(name)) return;
1514
+ const session = new ProviderClientSession(name, this._openClient(name));
1515
+ this._sessions.set(name, session);
1516
+ session.onCatalogChanged = () => {
1517
+ this._catalog.setProvider(name, { tools: session.tools, prompts: session.prompts });
1518
+ this._emitListChanged();
1519
+ };
1520
+ session.onClosed = () => this.removeProvider(name);
1521
+ try {
1522
+ await session.initialize();
1523
+ } catch {
1524
+ this.removeProvider(name);
1525
+ }
1526
+ }
1527
+ /** Removes a provider from the aggregate. */
1528
+ removeProvider(name) {
1529
+ const session = this._sessions.get(name);
1530
+ if (!session) return;
1531
+ this._sessions.delete(name);
1532
+ session.close();
1533
+ this._catalog.removeProvider(name);
1534
+ this._emitListChanged();
1535
+ }
1536
+ _subjectFor(principal) {
1537
+ if (principal?.subject) return principal.subject;
1538
+ if (!principal) return { ids: [] };
1539
+ try {
1540
+ return this._authorization?.subjectMapper.map(principal.claims) ?? {
1541
+ ids: [],
1542
+ claims: principal.claims
1543
+ };
1544
+ } catch (error) {
1545
+ if (!(error instanceof SubjectMappingError)) {
1546
+ console.error("[broker] aggregate authorization subject mapping failed.");
1547
+ }
1548
+ return { ids: [], claims: principal.claims };
1549
+ }
1550
+ }
1551
+ _scopeAllows(principal, provider) {
1552
+ const filter = this._scopeFilter;
1553
+ return !filter || !principal || filter(principal, provider);
1554
+ }
1555
+ _policyAllows(principal, provider, operation) {
1556
+ const authorization = this._authorization;
1557
+ if (!authorization) return true;
1558
+ const subject = this._subjectFor(principal);
1559
+ const resource = authorization.slotResourceResolver.resolve(provider);
1560
+ if (!resource) {
1561
+ const decision = { allowed: false, reason: "unknown-resource" };
1562
+ writeAuthorizationAuditEvent(makeAuthorizationAuditEvent({ subject, slot: _AggregateServer.SLOT, provider }, decision));
1563
+ return false;
1564
+ }
1565
+ try {
1566
+ const classified = authorization.capabilityClassifier.classify(operation, resource, provider);
1567
+ if (!classified) return true;
1568
+ const decision = authorization.engine.authorize({
1569
+ subject,
1570
+ capability: classified.capability,
1571
+ resource,
1572
+ provider,
1573
+ tool: classified.tool
1574
+ });
1575
+ if (!decision.allowed || authorization.audit.logAllowed) {
1576
+ writeAuthorizationAuditEvent(
1577
+ makeAuthorizationAuditEvent(
1578
+ {
1579
+ subject,
1580
+ slot: _AggregateServer.SLOT,
1581
+ resource,
1582
+ capability: classified.capability,
1583
+ provider,
1584
+ tool: classified.tool
1585
+ },
1586
+ decision
1587
+ )
1588
+ );
1589
+ }
1590
+ return decision.allowed;
1591
+ } catch {
1592
+ console.error("[broker] aggregate policy evaluation failed.");
1593
+ const decision = { allowed: false, reason: "no-matching-grant" };
1594
+ writeAuthorizationAuditEvent(
1595
+ makeAuthorizationAuditEvent(
1596
+ {
1597
+ subject,
1598
+ slot: _AggregateServer.SLOT,
1599
+ resource,
1600
+ provider
1601
+ },
1602
+ decision
1603
+ )
1604
+ );
1605
+ return false;
1606
+ }
1607
+ }
1608
+ _allows(principal, provider, operation) {
1609
+ return this._scopeAllows(principal, provider) && this._policyAllows(principal, provider, operation);
1610
+ }
1611
+ async _handleClientMessage(data, principal) {
1612
+ let msg;
1613
+ try {
1614
+ msg = JSON.parse(data);
1615
+ } catch {
1616
+ return;
1617
+ }
1618
+ const id = msg.id;
1619
+ if (id == null) return;
1620
+ switch (msg.method) {
1621
+ case "initialize":
1622
+ this._reply(id, {
1623
+ result: {
1624
+ protocolVersion: PROTOCOL_VERSION2,
1625
+ serverInfo: { name: _AggregateServer.SLOT, version: "0" },
1626
+ capabilities: { tools: { listChanged: true }, prompts: { listChanged: true } }
1627
+ }
1628
+ });
1629
+ break;
1630
+ case "ping":
1631
+ this._reply(id, { result: {} });
1632
+ break;
1633
+ case "tools/list":
1634
+ this._reply(id, {
1635
+ result: {
1636
+ tools: this._catalog.toolsFor((provider) => this._allows(principal, provider, { method: "tools/list" }))
1637
+ }
1638
+ });
1639
+ break;
1640
+ case "prompts/list":
1641
+ this._reply(id, {
1642
+ result: {
1643
+ prompts: this._catalog.promptsFor((provider) => this._allows(principal, provider, { method: "prompts/list" }))
1644
+ }
1645
+ });
1646
+ break;
1647
+ case "tools/call":
1648
+ await this._route(id, msg.params, "tool", principal);
1649
+ break;
1650
+ case "prompts/get":
1651
+ await this._route(id, msg.params, "prompt", principal);
1652
+ break;
1653
+ default:
1654
+ this._reply(id, { error: { code: -32601, message: `Method not found: ${msg.method ?? "(none)"}` } });
1655
+ }
1656
+ }
1657
+ async _route(id, params, kind, principal) {
1658
+ const p = params ?? {};
1659
+ const route = p.name ? kind === "tool" ? this._catalog.resolveTool(p.name) : this._catalog.resolvePrompt(p.name) : void 0;
1660
+ const operation = route ? kind === "tool" ? { method: "tools/call", params: { name: route.original } } : { method: "prompts/get", params: { name: route.original } } : void 0;
1661
+ if (!route || !operation || !this._allows(principal, route.provider, operation)) {
1662
+ this._reply(id, { error: { code: -32602, message: `Unknown aggregated ${kind}: ${p.name ?? "(none)"}` } });
1663
+ return;
1664
+ }
1665
+ const session = this._sessions.get(route.provider);
1666
+ if (!session) {
1667
+ this._reply(id, { error: { code: -32e3, message: `Provider "${route.provider}" is no longer connected` } });
1668
+ return;
1669
+ }
1670
+ const args = p.arguments ?? {};
1671
+ const outcome = kind === "tool" ? await session.callTool(route.original, args) : await session.getPrompt(route.original, args);
1672
+ this._reply(id, outcome.error !== void 0 ? { error: outcome.error } : { result: outcome.result });
1673
+ }
1674
+ _reply(id, body) {
1675
+ this.onMessage?.(JSON.stringify({ jsonrpc: "2.0", id, ...body }));
1676
+ }
1677
+ _emitListChanged() {
1678
+ if (!this._running) return;
1679
+ this.onMessage?.(JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }));
1680
+ this.onMessage?.(JSON.stringify({ jsonrpc: "2.0", method: "notifications/prompts/list_changed" }));
1681
+ }
1682
+ };
1683
+ var AuthError = McpAuthError;
1684
+ function scopesOf(claims) {
1685
+ const set = new Set(scopesOf$1(claims));
1686
+ const arrayForm = claims["scopes"];
1687
+ if (Array.isArray(arrayForm)) {
1688
+ for (const scope of arrayForm) if (typeof scope === "string" && scope) set.add(scope);
1689
+ }
1690
+ return set;
1691
+ }
1692
+ var JwtTokenValidator = class {
1693
+ _jwks;
1694
+ _issuer;
1695
+ _clockTolerance;
1696
+ constructor(options) {
1697
+ this._jwks = createRemoteJWKSet(new URL(options.jwksUri));
1698
+ this._issuer = options.issuer;
1699
+ this._clockTolerance = options.clockToleranceSec ?? 0;
1700
+ }
1701
+ async validate(token, resource) {
1702
+ try {
1703
+ const { payload } = await jwtVerify(token, this._jwks, {
1704
+ audience: resource,
1705
+ issuer: this._issuer,
1706
+ clockTolerance: this._clockTolerance
1707
+ });
1708
+ return payload;
1709
+ } catch (err) {
1710
+ if (err instanceof errors.JOSEError) {
1711
+ throw new AuthError(401, "invalid_token", err.message);
1712
+ }
1713
+ throw err;
1714
+ }
1715
+ }
1716
+ };
1717
+ function buildResourceMetadata(params) {
1718
+ return buildProtectedResourceMetadata({
1719
+ resource: params.resource,
1720
+ authorizationServers: params.authorizationServers,
1721
+ scopesSupported: params.scopesSupported
1722
+ });
1723
+ }
1724
+ var PRM_PREFIX = `${PROTECTED_RESOURCE_METADATA_PREFIX}/`;
1725
+ var HttpAuthGuard = class {
1726
+ _auth;
1727
+ /** The `/mcp` suffix without a leading slash, e.g. `"mcp"`. */
1728
+ _mcpSuffix;
1729
+ constructor(auth, mcpSuffix) {
1730
+ this._auth = auth;
1731
+ this._mcpSuffix = mcpSuffix.replace(/^\//, "");
1732
+ }
1733
+ /** Canonical resource identifier for a slot (RFC 8707 §2). */
1734
+ resourceFor(slot) {
1735
+ return `${this._auth.publicBaseUrl}/${encodeURIComponent(slot)}/${this._mcpSuffix}`;
1736
+ }
1737
+ /** RFC 9728 metadata URL for a slot, advertised in the `401` challenge. */
1738
+ metadataUrlFor(slot) {
1739
+ return `${this._auth.publicBaseUrl}${PRM_PREFIX}${encodeURIComponent(slot)}/${this._mcpSuffix}`;
1740
+ }
1741
+ /** Required scope(s) for a slot: per-slot override, else the baseline. */
1742
+ requiredScopesFor(slot) {
1743
+ return this._auth.perSlotScopes?.[slot] ?? this._auth.requiredScopes ?? [];
1744
+ }
1745
+ /** The RFC 9728 metadata document for a slot. */
1746
+ metadataFor(slot) {
1747
+ return buildResourceMetadata({
1748
+ resource: this.resourceFor(slot),
1749
+ authorizationServers: this._auth.authorizationServers,
1750
+ scopesSupported: this._auth.scopesSupported
1751
+ });
1752
+ }
1753
+ /**
1754
+ * If `rawUrl` is a Protected Resource Metadata request
1755
+ * (`/.well-known/oauth-protected-resource/<slot>/<mcp>`), returns the slot;
1756
+ * otherwise `null`.
1757
+ */
1758
+ matchMetadataRequest(rawUrl) {
1759
+ if (!rawUrl.startsWith(PRM_PREFIX)) return null;
1760
+ const parts = rawUrl.slice(PRM_PREFIX.length).split("/").filter(Boolean);
1761
+ if (parts.length !== 2 || parts[1] !== this._mcpSuffix) return null;
1762
+ return decodeURIComponent(parts[0]);
1763
+ }
1764
+ /**
1765
+ * Validates the request's bearer token against the slot's canonical resource
1766
+ * and enforces the slot's required scopes. Resolves to the {@link IPrincipal}
1767
+ * on success; rejects with an {@link AuthError} (`401` missing/invalid token,
1768
+ * `403` insufficient scope) otherwise.
1769
+ */
1770
+ async authorize(req, slot) {
1771
+ const token = bearerToken(req.headers["authorization"]);
1772
+ if (!token) {
1773
+ throw new AuthError(401, "invalid_token", "Missing bearer token");
1774
+ }
1775
+ const claims = await this._auth.validator.validate(token, this.resourceFor(slot));
1776
+ const scopes = scopesOf(claims);
1777
+ const required = this.requiredScopesFor(slot);
1778
+ const missing = required.filter((s) => !scopes.has(s));
1779
+ if (missing.length > 0) {
1780
+ throw new AuthError(403, "insufficient_scope", `Missing required scope(s): ${missing.join(" ")}`, required.join(" "));
1781
+ }
1782
+ let subject;
1783
+ try {
1784
+ subject = this._auth.authorization?.subjectMapper.map(claims);
1785
+ } catch (error) {
1786
+ if (error instanceof SubjectMappingError) {
1787
+ throw new AuthError(403, "insufficient_scope", "Malformed configured JWT subject claim");
1788
+ }
1789
+ throw error;
1790
+ }
1791
+ return subject ? { claims, scopes, subject } : { claims, scopes };
1792
+ }
1793
+ /**
1794
+ * Builds the RFC 9728 §5.1 `WWW-Authenticate` header value for a challenge.
1795
+ * Always points the client at the slot's metadata URL so it can discover the
1796
+ * authorization server and retry. Shared by the HTTP and WebSocket paths.
1797
+ */
1798
+ challengeHeader(slot, err) {
1799
+ return buildChallengeHeader({
1800
+ resourceMetadata: this.metadataUrlFor(slot),
1801
+ error: err.code,
1802
+ errorDescription: err.description,
1803
+ scope: err.scope
1804
+ });
1805
+ }
1806
+ /**
1807
+ * Writes an RFC 9728 §5.1 `WWW-Authenticate` challenge and the matching
1808
+ * status to an HTTP response.
1809
+ */
1810
+ writeChallenge(res, slot, err) {
1811
+ res.setHeader("WWW-Authenticate", this.challengeHeader(slot, err));
1812
+ res.writeHead(err.status, { "Content-Type": "application/json; charset=utf-8" });
1813
+ res.end(JSON.stringify({ error: err.code, error_description: err.description }));
1814
+ }
1815
+ };
1816
+
1817
+ // src/auth/auth.config.ts
1818
+ function makeProviderScopeFilter(providerScopes) {
1819
+ return (principal, providerName) => {
1820
+ const required = providerScopes[providerName];
1821
+ if (!required || required.length === 0) return true;
1822
+ return required.some((scope) => principal.scopes.has(scope));
1823
+ };
1824
+ }
1825
+ function stripTrailingSlash(url) {
1826
+ return url.endsWith("/") ? url.slice(0, -1) : url;
1827
+ }
1828
+ function buildJwtAuth(options) {
1829
+ if (!options.publicBaseUrl) {
1830
+ throw new Error("auth: publicBaseUrl is required.");
1831
+ }
1832
+ if (!options.authorizationServers || options.authorizationServers.length === 0) {
1833
+ throw new Error("auth: at least one authorizationServers entry is required.");
1834
+ }
1835
+ if (!options.jwksUri) {
1836
+ throw new Error("auth: jwksUri is required.");
1837
+ }
1838
+ const publicBaseUrl = stripTrailingSlash(options.publicBaseUrl);
1839
+ const issuer = options.issuer ?? (options.authorizationServers.length === 1 ? options.authorizationServers[0] : void 0);
1840
+ const validator = new JwtTokenValidator({
1841
+ jwksUri: options.jwksUri,
1842
+ issuer,
1843
+ clockToleranceSec: options.clockToleranceSec
1844
+ });
1845
+ const resolved = {
1846
+ publicBaseUrl,
1847
+ authorizationServers: options.authorizationServers,
1848
+ validator
1849
+ };
1850
+ if (options.scopesSupported) resolved.scopesSupported = options.scopesSupported;
1851
+ if (options.requiredScopes) resolved.requiredScopes = options.requiredScopes;
1852
+ if (options.perSlotScopes) resolved.perSlotScopes = options.perSlotScopes;
1853
+ if (options.providerScopes && Object.keys(options.providerScopes).length > 0) {
1854
+ console.warn("[mcp-broker] auth.providerScopes is deprecated; migrate to roles and hierarchical assignments.");
1855
+ resolved.aggregateScopeFilter = makeProviderScopeFilter(options.providerScopes);
1856
+ }
1857
+ if (options.slotResources && Object.keys(options.slotResources).length > 0) {
1858
+ resolved.slotResourceResolver = new DefaultSlotResourceResolver(options.slotResources);
1859
+ }
1860
+ if (hasAuthorizationPolicies(options)) {
1861
+ resolved.authorization = compileAuthorizationPolicy(options);
1862
+ resolved.slotResourceResolver = resolved.authorization.slotResourceResolver;
1863
+ }
1864
+ return resolved;
1865
+ }
1866
+ function normalizeProviderAuthentication(result, fallbackId = "legacy-provider") {
1867
+ if (typeof result === "boolean") {
1868
+ return result ? { authenticated: true, principal: { id: fallbackId, allowedResources: ["**"] } } : { authenticated: false };
1869
+ }
1870
+ if (!result.authenticated) return result;
1871
+ if (!result.principal.id) return { authenticated: false, reason: "provider principal id is empty" };
1872
+ return result;
1873
+ }
1874
+ function providerMayPublish(principal, resource) {
1875
+ const allowed = principal.allowedResources;
1876
+ if (allowed === void 0) return true;
1877
+ if (allowed.length === 0) return false;
1878
+ try {
1879
+ const patterns = allowed.map((pattern) => ResourcePathPattern.parse(pattern));
1880
+ return patterns.some((pattern) => pattern.matches(resource));
1881
+ } catch {
1882
+ return false;
1883
+ }
1884
+ }
1885
+ function safeEqual(a, b) {
1886
+ const ab = Buffer.from(a);
1887
+ const bb = Buffer.from(b);
1888
+ if (ab.length !== bb.length) return false;
1889
+ return timingSafeEqual(ab, bb);
1890
+ }
1891
+ function presentedSecret(req) {
1892
+ const xheader = req.headers["x-provider-token"];
1893
+ if (typeof xheader === "string" && xheader.trim()) return xheader.trim();
1894
+ const auth = req.headers["authorization"];
1895
+ if (typeof auth === "string") {
1896
+ const match = /^Bearer\s+(.+)$/i.exec(auth.trim());
1897
+ if (match) return match[1].trim();
1898
+ }
1899
+ return null;
1900
+ }
1901
+ var SharedSecretProviderAuthenticator = class {
1902
+ _secret;
1903
+ constructor(secret) {
1904
+ if (!secret) {
1905
+ throw new Error("provider auth: a non-empty shared secret is required.");
1906
+ }
1907
+ this._secret = secret;
1908
+ }
1909
+ authenticate(req) {
1910
+ const presented = presentedSecret(req);
1911
+ if (!presented || !safeEqual(presented, this._secret)) {
1912
+ return { authenticated: false };
1913
+ }
1914
+ return {
1915
+ authenticated: true,
1916
+ principal: {
1917
+ id: "shared-secret",
1918
+ allowedResources: ["**"]
1919
+ }
1920
+ };
1921
+ }
1922
+ };
1923
+ var require2 = createRequire(import.meta.url);
1924
+ var pkg = require2("../package.json");
1925
+ var VERSION = pkg.version;
1926
+ var PACKAGE_NAME = pkg.name;
1927
+
1928
+ // src/ws/ws.tunnel.ts
1929
+ var MIME = {
1930
+ ".html": "text/html; charset=utf-8",
1931
+ ".js": "application/javascript; charset=utf-8",
1932
+ ".mjs": "application/javascript; charset=utf-8",
1933
+ ".css": "text/css; charset=utf-8",
1934
+ ".json": "application/json; charset=utf-8",
1935
+ ".map": "application/json; charset=utf-8",
1936
+ ".svg": "image/svg+xml",
1937
+ ".png": "image/png",
1938
+ ".ico": "image/x-icon",
1939
+ ".woff2": "font/woff2",
1940
+ ".woff": "font/woff"
1941
+ };
1942
+ function requestIdOf(frame) {
1943
+ try {
1944
+ const parsed = JSON.parse(frame);
1945
+ if (typeof parsed?.id === "string" || typeof parsed?.id === "number") return parsed.id;
1946
+ } catch {
1947
+ }
1948
+ return void 0;
1949
+ }
1950
+ function originPredicate(allowed) {
1951
+ if (!allowed) return () => false;
1952
+ if (typeof allowed === "function") return allowed;
1953
+ if (allowed instanceof RegExp) {
1954
+ return (origin) => {
1955
+ allowed.lastIndex = 0;
1956
+ return allowed.test(origin);
1957
+ };
1958
+ }
1959
+ const exact = new Set(allowed);
1960
+ return (origin) => exact.has(origin);
1961
+ }
1962
+ function openTransport(transport) {
1963
+ if ("connect" in transport && typeof transport.connect === "function") {
1964
+ transport.connect();
1965
+ }
1966
+ }
1967
+ var WsTunnel = class {
1968
+ _options;
1969
+ _httpServer = null;
1970
+ _wss = null;
1971
+ /**
1972
+ * Per-provider state, keyed by provider name.
1973
+ * Created lazily: a slot is allocated the first time any client references
1974
+ * a provider name, even before the provider WebSocket connects.
1975
+ */
1976
+ _providers = /* @__PURE__ */ new Map();
1977
+ /** Maps a multiplexed WebSocket to the set of provider names it feeds. */
1978
+ _multiplexSockets = /* @__PURE__ */ new Map();
1979
+ /** Upstream providers (stdio child processes and remote URL servers), keyed by name. */
1980
+ _upstreams = /* @__PURE__ */ new Map();
1981
+ /**
1982
+ * In-process loopback transports registered as provider slots.
1983
+ * Used by the embedded broker server (`_broker`) and any other component
1984
+ * that wants to expose itself as a provider without going through a network.
1985
+ */
1986
+ _loopbackProviders = /* @__PURE__ */ new Map();
1987
+ /** The embedded broker MCP server, when {@link IWsTunnelOptions.enableBrokerProvider} is on. */
1988
+ _brokerServer = null;
1989
+ /** The aggregate MCP server (`_all` slot), when {@link IWsTunnelOptions.enableAggregateProvider} is on. */
1990
+ _aggregateServer = null;
1991
+ /** Provider name that the stdio client transport is bridged to, or null when disabled. */
1992
+ _stdioClientProvider = null;
1993
+ /** mcp-core transport connected to the broker process stdin/stdout. */
1994
+ _stdioClientTransport = null;
1995
+ /** Timestamp of the most recent successful `start()`. */
1996
+ _startedAt = null;
1997
+ /** HTTP resource-server enforcement point, or `null` when auth is disabled. */
1998
+ _authGuard;
1999
+ /** Provider (engine) authenticator, or `null` when provider auth is disabled. */
2000
+ _providerAuth;
2001
+ /** Compiled hierarchical authorization, or `null` for legacy behavior. */
2002
+ _authorization;
2003
+ /** Stable technical-slot to hierarchical-resource mapping. */
2004
+ _slotResourceResolver;
2005
+ /** Origin check applied by every slot's Streamable HTTP endpoint. */
2006
+ _allowedOrigins;
2007
+ /** Principal captured at a client's WS upgrade, keyed by the upgrade request. */
2008
+ _pendingClientPrincipals = /* @__PURE__ */ new WeakMap();
2009
+ /** Authenticated principal per raw WS client socket, for `_all` scope filtering. */
2010
+ _clientPrincipals = /* @__PURE__ */ new WeakMap();
2011
+ /** Authenticated principal attached to long-lived HTTP/SSE streams. */
2012
+ _streamPrincipals = /* @__PURE__ */ new WeakMap();
2013
+ /** Provider principals captured during successful WebSocket upgrades. */
2014
+ _pendingProviderPrincipals = /* @__PURE__ */ new WeakMap();
2015
+ _providerPrincipals = /* @__PURE__ */ new WeakMap();
2016
+ constructor(options) {
2017
+ this._options = options;
2018
+ this._authGuard = options.auth ? new HttpAuthGuard(options.auth, options.mcpPath ?? "/mcp") : null;
2019
+ this._providerAuth = options.providerAuth ?? null;
2020
+ this._allowedOrigins = originPredicate(options.allowedOrigins);
2021
+ this._authorization = options.authorization ?? options.auth?.authorization ?? null;
2022
+ this._slotResourceResolver = options.slotResourceResolver ?? this._authorization?.slotResourceResolver ?? options.auth?.slotResourceResolver ?? new DefaultSlotResourceResolver();
2023
+ }
2024
+ // -------------------------------------------------------------------------
2025
+ // IBrokerContext implementation
2026
+ // -------------------------------------------------------------------------
2027
+ get version() {
2028
+ return VERSION;
2029
+ }
2030
+ get name() {
2031
+ return this._options.brokerName ?? PACKAGE_NAME;
2032
+ }
2033
+ get startedAt() {
2034
+ return this._startedAt;
2035
+ }
2036
+ get uptimeSeconds() {
2037
+ if (!this._startedAt) return 0;
2038
+ return Math.floor((Date.now() - this._startedAt.getTime()) / 1e3);
2039
+ }
2040
+ get host() {
2041
+ return this._options.host;
2042
+ }
2043
+ get port() {
2044
+ return this._options.port;
2045
+ }
2046
+ get tls() {
2047
+ return !!this._options.tls;
2048
+ }
2049
+ get paths() {
2050
+ const o = this._options;
2051
+ return {
2052
+ provider: o.providerPath ?? "/provider",
2053
+ providers: o.providersPath ?? "/providers",
2054
+ client: o.clientPath ?? "/",
2055
+ mcp: o.mcpPath ?? "/mcp",
2056
+ sse: o.ssePath ?? "/sse",
2057
+ messages: o.messagesPath ?? "/messages"
2058
+ };
2059
+ }
2060
+ getProvidersInfo() {
2061
+ const out = [];
2062
+ for (const [name, state] of this._providers) {
2063
+ out.push(this._buildProviderInfo(name, state));
2064
+ }
2065
+ return out;
2066
+ }
2067
+ getProviderInfo(name) {
2068
+ const state = this._providers.get(name);
2069
+ if (!state) return void 0;
2070
+ return this._buildProviderInfo(name, state);
2071
+ }
2072
+ _buildProviderInfo(name, state) {
2073
+ let transport;
2074
+ let connected;
2075
+ if (this._loopbackProviders.get(name)?.isOpen) {
2076
+ transport = "loopback";
2077
+ connected = true;
2078
+ } else if (this._upstreams.get(name)?.isOpen) {
2079
+ transport = "stdio";
2080
+ connected = true;
2081
+ } else if (state.ws?.readyState === WebSocket.OPEN) {
2082
+ transport = this._multiplexSockets.has(state.ws) ? "ws-multiplex" : "ws";
2083
+ connected = true;
2084
+ } else {
2085
+ transport = "none";
2086
+ connected = false;
2087
+ }
2088
+ return {
2089
+ name,
2090
+ transport,
2091
+ connected,
2092
+ clientCount: state.wsClients.size,
2093
+ sessionCount: state.sseSessions.size + state.httpSessions.size,
2094
+ pendingCount: state.pending.size
2095
+ };
2096
+ }
2097
+ // -------------------------------------------------------------------------
2098
+ // Loopback provider registration (in-process transports)
2099
+ // -------------------------------------------------------------------------
2100
+ /**
2101
+ * Registers an in-process transport as a provider slot. Used by the embedded
2102
+ * broker server and may be used by application code that wants to host an
2103
+ * MCP server inside the same process without opening a real WebSocket.
2104
+ *
2105
+ * @throws if the name is already used by a stdio upstream or another loopback.
2106
+ */
2107
+ registerLoopbackProvider(name, transport) {
2108
+ if (this._loopbackProviders.has(name)) {
2109
+ throw new Error(`Loopback provider "${name}" is already registered.`);
2110
+ }
2111
+ if (this._upstreams.has(name)) {
2112
+ throw new Error(`Cannot register loopback "${name}": a stdio upstream with the same name already exists.`);
2113
+ }
2114
+ const state = this._getOrCreateProviderState(name);
2115
+ this._loopbackProviders.set(name, transport);
2116
+ transport.onMessage = (data) => this._routeFromProvider(state, name, data);
2117
+ transport.onClose = () => {
2118
+ this._loopbackProviders.delete(name);
2119
+ this._failProviderDisconnected(state, name);
2120
+ };
2121
+ }
2122
+ /**
2123
+ * Opens an in-process client to a provider slot. The returned handle can
2124
+ * issue MCP requests and receives both the responses and the provider's
2125
+ * broadcast notifications. Used by the aggregate server to fan a single
2126
+ * in-process client out to every aggregated provider.
2127
+ *
2128
+ * The slot does not need a provider attached yet, `send` returns a
2129
+ * JSON-RPC error while the provider is disconnected.
2130
+ */
2131
+ openInternalClient(providerName) {
2132
+ const state = this._getOrCreateProviderState(providerName);
2133
+ let closed = false;
2134
+ const client = {
2135
+ onMessage: null,
2136
+ onClose: null,
2137
+ send: (message) => {
2138
+ if (closed) return;
2139
+ let id = null;
2140
+ try {
2141
+ const parsed = JSON.parse(message);
2142
+ if (parsed?.id != null) id = parsed.id;
2143
+ } catch {
2144
+ }
2145
+ if (this._isProviderConnected(providerName, state)) {
2146
+ if (id != null) state.pending.set(id, { type: "internal", client });
2147
+ this._sendToProvider(state, providerName, message);
2148
+ } else if (id != null) {
2149
+ client.onMessage?.(
2150
+ JSON.stringify({
2151
+ jsonrpc: "2.0",
2152
+ id,
2153
+ error: { code: -32e3, message: `Provider "${providerName}" not connected` }
2154
+ })
2155
+ );
2156
+ }
2157
+ },
2158
+ close: () => {
2159
+ if (closed) return;
2160
+ closed = true;
2161
+ state.internalClients.delete(client);
2162
+ for (const [id, sink] of state.pending) {
2163
+ if (sink.type === "internal" && sink.client === client) state.pending.delete(id);
2164
+ }
2165
+ }
2166
+ };
2167
+ state.internalClients.add(client);
2168
+ return client;
2169
+ }
2170
+ // -------------------------------------------------------------------------
2171
+ // Public state
2172
+ // -------------------------------------------------------------------------
2173
+ get isListening() {
2174
+ return this._httpServer?.listening ?? false;
2175
+ }
2176
+ /** Total number of connected MCP clients across all providers. */
2177
+ get clientCount() {
2178
+ let n = 0;
2179
+ for (const s of this._providers.values()) {
2180
+ n += s.wsClients.size + s.sseSessions.size + s.httpSessions.size;
2181
+ }
2182
+ return n;
2183
+ }
2184
+ /** Names of all providers that currently have an active connection. */
2185
+ get providerNames() {
2186
+ return [...this._providers.entries()].filter(([name, s]) => this._isProviderConnected(name, s)).map(([name]) => name);
2187
+ }
2188
+ /** @deprecated Check `providerNames.length > 0` instead. */
2189
+ get hasProvider() {
2190
+ return this.providerNames.length > 0;
2191
+ }
2192
+ // -------------------------------------------------------------------------
2193
+ // Lifecycle
2194
+ // -------------------------------------------------------------------------
2195
+ /**
2196
+ * Starts the broker. Resolves once the HTTP server is listening.
2197
+ */
2198
+ start() {
2199
+ return new Promise((resolve5) => {
2200
+ const handler = (req, res) => this._handleHttp(req, res);
2201
+ this._httpServer = this._options.tls ? https.createServer({ cert: this._options.tls.cert, key: this._options.tls.key }, handler) : http.createServer(handler);
2202
+ this._wss = new WebSocketServer({ server: this._httpServer, perMessageDeflate: false, verifyClient: this._makeVerifyClient() });
2203
+ this._wss.on("connection", (ws, req) => {
2204
+ const url = req.url ?? "/";
2205
+ const providerPath = this._options.providerPath ?? "/provider";
2206
+ const providersPath = this._options.providersPath ?? "/providers";
2207
+ if (url === providersPath || url.startsWith(providersPath + "?")) {
2208
+ this._onMultiplexProviderConnect(ws, req);
2209
+ } else if (url.startsWith(providerPath + "/") || url === providerPath) {
2210
+ const raw = url.slice(providerPath.length).replace(/^\//, "");
2211
+ const name = decodeURIComponent(raw.split("?")[0]) || "(unnamed)";
2212
+ this._onProviderConnect(ws, name, req);
2213
+ } else {
2214
+ const raw = url.replace(/^\//, "").split("?")[0];
2215
+ const name = decodeURIComponent(raw) || "";
2216
+ this._onClientConnect(ws, name, req);
2217
+ }
2218
+ });
2219
+ this._httpServer.listen(this._options.port, this._options.host ?? "0.0.0.0", () => {
2220
+ this._maybeStartAggregateServer();
2221
+ const wireUpstream = (cfg, upstream) => {
2222
+ upstream.onMessage = (data) => {
2223
+ const state = this._getOrCreateProviderState(cfg.name);
2224
+ this._routeFromProvider(state, cfg.name, data);
2225
+ };
2226
+ upstream.onError = (err) => {
2227
+ console.error(`[broker] ${err.message}`);
2228
+ };
2229
+ upstream.onClose = () => {
2230
+ const state = this._providers.get(cfg.name);
2231
+ if (state) this._failProviderDisconnected(state, cfg.name);
2232
+ };
2233
+ if (cfg.aggregate) {
2234
+ upstream.onOpen = () => void this._aggregateServer?.addProvider(cfg.name);
2235
+ }
2236
+ this._upstreams.set(cfg.name, upstream);
2237
+ upstream.connect();
2238
+ };
2239
+ for (const cfg of this._options.stdioUpstreams ?? []) wireUpstream(cfg, new StdioUpstream(cfg));
2240
+ for (const cfg of this._options.remoteUpstreams ?? []) wireUpstream(cfg, new RemoteUpstream(cfg));
2241
+ if (this._options.stdioClient) {
2242
+ this._stdioClientProvider = this._options.stdioClient.providerName;
2243
+ const transport = new StdioTransport();
2244
+ this._stdioClientTransport = transport;
2245
+ transport.onMessage = (data) => {
2246
+ const state = this._getOrCreateProviderState(this._stdioClientProvider);
2247
+ this._routeFromStdioClient(state, data);
2248
+ };
2249
+ transport.onError = (err) => {
2250
+ console.error(`[broker] stdio client transport: ${err.message}`);
2251
+ };
2252
+ transport.onClose = () => {
2253
+ };
2254
+ transport.connect();
2255
+ }
2256
+ this._startedAt = /* @__PURE__ */ new Date();
2257
+ void this._maybeStartBrokerServer().then(
2258
+ () => resolve5(),
2259
+ (err) => {
2260
+ console.error("[broker] embedded broker server failed to start:", err);
2261
+ resolve5();
2262
+ }
2263
+ );
2264
+ });
2265
+ });
2266
+ }
2267
+ /**
2268
+ * Starts the in-process MCP server that exposes the broker's own behaviors
2269
+ * (`broker_info`, `providers_list`, `provider_status`) under the reserved
2270
+ * provider slot `_broker`. No-op when {@link IWsTunnelOptions.enableBrokerProvider}
2271
+ * is `false`.
2272
+ */
2273
+ async _maybeStartBrokerServer() {
2274
+ if (this._options.enableBrokerProvider === false) return;
2275
+ const { server, clientTransport } = await startBrokerServer(this, {
2276
+ grammarResolverOptions: this._options.brokerGrammarResolverOptions,
2277
+ localGrammarsDir: this._options.brokerLocalGrammarsDir
2278
+ });
2279
+ this._brokerServer = server;
2280
+ this.registerLoopbackProvider(BROKER_PROVIDER_NAME, clientTransport);
2281
+ void this._aggregateServer?.addProvider(BROKER_PROVIDER_NAME);
2282
+ }
2283
+ /**
2284
+ * Starts the aggregate MCP server and registers it on the reserved `_all`
2285
+ * slot. No-op when {@link IWsTunnelOptions.enableAggregateProvider} is `false`.
2286
+ */
2287
+ _maybeStartAggregateServer() {
2288
+ if (this._options.enableAggregateProvider === false) return;
2289
+ try {
2290
+ const server = new AggregateServer((providerName) => this.openInternalClient(providerName));
2291
+ server.setScopeFilter(this._options.auth?.aggregateScopeFilter ?? null);
2292
+ server.setPolicyAuthorization(this._authorization);
2293
+ server.start();
2294
+ this.registerLoopbackProvider(AggregateServer.SLOT, server);
2295
+ this._aggregateServer = server;
2296
+ } catch (err) {
2297
+ console.error(`[broker] aggregate server failed to start: ${err.message}`);
2298
+ }
2299
+ }
2300
+ /**
2301
+ * Gracefully closes all connections and stops the HTTP server.
2302
+ */
2303
+ async stop() {
2304
+ const brokerServer = this._brokerServer;
2305
+ this._brokerServer = null;
2306
+ if (brokerServer) {
2307
+ try {
2308
+ await brokerServer.stop();
2309
+ } catch {
2310
+ }
2311
+ }
2312
+ const aggregateServer = this._aggregateServer;
2313
+ this._aggregateServer = null;
2314
+ if (aggregateServer) {
2315
+ try {
2316
+ aggregateServer.close();
2317
+ } catch {
2318
+ }
2319
+ }
2320
+ this._stdioClientTransport?.close();
2321
+ this._stdioClientTransport = null;
2322
+ this._stdioClientProvider = null;
2323
+ await Promise.all([...this._providers.values()].map((state) => state.httpEndpoint?.closeAll() ?? Promise.resolve()));
2324
+ return new Promise((resolve5, reject) => {
2325
+ for (const state of this._providers.values()) {
2326
+ for (const res of state.sseSessions.values()) res.end();
2327
+ state.sseSessions.clear();
2328
+ state.httpSessions.clear();
2329
+ state.httpEndpoint = null;
2330
+ for (const client of state.wsClients) client.close();
2331
+ state.wsClients.clear();
2332
+ state.ws?.close();
2333
+ }
2334
+ this._providers.clear();
2335
+ this._multiplexSockets.clear();
2336
+ for (const upstream of this._upstreams.values()) upstream.close();
2337
+ this._upstreams.clear();
2338
+ for (const loopback of this._loopbackProviders.values()) loopback.close();
2339
+ this._loopbackProviders.clear();
2340
+ this._startedAt = null;
2341
+ this._wss?.close();
2342
+ this._httpServer?.close((err) => err ? reject(err) : resolve5());
2343
+ });
2344
+ }
2345
+ _authorizationSubject(principal) {
2346
+ if (principal?.subject) return principal.subject;
2347
+ if (!principal) return { ids: [] };
2348
+ try {
2349
+ return this._authorization?.subjectMapper.map(principal.claims) ?? {
2350
+ ids: [],
2351
+ claims: principal.claims
2352
+ };
2353
+ } catch (error) {
2354
+ if (!(error instanceof SubjectMappingError)) {
2355
+ console.error("[broker] authorization subject mapping failed.");
2356
+ }
2357
+ return { ids: [], claims: principal.claims };
2358
+ }
2359
+ }
2360
+ _auditDecision(subject, slot, resource, capability, tool, decision) {
2361
+ const authorization = this._authorization;
2362
+ if (!authorization || decision.allowed && !authorization.audit.logAllowed) return;
2363
+ writeAuthorizationAuditEvent(
2364
+ makeAuthorizationAuditEvent(
2365
+ {
2366
+ subject,
2367
+ slot,
2368
+ resource,
2369
+ capability,
2370
+ provider: slot,
2371
+ tool
2372
+ },
2373
+ decision
2374
+ )
2375
+ );
2376
+ }
2377
+ /**
2378
+ * Applies hierarchical policy to the MCP operations carried in one
2379
+ * JSON-RPC frame. `_all` performs provider-specific checks internally.
2380
+ */
2381
+ _authorizeMcpFrame(providerName, data, principal) {
2382
+ const authorization = this._authorization;
2383
+ if (!authorization || providerName === AggregateServer.SLOT) return true;
2384
+ const subject = this._authorizationSubject(principal);
2385
+ const resource = this._slotResourceResolver.resolve(providerName);
2386
+ if (!resource) {
2387
+ const decision = { allowed: false, reason: "unknown-resource" };
2388
+ this._auditDecision(subject, providerName, void 0, void 0, void 0, decision);
2389
+ return false;
2390
+ }
2391
+ let parsed;
2392
+ try {
2393
+ parsed = JSON.parse(data);
2394
+ } catch {
2395
+ return true;
2396
+ }
2397
+ const operations = Array.isArray(parsed) ? parsed : [parsed];
2398
+ for (const value of operations) {
2399
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
2400
+ const operation = value;
2401
+ try {
2402
+ const classified = authorization.capabilityClassifier.classify(operation, resource, providerName);
2403
+ if (!classified) continue;
2404
+ const decision = authorization.engine.authorize({
2405
+ subject,
2406
+ capability: classified.capability,
2407
+ resource,
2408
+ provider: providerName,
2409
+ tool: classified.tool
2410
+ });
2411
+ this._auditDecision(subject, providerName, resource, classified.capability, classified.tool, decision);
2412
+ if (!decision.allowed) return false;
2413
+ } catch {
2414
+ console.error("[broker] policy evaluation failed.");
2415
+ const decision = { allowed: false, reason: "no-matching-grant" };
2416
+ this._auditDecision(subject, providerName, resource, void 0, void 0, decision);
2417
+ return false;
2418
+ }
2419
+ }
2420
+ return true;
2421
+ }
2422
+ _policyDeniedPayload(data) {
2423
+ let id = null;
2424
+ try {
2425
+ const parsed = JSON.parse(data);
2426
+ if (typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null) {
2427
+ id = parsed.id;
2428
+ }
2429
+ } catch {
2430
+ }
2431
+ return JSON.stringify({
2432
+ jsonrpc: "2.0",
2433
+ id,
2434
+ error: { code: -32001, message: "Forbidden" }
2435
+ });
2436
+ }
2437
+ /** The JSON-RPC error returned when the slot has nobody behind it. */
2438
+ _notConnectedPayload(providerName, frame) {
2439
+ return JSON.stringify({
2440
+ jsonrpc: "2.0",
2441
+ id: requestIdOf(frame) ?? null,
2442
+ error: { code: -32e3, message: `Provider "${providerName}" not connected` }
2443
+ });
2444
+ }
2445
+ // -------------------------------------------------------------------------
2446
+ // HTTP dispatcher
2447
+ // -------------------------------------------------------------------------
2448
+ _handleHttp(req, res) {
2449
+ const method = req.method ?? "GET";
2450
+ const rawUrl = (req.url ?? "/").split("?")[0];
2451
+ res.setHeader("Access-Control-Allow-Origin", "*");
2452
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
2453
+ res.setHeader("Access-Control-Allow-Headers", req.headers["access-control-request-headers"] ?? "Content-Type, Accept, Mcp-Session-Id");
2454
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
2455
+ if (method === "OPTIONS") {
2456
+ res.writeHead(204);
2457
+ res.end();
2458
+ return;
2459
+ }
2460
+ const samplesIndexPath = this._options.samplesIndexPath ?? "/__samples_index__";
2461
+ if (method === "GET" && rawUrl === samplesIndexPath) {
2462
+ this._handleSamplesIndex(res);
2463
+ return;
2464
+ }
2465
+ if (this._authGuard && method === "GET") {
2466
+ const metaSlot = this._authGuard.matchMetadataRequest(rawUrl);
2467
+ if (metaSlot) {
2468
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
2469
+ res.end(JSON.stringify(this._authGuard.metadataFor(metaSlot)));
2470
+ return;
2471
+ }
2472
+ }
2473
+ const route = this._parseProviderRoute(rawUrl);
2474
+ if (route) {
2475
+ const { providerName, endpoint } = route;
2476
+ const kind = this._mcpEndpointKind(endpoint, method);
2477
+ if (kind) {
2478
+ if (this._authGuard) {
2479
+ const guard = this._authGuard;
2480
+ void guard.authorize(req, providerName).then(
2481
+ (principal) => this._dispatchMcpEndpoint(kind, req, res, providerName, principal),
2482
+ (err) => this._handleAuthFailure(guard, res, providerName, err)
2483
+ );
2484
+ } else {
2485
+ this._dispatchMcpEndpoint(kind, req, res, providerName, null);
2486
+ }
2487
+ return;
2488
+ }
2489
+ }
2490
+ if (this._options.staticMounts?.length) {
2491
+ this._serveStatic(req, res);
2492
+ } else {
2493
+ res.writeHead(404);
2494
+ res.end();
2495
+ }
2496
+ }
2497
+ /**
2498
+ * Parses `/<providerName>/<endpoint>` from a URL path.
2499
+ * Returns `null` if the URL does not match this two-segment pattern.
2500
+ */
2501
+ _parseProviderRoute(rawUrl) {
2502
+ const parts = rawUrl.split("/").filter(Boolean);
2503
+ if (parts.length !== 2) return null;
2504
+ const providerName = decodeURIComponent(parts[0]);
2505
+ const endpoint = decodeURIComponent(parts[1]);
2506
+ if (!providerName || !endpoint) return null;
2507
+ return { providerName, endpoint };
2508
+ }
2509
+ /**
2510
+ * Classifies a `(endpoint, method)` pair as one of the four MCP/SSE client
2511
+ * handlers, or `null` when it is not a client transport request (the caller
2512
+ * then falls through to static-file serving, preserving prior behavior).
2513
+ */
2514
+ _mcpEndpointKind(endpoint, method) {
2515
+ const mcpSuffix = (this._options.mcpPath ?? "/mcp").replace(/^\//, "");
2516
+ const sseSuffix = (this._options.ssePath ?? "/sse").replace(/^\//, "");
2517
+ const messagesSuffix = (this._options.messagesPath ?? "/messages").replace(/^\//, "");
2518
+ if (endpoint === mcpSuffix) return "mcp";
2519
+ if (endpoint === sseSuffix && method === "GET") return "sse-connect";
2520
+ if (endpoint === messagesSuffix && method === "POST") return "sse-message";
2521
+ return null;
2522
+ }
2523
+ /** Routes an already-authorized (or auth-disabled) request to its handler. */
2524
+ _dispatchMcpEndpoint(kind, req, res, providerName, principal) {
2525
+ switch (kind) {
2526
+ case "mcp":
2527
+ this._handleStreamableHttp(req, res, providerName, principal);
2528
+ return;
2529
+ case "sse-connect":
2530
+ this._handleSseConnect(req, res, providerName, principal);
2531
+ return;
2532
+ case "sse-message":
2533
+ this._handleSseMessage(req, res, providerName, principal);
2534
+ return;
2535
+ }
2536
+ }
2537
+ /** Turns a rejected {@link HttpAuthGuard.authorize} into an HTTP response. */
2538
+ _handleAuthFailure(guard, res, providerName, err) {
2539
+ if (err instanceof AuthError) {
2540
+ guard.writeChallenge(res, providerName, err);
2541
+ return;
2542
+ }
2543
+ console.error(`[broker] token validation error for "${providerName}":`, err);
2544
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
2545
+ res.end(JSON.stringify({ error: "server_error" }));
2546
+ }
2547
+ /**
2548
+ * Builds the `ws` verifyClient hook that authenticates every WebSocket
2549
+ * upgrade before the connection is accepted, returning a real `401`/`403`
2550
+ * during the handshake rather than a post-handshake close:
2551
+ *
2552
+ * - **Raw MCP clients** (`/<slot>`) are gated by the OAuth 2.1 resource
2553
+ * server ({@link _authGuard}) with the RFC 9728 `WWW-Authenticate` challenge.
2554
+ * - **Providers** (`/provider/<slot>`, `/providers`) are gated by the
2555
+ * {@link _providerAuth} shared-secret / custom authenticator.
2556
+ *
2557
+ * Each side is independent: a branch with no authenticator configured is let
2558
+ * through unchanged. Returns `undefined` (no hook) when neither is set.
2559
+ */
2560
+ _makeVerifyClient() {
2561
+ const guard = this._authGuard;
2562
+ const providerAuth = this._providerAuth;
2563
+ if (!guard && !providerAuth) return void 0;
2564
+ const providerPath = this._options.providerPath ?? "/provider";
2565
+ const providersPath = this._options.providersPath ?? "/providers";
2566
+ return (info, cb) => {
2567
+ const url = info.req.url ?? "/";
2568
+ const isMultiplex = url === providersPath || url.startsWith(providersPath + "?");
2569
+ const isDedicated = url.startsWith(providerPath + "/") || url === providerPath;
2570
+ if (isMultiplex || isDedicated) {
2571
+ if (!providerAuth) {
2572
+ cb(true);
2573
+ return;
2574
+ }
2575
+ const slot2 = isDedicated ? decodeURIComponent(url.slice(providerPath.length).replace(/^\//, "").split("?")[0]) || void 0 : void 0;
2576
+ Promise.resolve(providerAuth.authenticate(info.req, slot2)).then(
2577
+ (rawResult) => {
2578
+ const result = normalizeProviderAuthentication(rawResult);
2579
+ if (!result.authenticated) {
2580
+ cb(false, 401, "Unauthorized", { "WWW-Authenticate": 'Bearer realm="provider"' });
2581
+ return;
2582
+ }
2583
+ if (slot2 !== void 0) {
2584
+ const resource = this._slotResourceResolver.resolve(slot2);
2585
+ if (!resource || !providerMayPublish(result.principal, resource)) {
2586
+ this._logProviderRegistration(result.principal, slot2, resource, false);
2587
+ cb(false, 403, "Forbidden");
2588
+ return;
2589
+ }
2590
+ }
2591
+ this._pendingProviderPrincipals.set(info.req, result.principal);
2592
+ cb(true);
2593
+ },
2594
+ () => {
2595
+ console.error(`[broker] provider authentication error for "${slot2 ?? "(multiplex)"}".`);
2596
+ cb(false, 500, "server_error");
2597
+ }
2598
+ );
2599
+ return;
2600
+ }
2601
+ if (!guard) {
2602
+ cb(true);
2603
+ return;
2604
+ }
2605
+ const slot = decodeURIComponent(url.replace(/^\//, "").split("?")[0]);
2606
+ void guard.authorize(info.req, slot).then(
2607
+ (principal) => {
2608
+ this._pendingClientPrincipals.set(info.req, principal);
2609
+ cb(true);
2610
+ },
2611
+ (err) => {
2612
+ if (err instanceof AuthError) {
2613
+ cb(false, err.status, err.code, { "WWW-Authenticate": guard.challengeHeader(slot, err) });
2614
+ } else {
2615
+ console.error(`[broker] token validation error for WS client "${slot}":`, err);
2616
+ cb(false, 500, "server_error");
2617
+ }
2618
+ }
2619
+ );
2620
+ };
2621
+ }
2622
+ // -------------------------------------------------------------------------
2623
+ // MCP / SSE transport (per provider)
2624
+ // -------------------------------------------------------------------------
2625
+ /**
2626
+ * Handles `GET /<providerName>/sse`, opens a long-lived SSE stream for Claude.
2627
+ * Sends an `endpoint` event so Claude knows where to POST its requests.
2628
+ */
2629
+ _handleSseConnect(req, res, providerName, principal) {
2630
+ const sessionId = randomUUID();
2631
+ const messagesSuffix = (this._options.messagesPath ?? "/messages").replace(/^\//, "");
2632
+ const messagesUrl = `/${encodeURIComponent(providerName)}/${messagesSuffix}`;
2633
+ res.writeHead(200, {
2634
+ "Content-Type": "text/event-stream",
2635
+ "Cache-Control": "no-cache, no-transform",
2636
+ Connection: "keep-alive"
2637
+ });
2638
+ res.write(`event: endpoint
2639
+ data: ${messagesUrl}?sessionId=${sessionId}
2640
+
2641
+ `);
2642
+ const state = this._getOrCreateProviderState(providerName);
2643
+ state.sseSessions.set(sessionId, res);
2644
+ if (principal) this._streamPrincipals.set(res, principal);
2645
+ req.on("close", () => {
2646
+ state.sseSessions.delete(sessionId);
2647
+ for (const [id, sink] of state.pending) {
2648
+ if (sink.type === "sse" && sink.sessionId === sessionId) state.pending.delete(id);
2649
+ }
2650
+ });
2651
+ }
2652
+ /**
2653
+ * Handles `POST /<providerName>/messages?sessionId=…`, receives a JSON-RPC
2654
+ * request from Claude and forwards it to the provider.
2655
+ * Always responds 202 Accepted; the real response arrives over SSE.
2656
+ */
2657
+ _handleSseMessage(req, res, providerName, principal) {
2658
+ const params = new URL(req.url ?? "", "http://localhost").searchParams;
2659
+ const sessionId = params.get("sessionId") ?? "";
2660
+ const state = this._getOrCreateProviderState(providerName);
2661
+ if (!state.sseSessions.has(sessionId)) {
2662
+ res.writeHead(400, { "Content-Type": "text/plain" });
2663
+ res.end("Unknown or expired session");
2664
+ return;
2665
+ }
2666
+ let body = "";
2667
+ req.on("data", (chunk) => {
2668
+ body += chunk.toString();
2669
+ });
2670
+ req.on("end", () => {
2671
+ if (!this._authorizeMcpFrame(providerName, body, principal)) {
2672
+ res.writeHead(403, { "Content-Type": "application/json; charset=utf-8" });
2673
+ res.end(this._policyDeniedPayload(body));
2674
+ return;
2675
+ }
2676
+ try {
2677
+ const msg = JSON.parse(body);
2678
+ if (msg.id != null) state.pending.set(msg.id, { type: "sse", sessionId });
2679
+ } catch {
2680
+ }
2681
+ if (this._isProviderConnected(providerName, state)) {
2682
+ this._sendToProvider(state, providerName, body, principal);
2683
+ } else {
2684
+ const sseRes = state.sseSessions.get(sessionId);
2685
+ if (sseRes) {
2686
+ let errId = null;
2687
+ try {
2688
+ errId = JSON.parse(body).id ?? null;
2689
+ } catch {
2690
+ }
2691
+ this._sendSseEvent(
2692
+ sseRes,
2693
+ JSON.stringify({
2694
+ jsonrpc: "2.0",
2695
+ id: errId,
2696
+ error: { code: -32e3, message: `Provider "${providerName}" not connected` }
2697
+ })
2698
+ );
2699
+ }
2700
+ }
2701
+ res.writeHead(202);
2702
+ res.end();
2703
+ });
2704
+ }
2705
+ /**
2706
+ * Serves `/<providerName>/mcp` by handing the request to the slot's
2707
+ * Streamable HTTP endpoint.
2708
+ *
2709
+ * Everything protocol-shaped, sessions, `Mcp-Session-Id`, `DELETE`, the
2710
+ * `404` on a terminated session, `Origin` and `MCP-Protocol-Version`
2711
+ * validation, `202` on a notification, belongs to `mcp-core` and is no
2712
+ * longer reimplemented here. What stays is the broker's own business:
2713
+ * deciding who may speak (already done upstream by the auth guard) and
2714
+ * relaying frames to a provider that lives somewhere else entirely.
2715
+ */
2716
+ _handleStreamableHttp(req, res, providerName, principal) {
2717
+ const state = this._getOrCreateProviderState(providerName);
2718
+ const sessionId = req.headers["mcp-session-id"];
2719
+ if (typeof sessionId === "string") {
2720
+ const session = state.httpSessions.get(sessionId);
2721
+ if (session) session.principal = principal;
2722
+ }
2723
+ void this._endpointFor(providerName, state).handleRequest(req, res);
2724
+ }
2725
+ /**
2726
+ * The slot's Streamable HTTP endpoint, built on first use.
2727
+ *
2728
+ * Its factory does not create an MCP server: the server is the provider,
2729
+ * reachable only through the tunnel. It creates a bridge instead: frames the
2730
+ * client sends go out to the provider, and frames coming back are addressed
2731
+ * to this session by id.
2732
+ */
2733
+ _endpointFor(providerName, state) {
2734
+ if (state.httpEndpoint) return state.httpEndpoint;
2735
+ state.httpEndpoint = new StreamableHttpEndpoint({
2736
+ allowedOrigins: this._allowedOrigins,
2737
+ createServer: (transport, sessionId, _principal) => {
2738
+ const session = { transport, principal: null };
2739
+ state.httpSessions.set(sessionId, session);
2740
+ transport.onMessage = (frame) => this._fromHttpSession(state, providerName, sessionId, session, frame);
2741
+ return {
2742
+ // No server to launch: the one serving this session is the
2743
+ // provider, at the far end of the tunnel. What `start` still
2744
+ // owes is opening the transport, since a closed one drops
2745
+ // every frame it is handed.
2746
+ start: () => openTransport(transport),
2747
+ stop: () => {
2748
+ state.httpSessions.delete(sessionId);
2749
+ for (const [id, sink] of state.pending) {
2750
+ if (sink.type === "http-session" && sink.sessionId === sessionId) state.pending.delete(id);
2751
+ }
2752
+ }
2753
+ };
2754
+ }
2755
+ });
2756
+ return state.httpEndpoint;
2757
+ }
2758
+ /** Relays one frame from an HTTP session to the provider behind the slot. */
2759
+ _fromHttpSession(state, providerName, sessionId, session, frame) {
2760
+ if (!this._authorizeMcpFrame(providerName, frame, session.principal)) {
2761
+ session.transport.send(this._policyDeniedPayload(frame));
2762
+ return;
2763
+ }
2764
+ if (!this._isProviderConnected(providerName, state)) {
2765
+ session.transport.send(this._notConnectedPayload(providerName, frame));
2766
+ return;
2767
+ }
2768
+ const id = requestIdOf(frame);
2769
+ if (id !== void 0) state.pending.set(id, { type: "http-session", sessionId });
2770
+ this._sendToProvider(state, providerName, frame, session.principal);
2771
+ }
2772
+ /** Writes one JSON-RPC message as an SSE `message` event. */
2773
+ _sendSseEvent(res, data) {
2774
+ res.write(`event: message
2775
+ data: ${data}
2776
+
2777
+ `);
2778
+ }
2779
+ // -------------------------------------------------------------------------
2780
+ // WebSocket connection handlers
2781
+ // -------------------------------------------------------------------------
2782
+ _logProviderRegistration(principal, slot, resource, allowed) {
2783
+ const event = {
2784
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2785
+ providerId: principal.id,
2786
+ slot,
2787
+ resource: resource?.value,
2788
+ allowed
2789
+ };
2790
+ const line = `[broker] provider-registration ${JSON.stringify(event)}`;
2791
+ if (allowed) console.info(line);
2792
+ else console.warn(line);
2793
+ }
2794
+ _onProviderConnect(ws, name, req) {
2795
+ const providerPrincipal = this._pendingProviderPrincipals.get(req);
2796
+ if (providerPrincipal) {
2797
+ this._pendingProviderPrincipals.delete(req);
2798
+ this._providerPrincipals.set(ws, providerPrincipal);
2799
+ }
2800
+ if (this._upstreams.has(name)) {
2801
+ console.warn(
2802
+ `[broker] WARNING: WebSocket provider "${name}" rejected: a stdio upstream with the same name is already configured. Rename one of them to avoid the conflict.`
2803
+ );
2804
+ ws.close(1008, `Provider "${name}" is managed by a stdio upstream`);
2805
+ return;
2806
+ }
2807
+ if (this._loopbackProviders.has(name)) {
2808
+ console.warn(`[broker] WARNING: WebSocket provider "${name}" rejected: the slot is held by an in-process loopback (reserved system slot).`);
2809
+ ws.close(1008, `Provider "${name}" is reserved by the broker`);
2810
+ return;
2811
+ }
2812
+ const existing = this._providers.get(name);
2813
+ if (existing?.ws?.readyState === WebSocket.OPEN) {
2814
+ ws.close(1008, `Provider "${name}" is already connected`);
2815
+ return;
2816
+ }
2817
+ const state = this._getOrCreateProviderState(name);
2818
+ state.ws = ws;
2819
+ if (providerPrincipal) {
2820
+ this._logProviderRegistration(providerPrincipal, name, this._slotResourceResolver.resolve(name), true);
2821
+ }
2822
+ let registrationChecked = false;
2823
+ ws.on("message", (data) => {
2824
+ const text = data.toString();
2825
+ if (!registrationChecked) {
2826
+ registrationChecked = true;
2827
+ if (this._tryHandleRegistration(name, text)) return;
2828
+ }
2829
+ this._routeFromProvider(state, name, text);
2830
+ });
2831
+ ws.on("close", () => {
2832
+ state.ws = null;
2833
+ this._failProviderDisconnected(state, name);
2834
+ });
2835
+ }
2836
+ /**
2837
+ * Inspects a provider's first WebSocket message for an optional registration
2838
+ * control frame `{ "type": "register", "aggregate": boolean }`. Returns
2839
+ * `true` when the message was a registration frame: and thus consumed, not
2840
+ * routed as MCP traffic. A normal MCP frame always carries `jsonrpc`, so it
2841
+ * returns `false` and the provider stays non-aggregated.
2842
+ */
2843
+ _tryHandleRegistration(name, text) {
2844
+ let frame;
2845
+ try {
2846
+ frame = JSON.parse(text);
2847
+ } catch {
2848
+ return false;
2849
+ }
2850
+ if (frame.jsonrpc !== void 0 || frame.type !== "register") return false;
2851
+ if (frame.aggregate === true) {
2852
+ void this._aggregateServer?.addProvider(name);
2853
+ }
2854
+ return true;
2855
+ }
2856
+ _onClientConnect(ws, providerName, req) {
2857
+ const state = this._getOrCreateProviderState(providerName);
2858
+ state.wsClients.add(ws);
2859
+ const principal = this._pendingClientPrincipals.get(req);
2860
+ if (principal) {
2861
+ this._clientPrincipals.set(ws, principal);
2862
+ this._pendingClientPrincipals.delete(req);
2863
+ }
2864
+ ws.on("message", (data) => this._routeFromClient(ws, state, providerName, data.toString()));
2865
+ ws.on("close", () => {
2866
+ state.wsClients.delete(ws);
2867
+ for (const [id, sink] of state.pending) {
2868
+ if (sink.type === "ws" && sink.socket === ws) state.pending.delete(id);
2869
+ }
2870
+ });
2871
+ }
2872
+ /**
2873
+ * Handles a multiplexed provider WebSocket (`/providers`).
2874
+ * A single socket carries traffic for multiple providers using the
2875
+ * envelope format `{ provider: string, payload: object }`.
2876
+ * Provider names are registered lazily on first message.
2877
+ */
2878
+ _onMultiplexProviderConnect(ws, req) {
2879
+ const providerNames = /* @__PURE__ */ new Set();
2880
+ this._multiplexSockets.set(ws, providerNames);
2881
+ const providerPrincipal = this._pendingProviderPrincipals.get(req);
2882
+ if (providerPrincipal) {
2883
+ this._pendingProviderPrincipals.delete(req);
2884
+ this._providerPrincipals.set(ws, providerPrincipal);
2885
+ }
2886
+ ws.on("message", (data) => {
2887
+ const envelope = decodeEnvelope(data.toString());
2888
+ if (!envelope) return;
2889
+ const name = envelope.provider;
2890
+ if (!providerNames.has(name)) {
2891
+ if (providerPrincipal) {
2892
+ const resource = this._slotResourceResolver.resolve(name);
2893
+ if (!resource || !providerMayPublish(providerPrincipal, resource)) {
2894
+ this._logProviderRegistration(providerPrincipal, name, resource, false);
2895
+ ws.send(encodeErrorEnvelope(name, TunnelErrorCodes.RegistrationForbidden, "Provider registration forbidden"));
2896
+ return;
2897
+ }
2898
+ }
2899
+ if (this._upstreams.has(name)) {
2900
+ console.warn(
2901
+ `[broker] WARNING: Multiplexed WebSocket provider "${name}" rejected: a stdio upstream with the same name is already configured. Rename one of them to avoid the conflict.`
2902
+ );
2903
+ ws.send(encodeErrorEnvelope(name, TunnelErrorCodes.ProviderUnavailable, `Provider "${name}" is managed by a stdio upstream`));
2904
+ return;
2905
+ }
2906
+ if (this._loopbackProviders.has(name)) {
2907
+ ws.send(encodeErrorEnvelope(name, TunnelErrorCodes.ProviderUnavailable, `Provider "${name}" is reserved by the broker`));
2908
+ return;
2909
+ }
2910
+ const existing = this._providers.get(name);
2911
+ if (existing?.ws?.readyState === WebSocket.OPEN) {
2912
+ ws.send(encodeErrorEnvelope(name, TunnelErrorCodes.ProviderUnavailable, `Provider "${name}" is already connected`));
2913
+ return;
2914
+ }
2915
+ providerNames.add(name);
2916
+ const state2 = this._getOrCreateProviderState(name);
2917
+ state2.ws = ws;
2918
+ if (providerPrincipal) {
2919
+ this._logProviderRegistration(providerPrincipal, name, this._slotResourceResolver.resolve(name), true);
2920
+ }
2921
+ }
2922
+ const state = this._providers.get(name);
2923
+ this._routeFromProvider(state, name, envelopeFrame(envelope));
2924
+ });
2925
+ ws.on("close", () => {
2926
+ for (const name of providerNames) {
2927
+ const state = this._providers.get(name);
2928
+ if (state && state.ws === ws) {
2929
+ state.ws = null;
2930
+ this._failProviderDisconnected(state, name);
2931
+ }
2932
+ }
2933
+ this._multiplexSockets.delete(ws);
2934
+ });
2935
+ }
2936
+ // -------------------------------------------------------------------------
2937
+ // Message routing
2938
+ // -------------------------------------------------------------------------
2939
+ /**
2940
+ * Sends a raw JSON-RPC message to a provider, wrapping it in a multiplex
2941
+ * envelope when the provider's WebSocket is a multiplexed connection.
2942
+ */
2943
+ _sendToProvider(state, providerName, data, principal = null) {
2944
+ if (providerName === AggregateServer.SLOT && this._aggregateServer) {
2945
+ this._aggregateServer.sendAs(data, principal);
2946
+ return;
2947
+ }
2948
+ const upstream = this._upstreams.get(providerName);
2949
+ if (upstream?.isOpen) {
2950
+ upstream.send(data);
2951
+ return;
2952
+ }
2953
+ const loopback = this._loopbackProviders.get(providerName);
2954
+ if (loopback?.isOpen) {
2955
+ loopback.send(data);
2956
+ return;
2957
+ }
2958
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
2959
+ if (this._multiplexSockets.has(state.ws)) {
2960
+ state.ws.send(encodeEnvelope(providerName, data));
2961
+ } else {
2962
+ state.ws.send(data);
2963
+ }
2964
+ }
2965
+ _routeFromStdioClient(state, data) {
2966
+ try {
2967
+ const msg = JSON.parse(data);
2968
+ if (msg?.id != null) state.pending.set(msg.id, { type: "stdio" });
2969
+ } catch {
2970
+ }
2971
+ if (this._isProviderConnected(this._stdioClientProvider, state)) {
2972
+ this._sendToProvider(state, this._stdioClientProvider, data);
2973
+ } else {
2974
+ let errId = null;
2975
+ try {
2976
+ errId = JSON.parse(data).id ?? null;
2977
+ } catch {
2978
+ }
2979
+ if (errId != null) {
2980
+ this._stdioClientTransport?.send(
2981
+ JSON.stringify({
2982
+ jsonrpc: "2.0",
2983
+ id: errId,
2984
+ error: { code: -32e3, message: `Provider "${this._stdioClientProvider}" not connected` }
2985
+ })
2986
+ );
2987
+ }
2988
+ }
2989
+ }
2990
+ _routeFromClient(client, state, providerName, data) {
2991
+ const principal = this._clientPrincipals.get(client) ?? null;
2992
+ if (!this._authorizeMcpFrame(providerName, data, principal)) {
2993
+ client.send(this._policyDeniedPayload(data));
2994
+ return;
2995
+ }
2996
+ try {
2997
+ const msg = JSON.parse(data);
2998
+ if (msg?.id != null) state.pending.set(msg.id, { type: "ws", socket: client });
2999
+ } catch {
3000
+ }
3001
+ if (this._isProviderConnected(providerName, state)) {
3002
+ this._sendToProvider(state, providerName, data, principal);
3003
+ } else {
3004
+ client.send(
3005
+ JSON.stringify({
3006
+ jsonrpc: "2.0",
3007
+ id: null,
3008
+ error: { code: -32e3, message: "No provider connected" }
3009
+ })
3010
+ );
3011
+ }
3012
+ }
3013
+ _routeFromProvider(state, providerName, data) {
3014
+ try {
3015
+ const msg = JSON.parse(data);
3016
+ if (msg.id != null) {
3017
+ const sink = state.pending.get(msg.id);
3018
+ if (sink?.type === "ws" && sink.socket.readyState === WebSocket.OPEN) {
3019
+ sink.socket.send(data);
3020
+ } else if (sink?.type === "sse") {
3021
+ const sseRes = state.sseSessions.get(sink.sessionId);
3022
+ if (sseRes) this._sendSseEvent(sseRes, data);
3023
+ } else if (sink?.type === "http-session") {
3024
+ state.httpSessions.get(sink.sessionId)?.transport.send(data);
3025
+ } else if (sink?.type === "stdio") {
3026
+ this._stdioClientTransport?.send(data);
3027
+ } else if (sink?.type === "internal") {
3028
+ sink.client.onMessage?.(data);
3029
+ }
3030
+ state.pending.delete(msg.id);
3031
+ } else {
3032
+ this._broadcast(state, providerName, data);
3033
+ }
3034
+ } catch {
3035
+ this._broadcast(state, providerName, data);
3036
+ }
3037
+ }
3038
+ /** Sends a message to all clients connected to one provider. */
3039
+ _broadcast(state, providerName, data) {
3040
+ for (const client of state.wsClients) {
3041
+ const principal = this._clientPrincipals.get(client) ?? null;
3042
+ if (client.readyState === WebSocket.OPEN && this._authorizeMcpFrame(providerName, data, principal)) {
3043
+ client.send(data);
3044
+ }
3045
+ }
3046
+ for (const sseRes of state.sseSessions.values()) {
3047
+ const principal = this._streamPrincipals.get(sseRes) ?? null;
3048
+ if (this._authorizeMcpFrame(providerName, data, principal)) {
3049
+ this._sendSseEvent(sseRes, data);
3050
+ }
3051
+ }
3052
+ for (const session of state.httpSessions.values()) {
3053
+ if (this._authorizeMcpFrame(providerName, data, session.principal)) {
3054
+ session.transport.send(data);
3055
+ }
3056
+ }
3057
+ for (const ic of state.internalClients) {
3058
+ ic.onMessage?.(data);
3059
+ }
3060
+ if (this._stdioClientProvider && this._providers.get(this._stdioClientProvider) === state) {
3061
+ this._stdioClientTransport?.send(data);
3062
+ }
3063
+ }
3064
+ /**
3065
+ * Notifies every pending sink and internal client that the provider slot
3066
+ * has disconnected, then clears the pending map. Shared by all provider
3067
+ * close handlers (dedicated WS, multiplexed WS, loopback).
3068
+ */
3069
+ _failProviderDisconnected(state, name) {
3070
+ for (const [id, sink] of state.pending) {
3071
+ const error = JSON.stringify({
3072
+ jsonrpc: "2.0",
3073
+ id,
3074
+ error: { code: -32e3, message: `Provider "${name}" disconnected` }
3075
+ });
3076
+ if (sink.type === "ws" && sink.socket.readyState === WebSocket.OPEN) {
3077
+ sink.socket.send(error);
3078
+ } else if (sink.type === "sse") {
3079
+ const sseRes = state.sseSessions.get(sink.sessionId);
3080
+ if (sseRes) this._sendSseEvent(sseRes, error);
3081
+ } else if (sink.type === "http-session") {
3082
+ state.httpSessions.get(sink.sessionId)?.transport.send(error);
3083
+ } else if (sink.type === "stdio") {
3084
+ this._stdioClientTransport?.send(error);
3085
+ } else if (sink.type === "internal") {
3086
+ sink.client.onMessage?.(error);
3087
+ }
3088
+ }
3089
+ state.pending.clear();
3090
+ for (const ic of state.internalClients) ic.onClose?.();
3091
+ }
3092
+ // -------------------------------------------------------------------------
3093
+ // Provider state helpers
3094
+ // -------------------------------------------------------------------------
3095
+ /**
3096
+ * Returns `true` if the provider is reachable, via a WebSocket connection,
3097
+ * a stdio upstream, or an in-process loopback transport.
3098
+ */
3099
+ _isProviderConnected(providerName, state) {
3100
+ if (this._upstreams.get(providerName)?.isOpen) return true;
3101
+ if (this._loopbackProviders.get(providerName)?.isOpen) return true;
3102
+ if (state.ws?.readyState === WebSocket.OPEN) return true;
3103
+ return false;
3104
+ }
3105
+ /** Returns the state for `name`, creating it lazily if it doesn't exist yet. */
3106
+ _getOrCreateProviderState(name) {
3107
+ let state = this._providers.get(name);
3108
+ if (!state) {
3109
+ state = {
3110
+ ws: null,
3111
+ pending: /* @__PURE__ */ new Map(),
3112
+ sseSessions: /* @__PURE__ */ new Map(),
3113
+ httpSessions: /* @__PURE__ */ new Map(),
3114
+ wsClients: /* @__PURE__ */ new Set(),
3115
+ internalClients: /* @__PURE__ */ new Set(),
3116
+ httpEndpoint: null
3117
+ };
3118
+ this._providers.set(name, state);
3119
+ }
3120
+ return state;
3121
+ }
3122
+ // -------------------------------------------------------------------------
3123
+ // Samples index
3124
+ // -------------------------------------------------------------------------
3125
+ _handleSamplesIndex(res) {
3126
+ const rootMount = (this._options.staticMounts ?? []).find((m) => m.urlPrefix === "/");
3127
+ let files = [];
3128
+ if (rootMount) {
3129
+ const samplesDir = nodePath.join(rootMount.dir, "samples");
3130
+ try {
3131
+ if (fs.existsSync(samplesDir) && fs.statSync(samplesDir).isDirectory()) {
3132
+ files = fs.readdirSync(samplesDir).filter((name) => fs.statSync(nodePath.join(samplesDir, name)).isFile());
3133
+ }
3134
+ } catch {
3135
+ }
3136
+ }
3137
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
3138
+ res.end(JSON.stringify({ files }));
3139
+ }
3140
+ // -------------------------------------------------------------------------
3141
+ // Static file serving
3142
+ // -------------------------------------------------------------------------
3143
+ _serveStatic(req, res) {
3144
+ const rawUrl = (req.url ?? "/").split("?")[0].split("#")[0];
3145
+ const mounts = this._options.staticMounts ?? [];
3146
+ const mount = [...mounts].filter((m) => {
3147
+ const prefix = m.urlPrefix.endsWith("/") ? m.urlPrefix : m.urlPrefix + "/";
3148
+ return rawUrl === m.urlPrefix || rawUrl.startsWith(prefix);
3149
+ }).sort((a, b) => b.urlPrefix.length - a.urlPrefix.length)[0];
3150
+ if (!mount) {
3151
+ res.writeHead(404, { "Content-Type": "text/plain" });
3152
+ res.end("Not found");
3153
+ return;
3154
+ }
3155
+ const relative2 = rawUrl.slice(mount.urlPrefix.length) || "/";
3156
+ const normalized = nodePath.normalize(relative2);
3157
+ if (normalized.startsWith("..")) {
3158
+ res.writeHead(403);
3159
+ res.end("Forbidden");
3160
+ return;
3161
+ }
3162
+ const mountAbs = nodePath.resolve(mount.dir);
3163
+ let filePath = nodePath.join(mountAbs, normalized);
3164
+ if (!filePath.startsWith(mountAbs + nodePath.sep) && filePath !== mountAbs) {
3165
+ res.writeHead(403);
3166
+ res.end("Forbidden");
3167
+ return;
3168
+ }
3169
+ try {
3170
+ if (fs.statSync(filePath).isDirectory()) filePath = nodePath.join(filePath, "index.html");
3171
+ } catch {
3172
+ res.writeHead(404);
3173
+ res.end("Not found");
3174
+ return;
3175
+ }
3176
+ if (!fs.existsSync(filePath)) {
3177
+ res.writeHead(404);
3178
+ res.end("Not found");
3179
+ return;
3180
+ }
3181
+ const ext = nodePath.extname(filePath).toLowerCase();
3182
+ res.writeHead(200, { "Content-Type": MIME[ext] ?? "application/octet-stream" });
3183
+ fs.createReadStream(filePath).pipe(res);
3184
+ }
3185
+ };
3186
+ var WsTunnelBuilder = class {
3187
+ _port = 3e3;
3188
+ _host;
3189
+ _providerPath = "/provider";
3190
+ _providersPath = "/providers";
3191
+ _clientPath = "/";
3192
+ _ssePath = "/sse";
3193
+ _messagesPath = "/messages";
3194
+ _mcpPath = "/mcp";
3195
+ _samplesIndexPath = "/__samples_index__";
3196
+ _allowedOrigins = void 0;
3197
+ _staticMounts = [];
3198
+ _stdioUpstreams = [];
3199
+ _remoteUpstreams = [];
3200
+ _stdioClient = void 0;
3201
+ _tls = void 0;
3202
+ _brokerLocalGrammarsDir = void 0;
3203
+ _auth = void 0;
3204
+ _providerAuth = void 0;
3205
+ _authorization = void 0;
3206
+ _slotResourceResolver = void 0;
3207
+ /** Sets the TCP port the broker listens on. */
3208
+ withPort(port) {
3209
+ this._port = port;
3210
+ return this;
3211
+ }
3212
+ /**
3213
+ * Sets the host/interface to bind to.
3214
+ * @default "0.0.0.0" (all interfaces)
3215
+ */
3216
+ withHost(host) {
3217
+ this._host = host;
3218
+ return this;
3219
+ }
3220
+ /**
3221
+ * Sets the URL path the MCP provider connects to.
3222
+ * @default "/provider"
3223
+ */
3224
+ withProviderPath(path) {
3225
+ this._providerPath = path;
3226
+ return this;
3227
+ }
3228
+ /**
3229
+ * Sets the URL path for multiplexed provider connections.
3230
+ * Multiple providers share a single WebSocket using the envelope protocol.
3231
+ * @default "/providers"
3232
+ */
3233
+ withProvidersPath(path) {
3234
+ this._providersPath = path;
3235
+ return this;
3236
+ }
3237
+ /**
3238
+ * Sets the URL path MCP clients connect to.
3239
+ * @default "/"
3240
+ */
3241
+ withClientPath(path) {
3242
+ this._clientPath = path;
3243
+ return this;
3244
+ }
3245
+ /**
3246
+ * Sets the URL path for the SSE stream (legacy Claude transport, GET).
3247
+ * @default "/sse"
3248
+ */
3249
+ withSsePath(path) {
3250
+ this._ssePath = path;
3251
+ return this;
3252
+ }
3253
+ /**
3254
+ * Sets the URL path for JSON-RPC POST requests (legacy Claude transport).
3255
+ * @default "/messages"
3256
+ */
3257
+ withMessagesPath(path) {
3258
+ this._messagesPath = path;
3259
+ return this;
3260
+ }
3261
+ /**
3262
+ * Sets the URL path for the Streamable HTTP transport (MCP 2025-03-26).
3263
+ * MCP Inspector and other 2025+ clients POST JSON-RPC here.
3264
+ * @default "/mcp"
3265
+ */
3266
+ withMcpPath(path) {
3267
+ this._mcpPath = path;
3268
+ return this;
3269
+ }
3270
+ /**
3271
+ * Sets the URL path that returns a `{ files: string[] }` listing of the
3272
+ * `samples/` subdirectory under the root static mount.
3273
+ * @default "/__samples_index__"
3274
+ */
3275
+ withSamplesIndexPath(path) {
3276
+ this._samplesIndexPath = path;
3277
+ return this;
3278
+ }
3279
+ /**
3280
+ * Allows browser origins to reach `/<slot>/mcp`.
3281
+ *
3282
+ * Without this call no browser origin is accepted, which is the safe
3283
+ * default: a request carrying an `Origin` is refused with `403`, while one
3284
+ * carrying none (any non-browser client) always passes. Call it only when a
3285
+ * web page really has to talk to the broker over HTTP.
3286
+ *
3287
+ * @example
3288
+ * ```typescript
3289
+ * builder.withAllowedOrigins(["https://app.example.com"]);
3290
+ * builder.withAllowedOrigins(/^https:\/\/[a-z0-9-]+\.example\.com$/);
3291
+ * builder.withAllowedOrigins((origin) => origin.endsWith(".example.com"));
3292
+ * ```
3293
+ */
3294
+ withAllowedOrigins(allowed) {
3295
+ this._allowedOrigins = allowed;
3296
+ return this;
3297
+ }
3298
+ /**
3299
+ * Adds a static-file mount served over plain HTTP.
3300
+ * Can be called multiple times; longest-prefix match wins at runtime.
3301
+ *
3302
+ * @param urlPrefix URL prefix that triggers this mount (e.g. `"/"` or `"/bundle"`).
3303
+ * @param dir Absolute path to the directory to serve.
3304
+ */
3305
+ withStaticMount(urlPrefix, dir) {
3306
+ this._staticMounts.push({ urlPrefix, dir });
3307
+ return this;
3308
+ }
3309
+ /**
3310
+ * Registers a stdio upstream provider. The broker spawns the configured
3311
+ * command and bridges its stdin/stdout as an MCP transport. Clients reach
3312
+ * it using `config.name` directly (e.g. `/<name>/mcp`).
3313
+ *
3314
+ * Can be called multiple times to register multiple providers.
3315
+ */
3316
+ withStdioUpstream(config) {
3317
+ this._stdioUpstreams.push(config);
3318
+ return this;
3319
+ }
3320
+ /**
3321
+ * Registers a remote MCP server reached by URL. The broker connects out to
3322
+ * it and exposes it as a provider slot named `config.name`, bridging the
3323
+ * Streamable HTTP / SSE / WebSocket transport for the slot's clients.
3324
+ *
3325
+ * Can be called multiple times to register multiple servers.
3326
+ */
3327
+ withRemoteUpstream(config) {
3328
+ this._remoteUpstreams.push(config);
3329
+ return this;
3330
+ }
3331
+ /**
3332
+ * Enables the stdio client transport. The broker will read JSON-RPC from
3333
+ * `process.stdin` and write responses to `process.stdout`, bridging Claude
3334
+ * Desktop (or any stdio MCP client) to the named provider.
3335
+ *
3336
+ * All console output is automatically redirected to stderr in this mode so
3337
+ * stdout stays clean for the JSON-RPC stream.
3338
+ *
3339
+ * @param providerName The provider the stdio client maps to.
3340
+ */
3341
+ withStdioClient(providerName) {
3342
+ this._stdioClient = { providerName };
3343
+ return this;
3344
+ }
3345
+ /**
3346
+ * Enables HTTPS/WSS mode by supplying PEM-encoded certificate and key strings directly.
3347
+ * Call this when you already have the PEM content in memory.
3348
+ */
3349
+ withTls(cert, key) {
3350
+ this._tls = { cert, key };
3351
+ return this;
3352
+ }
3353
+ /**
3354
+ * Enables HTTPS/WSS mode by reading the certificate and key from the given file paths.
3355
+ * Files are read synchronously at call time.
3356
+ *
3357
+ * @param certPath Path to the PEM certificate file (e.g. `fullchain.pem`).
3358
+ * @param keyPath Path to the PEM private-key file (e.g. `privkey.pem`).
3359
+ */
3360
+ withTlsFiles(certPath, keyPath) {
3361
+ return this.withTls(fs.readFileSync(certPath, "utf8"), fs.readFileSync(keyPath, "utf8"));
3362
+ }
3363
+ /**
3364
+ * Sets the path to a user-supplied grammars directory whose
3365
+ * `<userAgent>/<locale>.json` files are merged on top of the packaged
3366
+ * grammars used by the embedded broker server (the reserved `_broker`
3367
+ * provider slot). Typically pointed at `.mcp-broker/grammars/`.
3368
+ */
3369
+ withBrokerLocalGrammarsDir(dir) {
3370
+ this._brokerLocalGrammarsDir = dir;
3371
+ return this;
3372
+ }
3373
+ /**
3374
+ * Enables OAuth 2.1 resource-server authorization with a pre-resolved config
3375
+ * (e.g. a custom {@link IResolvedAuth} carrying your own `ITokenValidator`).
3376
+ * Prefer {@link withJwtAuth} for the standard JWKS-backed setup.
3377
+ */
3378
+ withAuth(auth) {
3379
+ this._auth = auth;
3380
+ return this;
3381
+ }
3382
+ /**
3383
+ * Enables OAuth 2.1 authorization with the default stateless JWT validator:
3384
+ * the broker verifies each bearer token's signature against the authorization
3385
+ * server's JWKS and its audience against the slot's canonical resource URI.
3386
+ *
3387
+ * @throws if `publicBaseUrl`, `authorizationServers`, or `jwksUri` are missing.
3388
+ */
3389
+ withJwtAuth(options) {
3390
+ this._auth = buildJwtAuth(options);
3391
+ return this;
3392
+ }
3393
+ /** Installs a custom policy engine behind the broker's transport-neutral API. */
3394
+ withPolicyEngine(engine) {
3395
+ this._authorization = authorizationWithEngine(engine, {
3396
+ slotResourceResolver: this._slotResourceResolver
3397
+ });
3398
+ return this;
3399
+ }
3400
+ /** Compiles and validates roles, assignments, denies, mappings, and paths. */
3401
+ withAuthorizationPolicy(config) {
3402
+ this._authorization = compileAuthorizationPolicy(config);
3403
+ return this;
3404
+ }
3405
+ /** Overrides the technical slot to hierarchical resource resolution strategy. */
3406
+ withSlotResourceResolver(resolver) {
3407
+ this._slotResourceResolver = resolver;
3408
+ return this;
3409
+ }
3410
+ /**
3411
+ * Authenticates providers (engines) with a custom {@link IProviderAuthenticator}.
3412
+ * Prefer {@link withProviderSecret} for the standard shared-secret setup.
3413
+ */
3414
+ withProviderAuth(authenticator) {
3415
+ this._providerAuth = authenticator;
3416
+ return this;
3417
+ }
3418
+ /**
3419
+ * Requires every provider connecting to `/provider/<slot>` or `/providers` to
3420
+ * present the given shared secret (via `X-Provider-Token` or `Authorization:
3421
+ * Bearer`). Closes off slot occupation by strangers.
3422
+ */
3423
+ withProviderSecret(secret) {
3424
+ this._providerAuth = new SharedSecretProviderAuthenticator(secret);
3425
+ return this;
3426
+ }
3427
+ /** Constructs and returns a configured {@link WsTunnel}. */
3428
+ build() {
3429
+ const baseAuthorization = this._authorization ?? this._auth?.authorization;
3430
+ const authorization = baseAuthorization && this._slotResourceResolver ? { ...baseAuthorization, slotResourceResolver: this._slotResourceResolver } : baseAuthorization;
3431
+ const options = {
3432
+ port: this._port,
3433
+ host: this._host,
3434
+ providerPath: this._providerPath,
3435
+ providersPath: this._providersPath,
3436
+ clientPath: this._clientPath,
3437
+ ssePath: this._ssePath,
3438
+ messagesPath: this._messagesPath,
3439
+ mcpPath: this._mcpPath,
3440
+ samplesIndexPath: this._samplesIndexPath,
3441
+ allowedOrigins: this._allowedOrigins,
3442
+ staticMounts: this._staticMounts.length > 0 ? [...this._staticMounts] : void 0,
3443
+ stdioUpstreams: this._stdioUpstreams.length > 0 ? [...this._stdioUpstreams] : void 0,
3444
+ remoteUpstreams: this._remoteUpstreams.length > 0 ? [...this._remoteUpstreams] : void 0,
3445
+ stdioClient: this._stdioClient,
3446
+ tls: this._tls,
3447
+ brokerLocalGrammarsDir: this._brokerLocalGrammarsDir,
3448
+ auth: this._auth,
3449
+ providerAuth: this._providerAuth,
3450
+ authorization,
3451
+ slotResourceResolver: this._slotResourceResolver ?? authorization?.slotResourceResolver ?? this._auth?.slotResourceResolver
3452
+ };
3453
+ return new WsTunnel(options);
3454
+ }
3455
+ };
3456
+ var EOCD_SIGNATURE = 101010256;
3457
+ var CENTRAL_HEADER_SIGNATURE = 33639248;
3458
+ var LOCAL_HEADER_SIGNATURE = 67324752;
3459
+ function findEocd(buf) {
3460
+ const minPos = Math.max(0, buf.length - 22 - 65535);
3461
+ for (let pos = buf.length - 22; pos >= minPos; pos--) {
3462
+ if (buf.readUInt32LE(pos) === EOCD_SIGNATURE) return pos;
3463
+ }
3464
+ throw new Error("not a ZIP archive (no End Of Central Directory record found)");
3465
+ }
3466
+ function unzipMcpb(mcpbPath, destDir) {
3467
+ const buf = readFileSync(mcpbPath);
3468
+ const eocd = findEocd(buf);
3469
+ const totalEntries = buf.readUInt16LE(eocd + 10);
3470
+ const centralDirOffset = buf.readUInt32LE(eocd + 16);
3471
+ if (centralDirOffset === 4294967295 || totalEntries === 65535) {
3472
+ throw new Error("ZIP64 archives are not supported");
3473
+ }
3474
+ const absDest = resolve(destDir);
3475
+ mkdirSync(absDest, { recursive: true });
3476
+ let pos = centralDirOffset;
3477
+ for (let i = 0; i < totalEntries; i++) {
3478
+ if (buf.readUInt32LE(pos) !== CENTRAL_HEADER_SIGNATURE) {
3479
+ throw new Error(`corrupt ZIP: bad central directory header at offset ${pos}`);
3480
+ }
3481
+ const method = buf.readUInt16LE(pos + 10);
3482
+ const compressedSize = buf.readUInt32LE(pos + 20);
3483
+ const uncompressedSize = buf.readUInt32LE(pos + 24);
3484
+ const nameLen = buf.readUInt16LE(pos + 28);
3485
+ const extraLen = buf.readUInt16LE(pos + 30);
3486
+ const commentLen = buf.readUInt16LE(pos + 32);
3487
+ const localHeaderOffset = buf.readUInt32LE(pos + 42);
3488
+ const name = buf.toString("utf8", pos + 46, pos + 46 + nameLen);
3489
+ pos += 46 + nameLen + extraLen + commentLen;
3490
+ if (compressedSize === 4294967295 || uncompressedSize === 4294967295 || localHeaderOffset === 4294967295) {
3491
+ throw new Error("ZIP64 archives are not supported");
3492
+ }
3493
+ const target = resolve(absDest, name);
3494
+ const rel = relative(absDest, target);
3495
+ if (rel.startsWith("..") || isAbsolute(rel)) {
3496
+ throw new Error(`unsafe ZIP entry path (zip-slip): ${name}`);
3497
+ }
3498
+ if (name.endsWith("/")) {
3499
+ mkdirSync(target, { recursive: true });
3500
+ continue;
3501
+ }
3502
+ if (buf.readUInt32LE(localHeaderOffset) !== LOCAL_HEADER_SIGNATURE) {
3503
+ throw new Error(`corrupt ZIP: bad local header for "${name}"`);
3504
+ }
3505
+ const localNameLen = buf.readUInt16LE(localHeaderOffset + 26);
3506
+ const localExtraLen = buf.readUInt16LE(localHeaderOffset + 28);
3507
+ const dataStart = localHeaderOffset + 30 + localNameLen + localExtraLen;
3508
+ const compressed = buf.subarray(dataStart, dataStart + compressedSize);
3509
+ let data;
3510
+ if (method === 0) {
3511
+ data = compressed;
3512
+ } else if (method === 8) {
3513
+ data = inflateRawSync(compressed);
3514
+ } else {
3515
+ throw new Error(`unsupported ZIP compression method ${method} for "${name}"`);
3516
+ }
3517
+ mkdirSync(dirname(target), { recursive: true });
3518
+ writeFileSync(target, data);
3519
+ }
3520
+ }
3521
+
3522
+ // src/mcpb/mcpb.loader.ts
3523
+ function loadPublicKey(pem) {
3524
+ if (pem.includes("BEGIN CERTIFICATE")) {
3525
+ return new X509Certificate(pem).publicKey;
3526
+ }
3527
+ return createPublicKey(pem);
3528
+ }
3529
+ function verifyDetachedSignature(data, signature, publicKey) {
3530
+ const keyType = publicKey.asymmetricKeyType;
3531
+ const algorithm = keyType === "ed25519" || keyType === "ed448" ? null : "sha256";
3532
+ return verify(algorithm, data, publicKey, signature);
3533
+ }
3534
+ function resolvePlaceholder(key, dirname4, userConfig) {
3535
+ if (key === "__dirname") return dirname4;
3536
+ if (key === "HOME") return homedir();
3537
+ if (key === "DESKTOP") return join(homedir(), "Desktop");
3538
+ if (key === "DOCUMENTS") return join(homedir(), "Documents");
3539
+ if (key === "DOWNLOADS") return join(homedir(), "Downloads");
3540
+ if (key === "pathSeparator" || key === "/") return sep;
3541
+ if (key.startsWith("user_config.")) {
3542
+ const name = key.slice("user_config.".length);
3543
+ const value = userConfig?.[name];
3544
+ if (value === void 0) throw new Error(`missing user_config value: "${name}"`);
3545
+ return value;
3546
+ }
3547
+ return void 0;
3548
+ }
3549
+ function expandScalar(input, dirname4, userConfig) {
3550
+ return input.replace(/\$\{([^}]+)\}/g, (match, key) => {
3551
+ const value = resolvePlaceholder(key, dirname4, userConfig);
3552
+ if (value === void 0) return match;
3553
+ if (Array.isArray(value)) throw new Error(`placeholder "\${${key}}" is multi-valued and cannot be used here`);
3554
+ return String(value);
3555
+ });
3556
+ }
3557
+ function expandArg(arg, dirname4, userConfig) {
3558
+ const standalone = /^\$\{(user_config\.[^}]+)\}$/.exec(arg);
3559
+ if (standalone) {
3560
+ const value = resolvePlaceholder(standalone[1], dirname4, userConfig);
3561
+ if (Array.isArray(value)) return value.map(String);
3562
+ return [String(value)];
3563
+ }
3564
+ return [expandScalar(arg, dirname4, userConfig)];
3565
+ }
3566
+ async function loadMcpbBundle(cfg, baseDir) {
3567
+ const tag2 = `[mcp-broker] mcpb bundle "${cfg.name}"`;
3568
+ try {
3569
+ const mcpbPath = resolve(baseDir, cfg.path);
3570
+ const publicKeyPath = resolve(baseDir, cfg.publicKey);
3571
+ const signaturePath = cfg.signature ? resolve(baseDir, cfg.signature) : `${mcpbPath}.sig`;
3572
+ for (const [label, file] of [
3573
+ ["bundle", mcpbPath],
3574
+ ["public key", publicKeyPath],
3575
+ ["signature", signaturePath]
3576
+ ]) {
3577
+ if (!existsSync(file)) {
3578
+ console.error(`${tag2}: ${label} file not found at ${file}, bundle refused.`);
3579
+ return null;
3580
+ }
3581
+ }
3582
+ const bundleBytes = readFileSync(mcpbPath);
3583
+ const signatureBytes = readFileSync(signaturePath);
3584
+ const publicKey = loadPublicKey(readFileSync(publicKeyPath, "utf8"));
3585
+ if (!verifyDetachedSignature(bundleBytes, signatureBytes, publicKey)) {
3586
+ console.error(`${tag2}: detached signature is invalid for the configured public key, bundle refused.`);
3587
+ return null;
3588
+ }
3589
+ const outputDir = resolve(baseDir, ".cache", "mcpb", cfg.name);
3590
+ rmSync(outputDir, { recursive: true, force: true });
3591
+ unzipMcpb(mcpbPath, outputDir);
3592
+ const manifestPath = join(outputDir, "manifest.json");
3593
+ if (!existsSync(manifestPath)) {
3594
+ console.error(`${tag2}: manifest.json missing inside the bundle, bundle refused.`);
3595
+ return null;
3596
+ }
3597
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
3598
+ const mcpConfig = manifest.server?.mcp_config;
3599
+ if (!mcpConfig) {
3600
+ console.error(`${tag2}: manifest has no server.mcp_config, bundle refused.`);
3601
+ return null;
3602
+ }
3603
+ const override = mcpConfig.platform_overrides?.[process.platform];
3604
+ const command = override?.command ?? mcpConfig.command;
3605
+ const rawArgs = override?.args ?? mcpConfig.args ?? [];
3606
+ const rawEnv = { ...mcpConfig.env, ...override?.env };
3607
+ if (!command) {
3608
+ console.error(`${tag2}: manifest mcp_config has no command, bundle refused.`);
3609
+ return null;
3610
+ }
3611
+ const expandedCommand = expandScalar(command, outputDir, cfg.userConfig);
3612
+ const expandedArgs = rawArgs.flatMap((arg) => expandArg(arg, outputDir, cfg.userConfig));
3613
+ const expandedEnv = {};
3614
+ for (const [key, value] of Object.entries(rawEnv)) {
3615
+ expandedEnv[key] = expandScalar(value, outputDir, cfg.userConfig);
3616
+ }
3617
+ return {
3618
+ name: cfg.name,
3619
+ command: expandedCommand,
3620
+ args: expandedArgs,
3621
+ env: Object.keys(expandedEnv).length > 0 ? expandedEnv : void 0,
3622
+ aggregate: cfg.aggregate ?? true
3623
+ };
3624
+ } catch (err) {
3625
+ console.error(`${tag2}: ${err.message}, bundle refused.`);
3626
+ return null;
3627
+ }
3628
+ }
3629
+ var DEFAULT_CONFIG_DIR = ".mcp-broker";
3630
+ var DEFAULT_CONFIG_FILENAME = "config.json";
3631
+ var LEGACY_CONFIG_FILENAME = "mcp-broker.config.json";
3632
+ function loadBrokerConfig(path) {
3633
+ const cwd = process.cwd();
3634
+ const envPath = process.env["MCP_BROKER_CONFIG"];
3635
+ let sourcePath = null;
3636
+ if (path) {
3637
+ sourcePath = resolve(cwd, path);
3638
+ } else if (envPath) {
3639
+ sourcePath = resolve(cwd, envPath);
3640
+ } else {
3641
+ const modern = resolve(cwd, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME);
3642
+ const legacy = resolve(cwd, LEGACY_CONFIG_FILENAME);
3643
+ if (existsSync(modern)) {
3644
+ sourcePath = modern;
3645
+ } else if (existsSync(legacy)) {
3646
+ sourcePath = legacy;
3647
+ process.stderr.write(
3648
+ `[mcp-broker] Using legacy config at ${legacy}. Move it to ${resolve(cwd, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME)} to silence this warning.
3649
+ `
3650
+ );
3651
+ }
3652
+ }
3653
+ if (!sourcePath || !existsSync(sourcePath)) {
3654
+ return { config: {}, baseDir: cwd, sourcePath: null };
3655
+ }
3656
+ const baseDir = dirname(sourcePath);
3657
+ try {
3658
+ const raw = readFileSync(sourcePath, "utf-8");
3659
+ const config = JSON.parse(raw);
3660
+ return { config, baseDir, sourcePath };
3661
+ } catch (err) {
3662
+ process.stderr.write(`[mcp-broker] Failed to parse config file at ${sourcePath}: ${err.message}
3663
+ `);
3664
+ return { config: {}, baseDir: cwd, sourcePath: null };
3665
+ }
3666
+ }
3667
+
3668
+ export { AuthError, BROKER_PROVIDER_NAME, BrokerInfoBehavior, BrokerProvidersBehavior, ConfigPolicyEngine, ConfiguredCapabilityClassifier, DEFAULT_CONFIG_FILENAME, DefaultSlotResourceResolver, HttpAuthGuard, JwtSubjectMapper, JwtTokenValidator, PACKAGE_NAME, RemoteUpstream, ResourcePath, ResourcePathPattern, SharedSecretProviderAuthenticator, StdioUpstream, SubjectMappingError, VERSION, WsTunnel, WsTunnelBuilder, authorizationWithEngine, brokerGrammarKey, buildJwtAuth, buildResourceMetadata, compileAuthorizationPolicy, hasAuthorizationPolicies, iterAvailableBrokerGrammars, iterBrokerGrammarsFrom, loadBrokerConfig, loadBrokerGrammar, loadMcpbBundle, normalizeProviderAuthentication, providerMayPublish, scopesOf, startBrokerServer, unzipMcpb, validateCapability };
3669
+ //# sourceMappingURL=chunk-FTDKH2C4.js.map
3670
+ //# sourceMappingURL=chunk-FTDKH2C4.js.map