@cyanmycelium/mcp-broker-provider 0.1.0 → 0.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/README.md +58 -10
- package/dist/index.d.ts +211 -8
- package/dist/index.js +569 -11
- package/dist/index.js.map +1 -1
- package/dist/protocol/index.d.ts +118 -1
- package/dist/protocol/index.js +57 -1
- package/dist/protocol/index.js.map +1 -1
- package/package.json +8 -7
- package/src/direct.transport.ts +141 -5
- package/src/index.ts +2 -2
- package/src/multiplex.transport.ts +230 -20
- package/src/protocol/envelope.ts +47 -3
- package/src/transport.support.ts +265 -0
- package/dist/direct.transport.d.ts +0 -28
- package/dist/direct.transport.js +0 -55
- package/dist/direct.transport.js.map +0 -1
- package/dist/multiplex.transport.d.ts +0 -81
- package/dist/multiplex.transport.js +0 -217
- package/dist/multiplex.transport.js.map +0 -1
- package/dist/protocol/envelope.d.ts +0 -85
- package/dist/protocol/envelope.js +0 -107
- package/dist/protocol/envelope.js.map +0 -1
package/README.md
CHANGED
|
@@ -32,21 +32,25 @@ A multiplexed tunnel socket carries traffic for several providers at once, so ev
|
|
|
32
32
|
{ "provider": "scene-1", "payload": { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } }
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`./protocol` is the single definition of that format
|
|
35
|
+
`./protocol` is the single definition of that format, and the broker imports it from here rather than re-declaring the shape inline, so the two ends cannot drift. The broker depends on this package because it is itself a provider: it publishes its own `_broker` introspection slot and its `_all` aggregate slot.
|
|
36
36
|
|
|
37
37
|
Beyond the envelope it also covers:
|
|
38
38
|
|
|
39
|
-
- `notifications/register`, sent as soon as the tunnel opens to claim a provider slot. Without it the broker only learns a provider name on its first real message, and an MCP client connecting in between is told the provider is not connected.
|
|
40
|
-
- The tunnel error codes: `-32001` when the provider's credentials do not allow publishing on the requested slot, `-32000` when the slot is unavailable.
|
|
39
|
+
- `notifications/register`, sent as soon as the tunnel opens to claim a provider slot. Without it the broker only learns a provider name on its first real message, and an MCP client connecting in between is told the provider is not connected. It optionally carries `params: { aggregate: true }`, which also joins the broker's `_all` slot, see [Joining the `_all` aggregate slot](#joining-the-_all-aggregate-slot).
|
|
40
|
+
- The tunnel error codes: `-32001` when the provider's credentials do not allow publishing on the requested slot, `-32000` when the slot is unavailable.
|
|
41
|
+
|
|
42
|
+
When the broker refuses a slot, the transport surfaces it through `onError` rather than forwarding it. An id-less error frame handed to an MCP server would be classified as an unknown notification and dropped without a word, so the publisher would never learn it was refused.
|
|
41
43
|
|
|
42
44
|
Malformed frames decode to `undefined` rather than throwing: a tunnel socket is a public surface, and a peer sending garbage must not take the receiver down.
|
|
43
45
|
|
|
44
46
|
## Transports
|
|
45
47
|
|
|
46
|
-
| Transport | Use case |
|
|
47
|
-
|
|
48
|
-
| `MultiplexTransport` | Several servers published by one application. A single socket carries them all, keyed by slot name |
|
|
49
|
-
| `DirectTransport` |
|
|
48
|
+
| Transport | Endpoint it speaks to | Framing | Reconnects | Use case |
|
|
49
|
+
|---|---|---|---|---|
|
|
50
|
+
| `MultiplexTransport` | the shared multiplex base, `ws://<broker>/providers` | envelopes `{ provider, payload }` | yes, on the shared socket | Several servers published by one application. A single socket carries them all, keyed by slot name |
|
|
51
|
+
| `DirectTransport` | a slot-scoped path, `ws://<broker>/provider/<name>` | plain JSON-RPC frames | **no** | One server, one socket |
|
|
52
|
+
|
|
53
|
+
**The transport and the path are a pair, not a preference.** The broker decides framing from the endpoint the socket landed on, so a mismatch does not fail the handshake: the socket opens, looks healthy, and every frame is dropped on one side or the other with nothing logged by the broker. Both transports warn on the console when they spot the mismatch at connect time. And `ws://<broker>/providers/<name>`, the natural-looking blend of the two, is neither: the broker accepts it as a *client* connection on a slot of that name.
|
|
50
54
|
|
|
51
55
|
```ts
|
|
52
56
|
import { McpServerBuilder } from "@cyanmycelium/mcp-core/server";
|
|
@@ -61,15 +65,59 @@ const server = new McpServerBuilder()
|
|
|
61
65
|
await server.start();
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
Transports created for the same tunnel URL share one WebSocket, whichever order they are opened in. Reconnection is handled by that shared socket, with exponential back-off and jitter
|
|
68
|
+
Transports created for the same tunnel URL share one WebSocket, whichever order they are opened in. Reconnection is handled by that shared socket, with exponential back-off and jitter, and individual transports never reconnect on their own. `DirectTransport` does not reconnect at all: when its socket closes it stays closed, and the application decides whether to call `connect()` again.
|
|
69
|
+
|
|
70
|
+
`server.start()` resolving means the transport reported itself open, not that the broker accepted the slot. A refusal arrives afterwards, and reaches you as an `onError` on the transport and a line on the console.
|
|
65
71
|
|
|
66
72
|
`@cyanmycelium/mcp-core` is a peer dependency: the transports import its `IMessageTransport` type and nothing else at runtime, so your application keeps a single copy of it.
|
|
67
73
|
|
|
74
|
+
## Joining the `_all` aggregate slot
|
|
75
|
+
|
|
76
|
+
The broker publishes an aggregate slot, `_all`, which exposes every opted-in provider's tools and prompts through one MCP connection. Membership is opt-in, because `_all` is a confidentiality boundary: a provider that does not ask for it stays reachable only on its own slot.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { DirectTransport, MultiplexTransport } from "@cyanmycelium/mcp-broker-provider";
|
|
80
|
+
|
|
81
|
+
// One socket per server
|
|
82
|
+
const direct = new DirectTransport("ws://localhost:3000/provider/scene-1", { aggregate: true });
|
|
83
|
+
|
|
84
|
+
// Or on the shared tunnel
|
|
85
|
+
const shared = MultiplexTransport.create("scene-1", "ws://localhost:3000/providers", { aggregate: true });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Either form sends the registration notification with `params: { aggregate: true }` as its first frame:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{ "jsonrpc": "2.0", "method": "notifications/register", "params": { "aggregate": true } }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`DirectTransport` sends it verbatim, since the slot-scoped path carries plain JSON-RPC and the broker already knows the slot name from the URL. `MultiplexTransport` sends it inside the usual envelope, as `{ "provider": "scene-1", "payload": { ... } }`.
|
|
95
|
+
|
|
96
|
+
**Wire the message handler before you connect.** The broker runs `initialize` against a newly aggregated provider immediately, and a provider that does not answer is dropped from `_all` silently. Handing the transport to an MCP server does this for you, since the server assigns `onMessage` before calling `connect()`. Assigning it yourself, after connecting, loses the handshake and the provider never appears in the aggregate.
|
|
97
|
+
|
|
98
|
+
## Diagnostics
|
|
99
|
+
|
|
100
|
+
This stack used to fail quietly. The transports now name what went wrong, on the console, because a browser-hosted provider has nowhere else to report:
|
|
101
|
+
|
|
102
|
+
- A frame written before the socket is open is queued (64 frames, oldest dropped first with a warning) and flushed on open, rather than discarded. That window covers the whole reconnect back-off, up to 30 seconds.
|
|
103
|
+
- A close with a code other than `1000` is reported through `onError` with the code and the broker's own reason, before `onClose`. A `1008` is a policy refusal: the slot is already connected, is reserved, or provider authentication rejected it.
|
|
104
|
+
- An incoming frame that is not an envelope, or one for a slot this socket does not publish, is logged with the likely cause and the fix. Repeats are sampled (the first in full, then one in fifty) so a mismatched tunnel cannot flood the console.
|
|
105
|
+
|
|
106
|
+
## In a browser
|
|
107
|
+
|
|
108
|
+
The transports are written for the browser, but the npm path needs a bundler: neither this package nor `@cyanmycelium/mcp-core` ships a UMD build or declares a `browser` field, so a bare `<script>` tag will not load them. Any bundler works; there is nothing to configure beyond resolving the two packages.
|
|
109
|
+
|
|
110
|
+
Import only the isomorphic entry points. `@cyanmycelium/mcp-core/server` and `@cyanmycelium/mcp-core/client` run in a browser; `@cyanmycelium/mcp-core/node` does not, and pulling it in is the usual cause of a build that fails on Node built-ins.
|
|
111
|
+
|
|
112
|
+
If you would rather not add a build step, the broker ships a dependency-free ES module that speaks the same tunnel, `web/js/lib/broker-tunnel.js` (inside the `@cyanmycelium/mcp-broker` package, at `node/packages/broker/web` in the repository). It is the zero-build alternative, not a replacement: it carries no MCP server implementation.
|
|
113
|
+
|
|
114
|
+
One limitation worth knowing before you deploy: if the broker is configured with provider authentication, a browser-hosted provider cannot connect. The broker reads its credential from the `X-Provider-Token` or `Authorization` header of the upgrade request, and the browser `WebSocket` constructor cannot set headers, so the handshake is refused with a 401 the page sees only as a generic error. A browser provider needs provider auth off, or an authenticating reverse proxy in front of the broker.
|
|
115
|
+
|
|
68
116
|
## Status
|
|
69
117
|
|
|
70
|
-
|
|
118
|
+
This package is the only home of the tunnel transports. They also shipped in `@cyanmycelium/mcp-core@0.4.x`, were removed there in `0.5.0`, and are absent from `0.7.x` and `1.x`, so migrate those imports here before upgrading `mcp-core`.
|
|
71
119
|
|
|
72
|
-
The protocol is shared with the broker and
|
|
120
|
+
The protocol is shared with the broker, and later with the consumer side. Import it through the `./protocol` subpath rather than the package root, so it can move to a package of its own one day without touching your call sites.
|
|
73
121
|
|
|
74
122
|
## License
|
|
75
123
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,214 @@
|
|
|
1
|
+
export { ITunnelRegisterOptions, TUNNEL_REGISTER_METHOD, TunnelEnvelope, TunnelError, TunnelErrorCode, TunnelErrorCodes, decodeEnvelope, encodeEnvelope, encodeEnvelopeMessage, encodeErrorEnvelope, encodeRegisterEnvelope, encodeRegisterFrame, envelopeFrame, tunnelErrorOf } from './protocol/index.js';
|
|
2
|
+
import { IMessageTransport } from '@cyanmycelium/mcp-core';
|
|
3
|
+
|
|
4
|
+
/** Options accepted by {@link DirectTransport}. */
|
|
5
|
+
interface IDirectTransportOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Join the broker's `_all` aggregate slot as well as this provider's own
|
|
8
|
+
* slot, by sending the registration notification
|
|
9
|
+
* `{"jsonrpc":"2.0","method":"notifications/register","params":{"aggregate":true}}`
|
|
10
|
+
* as the first frame on the socket.
|
|
11
|
+
*
|
|
12
|
+
* Opt-in on purpose: `_all` exposes this provider's tools and prompts to
|
|
13
|
+
* every client of the aggregate slot, so a provider that does not ask for it
|
|
14
|
+
* stays reachable only on its own slot.
|
|
15
|
+
*
|
|
16
|
+
* ORDERING: the broker runs `initialize` against a newly aggregated provider
|
|
17
|
+
* immediately, and drops it from `_all` without a word if the handshake times
|
|
18
|
+
* out. The frame therefore goes out on `open`, never from the constructor, so
|
|
19
|
+
* assign `onMessage` (or hand this transport to an MCP server, which assigns
|
|
20
|
+
* it for you) *before* calling {@link DirectTransport.connect}. Connecting
|
|
21
|
+
* first and wiring the handler afterwards loses the broker's `initialize` and
|
|
22
|
+
* the provider silently never appears in `_all`.
|
|
23
|
+
*/
|
|
24
|
+
aggregate?: boolean;
|
|
25
|
+
}
|
|
1
26
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
27
|
+
* 1:1 WebSocket transport, wraps a single `WebSocket` connection to a broker
|
|
28
|
+
* provider slot, typically `ws://<broker>/provider/<name>`.
|
|
4
29
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
30
|
+
* One server owns one socket. When an application publishes several servers
|
|
31
|
+
* through the same broker, prefer {@link MultiplexTransport}, which shares a
|
|
32
|
+
* single socket between them.
|
|
33
|
+
*
|
|
34
|
+
* This transport does **not** reconnect: when the socket closes, it stays
|
|
35
|
+
* closed until the application calls {@link connect} again. Only
|
|
36
|
+
* {@link MultiplexTransport}'s shared socket reconnects on its own.
|
|
37
|
+
*
|
|
38
|
+
* Call {@link connect} after setting the event callbacks to open the socket.
|
|
39
|
+
*/
|
|
40
|
+
declare class DirectTransport implements IMessageTransport {
|
|
41
|
+
private readonly _wsUrl;
|
|
42
|
+
private readonly _aggregate;
|
|
43
|
+
private readonly _pending;
|
|
44
|
+
/** Throttles the "wrote to a closed transport" line, which repeats per frame. */
|
|
45
|
+
private readonly _afterCloseNotice;
|
|
46
|
+
private _ws;
|
|
47
|
+
private _closed;
|
|
48
|
+
onMessage: ((data: string) => void) | null;
|
|
49
|
+
onOpen: (() => void) | null;
|
|
50
|
+
onClose: (() => void) | null;
|
|
51
|
+
onError: ((error: Error) => void) | null;
|
|
52
|
+
constructor(wsUrl: string, options?: IDirectTransportOptions);
|
|
53
|
+
get isOpen(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Opens the WebSocket connection and wires its events to the transport
|
|
56
|
+
* callbacks. Must be called after assigning `onOpen` / `onMessage` / etc.
|
|
57
|
+
*/
|
|
58
|
+
connect(): void;
|
|
59
|
+
send(data: string): void;
|
|
60
|
+
close(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Sends the registration notification when the caller asked for a specific
|
|
63
|
+
* aggregate membership. The slot name is not in the frame: on the
|
|
64
|
+
* slot-scoped path the broker takes it from the URL at connect time.
|
|
65
|
+
*/
|
|
66
|
+
private _sendRegistration;
|
|
67
|
+
/** Writes out everything queued while the socket was connecting. */
|
|
68
|
+
private _flush;
|
|
69
|
+
/**
|
|
70
|
+
* Builds the message for a close worth reporting, or `undefined` when there
|
|
71
|
+
* is nothing to say.
|
|
72
|
+
*
|
|
73
|
+
* A code of 1000 is a normal close and stays silent. An environment that
|
|
74
|
+
* calls `onclose` with no event at all leaves nothing to distinguish a
|
|
75
|
+
* refusal from a clean shutdown, so that stays silent too.
|
|
76
|
+
*/
|
|
77
|
+
private _closeError;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Manages a single WebSocket connection shared by multiple {@link MultiplexTransport}
|
|
82
|
+
* instances. All traffic goes through the tunnel envelope protocol, whose
|
|
83
|
+
* definition lives in `./protocol` and is shared with the broker.
|
|
84
|
+
*
|
|
85
|
+
* Reconnection is handled centrally here, individual transports do not reconnect.
|
|
86
|
+
* Use {@link getOrCreate} to obtain a per-URL singleton.
|
|
87
|
+
*/
|
|
88
|
+
declare class MultiplexSocket {
|
|
89
|
+
/** Per-URL cache so all transports targeting the same tunnel share one socket. */
|
|
90
|
+
private static readonly _instances;
|
|
91
|
+
private readonly _wsUrl;
|
|
92
|
+
private readonly _transports;
|
|
93
|
+
/** Aggregate opt-in per slot, absent when the caller did not express one. */
|
|
94
|
+
private readonly _aggregates;
|
|
95
|
+
/** Frames written while the socket is connecting, reconnecting or backing off. */
|
|
96
|
+
private readonly _pending;
|
|
97
|
+
private _ws;
|
|
98
|
+
private _reconnectAttempts;
|
|
99
|
+
private _reconnectTimer;
|
|
100
|
+
private _stopped;
|
|
101
|
+
/** Set once the last transport left and this instance gave up its URL. */
|
|
102
|
+
private _dead;
|
|
103
|
+
/** The URL guard is about the URL, not the socket, so it is said once. */
|
|
104
|
+
private _pathWarned;
|
|
105
|
+
/** Throttles the "wrote to a closed tunnel" line, which repeats per frame. */
|
|
106
|
+
private readonly _afterStopNotice;
|
|
107
|
+
private constructor();
|
|
108
|
+
/** Returns (or creates) the singleton socket for a given tunnel URL. */
|
|
109
|
+
static getOrCreate(wsUrl: string): MultiplexSocket;
|
|
110
|
+
get isOpen(): boolean;
|
|
111
|
+
register(name: string, transport: MultiplexTransport, aggregate?: boolean): void;
|
|
112
|
+
unregister(name: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Brings a torn-down instance back into service when a transport that
|
|
115
|
+
* captured it is reactivated.
|
|
116
|
+
*
|
|
117
|
+
* Reclaims the URL when nothing else holds it. When another instance already
|
|
118
|
+
* does, the two would race for the same slot names, so this says exactly that
|
|
119
|
+
* rather than letting the loser fail with a refusal nobody reads.
|
|
120
|
+
*/
|
|
121
|
+
private _revive;
|
|
122
|
+
/**
|
|
123
|
+
* Claims the slot for `name` so the broker eagerly creates its provider
|
|
124
|
+
* state before any MCP client connects. Without it the broker only learns
|
|
125
|
+
* about a provider on its first real message, and a client connecting in
|
|
126
|
+
* between is told the provider is not connected.
|
|
127
|
+
*
|
|
128
|
+
* Carries the aggregate opt-in when the caller expressed one, which is why
|
|
129
|
+
* it must go out before any traffic: the broker runs `initialize` against a
|
|
130
|
+
* newly aggregated provider straight away.
|
|
131
|
+
*/
|
|
132
|
+
private _announceProvider;
|
|
133
|
+
send(provider: string, data: string): void;
|
|
134
|
+
private _connect;
|
|
135
|
+
/** Writes out everything queued while the socket was down. */
|
|
136
|
+
private _flush;
|
|
137
|
+
private _routeIncoming;
|
|
138
|
+
private _scheduleReconnect;
|
|
139
|
+
/** Disarms a pending reconnect, so nothing opens a socket behind our back. */
|
|
140
|
+
private _cancelReconnect;
|
|
141
|
+
}
|
|
142
|
+
/** Options accepted by {@link MultiplexTransport.create}. */
|
|
143
|
+
interface IMultiplexTransportOptions {
|
|
144
|
+
/**
|
|
145
|
+
* Join the broker's `_all` aggregate slot as well as this provider's own
|
|
146
|
+
* slot, by carrying `params: { aggregate: true }` on the registration
|
|
147
|
+
* notification the shared socket sends when it claims the slot.
|
|
148
|
+
*
|
|
149
|
+
* Opt-in on purpose: `_all` exposes this provider's tools and prompts to
|
|
150
|
+
* every client of the aggregate slot, so a provider that does not ask for it
|
|
151
|
+
* stays reachable only on its own slot.
|
|
152
|
+
*
|
|
153
|
+
* ORDERING: the broker runs `initialize` against a newly aggregated provider
|
|
154
|
+
* immediately, and drops it from `_all` without a word if the handshake times
|
|
155
|
+
* out. The registration goes out from {@link MultiplexTransport.connect}, so
|
|
156
|
+
* assign `onMessage` (or hand this transport to an MCP server, which assigns
|
|
157
|
+
* it for you) *before* connecting. Connecting first and wiring the handler
|
|
158
|
+
* afterwards loses the broker's `initialize`, and the provider silently never
|
|
159
|
+
* appears in `_all`.
|
|
160
|
+
*/
|
|
161
|
+
aggregate?: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A transport that multiplexes multiple MCP servers over a single shared
|
|
165
|
+
* WebSocket connection using the envelope protocol `{ provider, payload }`.
|
|
166
|
+
*
|
|
167
|
+
* Use the static {@link create} factory to obtain an instance:
|
|
168
|
+
* ```typescript
|
|
169
|
+
* const t1 = MultiplexTransport.create("scene-1", "ws://localhost:3000/providers");
|
|
170
|
+
* const t2 = MultiplexTransport.create("scene-2", "ws://localhost:3000/providers");
|
|
171
|
+
* // t1 and t2 share a single WebSocket under the hood.
|
|
172
|
+
* ```
|
|
8
173
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
174
|
+
declare class MultiplexTransport implements IMessageTransport {
|
|
175
|
+
private readonly _name;
|
|
176
|
+
private readonly _socket;
|
|
177
|
+
private readonly _aggregate;
|
|
178
|
+
private _registered;
|
|
179
|
+
onMessage: ((data: string) => void) | null;
|
|
180
|
+
onOpen: (() => void) | null;
|
|
181
|
+
onClose: (() => void) | null;
|
|
182
|
+
onError: ((error: Error) => void) | null;
|
|
183
|
+
constructor(name: string, socket: MultiplexSocket, options?: IMultiplexTransportOptions);
|
|
184
|
+
/**
|
|
185
|
+
* Convenience factory: creates a {@link MultiplexTransport} backed by a
|
|
186
|
+
* shared {@link MultiplexSocket} for the given tunnel URL.
|
|
187
|
+
*
|
|
188
|
+
* Transports targeting the same `wsUrl` automatically share one WebSocket.
|
|
189
|
+
*
|
|
190
|
+
* @param wsUrl The broker's shared multiplex base, `ws://<broker>/providers`.
|
|
191
|
+
* A slot-scoped `/provider/<name>` URL belongs to
|
|
192
|
+
* {@link DirectTransport} and is warned about on connect.
|
|
193
|
+
*/
|
|
194
|
+
static create(name: string, wsUrl: string, options?: IMultiplexTransportOptions): MultiplexTransport;
|
|
195
|
+
get isOpen(): boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Registers this transport with the shared socket.
|
|
198
|
+
*
|
|
199
|
+
* Safe to call multiple times, subsequent calls are no-ops.
|
|
200
|
+
*/
|
|
201
|
+
activate(): void;
|
|
202
|
+
/**
|
|
203
|
+
* Opens the transport, the same way every other transport does.
|
|
204
|
+
*
|
|
205
|
+
* An alias of {@link activate} so callers never have to special-case this
|
|
206
|
+
* class: an MCP server or client just calls `connect()` on whatever
|
|
207
|
+
* transport it was handed.
|
|
208
|
+
*/
|
|
209
|
+
connect(): void;
|
|
210
|
+
send(data: string): void;
|
|
211
|
+
close(): void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { DirectTransport, type IDirectTransportOptions, type IMultiplexTransportOptions, MultiplexTransport };
|