@cyanmycelium/mcp-broker-provider 0.1.0 → 0.1.1
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 +6 -4
- package/dist/index.d.ts +109 -8
- package/dist/index.js +265 -11
- package/dist/index.js.map +1 -1
- package/dist/protocol/index.d.ts +87 -1
- package/dist/protocol/index.js +47 -1
- package/dist/protocol/index.js.map +1 -1
- package/package.json +8 -7
- package/src/direct.transport.ts +1 -1
- package/src/multiplex.transport.ts +6 -6
- 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,12 +32,14 @@ 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
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.
|
|
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
|
|
|
@@ -67,9 +69,9 @@ Transports created for the same tunnel URL share one WebSocket, whichever order
|
|
|
67
69
|
|
|
68
70
|
## Status
|
|
69
71
|
|
|
70
|
-
`0.1.0` ships the protocol module and both transports. They still exist in `@cyanmycelium/mcp-core@0.4.x` as well, and are removed there in `0.5.0
|
|
72
|
+
`0.1.0` ships the protocol module and both transports. They still exist in `@cyanmycelium/mcp-core@0.4.x` as well, and are removed there in `0.5.0`, migrate your imports before upgrading.
|
|
71
73
|
|
|
72
|
-
The protocol is shared with the broker and
|
|
74
|
+
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
75
|
|
|
74
76
|
## License
|
|
75
77
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,112 @@
|
|
|
1
|
+
export { TUNNEL_REGISTER_METHOD, TunnelEnvelope, TunnelError, TunnelErrorCode, TunnelErrorCodes, decodeEnvelope, encodeEnvelope, encodeEnvelopeMessage, encodeErrorEnvelope, encodeRegisterEnvelope, envelopeFrame, tunnelErrorOf } from './protocol/index.js';
|
|
2
|
+
import { IMessageTransport } from '@cyanmycelium/mcp-core';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
5
|
+
* 1:1 WebSocket transport, wraps a single `WebSocket` connection to a broker
|
|
6
|
+
* provider slot, typically `ws://<broker>/provider/<name>`.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* One server owns one socket. When an application publishes several servers
|
|
9
|
+
* through the same broker, prefer {@link MultiplexTransport}, which shares a
|
|
10
|
+
* single socket between them.
|
|
11
|
+
*
|
|
12
|
+
* Call {@link connect} after setting the event callbacks to open the socket.
|
|
13
|
+
*/
|
|
14
|
+
declare class DirectTransport implements IMessageTransport {
|
|
15
|
+
private readonly _wsUrl;
|
|
16
|
+
private _ws;
|
|
17
|
+
onMessage: ((data: string) => void) | null;
|
|
18
|
+
onOpen: (() => void) | null;
|
|
19
|
+
onClose: (() => void) | null;
|
|
20
|
+
onError: ((error: Error) => void) | null;
|
|
21
|
+
constructor(wsUrl: string);
|
|
22
|
+
get isOpen(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Opens the WebSocket connection and wires its events to the transport
|
|
25
|
+
* callbacks. Must be called after assigning `onOpen` / `onMessage` / etc.
|
|
26
|
+
*/
|
|
27
|
+
connect(): void;
|
|
28
|
+
send(data: string): void;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Manages a single WebSocket connection shared by multiple {@link MultiplexTransport}
|
|
34
|
+
* instances. All traffic goes through the tunnel envelope protocol, whose
|
|
35
|
+
* definition lives in `./protocol` and is shared with the broker.
|
|
36
|
+
*
|
|
37
|
+
* Reconnection is handled centrally here, individual transports do not reconnect.
|
|
38
|
+
* Use {@link getOrCreate} to obtain a per-URL singleton.
|
|
39
|
+
*/
|
|
40
|
+
declare class MultiplexSocket {
|
|
41
|
+
/** Per-URL cache so all transports targeting the same tunnel share one socket. */
|
|
42
|
+
private static readonly _instances;
|
|
43
|
+
private readonly _wsUrl;
|
|
44
|
+
private readonly _transports;
|
|
45
|
+
private _ws;
|
|
46
|
+
private _reconnectAttempts;
|
|
47
|
+
private _stopped;
|
|
48
|
+
private constructor();
|
|
49
|
+
/** Returns (or creates) the singleton socket for a given tunnel URL. */
|
|
50
|
+
static getOrCreate(wsUrl: string): MultiplexSocket;
|
|
51
|
+
get isOpen(): boolean;
|
|
52
|
+
register(name: string, transport: MultiplexTransport): void;
|
|
53
|
+
unregister(name: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Claims the slot for `name` so the broker eagerly creates its provider
|
|
56
|
+
* state before any MCP client connects. Without it the broker only learns
|
|
57
|
+
* about a provider on its first real message, and a client connecting in
|
|
58
|
+
* between is told the provider is not connected.
|
|
59
|
+
*/
|
|
60
|
+
private _announceProvider;
|
|
61
|
+
send(provider: string, data: string): void;
|
|
62
|
+
private _connect;
|
|
63
|
+
private _routeIncoming;
|
|
64
|
+
private _scheduleReconnect;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A transport that multiplexes multiple MCP servers over a single shared
|
|
68
|
+
* WebSocket connection using the envelope protocol `{ provider, payload }`.
|
|
69
|
+
*
|
|
70
|
+
* Use the static {@link create} factory to obtain an instance:
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const t1 = MultiplexTransport.create("scene-1", "ws://localhost:3000/providers");
|
|
73
|
+
* const t2 = MultiplexTransport.create("scene-2", "ws://localhost:3000/providers");
|
|
74
|
+
* // t1 and t2 share a single WebSocket under the hood.
|
|
75
|
+
* ```
|
|
8
76
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
77
|
+
declare class MultiplexTransport implements IMessageTransport {
|
|
78
|
+
private readonly _name;
|
|
79
|
+
private readonly _socket;
|
|
80
|
+
private _registered;
|
|
81
|
+
onMessage: ((data: string) => void) | null;
|
|
82
|
+
onOpen: (() => void) | null;
|
|
83
|
+
onClose: (() => void) | null;
|
|
84
|
+
onError: ((error: Error) => void) | null;
|
|
85
|
+
constructor(name: string, socket: MultiplexSocket);
|
|
86
|
+
/**
|
|
87
|
+
* Convenience factory: creates a {@link MultiplexTransport} backed by a
|
|
88
|
+
* shared {@link MultiplexSocket} for the given tunnel URL.
|
|
89
|
+
*
|
|
90
|
+
* Transports targeting the same `wsUrl` automatically share one WebSocket.
|
|
91
|
+
*/
|
|
92
|
+
static create(name: string, wsUrl: string): MultiplexTransport;
|
|
93
|
+
get isOpen(): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Registers this transport with the shared socket.
|
|
96
|
+
*
|
|
97
|
+
* Safe to call multiple times, subsequent calls are no-ops.
|
|
98
|
+
*/
|
|
99
|
+
activate(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Opens the transport, the same way every other transport does.
|
|
102
|
+
*
|
|
103
|
+
* An alias of {@link activate} so callers never have to special-case this
|
|
104
|
+
* class: an MCP server or client just calls `connect()` on whatever
|
|
105
|
+
* transport it was handed.
|
|
106
|
+
*/
|
|
107
|
+
connect(): void;
|
|
108
|
+
send(data: string): void;
|
|
109
|
+
close(): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { DirectTransport, MultiplexTransport };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,266 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// src/protocol/envelope.ts
|
|
2
|
+
var TUNNEL_REGISTER_METHOD = "notifications/register";
|
|
3
|
+
var TunnelErrorCodes = {
|
|
4
|
+
/** The slot is taken by another upstream, or the provider is not connected. */
|
|
5
|
+
ProviderUnavailable: -32e3,
|
|
6
|
+
/** The provider's credentials do not allow publishing on this slot. */
|
|
7
|
+
RegistrationForbidden: -32001
|
|
8
|
+
};
|
|
9
|
+
function encodeEnvelope(provider, frame) {
|
|
10
|
+
return encodeEnvelopeMessage(provider, JSON.parse(frame));
|
|
11
|
+
}
|
|
12
|
+
function encodeEnvelopeMessage(provider, payload) {
|
|
13
|
+
const envelope = { provider, payload };
|
|
14
|
+
return JSON.stringify(envelope);
|
|
15
|
+
}
|
|
16
|
+
function decodeEnvelope(raw) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(raw);
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
24
|
+
const { provider, payload } = parsed;
|
|
25
|
+
if (typeof provider !== "string" || provider.length === 0 || payload === void 0) return void 0;
|
|
26
|
+
return { provider, payload };
|
|
27
|
+
}
|
|
28
|
+
function envelopeFrame(envelope) {
|
|
29
|
+
return JSON.stringify(envelope.payload);
|
|
30
|
+
}
|
|
31
|
+
function encodeRegisterEnvelope(provider) {
|
|
32
|
+
return encodeEnvelopeMessage(provider, { jsonrpc: "2.0", method: TUNNEL_REGISTER_METHOD });
|
|
33
|
+
}
|
|
34
|
+
function encodeErrorEnvelope(provider, code, message) {
|
|
35
|
+
return encodeEnvelopeMessage(provider, { jsonrpc: "2.0", id: null, error: { code, message } });
|
|
36
|
+
}
|
|
37
|
+
function tunnelErrorOf(payload) {
|
|
38
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
39
|
+
const { error } = payload;
|
|
40
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
41
|
+
const { code, message } = error;
|
|
42
|
+
if (typeof code !== "number" || typeof message !== "string") return void 0;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/direct.transport.ts
|
|
47
|
+
var DirectTransport = class {
|
|
48
|
+
_wsUrl;
|
|
49
|
+
_ws = null;
|
|
50
|
+
onMessage = null;
|
|
51
|
+
onOpen = null;
|
|
52
|
+
onClose = null;
|
|
53
|
+
onError = null;
|
|
54
|
+
constructor(wsUrl) {
|
|
55
|
+
this._wsUrl = wsUrl;
|
|
56
|
+
}
|
|
57
|
+
get isOpen() {
|
|
58
|
+
return this._ws?.readyState === WebSocket.OPEN;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Opens the WebSocket connection and wires its events to the transport
|
|
62
|
+
* callbacks. Must be called after assigning `onOpen` / `onMessage` / etc.
|
|
63
|
+
*/
|
|
64
|
+
connect() {
|
|
65
|
+
const ws = new WebSocket(this._wsUrl);
|
|
66
|
+
ws.onopen = () => {
|
|
67
|
+
this._ws = ws;
|
|
68
|
+
this.onOpen?.();
|
|
69
|
+
};
|
|
70
|
+
ws.onerror = () => {
|
|
71
|
+
this.onError?.(new Error(`DirectTransport: WebSocket error on ${this._wsUrl}`));
|
|
72
|
+
};
|
|
73
|
+
ws.onclose = () => {
|
|
74
|
+
this._ws = null;
|
|
75
|
+
this.onClose?.();
|
|
76
|
+
};
|
|
77
|
+
ws.onmessage = (event) => {
|
|
78
|
+
this.onMessage?.(event.data);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
send(data) {
|
|
82
|
+
if (this._ws?.readyState === WebSocket.OPEN) {
|
|
83
|
+
this._ws.send(data);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
close() {
|
|
87
|
+
this._ws?.close();
|
|
88
|
+
this._ws = null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/multiplex.transport.ts
|
|
93
|
+
var MultiplexSocket = class _MultiplexSocket {
|
|
94
|
+
/** Per-URL cache so all transports targeting the same tunnel share one socket. */
|
|
95
|
+
static _instances = /* @__PURE__ */ new Map();
|
|
96
|
+
_wsUrl;
|
|
97
|
+
_transports = /* @__PURE__ */ new Map();
|
|
98
|
+
_ws = null;
|
|
99
|
+
_reconnectAttempts = 0;
|
|
100
|
+
_stopped = false;
|
|
101
|
+
constructor(wsUrl) {
|
|
102
|
+
this._wsUrl = wsUrl;
|
|
103
|
+
}
|
|
104
|
+
/** Returns (or creates) the singleton socket for a given tunnel URL. */
|
|
105
|
+
static getOrCreate(wsUrl) {
|
|
106
|
+
let instance = _MultiplexSocket._instances.get(wsUrl);
|
|
107
|
+
if (!instance) {
|
|
108
|
+
instance = new _MultiplexSocket(wsUrl);
|
|
109
|
+
_MultiplexSocket._instances.set(wsUrl, instance);
|
|
110
|
+
}
|
|
111
|
+
return instance;
|
|
112
|
+
}
|
|
113
|
+
get isOpen() {
|
|
114
|
+
return this._ws?.readyState === WebSocket.OPEN;
|
|
115
|
+
}
|
|
116
|
+
// ── Registration ────────────────────────────────────────────────────────
|
|
117
|
+
register(name, transport) {
|
|
118
|
+
this._transports.set(name, transport);
|
|
119
|
+
if (this.isOpen) {
|
|
120
|
+
this._announceProvider(name);
|
|
121
|
+
transport.onOpen?.();
|
|
122
|
+
} else if (!this._ws) {
|
|
123
|
+
this._stopped = false;
|
|
124
|
+
this._connect();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
unregister(name) {
|
|
128
|
+
this._transports.delete(name);
|
|
129
|
+
if (this._transports.size === 0) {
|
|
130
|
+
this._stopped = true;
|
|
131
|
+
this._ws?.close();
|
|
132
|
+
this._ws = null;
|
|
133
|
+
_MultiplexSocket._instances.delete(this._wsUrl);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Claims the slot for `name` so the broker eagerly creates its provider
|
|
138
|
+
* state before any MCP client connects. Without it the broker only learns
|
|
139
|
+
* about a provider on its first real message, and a client connecting in
|
|
140
|
+
* between is told the provider is not connected.
|
|
141
|
+
*/
|
|
142
|
+
_announceProvider(name) {
|
|
143
|
+
if (this._ws?.readyState !== WebSocket.OPEN) return;
|
|
144
|
+
this._ws.send(encodeRegisterEnvelope(name));
|
|
145
|
+
}
|
|
146
|
+
// ── Sending ─────────────────────────────────────────────────────────────
|
|
147
|
+
send(provider, data) {
|
|
148
|
+
if (this._ws?.readyState !== WebSocket.OPEN) return;
|
|
149
|
+
this._ws.send(encodeEnvelope(provider, data));
|
|
150
|
+
}
|
|
151
|
+
// ── Connection lifecycle ────────────────────────────────────────────────
|
|
152
|
+
_connect() {
|
|
153
|
+
const ws = new WebSocket(this._wsUrl);
|
|
154
|
+
this._ws = ws;
|
|
155
|
+
ws.onopen = () => {
|
|
156
|
+
this._reconnectAttempts = 0;
|
|
157
|
+
for (const name of this._transports.keys()) {
|
|
158
|
+
this._announceProvider(name);
|
|
159
|
+
}
|
|
160
|
+
for (const transport of this._transports.values()) {
|
|
161
|
+
transport.onOpen?.();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
ws.onerror = () => {
|
|
165
|
+
for (const transport of this._transports.values()) {
|
|
166
|
+
transport.onError?.(new Error(`MultiplexSocket: WebSocket error on ${this._wsUrl}`));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
ws.onclose = () => {
|
|
170
|
+
this._ws = null;
|
|
171
|
+
for (const transport of this._transports.values()) {
|
|
172
|
+
transport.onClose?.();
|
|
173
|
+
}
|
|
174
|
+
if (!this._stopped) {
|
|
175
|
+
this._scheduleReconnect();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
ws.onmessage = (event) => {
|
|
179
|
+
this._routeIncoming(event.data);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
_routeIncoming(raw) {
|
|
183
|
+
const envelope = decodeEnvelope(raw);
|
|
184
|
+
if (!envelope) return;
|
|
185
|
+
const transport = this._transports.get(envelope.provider);
|
|
186
|
+
if (!transport) return;
|
|
187
|
+
const payload = envelope.payload;
|
|
188
|
+
if (payload !== null && (payload.id === null || payload.id === void 0)) {
|
|
189
|
+
const error = tunnelErrorOf(envelope.payload);
|
|
190
|
+
if (error) {
|
|
191
|
+
transport.onError?.(new Error(`Tunnel error ${error.code} on provider "${envelope.provider}": ${error.message}`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
transport.onMessage?.(envelopeFrame(envelope));
|
|
196
|
+
}
|
|
197
|
+
_scheduleReconnect() {
|
|
198
|
+
const base = 1e3;
|
|
199
|
+
const max = 3e4;
|
|
200
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
201
|
+
const delay = Math.min(base * 2 ** this._reconnectAttempts, max) * jitter;
|
|
202
|
+
this._reconnectAttempts++;
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (!this._stopped) this._connect();
|
|
205
|
+
}, delay);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var MultiplexTransport = class _MultiplexTransport {
|
|
209
|
+
_name;
|
|
210
|
+
_socket;
|
|
211
|
+
_registered = false;
|
|
212
|
+
onMessage = null;
|
|
213
|
+
onOpen = null;
|
|
214
|
+
onClose = null;
|
|
215
|
+
onError = null;
|
|
216
|
+
constructor(name, socket) {
|
|
217
|
+
this._name = name;
|
|
218
|
+
this._socket = socket;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Convenience factory: creates a {@link MultiplexTransport} backed by a
|
|
222
|
+
* shared {@link MultiplexSocket} for the given tunnel URL.
|
|
223
|
+
*
|
|
224
|
+
* Transports targeting the same `wsUrl` automatically share one WebSocket.
|
|
225
|
+
*/
|
|
226
|
+
static create(name, wsUrl) {
|
|
227
|
+
return new _MultiplexTransport(name, MultiplexSocket.getOrCreate(wsUrl));
|
|
228
|
+
}
|
|
229
|
+
get isOpen() {
|
|
230
|
+
return this._socket.isOpen;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Registers this transport with the shared socket.
|
|
234
|
+
*
|
|
235
|
+
* Safe to call multiple times, subsequent calls are no-ops.
|
|
236
|
+
*/
|
|
237
|
+
activate() {
|
|
238
|
+
if (!this._registered) {
|
|
239
|
+
this._registered = true;
|
|
240
|
+
this._socket.register(this._name, this);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Opens the transport, the same way every other transport does.
|
|
245
|
+
*
|
|
246
|
+
* An alias of {@link activate} so callers never have to special-case this
|
|
247
|
+
* class: an MCP server or client just calls `connect()` on whatever
|
|
248
|
+
* transport it was handed.
|
|
249
|
+
*/
|
|
250
|
+
connect() {
|
|
251
|
+
this.activate();
|
|
252
|
+
}
|
|
253
|
+
send(data) {
|
|
254
|
+
this._socket.send(this._name, data);
|
|
255
|
+
}
|
|
256
|
+
close() {
|
|
257
|
+
if (this._registered) {
|
|
258
|
+
this._registered = false;
|
|
259
|
+
this._socket.unregister(this._name);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export { DirectTransport, MultiplexTransport, TUNNEL_REGISTER_METHOD, TunnelErrorCodes, decodeEnvelope, encodeEnvelope, encodeEnvelopeMessage, encodeErrorEnvelope, encodeRegisterEnvelope, envelopeFrame, tunnelErrorOf };
|
|
265
|
+
//# sourceMappingURL=index.js.map
|
|
12
266
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"sources":["../src/protocol/envelope.ts","../src/direct.transport.ts","../src/multiplex.transport.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The CyanMycelium tunnel envelope protocol.\n *\n * A multiplexed tunnel socket carries traffic for several providers at once, so\n * every JSON-RPC message is wrapped with the name of the provider slot it\n * belongs to. Both ends of the tunnel encode and decode with the helpers here:\n * the client transports that publish a provider, and the broker that routes\n * between providers and MCP clients.\n *\n * This module is the single definition of that wire format. It is deliberately\n * dependency-free and isomorphic, so the browser side and the Node broker share\n * exactly one implementation rather than two that drift apart.\n *\n * Wire format:\n * ```json\n * { \"provider\": \"scene-1\", \"payload\": { \"jsonrpc\": \"2.0\", \"id\": 1, \"result\": {} } }\n * ```\n */\n\n/** One framed message on a multiplexed tunnel socket. */\nexport interface TunnelEnvelope {\n /** Name of the provider slot this message belongs to. */\n provider: string;\n\n /** The JSON-RPC message itself, already parsed. */\n payload: unknown;\n}\n\n/**\n * Notification a client sends to claim a provider slot as soon as the tunnel\n * opens, before any MCP client shows up.\n *\n * Without it the broker only discovers a provider name on its first real\n * message, so an MCP client connecting in between is told the provider is not\n * connected. It is a plain JSON-RPC notification, which any peer that does not\n * recognize it ignores.\n */\nexport const TUNNEL_REGISTER_METHOD = \"notifications/register\";\n\n/** JSON-RPC error codes the broker returns on the tunnel itself. */\nexport const TunnelErrorCodes = {\n /** The slot is taken by another upstream, or the provider is not connected. */\n ProviderUnavailable: -32000,\n\n /** The provider's credentials do not allow publishing on this slot. */\n RegistrationForbidden: -32001,\n} as const;\n\nexport type TunnelErrorCode = (typeof TunnelErrorCodes)[keyof typeof TunnelErrorCodes];\n\n/** A JSON-RPC error as carried inside an envelope payload. */\nexport interface TunnelError {\n code: number;\n message: string;\n data?: unknown;\n}\n\n/**\n * Wraps an already-serialized JSON-RPC frame for `provider`.\n *\n * @throws SyntaxError when `frame` is not valid JSON. Callers hold a frame they\n * just serialized, so a failure here is a bug rather than bad input.\n */\nexport function encodeEnvelope(provider: string, frame: string): string {\n return encodeEnvelopeMessage(provider, JSON.parse(frame));\n}\n\n/** Wraps an already-parsed JSON-RPC message for `provider`. */\nexport function encodeEnvelopeMessage(provider: string, payload: unknown): string {\n const envelope: TunnelEnvelope = { provider, payload };\n return JSON.stringify(envelope);\n}\n\n/**\n * Parses a raw tunnel frame.\n *\n * Returns `undefined` for anything malformed rather than throwing: a tunnel\n * socket is a public surface, and a peer sending garbage must not take the\n * receiver down. Both ends drop such frames silently.\n */\nexport function decodeEnvelope(raw: string): TunnelEnvelope | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return undefined;\n\n const { provider, payload } = parsed as Partial<TunnelEnvelope>;\n if (typeof provider !== \"string\" || provider.length === 0 || payload === undefined) return undefined;\n\n return { provider, payload };\n}\n\n/** Serializes an envelope's payload back into a plain JSON-RPC frame. */\nexport function envelopeFrame(envelope: TunnelEnvelope): string {\n return JSON.stringify(envelope.payload);\n}\n\n/** Builds the registration notification that claims `provider`. */\nexport function encodeRegisterEnvelope(provider: string): string {\n return encodeEnvelopeMessage(provider, { jsonrpc: \"2.0\", method: TUNNEL_REGISTER_METHOD });\n}\n\n/**\n * Builds the error envelope the broker returns when it refuses a slot.\n *\n * The id is `null` because the refusal answers no particular request: it\n * reacts to the registration itself.\n */\nexport function encodeErrorEnvelope(provider: string, code: TunnelErrorCode | number, message: string): string {\n return encodeEnvelopeMessage(provider, { jsonrpc: \"2.0\", id: null, error: { code, message } });\n}\n\n/**\n * Reads the JSON-RPC error out of an envelope payload, when there is one.\n *\n * Lets the client side notice a refused registration instead of handing an\n * `id: null` error frame to an MCP server, which would classify it as an\n * unknown notification and drop it without a word.\n */\nexport function tunnelErrorOf(payload: unknown): TunnelError | undefined {\n if (typeof payload !== \"object\" || payload === null) return undefined;\n\n const { error } = payload as { error?: unknown };\n if (typeof error !== \"object\" || error === null) return undefined;\n\n const { code, message } = error as Partial<TunnelError>;\n if (typeof code !== \"number\" || typeof message !== \"string\") return undefined;\n\n return error as TunnelError;\n}\n","import type { IMessageTransport } from \"@cyanmycelium/mcp-core\";\n\n/**\n * 1:1 WebSocket transport, wraps a single `WebSocket` connection to a broker\n * provider slot, typically `ws://<broker>/provider/<name>`.\n *\n * One server owns one socket. When an application publishes several servers\n * through the same broker, prefer {@link MultiplexTransport}, which shares a\n * single socket between them.\n *\n * Call {@link connect} after setting the event callbacks to open the socket.\n */\nexport class DirectTransport implements IMessageTransport {\n private readonly _wsUrl: string;\n private _ws: WebSocket | null = null;\n\n onMessage: ((data: string) => void) | null = null;\n onOpen: (() => void) | null = null;\n onClose: (() => void) | null = null;\n onError: ((error: Error) => void) | null = null;\n\n constructor(wsUrl: string) {\n this._wsUrl = wsUrl;\n }\n\n get isOpen(): boolean {\n return this._ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Opens the WebSocket connection and wires its events to the transport\n * callbacks. Must be called after assigning `onOpen` / `onMessage` / etc.\n */\n connect(): void {\n const ws = new WebSocket(this._wsUrl);\n\n ws.onopen = () => {\n this._ws = ws;\n this.onOpen?.();\n };\n\n ws.onerror = () => {\n this.onError?.(new Error(`DirectTransport: WebSocket error on ${this._wsUrl}`));\n };\n\n ws.onclose = () => {\n this._ws = null;\n this.onClose?.();\n };\n\n ws.onmessage = (event: MessageEvent<string>) => {\n this.onMessage?.(event.data);\n };\n }\n\n send(data: string): void {\n if (this._ws?.readyState === WebSocket.OPEN) {\n this._ws.send(data);\n }\n }\n\n close(): void {\n this._ws?.close();\n this._ws = null;\n }\n}\n","import type { IMessageTransport } from \"@cyanmycelium/mcp-core\";\nimport { decodeEnvelope, encodeEnvelope, encodeRegisterEnvelope, envelopeFrame, tunnelErrorOf } from \"./protocol/index\";\n\n// ---------------------------------------------------------------------------\n// MultiplexSocket, shared WebSocket singleton (internal)\n// ---------------------------------------------------------------------------\n\n/**\n * Manages a single WebSocket connection shared by multiple {@link MultiplexTransport}\n * instances. All traffic goes through the tunnel envelope protocol, whose\n * definition lives in `./protocol` and is shared with the broker.\n *\n * Reconnection is handled centrally here, individual transports do not reconnect.\n * Use {@link getOrCreate} to obtain a per-URL singleton.\n */\nclass MultiplexSocket {\n /** Per-URL cache so all transports targeting the same tunnel share one socket. */\n private static readonly _instances = new Map<string, MultiplexSocket>();\n\n private readonly _wsUrl: string;\n private readonly _transports = new Map<string, MultiplexTransport>();\n private _ws: WebSocket | null = null;\n private _reconnectAttempts = 0;\n private _stopped = false;\n\n private constructor(wsUrl: string) {\n this._wsUrl = wsUrl;\n }\n\n /** Returns (or creates) the singleton socket for a given tunnel URL. */\n static getOrCreate(wsUrl: string): MultiplexSocket {\n let instance = MultiplexSocket._instances.get(wsUrl);\n if (!instance) {\n instance = new MultiplexSocket(wsUrl);\n MultiplexSocket._instances.set(wsUrl, instance);\n }\n return instance;\n }\n\n get isOpen(): boolean {\n return this._ws?.readyState === WebSocket.OPEN;\n }\n\n // ── Registration ────────────────────────────────────────────────────────\n\n register(name: string, transport: MultiplexTransport): void {\n this._transports.set(name, transport);\n\n // If the shared socket is already open, announce the new provider and\n // notify the transport immediately.\n if (this.isOpen) {\n this._announceProvider(name);\n transport.onOpen?.();\n } else if (!this._ws) {\n // First registration, open the connection.\n this._stopped = false;\n this._connect();\n }\n }\n\n unregister(name: string): void {\n this._transports.delete(name);\n\n // Tear down the shared socket when no transports remain.\n if (this._transports.size === 0) {\n this._stopped = true;\n this._ws?.close();\n this._ws = null;\n MultiplexSocket._instances.delete(this._wsUrl);\n }\n }\n\n /**\n * Claims the slot for `name` so the broker eagerly creates its provider\n * state before any MCP client connects. Without it the broker only learns\n * about a provider on its first real message, and a client connecting in\n * between is told the provider is not connected.\n */\n private _announceProvider(name: string): void {\n if (this._ws?.readyState !== WebSocket.OPEN) return;\n this._ws.send(encodeRegisterEnvelope(name));\n }\n\n // ── Sending ─────────────────────────────────────────────────────────────\n\n send(provider: string, data: string): void {\n if (this._ws?.readyState !== WebSocket.OPEN) return;\n this._ws.send(encodeEnvelope(provider, data));\n }\n\n // ── Connection lifecycle ────────────────────────────────────────────────\n\n private _connect(): void {\n const ws = new WebSocket(this._wsUrl);\n\n // Held from construction, not from `onopen`: a transport registering\n // while the handshake is still in flight must find this socket rather\n // than open a second one. Readiness is decided by `readyState`, so a\n // connecting socket is never mistaken for a usable one.\n this._ws = ws;\n\n ws.onopen = () => {\n this._reconnectAttempts = 0;\n // Announce all registered providers to the broker so it eagerly\n // creates their slots before any MCP client connects.\n for (const name of this._transports.keys()) {\n this._announceProvider(name);\n }\n for (const transport of this._transports.values()) {\n transport.onOpen?.();\n }\n };\n\n ws.onerror = () => {\n for (const transport of this._transports.values()) {\n transport.onError?.(new Error(`MultiplexSocket: WebSocket error on ${this._wsUrl}`));\n }\n };\n\n ws.onclose = () => {\n this._ws = null;\n for (const transport of this._transports.values()) {\n transport.onClose?.();\n }\n if (!this._stopped) {\n this._scheduleReconnect();\n }\n };\n\n ws.onmessage = (event: MessageEvent<string>) => {\n this._routeIncoming(event.data);\n };\n }\n\n private _routeIncoming(raw: string): void {\n const envelope = decodeEnvelope(raw);\n if (!envelope) return; // malformed, drop silently\n\n const transport = this._transports.get(envelope.provider);\n if (!transport) return;\n\n // A tunnel-level refusal (a rejected slot, an unavailable provider)\n // carries no request id. Handing it to an MCP server would get it\n // classified as an unknown notification and dropped without a word, so\n // surface it as a transport error instead.\n const payload = envelope.payload as { id?: unknown } | null;\n if (payload !== null && (payload.id === null || payload.id === undefined)) {\n const error = tunnelErrorOf(envelope.payload);\n if (error) {\n transport.onError?.(new Error(`Tunnel error ${error.code} on provider \"${envelope.provider}\": ${error.message}`));\n return;\n }\n }\n\n transport.onMessage?.(envelopeFrame(envelope));\n }\n\n private _scheduleReconnect(): void {\n const base = 1_000;\n const max = 30_000;\n const jitter = 0.5 + Math.random() * 0.5;\n const delay = Math.min(base * 2 ** this._reconnectAttempts, max) * jitter;\n\n this._reconnectAttempts++;\n setTimeout(() => {\n if (!this._stopped) this._connect();\n }, delay);\n }\n}\n\n// ---------------------------------------------------------------------------\n// MultiplexTransport, per-server transport (public)\n// ---------------------------------------------------------------------------\n\n/**\n * A transport that multiplexes multiple MCP servers over a single shared\n * WebSocket connection using the envelope protocol `{ provider, payload }`.\n *\n * Use the static {@link create} factory to obtain an instance:\n * ```typescript\n * const t1 = MultiplexTransport.create(\"scene-1\", \"ws://localhost:3000/providers\");\n * const t2 = MultiplexTransport.create(\"scene-2\", \"ws://localhost:3000/providers\");\n * // t1 and t2 share a single WebSocket under the hood.\n * ```\n */\nexport class MultiplexTransport implements IMessageTransport {\n private readonly _name: string;\n private readonly _socket: MultiplexSocket;\n private _registered = false;\n\n onMessage: ((data: string) => void) | null = null;\n onOpen: (() => void) | null = null;\n onClose: (() => void) | null = null;\n onError: ((error: Error) => void) | null = null;\n\n constructor(name: string, socket: MultiplexSocket) {\n this._name = name;\n this._socket = socket;\n }\n\n /**\n * Convenience factory: creates a {@link MultiplexTransport} backed by a\n * shared {@link MultiplexSocket} for the given tunnel URL.\n *\n * Transports targeting the same `wsUrl` automatically share one WebSocket.\n */\n static create(name: string, wsUrl: string): MultiplexTransport {\n return new MultiplexTransport(name, MultiplexSocket.getOrCreate(wsUrl));\n }\n\n get isOpen(): boolean {\n return this._socket.isOpen;\n }\n\n /**\n * Registers this transport with the shared socket.\n *\n * Safe to call multiple times, subsequent calls are no-ops.\n */\n activate(): void {\n if (!this._registered) {\n this._registered = true;\n this._socket.register(this._name, this);\n }\n }\n\n /**\n * Opens the transport, the same way every other transport does.\n *\n * An alias of {@link activate} so callers never have to special-case this\n * class: an MCP server or client just calls `connect()` on whatever\n * transport it was handed.\n */\n connect(): void {\n this.activate();\n }\n\n send(data: string): void {\n this._socket.send(this._name, data);\n }\n\n close(): void {\n if (this._registered) {\n this._registered = false;\n this._socket.unregister(this._name);\n }\n }\n}\n"]}
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -1 +1,87 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The CyanMycelium tunnel envelope protocol.
|
|
3
|
+
*
|
|
4
|
+
* A multiplexed tunnel socket carries traffic for several providers at once, so
|
|
5
|
+
* every JSON-RPC message is wrapped with the name of the provider slot it
|
|
6
|
+
* belongs to. Both ends of the tunnel encode and decode with the helpers here:
|
|
7
|
+
* the client transports that publish a provider, and the broker that routes
|
|
8
|
+
* between providers and MCP clients.
|
|
9
|
+
*
|
|
10
|
+
* This module is the single definition of that wire format. It is deliberately
|
|
11
|
+
* dependency-free and isomorphic, so the browser side and the Node broker share
|
|
12
|
+
* exactly one implementation rather than two that drift apart.
|
|
13
|
+
*
|
|
14
|
+
* Wire format:
|
|
15
|
+
* ```json
|
|
16
|
+
* { "provider": "scene-1", "payload": { "jsonrpc": "2.0", "id": 1, "result": {} } }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
/** One framed message on a multiplexed tunnel socket. */
|
|
20
|
+
interface TunnelEnvelope {
|
|
21
|
+
/** Name of the provider slot this message belongs to. */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** The JSON-RPC message itself, already parsed. */
|
|
24
|
+
payload: unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Notification a client sends to claim a provider slot as soon as the tunnel
|
|
28
|
+
* opens, before any MCP client shows up.
|
|
29
|
+
*
|
|
30
|
+
* Without it the broker only discovers a provider name on its first real
|
|
31
|
+
* message, so an MCP client connecting in between is told the provider is not
|
|
32
|
+
* connected. It is a plain JSON-RPC notification, which any peer that does not
|
|
33
|
+
* recognize it ignores.
|
|
34
|
+
*/
|
|
35
|
+
declare const TUNNEL_REGISTER_METHOD = "notifications/register";
|
|
36
|
+
/** JSON-RPC error codes the broker returns on the tunnel itself. */
|
|
37
|
+
declare const TunnelErrorCodes: {
|
|
38
|
+
/** The slot is taken by another upstream, or the provider is not connected. */
|
|
39
|
+
readonly ProviderUnavailable: -32000;
|
|
40
|
+
/** The provider's credentials do not allow publishing on this slot. */
|
|
41
|
+
readonly RegistrationForbidden: -32001;
|
|
42
|
+
};
|
|
43
|
+
type TunnelErrorCode = (typeof TunnelErrorCodes)[keyof typeof TunnelErrorCodes];
|
|
44
|
+
/** A JSON-RPC error as carried inside an envelope payload. */
|
|
45
|
+
interface TunnelError {
|
|
46
|
+
code: number;
|
|
47
|
+
message: string;
|
|
48
|
+
data?: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wraps an already-serialized JSON-RPC frame for `provider`.
|
|
52
|
+
*
|
|
53
|
+
* @throws SyntaxError when `frame` is not valid JSON. Callers hold a frame they
|
|
54
|
+
* just serialized, so a failure here is a bug rather than bad input.
|
|
55
|
+
*/
|
|
56
|
+
declare function encodeEnvelope(provider: string, frame: string): string;
|
|
57
|
+
/** Wraps an already-parsed JSON-RPC message for `provider`. */
|
|
58
|
+
declare function encodeEnvelopeMessage(provider: string, payload: unknown): string;
|
|
59
|
+
/**
|
|
60
|
+
* Parses a raw tunnel frame.
|
|
61
|
+
*
|
|
62
|
+
* Returns `undefined` for anything malformed rather than throwing: a tunnel
|
|
63
|
+
* socket is a public surface, and a peer sending garbage must not take the
|
|
64
|
+
* receiver down. Both ends drop such frames silently.
|
|
65
|
+
*/
|
|
66
|
+
declare function decodeEnvelope(raw: string): TunnelEnvelope | undefined;
|
|
67
|
+
/** Serializes an envelope's payload back into a plain JSON-RPC frame. */
|
|
68
|
+
declare function envelopeFrame(envelope: TunnelEnvelope): string;
|
|
69
|
+
/** Builds the registration notification that claims `provider`. */
|
|
70
|
+
declare function encodeRegisterEnvelope(provider: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Builds the error envelope the broker returns when it refuses a slot.
|
|
73
|
+
*
|
|
74
|
+
* The id is `null` because the refusal answers no particular request: it
|
|
75
|
+
* reacts to the registration itself.
|
|
76
|
+
*/
|
|
77
|
+
declare function encodeErrorEnvelope(provider: string, code: TunnelErrorCode | number, message: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Reads the JSON-RPC error out of an envelope payload, when there is one.
|
|
80
|
+
*
|
|
81
|
+
* Lets the client side notice a refused registration instead of handing an
|
|
82
|
+
* `id: null` error frame to an MCP server, which would classify it as an
|
|
83
|
+
* unknown notification and drop it without a word.
|
|
84
|
+
*/
|
|
85
|
+
declare function tunnelErrorOf(payload: unknown): TunnelError | undefined;
|
|
86
|
+
|
|
87
|
+
export { TUNNEL_REGISTER_METHOD, type TunnelEnvelope, type TunnelError, type TunnelErrorCode, TunnelErrorCodes, decodeEnvelope, encodeEnvelope, encodeEnvelopeMessage, encodeErrorEnvelope, encodeRegisterEnvelope, envelopeFrame, tunnelErrorOf };
|
package/dist/protocol/index.js
CHANGED
|
@@ -1,2 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
// src/protocol/envelope.ts
|
|
2
|
+
var TUNNEL_REGISTER_METHOD = "notifications/register";
|
|
3
|
+
var TunnelErrorCodes = {
|
|
4
|
+
/** The slot is taken by another upstream, or the provider is not connected. */
|
|
5
|
+
ProviderUnavailable: -32e3,
|
|
6
|
+
/** The provider's credentials do not allow publishing on this slot. */
|
|
7
|
+
RegistrationForbidden: -32001
|
|
8
|
+
};
|
|
9
|
+
function encodeEnvelope(provider, frame) {
|
|
10
|
+
return encodeEnvelopeMessage(provider, JSON.parse(frame));
|
|
11
|
+
}
|
|
12
|
+
function encodeEnvelopeMessage(provider, payload) {
|
|
13
|
+
const envelope = { provider, payload };
|
|
14
|
+
return JSON.stringify(envelope);
|
|
15
|
+
}
|
|
16
|
+
function decodeEnvelope(raw) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(raw);
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
24
|
+
const { provider, payload } = parsed;
|
|
25
|
+
if (typeof provider !== "string" || provider.length === 0 || payload === void 0) return void 0;
|
|
26
|
+
return { provider, payload };
|
|
27
|
+
}
|
|
28
|
+
function envelopeFrame(envelope) {
|
|
29
|
+
return JSON.stringify(envelope.payload);
|
|
30
|
+
}
|
|
31
|
+
function encodeRegisterEnvelope(provider) {
|
|
32
|
+
return encodeEnvelopeMessage(provider, { jsonrpc: "2.0", method: TUNNEL_REGISTER_METHOD });
|
|
33
|
+
}
|
|
34
|
+
function encodeErrorEnvelope(provider, code, message) {
|
|
35
|
+
return encodeEnvelopeMessage(provider, { jsonrpc: "2.0", id: null, error: { code, message } });
|
|
36
|
+
}
|
|
37
|
+
function tunnelErrorOf(payload) {
|
|
38
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
39
|
+
const { error } = payload;
|
|
40
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
41
|
+
const { code, message } = error;
|
|
42
|
+
if (typeof code !== "number" || typeof message !== "string") return void 0;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { TUNNEL_REGISTER_METHOD, TunnelErrorCodes, decodeEnvelope, encodeEnvelope, encodeEnvelopeMessage, encodeErrorEnvelope, encodeRegisterEnvelope, envelopeFrame, tunnelErrorOf };
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
2
48
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/envelope.ts"],"names":[],"mappings":";AAqCO,IAAM,sBAAA,GAAyB;AAG/B,IAAM,gBAAA,GAAmB;AAAA;AAAA,EAE5B,mBAAA,EAAqB,KAAA;AAAA;AAAA,EAGrB,qBAAA,EAAuB;AAC3B;AAiBO,SAAS,cAAA,CAAe,UAAkB,KAAA,EAAuB;AACpE,EAAA,OAAO,qBAAA,CAAsB,QAAA,EAAU,IAAA,CAAK,KAAA,CAAM,KAAK,CAAC,CAAA;AAC5D;AAGO,SAAS,qBAAA,CAAsB,UAAkB,OAAA,EAA0B;AAC9E,EAAA,MAAM,QAAA,GAA2B,EAAE,QAAA,EAAU,OAAA,EAAQ;AACrD,EAAA,OAAO,IAAA,CAAK,UAAU,QAAQ,CAAA;AAClC;AASO,SAAS,eAAe,GAAA,EAAyC;AACpE,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACA,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EAC3B,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,MAAA;AAAA,EACX;AACA,EAAA,IAAI,OAAO,WAAW,QAAA,IAAY,MAAA,KAAW,QAAQ,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,MAAA;AAEnF,EAAA,MAAM,EAAE,QAAA,EAAU,OAAA,EAAQ,GAAI,MAAA;AAC9B,EAAA,IAAI,OAAO,aAAa,QAAA,IAAY,QAAA,CAAS,WAAW,CAAA,IAAK,OAAA,KAAY,QAAW,OAAO,MAAA;AAE3F,EAAA,OAAO,EAAE,UAAU,OAAA,EAAQ;AAC/B;AAGO,SAAS,cAAc,QAAA,EAAkC;AAC5D,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,QAAA,CAAS,OAAO,CAAA;AAC1C;AAGO,SAAS,uBAAuB,QAAA,EAA0B;AAC7D,EAAA,OAAO,sBAAsB,QAAA,EAAU,EAAE,SAAS,KAAA,EAAO,MAAA,EAAQ,wBAAwB,CAAA;AAC7F;AAQO,SAAS,mBAAA,CAAoB,QAAA,EAAkB,IAAA,EAAgC,OAAA,EAAyB;AAC3G,EAAA,OAAO,qBAAA,CAAsB,QAAA,EAAU,EAAE,OAAA,EAAS,KAAA,EAAO,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAQ,EAAG,CAAA;AACjG;AASO,SAAS,cAAc,OAAA,EAA2C;AACrE,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,KAAY,MAAM,OAAO,MAAA;AAE5D,EAAA,MAAM,EAAE,OAAM,GAAI,OAAA;AAClB,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,MAAM,OAAO,MAAA;AAExD,EAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,KAAA;AAC1B,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,OAAO,OAAA,KAAY,UAAU,OAAO,MAAA;AAEpE,EAAA,OAAO,KAAA;AACX","file":"index.js","sourcesContent":["/**\n * The CyanMycelium tunnel envelope protocol.\n *\n * A multiplexed tunnel socket carries traffic for several providers at once, so\n * every JSON-RPC message is wrapped with the name of the provider slot it\n * belongs to. Both ends of the tunnel encode and decode with the helpers here:\n * the client transports that publish a provider, and the broker that routes\n * between providers and MCP clients.\n *\n * This module is the single definition of that wire format. It is deliberately\n * dependency-free and isomorphic, so the browser side and the Node broker share\n * exactly one implementation rather than two that drift apart.\n *\n * Wire format:\n * ```json\n * { \"provider\": \"scene-1\", \"payload\": { \"jsonrpc\": \"2.0\", \"id\": 1, \"result\": {} } }\n * ```\n */\n\n/** One framed message on a multiplexed tunnel socket. */\nexport interface TunnelEnvelope {\n /** Name of the provider slot this message belongs to. */\n provider: string;\n\n /** The JSON-RPC message itself, already parsed. */\n payload: unknown;\n}\n\n/**\n * Notification a client sends to claim a provider slot as soon as the tunnel\n * opens, before any MCP client shows up.\n *\n * Without it the broker only discovers a provider name on its first real\n * message, so an MCP client connecting in between is told the provider is not\n * connected. It is a plain JSON-RPC notification, which any peer that does not\n * recognize it ignores.\n */\nexport const TUNNEL_REGISTER_METHOD = \"notifications/register\";\n\n/** JSON-RPC error codes the broker returns on the tunnel itself. */\nexport const TunnelErrorCodes = {\n /** The slot is taken by another upstream, or the provider is not connected. */\n ProviderUnavailable: -32000,\n\n /** The provider's credentials do not allow publishing on this slot. */\n RegistrationForbidden: -32001,\n} as const;\n\nexport type TunnelErrorCode = (typeof TunnelErrorCodes)[keyof typeof TunnelErrorCodes];\n\n/** A JSON-RPC error as carried inside an envelope payload. */\nexport interface TunnelError {\n code: number;\n message: string;\n data?: unknown;\n}\n\n/**\n * Wraps an already-serialized JSON-RPC frame for `provider`.\n *\n * @throws SyntaxError when `frame` is not valid JSON. Callers hold a frame they\n * just serialized, so a failure here is a bug rather than bad input.\n */\nexport function encodeEnvelope(provider: string, frame: string): string {\n return encodeEnvelopeMessage(provider, JSON.parse(frame));\n}\n\n/** Wraps an already-parsed JSON-RPC message for `provider`. */\nexport function encodeEnvelopeMessage(provider: string, payload: unknown): string {\n const envelope: TunnelEnvelope = { provider, payload };\n return JSON.stringify(envelope);\n}\n\n/**\n * Parses a raw tunnel frame.\n *\n * Returns `undefined` for anything malformed rather than throwing: a tunnel\n * socket is a public surface, and a peer sending garbage must not take the\n * receiver down. Both ends drop such frames silently.\n */\nexport function decodeEnvelope(raw: string): TunnelEnvelope | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return undefined;\n\n const { provider, payload } = parsed as Partial<TunnelEnvelope>;\n if (typeof provider !== \"string\" || provider.length === 0 || payload === undefined) return undefined;\n\n return { provider, payload };\n}\n\n/** Serializes an envelope's payload back into a plain JSON-RPC frame. */\nexport function envelopeFrame(envelope: TunnelEnvelope): string {\n return JSON.stringify(envelope.payload);\n}\n\n/** Builds the registration notification that claims `provider`. */\nexport function encodeRegisterEnvelope(provider: string): string {\n return encodeEnvelopeMessage(provider, { jsonrpc: \"2.0\", method: TUNNEL_REGISTER_METHOD });\n}\n\n/**\n * Builds the error envelope the broker returns when it refuses a slot.\n *\n * The id is `null` because the refusal answers no particular request: it\n * reacts to the registration itself.\n */\nexport function encodeErrorEnvelope(provider: string, code: TunnelErrorCode | number, message: string): string {\n return encodeEnvelopeMessage(provider, { jsonrpc: \"2.0\", id: null, error: { code, message } });\n}\n\n/**\n * Reads the JSON-RPC error out of an envelope payload, when there is one.\n *\n * Lets the client side notice a refused registration instead of handing an\n * `id: null` error frame to an MCP server, which would classify it as an\n * unknown notification and drop it without a word.\n */\nexport function tunnelErrorOf(payload: unknown): TunnelError | undefined {\n if (typeof payload !== \"object\" || payload === null) return undefined;\n\n const { error } = payload as { error?: unknown };\n if (typeof error !== \"object\" || error === null) return undefined;\n\n const { code, message } = error as Partial<TunnelError>;\n if (typeof code !== \"number\" || typeof message !== \"string\") return undefined;\n\n return error as TunnelError;\n}\n"]}
|