@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
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * No `@modelcontextprotocol/sdk` dependency: the broker stays SDK-free, the
5
5
  * official SDK is only used outside the broker (in the demo) to prove the
6
- * broker is implementation-agnostic. Each transport is a thin frame relay —
6
+ * broker is implementation-agnostic. Each transport is a thin frame relay ,
7
7
  * it does not interpret MCP, it pipes JSON-RPC strings both ways.
8
8
  */
9
9
  import * as http from "node:http";
@@ -179,7 +179,7 @@ class StreamableHttpRemoteTransport implements IRemoteTransport {
179
179
  ) {}
180
180
 
181
181
  connect(): void {
182
- // Streamable HTTP is stateless — there is no connection to open. The
182
+ // Streamable HTTP is stateless, there is no connection to open. The
183
183
  // transport is usable as soon as connect() is called; the session id
184
184
  // is captured later from the first response that carries one.
185
185
  this._closed = false;
@@ -244,7 +244,7 @@ class StreamableHttpRemoteTransport implements IRemoteTransport {
244
244
  return;
245
245
  }
246
246
  } catch {
247
- /* not JSON — forward verbatim */
247
+ /* not JSON, forward verbatim */
248
248
  }
249
249
  this.onMessage?.(trimmed);
250
250
  }
@@ -1,5 +1,5 @@
1
- import type { IUpstream } from "./upstream.js";
2
- import { createRemoteTransport, detectTransport, type IRemoteTransport, type RemoteTransportKind } from "./remote.transports.js";
1
+ import type { IUpstream } from "./upstream";
2
+ import { createRemoteTransport, detectTransport, type IRemoteTransport, type RemoteTransportKind } from "./remote.transports";
3
3
 
4
4
  export interface IRemoteUpstreamConfig {
5
5
  /** Provider slot name this upstream is bound to. */
@@ -19,7 +19,7 @@ export interface IRemoteUpstreamConfig {
19
19
  *
20
20
  * The broker-facing contract is identical to {@link StdioUpstream}: the broker
21
21
  * muxes the slot's clients onto this single upstream. The only difference is
22
- * the transport — Streamable HTTP / SSE / WebSocket instead of a child process.
22
+ * the transport, Streamable HTTP / SSE / WebSocket instead of a child process.
23
23
  */
24
24
  export class RemoteUpstream implements IUpstream {
25
25
  readonly name: string;
@@ -1,5 +1,5 @@
1
1
  import { spawn, ChildProcess } from "node:child_process";
2
- import type { IUpstream } from "./upstream.js";
2
+ import type { IUpstream } from "./upstream";
3
3
 
4
4
  // ---------------------------------------------------------------------------
5
5
  // Configuration
@@ -72,7 +72,7 @@ export class StdioUpstream implements IUpstream {
72
72
 
73
73
  this._proc.on("error", (err: Error) => {
74
74
  this._open = false;
75
- this.onError?.(new Error(`StdioUpstream "${this.name}": process error — ${err.message}`));
75
+ this.onError?.(new Error(`StdioUpstream "${this.name}": process error, ${err.message}`));
76
76
  });
77
77
 
78
78
  this._proc.on("spawn", () => {
package/src/upstream.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * onto the single upstream connection, regardless of the underlying transport.
6
6
  *
7
7
  * Implementations:
8
- * - `StdioUpstream` — a local child process (newline-delimited JSON-RPC over stdio).
9
- * - `RemoteUpstream` — a remote MCP server reached by URL (Streamable HTTP / SSE / WebSocket).
8
+ * - `StdioUpstream` : a local child process (newline-delimited JSON-RPC over stdio).
9
+ * - `RemoteUpstream`: a remote MCP server reached by URL (Streamable HTTP / SSE / WebSocket).
10
10
  *
11
11
  * `StdioUpstream` is reused as-is by the future `.mcpb` bundle loader: a bundle
12
12
  * is a local server whose `mcp_config` maps directly onto a stdio upstream.
@@ -0,0 +1,357 @@
1
+ import type { ServerResponse } from "http";
2
+ import type { WebSocket } from "ws";
3
+ import type { GrammarResolverOptions, IMessageTransport } from "@cyanmycelium/mcp-core";
4
+ import type { StreamableHttpEndpoint } from "@cyanmycelium/mcp-core/node";
5
+ import type { IStdioUpstreamConfig } from "../stdio.upstream";
6
+ import type { IRemoteUpstreamConfig } from "../remote.upstream";
7
+ import type { IResolvedAuth, IProviderAuthenticator, IPrincipal } from "../auth/index";
8
+ import type { IPolicyAuthorization, ISlotResourceResolver } from "../authorization/index";
9
+
10
+ /**
11
+ * Every type the WebSocket tunnel exchanges or is configured with.
12
+ *
13
+ * Split out of `ws.tunnel.ts` so the contract can be read without wading
14
+ * through the implementation: the options an operator sets, the handles the
15
+ * broker hands to in-process components, and the per-slot state the relay
16
+ * keeps. The internal ones are exported for the tunnel's own modules and are
17
+ * deliberately absent from the package entry point.
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Internal types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Where a JSON-RPC response should be delivered.
26
+ * Either a WebSocket socket (raw WS client), a legacy SSE session, a Streamable
27
+ * HTTP session, the process stdout (stdio transport for Claude Desktop), or an
28
+ * in-process internal client (e.g. the aggregate server).
29
+ *
30
+ * The Streamable HTTP sink names a **session**, not a held-open response: which
31
+ * HTTP exchange a frame answers is worked out by `HttpSessionTransport`, from
32
+ * the JSON-RPC id. The broker only has to know which session asked.
33
+ */
34
+ export type ResponseSink =
35
+ | { type: "ws"; socket: WebSocket }
36
+ | { type: "sse"; sessionId: string }
37
+ | { type: "http-session"; sessionId: string }
38
+ | { type: "stdio" }
39
+ | { type: "internal"; client: IInternalClient };
40
+
41
+ /**
42
+ * The client-transport handlers a provider route can resolve to.
43
+ *
44
+ * `mcp` covers GET, POST and DELETE at once, because the whole Streamable HTTP
45
+ * state machine is delegated to `StreamableHttpEndpoint`: the broker routes to
46
+ * it and no longer decides per method what to do.
47
+ */
48
+ export type McpEndpointKind = "mcp" | "sse-connect" | "sse-message";
49
+
50
+ /**
51
+ * How `/<slot>/mcp` decides whether a browser origin may reach it.
52
+ *
53
+ * - a list of origins, matched exactly against the whole `Origin` header
54
+ * - a `RegExp`, tested against the whole header
55
+ * - a predicate, when the decision needs more than the string
56
+ */
57
+ export type AllowedOrigins = readonly string[] | RegExp | ((origin: string) => boolean);
58
+
59
+ /** One Streamable HTTP session attached to a provider slot. */
60
+ export interface IHttpSession {
61
+ /** The session's server-side transport, as handed over by the endpoint. */
62
+ readonly transport: IMessageTransport;
63
+
64
+ /**
65
+ * The caller behind the most recent request on this session.
66
+ *
67
+ * Refreshed per request rather than fixed at creation: the frame-level
68
+ * policy check needs the identity that presented the current token, and a
69
+ * session outlives the token it started with.
70
+ */
71
+ principal: IPrincipal | null;
72
+ }
73
+
74
+ /**
75
+ * All mutable state for one named provider slot.
76
+ * Created lazily on first client connection; the WebSocket field is set when
77
+ * the provider actually connects (and cleared on disconnect).
78
+ */
79
+ export interface IProviderState {
80
+ /** The active provider WebSocket, or `null` when the provider is not connected. */
81
+ ws: WebSocket | null;
82
+ /** Pending JSON-RPC request ids → response sinks waiting for a reply. */
83
+ readonly pending: Map<string | number, ResponseSink>;
84
+ /** Active legacy SSE sessions (Claude), keyed by session id. */
85
+ readonly sseSessions: Map<string, ServerResponse>;
86
+ /** Active Streamable HTTP sessions, keyed by `Mcp-Session-Id`. */
87
+ readonly httpSessions: Map<string, IHttpSession>;
88
+ /** Raw WebSocket MCP clients connected to this provider. */
89
+ readonly wsClients: Set<WebSocket>;
90
+ /** In-process clients (e.g. the aggregate server) attached to this slot. */
91
+ readonly internalClients: Set<IInternalClient>;
92
+ /** Lazily built Streamable HTTP endpoint serving this slot. */
93
+ httpEndpoint: StreamableHttpEndpoint | null;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Public types
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * A single static-file mount: serves the contents of `dir` under `urlPrefix`.
102
+ *
103
+ * @example
104
+ * { urlPrefix: "/", dir: "/absolute/path/to/www" }
105
+ * { urlPrefix: "/bundle", dir: "/absolute/path/to/bundle" }
106
+ */
107
+ export interface IStaticMount {
108
+ /** URL prefix that triggers this mount (e.g. `"/"` or `"/bundle"`). */
109
+ urlPrefix: string;
110
+ /** Absolute path to the directory to serve. */
111
+ dir: string;
112
+ }
113
+
114
+ /**
115
+ * In-process client handle for a provider slot: the symmetric counterpart of
116
+ * {@link WsTunnel.registerLoopbackProvider}. Lets a component inside the broker
117
+ * process (e.g. the aggregate server) issue MCP requests to a provider slot and
118
+ * receive both the responses and the provider's broadcast notifications,
119
+ * without opening a real network connection.
120
+ */
121
+ export interface IInternalClient {
122
+ /**
123
+ * Sends a JSON-RPC message to the provider slot. When the message carries an
124
+ * `id`, the matching response is delivered to {@link onMessage}. When the
125
+ * provider is not connected, a JSON-RPC error is delivered synchronously.
126
+ */
127
+ send(message: string): void;
128
+ /** Receives responses to this client's requests and the provider's notifications. */
129
+ onMessage: ((data: string) => void) | null;
130
+ /** Fires when the provider slot loses its connection. */
131
+ onClose: (() => void) | null;
132
+ /** Detaches this internal client; pending requests are dropped. */
133
+ close(): void;
134
+ }
135
+
136
+ /**
137
+ * Configuration options for a {@link WsTunnel} instance.
138
+ */
139
+ export interface IWsTunnelOptions {
140
+ /** TCP port to listen on. */
141
+ port: number;
142
+
143
+ /**
144
+ * Host/interface to bind to.
145
+ * @default "0.0.0.0"
146
+ */
147
+ host?: string;
148
+
149
+ /**
150
+ * URL path **prefix** the MCP provider connects to via WebSocket.
151
+ * Each provider appends its name: `<providerPath>/<encodedName>`.
152
+ * @default "/provider"
153
+ */
154
+ providerPath?: string;
155
+
156
+ /**
157
+ * URL path for **multiplexed** provider connections.
158
+ * A single WebSocket carries traffic for multiple providers using the
159
+ * envelope protocol `{ provider: string, payload: object }`.
160
+ * @default "/providers"
161
+ */
162
+ providersPath?: string;
163
+
164
+ /**
165
+ * URL path raw WebSocket MCP clients connect to.
166
+ * @default "/"
167
+ */
168
+ clientPath?: string;
169
+
170
+ /**
171
+ * **Suffix** appended to a provider name for the SSE endpoint.
172
+ * Full URL: `/<providerName>/sse`
173
+ * @default "/sse"
174
+ */
175
+ ssePath?: string;
176
+
177
+ /**
178
+ * **Suffix** appended to a provider name for the legacy SSE POST endpoint.
179
+ * Full URL: `/<providerName>/messages`
180
+ * @default "/messages"
181
+ */
182
+ messagesPath?: string;
183
+
184
+ /**
185
+ * **Suffix** appended to a provider name for the Streamable HTTP endpoint (MCP 2025-03-26).
186
+ * Full URL: `/<providerName>/mcp`
187
+ * MCP Inspector connects here.
188
+ * @default "/mcp"
189
+ */
190
+ mcpPath?: string;
191
+
192
+ /**
193
+ * URL path that returns a `{ files: string[] }` JSON listing of every file
194
+ * inside the `samples/` subdirectory of the root static mount.
195
+ * @default "/__samples_index__"
196
+ */
197
+ samplesIndexPath?: string;
198
+
199
+ /**
200
+ * Browser origins allowed to reach `/<slot>/mcp`.
201
+ *
202
+ * The MCP specification requires the `Origin` header to be validated,
203
+ * because without it any web page the operator's browser happens to load
204
+ * can drive a broker that machine can reach. A request carrying **no**
205
+ * `Origin` is always allowed: that covers every non-browser client, which
206
+ * is what Claude Desktop, MCP Inspector and the server-side SDKs are.
207
+ *
208
+ * Omit this and every browser origin is refused with `403`. The default is
209
+ * deliberately closed; opening it is an operator decision.
210
+ *
211
+ * @default undefined, no browser origin is allowed
212
+ */
213
+ allowedOrigins?: AllowedOrigins;
214
+
215
+ /**
216
+ * Optional static-file mounts served over plain HTTP.
217
+ * Matched by longest URL prefix; directory requests fall back to `index.html`.
218
+ */
219
+ staticMounts?: IStaticMount[];
220
+
221
+ /**
222
+ * Stdio upstream providers. Each entry spawns a child process and wires its
223
+ * stdin/stdout as an MCP transport. Clients reach the process using its `name`
224
+ * directly.
225
+ *
226
+ * If a WebSocket provider connects with the same name as a stdio upstream, the
227
+ * connection is rejected and a warning is logged, stdio takes priority.
228
+ * @default undefined: no stdio providers
229
+ */
230
+ stdioUpstreams?: IStdioUpstreamConfig[];
231
+
232
+ /** Remote MCP servers reached by URL, exposed as provider slots. */
233
+ remoteUpstreams?: IRemoteUpstreamConfig[];
234
+
235
+ /**
236
+ * Stdio client transport. When set, the broker reads JSON-RPC from
237
+ * `process.stdin` and writes responses to `process.stdout`, bridging an
238
+ * external MCP client (e.g. Claude Desktop) to the named provider.
239
+ *
240
+ * In this mode ALL logging is redirected to stderr so stdout stays clean
241
+ * for the JSON-RPC stream.
242
+ *
243
+ * Claude Desktop config example:
244
+ * ```json
245
+ * {
246
+ * "command": "npx",
247
+ * "args": ["-y", "@cyanmycelium/mcp-broker"],
248
+ * "env": { "MCP_BROKER_STDIO_PROVIDER": "my-provider" }
249
+ * }
250
+ * ```
251
+ * @default undefined, stdio client transport disabled
252
+ */
253
+ stdioClient?: { providerName: string };
254
+
255
+ /**
256
+ * TLS configuration. When provided, the server uses HTTPS and WSS instead of HTTP and WS.
257
+ * Both `cert` and `key` must be PEM-encoded strings (file contents, not file paths).
258
+ * Use {@link WsTunnelBuilder.withTlsFiles} to load from disk paths.
259
+ * @default undefined, plain HTTP/WS
260
+ */
261
+ tls?: {
262
+ /** PEM-encoded TLS certificate. */
263
+ cert: string;
264
+ /** PEM-encoded private key. */
265
+ key: string;
266
+ };
267
+
268
+ /**
269
+ * When `true` (default), the broker exposes itself as an MCP server under the
270
+ * reserved slot `_broker`. Tier-1 behaviors (`broker_info`, `providers_list`,
271
+ * `provider_status`) become callable at `<host>/_broker/mcp`.
272
+ *
273
+ * Set to `false` to keep the broker invisible to MCP clients.
274
+ * @default true
275
+ */
276
+ enableBrokerProvider?: boolean;
277
+
278
+ /**
279
+ * When `true` (default), the broker exposes the reserved slot `_all`: an
280
+ * aggregate MCP server that unions the tools and prompts of every provider
281
+ * that opted in via the registration handshake. Reachable like any other
282
+ * slot (`<host>/_all/mcp`, etc.).
283
+ *
284
+ * Set to `false` to disable aggregation entirely.
285
+ * @default true
286
+ */
287
+ enableAggregateProvider?: boolean;
288
+
289
+ /**
290
+ * Logical name reported by `broker_info`. Useful when running multiple
291
+ * broker instances and you want to tell them apart from the agent side
292
+ * (e.g. `"broker-eu-west"`).
293
+ * @default PACKAGE_NAME, `@cyanmycelium/mcp-broker`
294
+ */
295
+ brokerName?: string;
296
+
297
+ /**
298
+ * Overrides for the embedded broker server's grammar resolver, passed
299
+ * straight through to `mcp-core`'s `grammarResolverFromOptions`. The
300
+ * broker installs its own default `localeSource` (reads
301
+ * `process.env.MCP_BROKER_LOCALE`); anything you set here wins.
302
+ *
303
+ * Use this to inject a custom `localeSource` (e.g. read from an HTTP
304
+ * header proxied by your transport), enable the optional `versionFrom`
305
+ * dimension, or extend the `agents` map with additional LLM families.
306
+ */
307
+ brokerGrammarResolverOptions?: Partial<GrammarResolverOptions>;
308
+
309
+ /**
310
+ * Path to a user-supplied grammars directory whose `<userAgent>/<locale>.json`
311
+ * files are registered alongside the packaged grammars used by the
312
+ * embedded broker server. Typically pointed at `.mcp-broker/grammars/`.
313
+ *
314
+ * The candidate-chain resolution in `McpServer.initialize`
315
+ * (mcp-core@0.3.0) handles cascade across user-agent and locale
316
+ * dimensions, so partial files no longer need to be pre-merged with a
317
+ * baseline.
318
+ */
319
+ brokerLocalGrammarsDir?: string;
320
+
321
+ /**
322
+ * OAuth 2.1 resource-server authorization. When set, every HTTP client
323
+ * request to a slot (`/<slot>/mcp`, `/<slot>/sse`, `/<slot>/messages`) must
324
+ * carry a valid `Authorization: Bearer` token issued for that slot, and the
325
+ * broker publishes Protected Resource Metadata (RFC 9728) under
326
+ * `/.well-known/oauth-protected-resource/<slot>/<mcp>`.
327
+ *
328
+ * When `undefined` (default), the broker performs **no** authentication ,
329
+ * appropriate only behind a trusted network boundary.
330
+ */
331
+ auth?: IResolvedAuth;
332
+
333
+ /**
334
+ * Authenticates **providers** (engines) connecting to `/provider/<slot>` and
335
+ * the multiplexed `/providers` socket. Independent of {@link auth} (which
336
+ * guards clients): set this to stop strangers from occupying a free slot and
337
+ * impersonating the real engine.
338
+ *
339
+ * When `undefined` (default), provider connections are **not** authenticated.
340
+ */
341
+ providerAuth?: IProviderAuthenticator;
342
+
343
+ /** Hierarchical policy runtime. Absent preserves legacy OAuth behavior. */
344
+ authorization?: IPolicyAuthorization;
345
+
346
+ /** Slot-to-resource resolver also used for provider namespace restrictions. */
347
+ slotResourceResolver?: ISlotResourceResolver;
348
+ }
349
+
350
+ /** @deprecated Use {@link IStaticMount}. */
351
+ export type StaticMount = IStaticMount;
352
+
353
+ /** @deprecated Use {@link IInternalClient}. */
354
+ export type InternalClient = IInternalClient;
355
+
356
+ /** @deprecated Use {@link IWsTunnelOptions}. */
357
+ export type WsTunnelOptions = IWsTunnelOptions;
@@ -1,8 +1,9 @@
1
1
  import * as fs from "fs";
2
- import { WsTunnel, type IWsTunnelOptions, type IStaticMount } from "./ws.tunnel.js";
3
- import type { IStdioUpstreamConfig } from "./stdio.upstream.js";
4
- import type { IRemoteUpstreamConfig } from "./remote.upstream.js";
5
- import { buildJwtAuth, SharedSecretProviderAuthenticator, type IJwtAuthOptions, type IResolvedAuth, type IProviderAuthenticator } from "./auth/index.js";
2
+ import { WsTunnel } from "./ws.tunnel";
3
+ import type { AllowedOrigins, IStaticMount, IWsTunnelOptions } from "./ws.interfaces";
4
+ import type { IStdioUpstreamConfig } from "../stdio.upstream";
5
+ import type { IRemoteUpstreamConfig } from "../remote.upstream";
6
+ import { buildJwtAuth, SharedSecretProviderAuthenticator, type IJwtAuthOptions, type IResolvedAuth, type IProviderAuthenticator } from "../auth/index";
6
7
  import {
7
8
  authorizationWithEngine,
8
9
  compileAuthorizationPolicy,
@@ -10,7 +11,7 @@ import {
10
11
  type IPolicyAuthorization,
11
12
  type IPolicyEngine,
12
13
  type ISlotResourceResolver,
13
- } from "./authorization/index.js";
14
+ } from "../authorization/index";
14
15
 
15
16
  /**
16
17
  * Fluent builder that constructs a configured {@link WsTunnel}.
@@ -39,6 +40,7 @@ export class WsTunnelBuilder {
39
40
  private _messagesPath = "/messages";
40
41
  private _mcpPath = "/mcp";
41
42
  private _samplesIndexPath = "/__samples_index__";
43
+ private _allowedOrigins: AllowedOrigins | undefined = undefined;
42
44
  private _staticMounts: IStaticMount[] = [];
43
45
  private _stdioUpstreams: IStdioUpstreamConfig[] = [];
44
46
  private _remoteUpstreams: IRemoteUpstreamConfig[] = [];
@@ -131,6 +133,26 @@ export class WsTunnelBuilder {
131
133
  return this;
132
134
  }
133
135
 
136
+ /**
137
+ * Allows browser origins to reach `/<slot>/mcp`.
138
+ *
139
+ * Without this call no browser origin is accepted, which is the safe
140
+ * default: a request carrying an `Origin` is refused with `403`, while one
141
+ * carrying none (any non-browser client) always passes. Call it only when a
142
+ * web page really has to talk to the broker over HTTP.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * builder.withAllowedOrigins(["https://app.example.com"]);
147
+ * builder.withAllowedOrigins(/^https:\/\/[a-z0-9-]+\.example\.com$/);
148
+ * builder.withAllowedOrigins((origin) => origin.endsWith(".example.com"));
149
+ * ```
150
+ */
151
+ withAllowedOrigins(allowed: AllowedOrigins): this {
152
+ this._allowedOrigins = allowed;
153
+ return this;
154
+ }
155
+
134
156
  /**
135
157
  * Adds a static-file mount served over plain HTTP.
136
158
  * Can be called multiple times; longest-prefix match wins at runtime.
@@ -288,6 +310,7 @@ export class WsTunnelBuilder {
288
310
  messagesPath: this._messagesPath,
289
311
  mcpPath: this._mcpPath,
290
312
  samplesIndexPath: this._samplesIndexPath,
313
+ allowedOrigins: this._allowedOrigins,
291
314
  staticMounts: this._staticMounts.length > 0 ? [...this._staticMounts] : undefined,
292
315
  stdioUpstreams: this._stdioUpstreams.length > 0 ? [...this._stdioUpstreams] : undefined,
293
316
  remoteUpstreams: this._remoteUpstreams.length > 0 ? [...this._remoteUpstreams] : undefined,