weboperator-mcp 1.5.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 +51 -0
- package/SKILL.md +115 -0
- package/agent-client.js +56 -0
- package/bridge.js +716 -0
- package/hermes-config.json +14 -0
- package/install.sh +61 -0
- package/mcp-server.js +608 -0
- package/native-host-template.json +10 -0
- package/native-host.sh +9 -0
- package/openclaw-tools.json +23 -0
- package/package.json +44 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hermes-weboperator",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Hermes Agent MCP harness configuration for WebOperator browser control",
|
|
5
|
+
"mcpServers": {
|
|
6
|
+
"weboperator": {
|
|
7
|
+
"command": "node",
|
|
8
|
+
"args": ["weboperator-bridge/mcp-server.js"],
|
|
9
|
+
"env": {
|
|
10
|
+
"WEBOPERATOR_AGENT_SOCKET": "/tmp/weboperator-bridge.sock"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/install.sh
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Install WebOperator Bridge Native Messaging Host for Chromium browsers.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
HOST_SCRIPT="$SCRIPT_DIR/native-host.sh"
|
|
8
|
+
EXT_ID="${WEBOPERATOR_EXTENSION_ID:-phbohkmfojcjbmgfnaikenmgemgckdpg}"
|
|
9
|
+
|
|
10
|
+
HOST_DIRS=()
|
|
11
|
+
|
|
12
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
13
|
+
HOST_DIRS=(
|
|
14
|
+
"$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
|
15
|
+
"$HOME/Library/Application Support/Google/Chrome Canary/NativeMessagingHosts"
|
|
16
|
+
"$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
|
|
17
|
+
"$HOME/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"
|
|
18
|
+
"$HOME/Library/Application Support/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts"
|
|
19
|
+
"$HOME/Library/Application Support/Microsoft Edge/NativeMessagingHosts"
|
|
20
|
+
"$HOME/Library/Application Support/Arc/User Data/NativeMessagingHosts"
|
|
21
|
+
)
|
|
22
|
+
elif [[ "$OSTYPE" == "linux"* ]]; then
|
|
23
|
+
HOST_DIRS=(
|
|
24
|
+
"$HOME/.config/google-chrome/NativeMessagingHosts"
|
|
25
|
+
"$HOME/.config/chromium/NativeMessagingHosts"
|
|
26
|
+
"$HOME/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts"
|
|
27
|
+
"$HOME/.config/microsoft-edge/NativeMessagingHosts"
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
echo "Unsupported OS."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
chmod +x "$HOST_SCRIPT"
|
|
35
|
+
chmod +x "$SCRIPT_DIR/bridge.js"
|
|
36
|
+
chmod +x "$SCRIPT_DIR/mcp-server.js" 2>/dev/null || true
|
|
37
|
+
|
|
38
|
+
for HOST_DIR in "${HOST_DIRS[@]}"; do
|
|
39
|
+
mkdir -p "$HOST_DIR"
|
|
40
|
+
HOST_FILE="$HOST_DIR/com.weboperator.bridge.json"
|
|
41
|
+
cat > "$HOST_FILE" << EOF
|
|
42
|
+
{
|
|
43
|
+
"name": "com.weboperator.bridge",
|
|
44
|
+
"description": "WebOperator local agent bridge",
|
|
45
|
+
"path": "$HOST_SCRIPT",
|
|
46
|
+
"type": "stdio",
|
|
47
|
+
"allowed_origins": [
|
|
48
|
+
"chrome-extension://$EXT_ID/"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
EOF
|
|
52
|
+
echo "Installed native host: $HOST_FILE"
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
echo ""
|
|
56
|
+
echo "Extension ID: $EXT_ID"
|
|
57
|
+
echo "Native Host: $HOST_SCRIPT"
|
|
58
|
+
echo "Agent socket: ${WEBOPERATOR_AGENT_SOCKET:-/tmp/weboperator-bridge.sock}"
|
|
59
|
+
echo "HTTP API: http://127.0.0.1:8765"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Reload the extension in chrome://extensions."
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WebOperator MCP (Model Context Protocol) Server
|
|
4
|
+
* Industry-standard tool provider for AI agents (Hermes, OpenClaw, Claude Desktop, Cursor, OpenHands).
|
|
5
|
+
* Communicates over STDIO using standard JSON-RPC 2.0.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const { randomUUID } = require('crypto');
|
|
12
|
+
|
|
13
|
+
const { version: SERVER_VERSION } = require('./package.json');
|
|
14
|
+
|
|
15
|
+
const SOCKET_PATH = process.env.WEBOPERATOR_AGENT_SOCKET || '/tmp/weboperator-bridge.sock';
|
|
16
|
+
const BRIDGE_HOST = process.env.WEBOPERATOR_BRIDGE_HOST || '127.0.0.1';
|
|
17
|
+
const BRIDGE_PORT = Number(process.env.WEBOPERATOR_BRIDGE_PORT || 8765);
|
|
18
|
+
const API_TOKEN = process.env.WEBOPERATOR_API_TOKEN || '';
|
|
19
|
+
|
|
20
|
+
process.stdout.on('error', (err) => {
|
|
21
|
+
if (err && err.code === 'EPIPE') process.exit(0);
|
|
22
|
+
});
|
|
23
|
+
process.on('uncaughtException', (err) => {
|
|
24
|
+
if (err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED')) process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: 'browser_snapshot',
|
|
31
|
+
description: 'Capture the structured accessibility tree and numbered interactive elements (buttons, inputs, links) from the current active browser tab.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'browser_navigate',
|
|
40
|
+
description: 'Navigate the active browser tab to a specified URL.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
url: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'The URL to navigate to (e.g. "https://google.com").',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ['url'],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'browser_click',
|
|
55
|
+
description: 'Click an element on the webpage by its numeric index (from browser_snapshot) or CSS selector.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
index: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
description: 'The numeric index of the interactive element from the snapshot.',
|
|
62
|
+
},
|
|
63
|
+
selector: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Alternative CSS selector of the element to click.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'browser_type',
|
|
73
|
+
description: 'Type text into an input field by element index or selector.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
index: {
|
|
78
|
+
type: 'number',
|
|
79
|
+
description: 'The numeric index of the input element from the snapshot.',
|
|
80
|
+
},
|
|
81
|
+
selector: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Alternative CSS selector of the input element.',
|
|
84
|
+
},
|
|
85
|
+
text: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'The text to type into the field.',
|
|
88
|
+
},
|
|
89
|
+
clear: {
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
description: 'Whether to clear existing text before typing (default: false).',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
required: ['text'],
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'browser_press',
|
|
100
|
+
description: 'Press a keyboard key on the active webpage (e.g. "Enter", "Tab", "Escape", "ArrowDown", "Backspace").',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
key: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'The key to press (e.g. "Enter", "Tab", "Escape").',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ['key'],
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'browser_scroll',
|
|
115
|
+
description: 'Scroll the active webpage up or down.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
direction: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
enum: ['down', 'up'],
|
|
122
|
+
description: 'Direction to scroll (default: "down").',
|
|
123
|
+
},
|
|
124
|
+
amount: {
|
|
125
|
+
type: 'number',
|
|
126
|
+
description: 'Number of pixels to scroll (default: 500).',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'browser_screenshot',
|
|
134
|
+
description: 'Capture a visual PNG screenshot of the current active browser tab.',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {},
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'browser_extract',
|
|
143
|
+
description: 'Extract text or structured data from the webpage according to an extraction instruction.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
instruction: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'Extraction guidance or prompt (e.g. "Extract all product prices and titles").',
|
|
150
|
+
},
|
|
151
|
+
selector: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Optional CSS selector to scope extraction.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
additionalProperties: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'browser_solve_captcha',
|
|
161
|
+
description: 'Attempt to detect and automatically solve or click Cloudflare Turnstile, reCAPTCHA, or hCaptcha verification challenges in the active tab.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
type: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
enum: ['cloudflare', 'recaptcha', 'hcaptcha', 'auto'],
|
|
168
|
+
description: 'Optional captcha type to target (default: auto).',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'weboperator_execute_goal',
|
|
176
|
+
description: 'Execute an autonomous browser goal end-to-end using WebOperator multi-step planner.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
goal: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'Natural language goal for the agent to achieve (e.g. "Find cheapest flight from Paris to Rome on Kayak").',
|
|
183
|
+
},
|
|
184
|
+
timeoutMs: {
|
|
185
|
+
type: 'number',
|
|
186
|
+
description: 'Maximum timeout in milliseconds (default: 120000).',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
required: ['goal'],
|
|
190
|
+
additionalProperties: false,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
async function callBridge(type, payload = {}, timeoutMs = 60_000) {
|
|
196
|
+
// Try Unix Domain Socket first
|
|
197
|
+
try {
|
|
198
|
+
return await callSocket(type, payload, timeoutMs);
|
|
199
|
+
} catch {
|
|
200
|
+
// Fallback to HTTP API
|
|
201
|
+
return await callHttp(type, payload, timeoutMs);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
function callSocket(type, payload, timeoutMs) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
let resolved = false;
|
|
209
|
+
const socket = net.createConnection(SOCKET_PATH);
|
|
210
|
+
const id = randomUUID();
|
|
211
|
+
const message = { id, type, payload, timeoutMs, ...(API_TOKEN ? { token: API_TOKEN } : {}) };
|
|
212
|
+
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
if (!resolved) {
|
|
215
|
+
resolved = true;
|
|
216
|
+
socket.destroy();
|
|
217
|
+
reject(new Error(`Bridge socket timeout for ${type}`));
|
|
218
|
+
}
|
|
219
|
+
}, timeoutMs);
|
|
220
|
+
|
|
221
|
+
let buffer = Buffer.alloc(0);
|
|
222
|
+
let nextLength = null;
|
|
223
|
+
|
|
224
|
+
socket.on('connect', () => {
|
|
225
|
+
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
226
|
+
const header = Buffer.alloc(4);
|
|
227
|
+
header.writeUInt32LE(body.length, 0);
|
|
228
|
+
socket.write(Buffer.concat([header, body]));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
socket.on('data', (chunk) => {
|
|
232
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
233
|
+
while (true) {
|
|
234
|
+
if (nextLength === null) {
|
|
235
|
+
if (buffer.length < 4) return;
|
|
236
|
+
nextLength = buffer.readUInt32LE(0);
|
|
237
|
+
buffer = buffer.slice(4);
|
|
238
|
+
}
|
|
239
|
+
if (buffer.length < nextLength) return;
|
|
240
|
+
const raw = buffer.slice(0, nextLength).toString('utf8');
|
|
241
|
+
buffer = buffer.slice(nextLength);
|
|
242
|
+
nextLength = null;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const msg = JSON.parse(raw);
|
|
246
|
+
if (msg.kind === 'event') continue;
|
|
247
|
+
if (!resolved) {
|
|
248
|
+
resolved = true;
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
socket.end();
|
|
251
|
+
if (msg.error) reject(new Error(msg.error));
|
|
252
|
+
else resolve(msg.result);
|
|
253
|
+
}
|
|
254
|
+
} catch (parseErr) {
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
resolved = true;
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
socket.destroy();
|
|
259
|
+
reject(parseErr);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
socket.on('error', (err) => {
|
|
266
|
+
if (!resolved) {
|
|
267
|
+
resolved = true;
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
reject(err);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function callHttp(type, payload, timeoutMs) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
let path = '/v1/tools/call';
|
|
278
|
+
const method = 'POST';
|
|
279
|
+
const bodyObj = { tool: type, arguments: payload, timeoutMs };
|
|
280
|
+
|
|
281
|
+
const headers = {
|
|
282
|
+
'content-type': 'application/json',
|
|
283
|
+
...(API_TOKEN ? { authorization: `Bearer ${API_TOKEN}`, 'x-weboperator-token': API_TOKEN } : {}),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const req = http.request({
|
|
287
|
+
host: BRIDGE_HOST,
|
|
288
|
+
port: BRIDGE_PORT,
|
|
289
|
+
path,
|
|
290
|
+
method,
|
|
291
|
+
headers,
|
|
292
|
+
timeout: timeoutMs,
|
|
293
|
+
}, (res) => {
|
|
294
|
+
let data = '';
|
|
295
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
296
|
+
res.on('end', () => {
|
|
297
|
+
try {
|
|
298
|
+
const json = JSON.parse(data);
|
|
299
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
300
|
+
reject(new Error(json.error || `HTTP ${res.statusCode}`));
|
|
301
|
+
} else {
|
|
302
|
+
resolve(json.result !== undefined ? json.result : json);
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
reject(new Error(`Failed to parse HTTP bridge response: ${data}`));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
req.on('error', reject);
|
|
312
|
+
req.on('timeout', () => {
|
|
313
|
+
req.destroy();
|
|
314
|
+
reject(new Error(`HTTP bridge timeout for ${type}`));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
req.write(JSON.stringify(bodyObj));
|
|
318
|
+
req.end();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function handleToolCall(name, args) {
|
|
323
|
+
switch (name) {
|
|
324
|
+
case 'browser_snapshot': {
|
|
325
|
+
const res = await callBridge('browser.snapshot', {}, 30_000);
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: 'text',
|
|
330
|
+
text: JSON.stringify(res, null, 2),
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
case 'browser_navigate': {
|
|
336
|
+
const res = await callBridge('browser.navigate', { url: args.url }, 60_000);
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: `Navigated to ${args.url}. Current title: ${res?.title || ''}`,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
case 'browser_click': {
|
|
347
|
+
const res = await callBridge('browser.click', { index: args.index, selector: args.selector }, 30_000);
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
text: `Clicked element. ${res?.status || 'ok'}`,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
case 'browser_type': {
|
|
358
|
+
const res = await callBridge('browser.type', { index: args.index, selector: args.selector, text: args.text, clear: args.clear }, 30_000);
|
|
359
|
+
return {
|
|
360
|
+
content: [
|
|
361
|
+
{
|
|
362
|
+
type: 'text',
|
|
363
|
+
text: `Typed into element: "${args.text}". ${res?.status || 'ok'}`,
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
case 'browser_press': {
|
|
369
|
+
const res = await callBridge('browser.press', { key: args.key }, 30_000);
|
|
370
|
+
return {
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: 'text',
|
|
374
|
+
text: `Pressed key "${args.key}". ${res?.status || 'ok'}`,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case 'browser_scroll': {
|
|
380
|
+
const res = await callBridge('browser.scroll', { direction: args.direction || 'down', amount: args.amount || 500 }, 30_000);
|
|
381
|
+
return {
|
|
382
|
+
content: [
|
|
383
|
+
{
|
|
384
|
+
type: 'text',
|
|
385
|
+
text: `Scrolled ${args.direction || 'down'}. ${res?.status || 'ok'}`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case 'browser_screenshot': {
|
|
391
|
+
const res = await callBridge('browser.screenshot', {}, 30_000);
|
|
392
|
+
const dataUri = res?.dataUri || res?.screenshot || '';
|
|
393
|
+
const base64Data = dataUri.replace(/^data:image\/[a-z]+;base64,/, '');
|
|
394
|
+
if (base64Data) {
|
|
395
|
+
return {
|
|
396
|
+
content: [
|
|
397
|
+
{
|
|
398
|
+
type: 'image',
|
|
399
|
+
data: base64Data,
|
|
400
|
+
mimeType: 'image/png',
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: 'text',
|
|
409
|
+
text: JSON.stringify(res, null, 2),
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
case 'browser_extract': {
|
|
415
|
+
const res = await callBridge('browser.extract', { instruction: args.instruction, selector: args.selector }, 30_000);
|
|
416
|
+
return {
|
|
417
|
+
content: [
|
|
418
|
+
{
|
|
419
|
+
type: 'text',
|
|
420
|
+
text: typeof res === 'string' ? res : JSON.stringify(res, null, 2),
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
case 'browser_solve_captcha': {
|
|
426
|
+
const res = await callBridge('browser.solve_captcha', { type: args.type === 'auto' ? undefined : args.type }, 30_000);
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: typeof res === 'string' ? res : JSON.stringify(res, null, 2),
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
case 'weboperator_execute_goal': {
|
|
437
|
+
const taskTimeout = Number(args.timeoutMs || 120_000);
|
|
438
|
+
const startRes = await callBridge('tasks.start', { goal: args.goal, timeoutMs: taskTimeout }, 30_000);
|
|
439
|
+
const taskId = startRes && startRes.id;
|
|
440
|
+
let finalTask = startRes;
|
|
441
|
+
if (taskId) {
|
|
442
|
+
finalTask = await callBridge('tasks.wait', { id: taskId, timeoutMs: taskTimeout }, taskTimeout + 10_000);
|
|
443
|
+
}
|
|
444
|
+
const formatted = formatTaskResultForAgent(finalTask || startRes);
|
|
445
|
+
const responseText = formatted.answer || JSON.stringify(formatted, null, 2);
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: 'text',
|
|
450
|
+
text: responseText,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
default:
|
|
456
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function formatTaskResultForAgent(task) {
|
|
461
|
+
if (!task) return { ok: false, status: 'failed', error: 'Task not found or timed out' };
|
|
462
|
+
|
|
463
|
+
const steps = Array.isArray(task.steps) ? task.steps : [];
|
|
464
|
+
let answer = '';
|
|
465
|
+
const extractedList = [];
|
|
466
|
+
|
|
467
|
+
for (const step of steps) {
|
|
468
|
+
if (step.toolCall) {
|
|
469
|
+
const args = step.toolCall.arguments || {};
|
|
470
|
+
if (step.toolCall.name === 'done') {
|
|
471
|
+
if (args.answer || args.text || args.note || args.summary) {
|
|
472
|
+
answer = String(args.answer || args.text || args.note || args.summary);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (step.toolCall.name === 'extract' && args.instruction) {
|
|
476
|
+
if (step.result && step.result.extracted) {
|
|
477
|
+
extractedList.push(step.result.extracted);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (step.result && step.result.extracted !== undefined) {
|
|
482
|
+
extractedList.push(step.result.extracted);
|
|
483
|
+
}
|
|
484
|
+
if (!answer && step.note && step.status === 'ok') {
|
|
485
|
+
answer = step.note;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!answer && extractedList.length > 0) {
|
|
490
|
+
answer = typeof extractedList[extractedList.length - 1] === 'string'
|
|
491
|
+
? extractedList[extractedList.length - 1]
|
|
492
|
+
: JSON.stringify(extractedList, null, 2);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!answer && task.plan && Array.isArray(task.plan.steps)) {
|
|
496
|
+
const doneSteps = task.plan.steps.filter((s) => s.status === 'done');
|
|
497
|
+
if (doneSteps.length > 0) {
|
|
498
|
+
answer = doneSteps.map((s) => `✓ ${s.description}`).join('\n');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!answer && task.status === 'done') {
|
|
503
|
+
answer = `Goal completed successfully: "${task.goal}"`;
|
|
504
|
+
} else if (!answer && task.status === 'failed') {
|
|
505
|
+
answer = `Goal failed: ${task.error || 'Unknown error'}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
ok: task.status === 'done',
|
|
510
|
+
status: task.status,
|
|
511
|
+
goal: task.goal,
|
|
512
|
+
answer,
|
|
513
|
+
extracted: extractedList.length > 0 ? extractedList : undefined,
|
|
514
|
+
stepCount: steps.length,
|
|
515
|
+
error: task.error,
|
|
516
|
+
modelUsed: task.modelUsed,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
function sendJsonRpc(obj) {
|
|
522
|
+
const line = JSON.stringify(obj) + '\n';
|
|
523
|
+
process.stdout.write(line);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function sendResponse(id, result) {
|
|
527
|
+
sendJsonRpc({ jsonrpc: '2.0', id, result });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function sendError(id, code, message, data) {
|
|
531
|
+
sendJsonRpc({ jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) } });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const rl = readline.createInterface({
|
|
535
|
+
input: process.stdin,
|
|
536
|
+
output: process.stdout,
|
|
537
|
+
terminal: false,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
rl.on('line', async (line) => {
|
|
541
|
+
const trimmed = line.trim();
|
|
542
|
+
if (!trimmed) return;
|
|
543
|
+
|
|
544
|
+
let msg;
|
|
545
|
+
try {
|
|
546
|
+
msg = JSON.parse(trimmed);
|
|
547
|
+
} catch {
|
|
548
|
+
sendError(null, -32700, 'Parse error');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
const { id, method, params } = msg;
|
|
554
|
+
|
|
555
|
+
if (method === 'initialize') {
|
|
556
|
+
sendResponse(id, {
|
|
557
|
+
protocolVersion: '2024-11-05',
|
|
558
|
+
capabilities: {
|
|
559
|
+
tools: {
|
|
560
|
+
listChanged: false,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
serverInfo: {
|
|
564
|
+
name: 'weboperator-mcp',
|
|
565
|
+
version: SERVER_VERSION,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (method === 'notifications/initialized' || method === 'initialized') {
|
|
572
|
+
// Client notification, no response required
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (method === 'ping') {
|
|
577
|
+
sendResponse(id, {});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (method === 'tools/list') {
|
|
582
|
+
sendResponse(id, { tools: TOOLS });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (method === 'tools/call') {
|
|
587
|
+
const { name, arguments: toolArgs } = params || {};
|
|
588
|
+
try {
|
|
589
|
+
const result = await handleToolCall(name, toolArgs || {});
|
|
590
|
+
sendResponse(id, result);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
sendResponse(id, {
|
|
593
|
+
isError: true,
|
|
594
|
+
content: [
|
|
595
|
+
{
|
|
596
|
+
type: 'text',
|
|
597
|
+
text: err instanceof Error ? err.message : String(err),
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (id !== undefined) {
|
|
606
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
607
|
+
}
|
|
608
|
+
});
|
package/native-host.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$HOME/.hermes/node/bin:$PATH"
|
|
5
|
+
|
|
6
|
+
NODE_BIN="$(command -v node 2>/dev/null || echo "/opt/homebrew/bin/node")"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
|
|
9
|
+
exec "$NODE_BIN" "$SCRIPT_DIR/bridge.js" "$@"
|