@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.
- package/.mcp-broker.example/README.md +3 -3
- package/README.md +6 -2
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +176 -253
- package/dist/bin.js.map +1 -1
- package/dist/chunk-FTDKH2C4.js +3670 -0
- package/dist/chunk-FTDKH2C4.js.map +1 -0
- package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
- package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
- package/dist/index.d.ts +1790 -22
- package/dist/index.js +2 -18
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/scripts/copy-assets.mjs +16 -10
- package/scripts/gen-cert.mjs +5 -5
- package/scripts/pack-mcpb.mjs +5 -5
- package/scripts/sign-bundle.mjs +6 -6
- package/src/auth/auth.config.ts +3 -3
- package/src/auth/auth.types.ts +34 -59
- package/src/auth/http.auth.ts +18 -22
- package/src/auth/index.ts +12 -12
- package/src/auth/jwt.validator.ts +3 -3
- package/src/auth/provider.auth.ts +1 -1
- package/src/auth/resource.metadata.ts +14 -21
- package/src/authorization/audit.ts +2 -2
- package/src/authorization/capability.classifier.ts +1 -1
- package/src/authorization/index.ts +13 -13
- package/src/authorization/policy.engine.ts +3 -3
- package/src/authorization/policy.types.ts +1 -1
- package/src/authorization/runtime.ts +5 -5
- package/src/authorization/slot.resource.ts +1 -1
- package/src/authorization/subject.mapper.ts +1 -1
- package/src/bin.ts +38 -8
- package/src/broker/adapters/broker.adapter.info.ts +1 -1
- package/src/broker/adapters/broker.adapter.providers.ts +1 -1
- package/src/broker/aggregate/aggregate.catalog.ts +1 -1
- package/src/broker/aggregate/aggregate.server.ts +7 -7
- package/src/broker/aggregate/provider.client.session.ts +2 -2
- package/src/broker/behaviors/broker.behavior.info.ts +3 -3
- package/src/broker/behaviors/broker.behavior.providers.ts +5 -5
- package/src/broker/broker.grammars.ts +9 -9
- package/src/broker/broker.server.ts +6 -6
- package/src/broker/grammars/claude/en.json +1 -1
- package/src/broker/grammars/claude/fr.json +1 -1
- package/src/broker/index.ts +9 -9
- package/src/config.ts +25 -3
- package/src/index.ts +24 -24
- package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +11 -11
- package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
- package/src/remote.transports.ts +3 -3
- package/src/remote.upstream.ts +3 -3
- package/src/stdio.upstream.ts +2 -2
- package/src/upstream.ts +2 -2
- package/src/ws/ws.interfaces.ts +357 -0
- package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +28 -5
- package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +231 -467
- package/web/README.md +3 -3
- package/web/broker-self-mcp.html +9 -9
- package/web/css/styles.css +1 -1
- package/web/demos/DemoPlaceholder.html +6 -6
- package/web/demos/broker-explorer/css/app.css +1 -1
- package/web/demos/broker-explorer/index.html +2 -2
- package/web/demos/broker-explorer/js/app.js +4 -4
- package/web/demos/broker-explorer/js/mcp-ws-client.js +2 -2
- package/web/demos/oauth-lab/README.md +26 -0
- package/web/demos/oauth-lab/config.json +1 -0
- package/web/demos/oauth-lab/js/app.js +40 -16
- package/web/demos/oauth-lab/server/smoke-test.mjs +57 -10
- package/web/demos/provider-tunnel/css/app.css +1 -1
- package/web/demos/provider-tunnel/index.html +3 -3
- package/web/demos/provider-tunnel/js/app.js +10 -10
- package/web/demos/provider-tunnel/js/toolbox-server.js +5 -5
- package/web/index.html +11 -11
- package/web/js/lib/broker-tunnel.js +12 -12
- package/dist/auth/auth.config.d.ts +0 -40
- package/dist/auth/auth.config.js +0 -62
- package/dist/auth/auth.config.js.map +0 -1
- package/dist/auth/auth.types.d.ts +0 -119
- package/dist/auth/auth.types.js +0 -38
- package/dist/auth/auth.types.js.map +0 -1
- package/dist/auth/http.auth.d.ts +0 -51
- package/dist/auth/http.auth.js +0 -121
- package/dist/auth/http.auth.js.map +0 -1
- package/dist/auth/index.d.ts +0 -16
- package/dist/auth/index.js +0 -12
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/jwt.validator.d.ts +0 -35
- package/dist/auth/jwt.validator.js +0 -46
- package/dist/auth/jwt.validator.js.map +0 -1
- package/dist/auth/provider.auth.d.ts +0 -51
- package/dist/auth/provider.auth.js +0 -80
- package/dist/auth/provider.auth.js.map +0 -1
- package/dist/auth/resource.metadata.d.ts +0 -32
- package/dist/auth/resource.metadata.js +0 -26
- package/dist/auth/resource.metadata.js.map +0 -1
- package/dist/authorization/audit.d.ts +0 -14
- package/dist/authorization/audit.js +0 -20
- package/dist/authorization/audit.js.map +0 -1
- package/dist/authorization/capability.classifier.d.ts +0 -30
- package/dist/authorization/capability.classifier.js +0 -85
- package/dist/authorization/capability.classifier.js.map +0 -1
- package/dist/authorization/index.d.ts +0 -13
- package/dist/authorization/index.js +0 -8
- package/dist/authorization/index.js.map +0 -1
- package/dist/authorization/policy.engine.d.ts +0 -11
- package/dist/authorization/policy.engine.js +0 -192
- package/dist/authorization/policy.engine.js.map +0 -1
- package/dist/authorization/policy.types.d.ts +0 -92
- package/dist/authorization/policy.types.js +0 -2
- package/dist/authorization/policy.types.js.map +0 -1
- package/dist/authorization/resource.path.d.ts +0 -27
- package/dist/authorization/resource.path.js +0 -122
- package/dist/authorization/resource.path.js.map +0 -1
- package/dist/authorization/runtime.d.ts +0 -22
- package/dist/authorization/runtime.js +0 -34
- package/dist/authorization/runtime.js.map +0 -1
- package/dist/authorization/slot.resource.d.ts +0 -12
- package/dist/authorization/slot.resource.js +0 -41
- package/dist/authorization/slot.resource.js.map +0 -1
- package/dist/authorization/subject.mapper.d.ts +0 -15
- package/dist/authorization/subject.mapper.js +0 -67
- package/dist/authorization/subject.mapper.js.map +0 -1
- package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
- package/dist/broker/adapters/broker.adapter.info.js +0 -43
- package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
- package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
- package/dist/broker/adapters/broker.adapter.providers.js +0 -61
- package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
- package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -66
- package/dist/broker/aggregate/aggregate.catalog.js +0 -119
- package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
- package/dist/broker/aggregate/aggregate.server.d.ts +0 -66
- package/dist/broker/aggregate/aggregate.server.js +0 -257
- package/dist/broker/aggregate/aggregate.server.js.map +0 -1
- package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
- package/dist/broker/aggregate/provider.client.session.js +0 -140
- package/dist/broker/aggregate/provider.client.session.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
- package/dist/broker/behaviors/broker.behavior.info.js +0 -41
- package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
- package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
- package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
- package/dist/broker/broker.context.d.ts +0 -63
- package/dist/broker/broker.context.js +0 -2
- package/dist/broker/broker.context.js.map +0 -1
- package/dist/broker/broker.grammars.d.ts +0 -132
- package/dist/broker/broker.grammars.js +0 -229
- package/dist/broker/broker.grammars.js.map +0 -1
- package/dist/broker/broker.server.d.ts +0 -68
- package/dist/broker/broker.server.js +0 -73
- package/dist/broker/broker.server.js.map +0 -1
- package/dist/broker/index.d.ts +0 -9
- package/dist/broker/index.js +0 -7
- package/dist/broker/index.js.map +0 -1
- package/dist/config.d.ts +0 -182
- package/dist/config.js +0 -61
- package/dist/config.js.map +0 -1
- package/dist/mcpb.loader.d.ts +0 -26
- package/dist/mcpb.loader.js +0 -161
- package/dist/mcpb.loader.js.map +0 -1
- package/dist/mcpb.unzip.d.ts +0 -6
- package/dist/mcpb.unzip.js +0 -95
- package/dist/mcpb.unzip.js.map +0 -1
- package/dist/remote.transports.d.ts +0 -18
- package/dist/remote.transports.js +0 -297
- package/dist/remote.transports.js.map +0 -1
- package/dist/remote.upstream.d.ts +0 -38
- package/dist/remote.upstream.js +0 -52
- package/dist/remote.upstream.js.map +0 -1
- package/dist/stdio.upstream.d.ts +0 -47
- package/dist/stdio.upstream.js +0 -85
- package/dist/stdio.upstream.js.map +0 -1
- package/dist/upstream.d.ts +0 -35
- package/dist/upstream.js +0 -2
- package/dist/upstream.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -9
- package/dist/version.js.map +0 -1
- package/dist/ws.tunnel.builder.d.ts +0 -176
- package/dist/ws.tunnel.builder.js +0 -271
- package/dist/ws.tunnel.builder.js.map +0 -1
- package/dist/ws.tunnel.d.ts +0 -455
- package/dist/ws.tunnel.js +0 -1425
- package/dist/ws.tunnel.js.map +0 -1
- /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
package/src/remote.transports.ts
CHANGED
|
@@ -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
|
|
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
|
|
247
|
+
/* not JSON, forward verbatim */
|
|
248
248
|
}
|
|
249
249
|
this.onMessage?.(trimmed);
|
|
250
250
|
}
|
package/src/remote.upstream.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { IUpstream } from "./upstream
|
|
2
|
-
import { createRemoteTransport, detectTransport, type IRemoteTransport, type RemoteTransportKind } from "./remote.transports
|
|
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
|
|
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;
|
package/src/stdio.upstream.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, ChildProcess } from "node:child_process";
|
|
2
|
-
import type { IUpstream } from "./upstream
|
|
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
|
|
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`
|
|
9
|
-
* - `RemoteUpstream
|
|
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
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
5
|
-
import
|
|
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 "
|
|
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,
|