@johnhenry/wsh 0.0.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/LICENSE +21 -0
- package/README.md +150 -0
- package/package.json +42 -0
- package/src/auth.mjs +314 -0
- package/src/cbor.mjs +389 -0
- package/src/client.mjs +1842 -0
- package/src/file-transfer.mjs +413 -0
- package/src/index.d.ts +1454 -0
- package/src/index.mjs +70 -0
- package/src/keystore.mjs +473 -0
- package/src/mcp-bridge.mjs +228 -0
- package/src/messages.gen.mjs +928 -0
- package/src/messages.mjs +3 -0
- package/src/recording.mjs +402 -0
- package/src/session.mjs +557 -0
- package/src/transport-ws.mjs +462 -0
- package/src/transport.mjs +343 -0
- package/src/virtual-session.mjs +160 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WshMcpBridge — bridges remote MCP tools over the wsh meta channel.
|
|
3
|
+
*
|
|
4
|
+
* Sends MCP_DISCOVER and MCP_CALL messages through the wsh control channel,
|
|
5
|
+
* enabling a wsh client to discover and invoke MCP tools hosted on the
|
|
6
|
+
* remote server. Tool specs are cached after discovery for efficient reuse.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { MSG, mcpDiscover, mcpCall } from './messages.mjs';
|
|
10
|
+
|
|
11
|
+
/** Default timeout for MCP operations (15 seconds). */
|
|
12
|
+
const MCP_TIMEOUT_MS = 15_000;
|
|
13
|
+
|
|
14
|
+
export class WshMcpBridge {
|
|
15
|
+
/** @type {object} WshClient reference. */
|
|
16
|
+
#client;
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, object>} Cached tool specs: name -> { name, description, parameters } */
|
|
19
|
+
#tools = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} client - A WshClient or transport exposing:
|
|
23
|
+
* - sendControl(msg): send a control message
|
|
24
|
+
* - addControlListener(fn) / removeControlListener(fn): message listeners
|
|
25
|
+
* (or _controlListeners array, or onControl callback)
|
|
26
|
+
*/
|
|
27
|
+
constructor(client) {
|
|
28
|
+
if (!client) throw new Error('WshMcpBridge requires a client');
|
|
29
|
+
this.#client = client;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Discover available MCP tools on the remote server.
|
|
34
|
+
*
|
|
35
|
+
* Sends an MCP_DISCOVER message and waits for the MCP_TOOLS response
|
|
36
|
+
* containing the list of available tools with their schemas.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @param {number} [opts.timeout] - Timeout in ms (default 15s)
|
|
40
|
+
* @returns {Promise<Array<{ name: string, description: string, parameters: object }>>}
|
|
41
|
+
*/
|
|
42
|
+
async discover({ timeout = MCP_TIMEOUT_MS } = {}) {
|
|
43
|
+
await this.#client.sendControl(mcpDiscover());
|
|
44
|
+
|
|
45
|
+
const response = await this._waitForMessage(
|
|
46
|
+
(msg) => msg.type === MSG.MCP_TOOLS || msg.type === MSG.ERROR,
|
|
47
|
+
timeout
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (response.type === MSG.ERROR) {
|
|
51
|
+
throw new Error(`MCP discovery failed: ${response.message || response.code || 'unknown error'}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tools = response.tools || [];
|
|
55
|
+
|
|
56
|
+
// Cache the discovered tools
|
|
57
|
+
this.#tools.clear();
|
|
58
|
+
for (const tool of tools) {
|
|
59
|
+
if (tool && tool.name) {
|
|
60
|
+
this.#tools.set(tool.name, {
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description || '',
|
|
63
|
+
parameters: tool.parameters || {},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Array.from(this.#tools.values());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Call a remote MCP tool by name.
|
|
73
|
+
*
|
|
74
|
+
* Sends an MCP_CALL message and waits for the MCP_RESULT response.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} toolName - Name of the tool to invoke
|
|
77
|
+
* @param {object} [args={}] - Arguments to pass to the tool
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
* @param {number} [opts.timeout] - Timeout in ms (default 15s)
|
|
80
|
+
* @returns {Promise<{ success: boolean, output: *, error?: string }>}
|
|
81
|
+
*/
|
|
82
|
+
async call(toolName, args = {}, { timeout = MCP_TIMEOUT_MS } = {}) {
|
|
83
|
+
if (!toolName || typeof toolName !== 'string') {
|
|
84
|
+
throw new Error('toolName is required');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate against cached tools if discovery has been performed
|
|
88
|
+
if (this.#tools.size > 0 && !this.#tools.has(toolName)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Unknown tool "${toolName}". Available tools: ${Array.from(this.#tools.keys()).join(', ')}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.#client.sendControl(mcpCall({ tool: toolName, arguments: args }));
|
|
95
|
+
|
|
96
|
+
const response = await this._waitForMessage(
|
|
97
|
+
(msg) => msg.type === MSG.MCP_RESULT || msg.type === MSG.ERROR,
|
|
98
|
+
timeout
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (response.type === MSG.ERROR) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
output: null,
|
|
105
|
+
error: response.message || `Error code: ${response.code}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = response.result;
|
|
110
|
+
|
|
111
|
+
// Normalize result into the standard { success, output, error? } shape
|
|
112
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
113
|
+
return {
|
|
114
|
+
success: Boolean(result.success),
|
|
115
|
+
output: result.output ?? result.data ?? null,
|
|
116
|
+
error: result.error || undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If the server returned a raw value, wrap it
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
output: result,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get cached tool specs (after discover() has been called).
|
|
129
|
+
*
|
|
130
|
+
* Returns tool specifications in a format compatible with the BrowserTool
|
|
131
|
+
* interface used by the Clawser agent, making it easy to register remote
|
|
132
|
+
* tools alongside local ones.
|
|
133
|
+
*
|
|
134
|
+
* @returns {Array<{ name: string, description: string, parameters: object }>}
|
|
135
|
+
*/
|
|
136
|
+
getToolSpecs() {
|
|
137
|
+
return Array.from(this.#tools.values()).map((tool) => ({
|
|
138
|
+
name: tool.name,
|
|
139
|
+
description: tool.description,
|
|
140
|
+
parameters: tool.parameters,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check whether a specific tool is available (based on cached discovery).
|
|
146
|
+
*
|
|
147
|
+
* @param {string} toolName
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
hasTool(toolName) {
|
|
151
|
+
return this.#tools.has(toolName);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the number of cached tools.
|
|
156
|
+
* @returns {number}
|
|
157
|
+
*/
|
|
158
|
+
get toolCount() {
|
|
159
|
+
return this.#tools.size;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clear the cached tool specs. Call discover() again to refresh.
|
|
164
|
+
*/
|
|
165
|
+
clearCache() {
|
|
166
|
+
this.#tools.clear();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Internal ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Wait for a control message matching a predicate.
|
|
173
|
+
*
|
|
174
|
+
* Temporarily hooks into the client's control message flow and resolves
|
|
175
|
+
* when a matching message arrives (or rejects on timeout).
|
|
176
|
+
*
|
|
177
|
+
* @param {function(object): boolean} predicate
|
|
178
|
+
* @param {number} timeoutMs
|
|
179
|
+
* @returns {Promise<object>}
|
|
180
|
+
*/
|
|
181
|
+
_waitForMessage(predicate, timeoutMs) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
let timer = null;
|
|
184
|
+
let settled = false;
|
|
185
|
+
|
|
186
|
+
const cleanup = () => {
|
|
187
|
+
settled = true;
|
|
188
|
+
if (timer !== null) clearTimeout(timer);
|
|
189
|
+
// Remove listener
|
|
190
|
+
if (this.#client.removeControlListener) {
|
|
191
|
+
this.#client.removeControlListener(listener);
|
|
192
|
+
} else if (this.#client._controlListeners) {
|
|
193
|
+
const idx = this.#client._controlListeners.indexOf(listener);
|
|
194
|
+
if (idx !== -1) this.#client._controlListeners.splice(idx, 1);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const listener = (msg) => {
|
|
199
|
+
if (settled) return;
|
|
200
|
+
if (predicate(msg)) {
|
|
201
|
+
cleanup();
|
|
202
|
+
resolve(msg);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Register listener on the client
|
|
207
|
+
if (this.#client.addControlListener) {
|
|
208
|
+
this.#client.addControlListener(listener);
|
|
209
|
+
} else if (this.#client._controlListeners) {
|
|
210
|
+
this.#client._controlListeners.push(listener);
|
|
211
|
+
} else {
|
|
212
|
+
// Fallback: wrap existing onControl
|
|
213
|
+
const prev = this.#client.onControl;
|
|
214
|
+
this.#client.onControl = (msg) => {
|
|
215
|
+
prev?.(msg);
|
|
216
|
+
listener(msg);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
timer = setTimeout(() => {
|
|
221
|
+
if (!settled) {
|
|
222
|
+
cleanup();
|
|
223
|
+
reject(new Error(`MCP operation timed out after ${timeoutMs}ms`));
|
|
224
|
+
}
|
|
225
|
+
}, timeoutMs);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|