@different-ai/opencode-browser 1.0.5 → 2.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/README.md +78 -52
- package/bin/cli.js +151 -251
- package/extension/background.js +3 -3
- package/extension/manifest.json +2 -3
- package/package.json +20 -9
- package/src/plugin.ts +450 -0
- package/src/daemon.js +0 -207
- package/src/host.js +0 -282
- package/src/server.js +0 -379
package/src/host.js
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Native Messaging Host for OpenCode Browser Automation
|
|
4
|
-
*
|
|
5
|
-
* This script is launched by Chrome when the extension connects.
|
|
6
|
-
* It communicates with Chrome via stdin/stdout using Chrome's native messaging protocol.
|
|
7
|
-
* It also connects to an MCP server (or acts as one) to receive tool requests.
|
|
8
|
-
*
|
|
9
|
-
* Chrome Native Messaging Protocol:
|
|
10
|
-
* - Messages are length-prefixed (4 bytes, little-endian, uint32)
|
|
11
|
-
* - Message body is JSON
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createServer } from "net";
|
|
15
|
-
import { writeFileSync, appendFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
|
|
19
|
-
const LOG_DIR = join(homedir(), ".opencode-browser", "logs");
|
|
20
|
-
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
21
|
-
const LOG_FILE = join(LOG_DIR, "host.log");
|
|
22
|
-
|
|
23
|
-
function log(...args) {
|
|
24
|
-
const timestamp = new Date().toISOString();
|
|
25
|
-
const message = `[${timestamp}] ${args.join(" ")}\n`;
|
|
26
|
-
appendFileSync(LOG_FILE, message);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
log("Native host started");
|
|
30
|
-
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Chrome Native Messaging Protocol
|
|
33
|
-
// ============================================================================
|
|
34
|
-
|
|
35
|
-
function readMessage() {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
let lengthBuffer = Buffer.alloc(0);
|
|
38
|
-
let messageBuffer = Buffer.alloc(0);
|
|
39
|
-
let messageLength = null;
|
|
40
|
-
|
|
41
|
-
const processData = () => {
|
|
42
|
-
// First, read the 4-byte length prefix
|
|
43
|
-
if (messageLength === null) {
|
|
44
|
-
const needed = 4 - lengthBuffer.length;
|
|
45
|
-
const chunk = process.stdin.read(needed);
|
|
46
|
-
if (chunk === null) {
|
|
47
|
-
process.stdin.once("readable", processData);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
52
|
-
lengthBuffer = Buffer.concat([lengthBuffer, chunkBuf]);
|
|
53
|
-
|
|
54
|
-
if (lengthBuffer.length < 4) {
|
|
55
|
-
process.stdin.once("readable", processData);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
messageLength = lengthBuffer.readUInt32LE(0);
|
|
60
|
-
if (messageLength === 0) {
|
|
61
|
-
resolve(null);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Now read the message body
|
|
67
|
-
const needed = messageLength - messageBuffer.length;
|
|
68
|
-
const chunk = process.stdin.read(needed);
|
|
69
|
-
if (chunk === null) {
|
|
70
|
-
process.stdin.once("readable", processData);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
75
|
-
messageBuffer = Buffer.concat([messageBuffer, chunkBuf]);
|
|
76
|
-
|
|
77
|
-
if (messageBuffer.length < messageLength) {
|
|
78
|
-
process.stdin.once("readable", processData);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const message = JSON.parse(messageBuffer.toString("utf8"));
|
|
84
|
-
resolve(message);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
reject(new Error(`Failed to parse message: ${e.message}`));
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
processData();
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function writeMessage(message) {
|
|
95
|
-
const json = JSON.stringify(message);
|
|
96
|
-
const buffer = Buffer.from(json, "utf8");
|
|
97
|
-
const lengthBuffer = Buffer.alloc(4);
|
|
98
|
-
lengthBuffer.writeUInt32LE(buffer.length, 0);
|
|
99
|
-
|
|
100
|
-
process.stdout.write(lengthBuffer);
|
|
101
|
-
process.stdout.write(buffer);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// MCP Server Connection
|
|
106
|
-
// ============================================================================
|
|
107
|
-
|
|
108
|
-
const SOCKET_PATH = join(homedir(), ".opencode-browser", "browser.sock");
|
|
109
|
-
let mcpConnected = false;
|
|
110
|
-
let mcpSocket = null;
|
|
111
|
-
let pendingRequests = new Map();
|
|
112
|
-
let requestId = 0;
|
|
113
|
-
|
|
114
|
-
function connectToMcpServer() {
|
|
115
|
-
// We'll create a Unix socket server that the MCP server connects to
|
|
116
|
-
// This way the host can receive tool requests from OpenCode
|
|
117
|
-
|
|
118
|
-
// Clean up old socket
|
|
119
|
-
try {
|
|
120
|
-
if (existsSync(SOCKET_PATH)) {
|
|
121
|
-
unlinkSync(SOCKET_PATH);
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
124
|
-
|
|
125
|
-
const server = createServer((socket) => {
|
|
126
|
-
log("MCP server connected");
|
|
127
|
-
mcpSocket = socket;
|
|
128
|
-
mcpConnected = true;
|
|
129
|
-
|
|
130
|
-
// Notify extension
|
|
131
|
-
writeMessage({ type: "mcp_connected" });
|
|
132
|
-
|
|
133
|
-
let buffer = "";
|
|
134
|
-
|
|
135
|
-
socket.on("data", (data) => {
|
|
136
|
-
buffer += data.toString();
|
|
137
|
-
|
|
138
|
-
// Process complete JSON messages (newline-delimited)
|
|
139
|
-
const lines = buffer.split("\n");
|
|
140
|
-
buffer = lines.pop() || "";
|
|
141
|
-
|
|
142
|
-
for (const line of lines) {
|
|
143
|
-
if (line.trim()) {
|
|
144
|
-
try {
|
|
145
|
-
const message = JSON.parse(line);
|
|
146
|
-
handleMcpMessage(message);
|
|
147
|
-
} catch (e) {
|
|
148
|
-
log("Failed to parse MCP message:", e.message);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
socket.on("close", () => {
|
|
155
|
-
log("MCP server disconnected");
|
|
156
|
-
mcpSocket = null;
|
|
157
|
-
mcpConnected = false;
|
|
158
|
-
writeMessage({ type: "mcp_disconnected" });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
socket.on("error", (err) => {
|
|
162
|
-
log("MCP socket error:", err.message);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
server.listen(SOCKET_PATH, () => {
|
|
167
|
-
log("Listening for MCP connections on", SOCKET_PATH);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
server.on("error", (err) => {
|
|
171
|
-
log("Server error:", err.message);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function handleMcpMessage(message) {
|
|
176
|
-
log("Received from MCP:", JSON.stringify(message));
|
|
177
|
-
|
|
178
|
-
if (message.type === "tool_request") {
|
|
179
|
-
// Forward tool request to Chrome extension
|
|
180
|
-
const id = ++requestId;
|
|
181
|
-
pendingRequests.set(id, message.id); // Map our ID to MCP's ID
|
|
182
|
-
|
|
183
|
-
writeMessage({
|
|
184
|
-
type: "tool_request",
|
|
185
|
-
id,
|
|
186
|
-
tool: message.tool,
|
|
187
|
-
args: message.args
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function sendToMcp(message) {
|
|
193
|
-
if (mcpSocket && !mcpSocket.destroyed) {
|
|
194
|
-
mcpSocket.write(JSON.stringify(message) + "\n");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ============================================================================
|
|
199
|
-
// Handle Messages from Chrome Extension
|
|
200
|
-
// ============================================================================
|
|
201
|
-
|
|
202
|
-
async function handleChromeMessage(message) {
|
|
203
|
-
log("Received from Chrome:", JSON.stringify(message));
|
|
204
|
-
|
|
205
|
-
switch (message.type) {
|
|
206
|
-
case "ping":
|
|
207
|
-
writeMessage({ type: "pong" });
|
|
208
|
-
break;
|
|
209
|
-
|
|
210
|
-
case "tool_response":
|
|
211
|
-
// Forward response back to MCP server
|
|
212
|
-
const mcpId = pendingRequests.get(message.id);
|
|
213
|
-
if (mcpId !== undefined) {
|
|
214
|
-
pendingRequests.delete(message.id);
|
|
215
|
-
sendToMcp({
|
|
216
|
-
type: "tool_response",
|
|
217
|
-
id: mcpId,
|
|
218
|
-
result: message.result,
|
|
219
|
-
error: message.error
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
|
|
224
|
-
case "get_status":
|
|
225
|
-
writeMessage({
|
|
226
|
-
type: "status_response",
|
|
227
|
-
mcpConnected
|
|
228
|
-
});
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ============================================================================
|
|
234
|
-
// Main Loop
|
|
235
|
-
// ============================================================================
|
|
236
|
-
|
|
237
|
-
async function main() {
|
|
238
|
-
process.stdin.on("end", () => {
|
|
239
|
-
log("stdin ended, Chrome disconnected");
|
|
240
|
-
process.exit(0);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
process.stdin.on("close", () => {
|
|
244
|
-
log("stdin closed, Chrome disconnected");
|
|
245
|
-
process.exit(0);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
connectToMcpServer();
|
|
249
|
-
|
|
250
|
-
while (true) {
|
|
251
|
-
try {
|
|
252
|
-
const message = await readMessage();
|
|
253
|
-
if (message === null) {
|
|
254
|
-
log("Received null message, exiting");
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
await handleChromeMessage(message);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
log("Error reading message:", error.message);
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
log("Native host exiting");
|
|
265
|
-
process.exit(0);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Handle graceful shutdown
|
|
269
|
-
process.on("SIGTERM", () => {
|
|
270
|
-
log("Received SIGTERM");
|
|
271
|
-
process.exit(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
process.on("SIGINT", () => {
|
|
275
|
-
log("Received SIGINT");
|
|
276
|
-
process.exit(0);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
main().catch((error) => {
|
|
280
|
-
log("Fatal error:", error.message);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
});
|
package/src/server.js
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* MCP Server for Browser Automation
|
|
4
|
-
*
|
|
5
|
-
* This server exposes browser automation tools to OpenCode via MCP.
|
|
6
|
-
* It connects to the native messaging host via Unix socket to execute commands.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import {
|
|
12
|
-
CallToolRequestSchema,
|
|
13
|
-
ListToolsRequestSchema,
|
|
14
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
import { createConnection } from "net";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
|
|
19
|
-
const SOCKET_PATH = join(homedir(), ".opencode-browser", "browser.sock");
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Socket Connection to Native Host
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
let socket = null;
|
|
26
|
-
let connected = false;
|
|
27
|
-
let pendingRequests = new Map();
|
|
28
|
-
let requestId = 0;
|
|
29
|
-
let buffer = "";
|
|
30
|
-
|
|
31
|
-
function connectToHost() {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
socket = createConnection(SOCKET_PATH);
|
|
34
|
-
|
|
35
|
-
socket.on("connect", () => {
|
|
36
|
-
console.error("[browser-mcp] Connected to native host");
|
|
37
|
-
connected = true;
|
|
38
|
-
resolve();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
socket.on("data", (data) => {
|
|
42
|
-
buffer += data.toString();
|
|
43
|
-
|
|
44
|
-
const lines = buffer.split("\n");
|
|
45
|
-
buffer = lines.pop() || "";
|
|
46
|
-
|
|
47
|
-
for (const line of lines) {
|
|
48
|
-
if (line.trim()) {
|
|
49
|
-
try {
|
|
50
|
-
const message = JSON.parse(line);
|
|
51
|
-
handleHostMessage(message);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.error("[browser-mcp] Failed to parse:", e.message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
socket.on("close", () => {
|
|
60
|
-
console.error("[browser-mcp] Disconnected from native host");
|
|
61
|
-
connected = false;
|
|
62
|
-
// Reject all pending requests
|
|
63
|
-
for (const [id, { reject }] of pendingRequests) {
|
|
64
|
-
reject(new Error("Connection closed"));
|
|
65
|
-
}
|
|
66
|
-
pendingRequests.clear();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
socket.on("error", (err) => {
|
|
70
|
-
console.error("[browser-mcp] Socket error:", err.message);
|
|
71
|
-
if (!connected) {
|
|
72
|
-
reject(err);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function handleHostMessage(message) {
|
|
79
|
-
if (message.type === "tool_response") {
|
|
80
|
-
const pending = pendingRequests.get(message.id);
|
|
81
|
-
if (pending) {
|
|
82
|
-
pendingRequests.delete(message.id);
|
|
83
|
-
if (message.error) {
|
|
84
|
-
pending.reject(new Error(message.error.content));
|
|
85
|
-
} else {
|
|
86
|
-
pending.resolve(message.result.content);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function executeTool(tool, args) {
|
|
93
|
-
if (!connected) {
|
|
94
|
-
// Try to reconnect
|
|
95
|
-
try {
|
|
96
|
-
await connectToHost();
|
|
97
|
-
} catch {
|
|
98
|
-
throw new Error("Not connected to browser extension. Make sure Chrome is running with the OpenCode extension installed.");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const id = ++requestId;
|
|
103
|
-
|
|
104
|
-
return new Promise((resolve, reject) => {
|
|
105
|
-
pendingRequests.set(id, { resolve, reject });
|
|
106
|
-
|
|
107
|
-
socket.write(JSON.stringify({
|
|
108
|
-
type: "tool_request",
|
|
109
|
-
id,
|
|
110
|
-
tool,
|
|
111
|
-
args
|
|
112
|
-
}) + "\n");
|
|
113
|
-
|
|
114
|
-
// Timeout after 60 seconds
|
|
115
|
-
setTimeout(() => {
|
|
116
|
-
if (pendingRequests.has(id)) {
|
|
117
|
-
pendingRequests.delete(id);
|
|
118
|
-
reject(new Error("Tool execution timed out"));
|
|
119
|
-
}
|
|
120
|
-
}, 60000);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ============================================================================
|
|
125
|
-
// MCP Server
|
|
126
|
-
// ============================================================================
|
|
127
|
-
|
|
128
|
-
const server = new Server(
|
|
129
|
-
{
|
|
130
|
-
name: "browser-mcp",
|
|
131
|
-
version: "1.0.0",
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
capabilities: {
|
|
135
|
-
tools: {},
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
// List available tools
|
|
141
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
142
|
-
return {
|
|
143
|
-
tools: [
|
|
144
|
-
{
|
|
145
|
-
name: "browser_navigate",
|
|
146
|
-
description: "Navigate to a URL in the browser",
|
|
147
|
-
inputSchema: {
|
|
148
|
-
type: "object",
|
|
149
|
-
properties: {
|
|
150
|
-
url: {
|
|
151
|
-
type: "string",
|
|
152
|
-
description: "The URL to navigate to"
|
|
153
|
-
},
|
|
154
|
-
tabId: {
|
|
155
|
-
type: "number",
|
|
156
|
-
description: "Optional tab ID. Uses active tab if not specified."
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
required: ["url"]
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: "browser_click",
|
|
164
|
-
description: "Click an element on the page using a CSS selector",
|
|
165
|
-
inputSchema: {
|
|
166
|
-
type: "object",
|
|
167
|
-
properties: {
|
|
168
|
-
selector: {
|
|
169
|
-
type: "string",
|
|
170
|
-
description: "CSS selector for the element to click"
|
|
171
|
-
},
|
|
172
|
-
tabId: {
|
|
173
|
-
type: "number",
|
|
174
|
-
description: "Optional tab ID"
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
required: ["selector"]
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
name: "browser_type",
|
|
182
|
-
description: "Type text into an input element",
|
|
183
|
-
inputSchema: {
|
|
184
|
-
type: "object",
|
|
185
|
-
properties: {
|
|
186
|
-
selector: {
|
|
187
|
-
type: "string",
|
|
188
|
-
description: "CSS selector for the input element"
|
|
189
|
-
},
|
|
190
|
-
text: {
|
|
191
|
-
type: "string",
|
|
192
|
-
description: "Text to type"
|
|
193
|
-
},
|
|
194
|
-
clear: {
|
|
195
|
-
type: "boolean",
|
|
196
|
-
description: "Clear the field before typing"
|
|
197
|
-
},
|
|
198
|
-
tabId: {
|
|
199
|
-
type: "number",
|
|
200
|
-
description: "Optional tab ID"
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
required: ["selector", "text"]
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
name: "browser_screenshot",
|
|
208
|
-
description: "Take a screenshot of the current page",
|
|
209
|
-
inputSchema: {
|
|
210
|
-
type: "object",
|
|
211
|
-
properties: {
|
|
212
|
-
tabId: {
|
|
213
|
-
type: "number",
|
|
214
|
-
description: "Optional tab ID"
|
|
215
|
-
},
|
|
216
|
-
fullPage: {
|
|
217
|
-
type: "boolean",
|
|
218
|
-
description: "Capture full page (not yet implemented)"
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
name: "browser_snapshot",
|
|
225
|
-
description: "Get an accessibility tree snapshot of the page. Returns interactive elements with selectors.",
|
|
226
|
-
inputSchema: {
|
|
227
|
-
type: "object",
|
|
228
|
-
properties: {
|
|
229
|
-
tabId: {
|
|
230
|
-
type: "number",
|
|
231
|
-
description: "Optional tab ID"
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
name: "browser_get_tabs",
|
|
238
|
-
description: "List all open browser tabs",
|
|
239
|
-
inputSchema: {
|
|
240
|
-
type: "object",
|
|
241
|
-
properties: {}
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
name: "browser_scroll",
|
|
246
|
-
description: "Scroll the page or scroll an element into view",
|
|
247
|
-
inputSchema: {
|
|
248
|
-
type: "object",
|
|
249
|
-
properties: {
|
|
250
|
-
selector: {
|
|
251
|
-
type: "string",
|
|
252
|
-
description: "CSS selector to scroll into view"
|
|
253
|
-
},
|
|
254
|
-
x: {
|
|
255
|
-
type: "number",
|
|
256
|
-
description: "Horizontal scroll amount in pixels"
|
|
257
|
-
},
|
|
258
|
-
y: {
|
|
259
|
-
type: "number",
|
|
260
|
-
description: "Vertical scroll amount in pixels"
|
|
261
|
-
},
|
|
262
|
-
tabId: {
|
|
263
|
-
type: "number",
|
|
264
|
-
description: "Optional tab ID"
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: "browser_wait",
|
|
271
|
-
description: "Wait for a specified duration",
|
|
272
|
-
inputSchema: {
|
|
273
|
-
type: "object",
|
|
274
|
-
properties: {
|
|
275
|
-
ms: {
|
|
276
|
-
type: "number",
|
|
277
|
-
description: "Milliseconds to wait (default: 1000)"
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: "browser_execute",
|
|
284
|
-
description: "Execute JavaScript code in the page context",
|
|
285
|
-
inputSchema: {
|
|
286
|
-
type: "object",
|
|
287
|
-
properties: {
|
|
288
|
-
code: {
|
|
289
|
-
type: "string",
|
|
290
|
-
description: "JavaScript code to execute"
|
|
291
|
-
},
|
|
292
|
-
tabId: {
|
|
293
|
-
type: "number",
|
|
294
|
-
description: "Optional tab ID"
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
required: ["code"]
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
]
|
|
301
|
-
};
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Handle tool calls
|
|
305
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
306
|
-
const { name, arguments: args } = request.params;
|
|
307
|
-
|
|
308
|
-
// Map MCP tool names to internal tool names
|
|
309
|
-
const toolMap = {
|
|
310
|
-
browser_navigate: "navigate",
|
|
311
|
-
browser_click: "click",
|
|
312
|
-
browser_type: "type",
|
|
313
|
-
browser_screenshot: "screenshot",
|
|
314
|
-
browser_snapshot: "snapshot",
|
|
315
|
-
browser_get_tabs: "get_tabs",
|
|
316
|
-
browser_scroll: "scroll",
|
|
317
|
-
browser_wait: "wait",
|
|
318
|
-
browser_execute: "execute_script"
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const internalTool = toolMap[name];
|
|
322
|
-
if (!internalTool) {
|
|
323
|
-
return {
|
|
324
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
325
|
-
isError: true
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
const result = await executeTool(internalTool, args || {});
|
|
331
|
-
|
|
332
|
-
// Handle screenshot specially - return as image
|
|
333
|
-
if (internalTool === "screenshot" && result.startsWith("data:image")) {
|
|
334
|
-
const base64Data = result.replace(/^data:image\/\w+;base64,/, "");
|
|
335
|
-
return {
|
|
336
|
-
content: [
|
|
337
|
-
{
|
|
338
|
-
type: "image",
|
|
339
|
-
data: base64Data,
|
|
340
|
-
mimeType: "image/png"
|
|
341
|
-
}
|
|
342
|
-
]
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
content: [{ type: "text", text: result }]
|
|
348
|
-
};
|
|
349
|
-
} catch (error) {
|
|
350
|
-
return {
|
|
351
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
352
|
-
isError: true
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// ============================================================================
|
|
358
|
-
// Main
|
|
359
|
-
// ============================================================================
|
|
360
|
-
|
|
361
|
-
async function main() {
|
|
362
|
-
// Try to connect to native host
|
|
363
|
-
try {
|
|
364
|
-
await connectToHost();
|
|
365
|
-
} catch (error) {
|
|
366
|
-
console.error("[browser-mcp] Warning: Could not connect to native host:", error.message);
|
|
367
|
-
console.error("[browser-mcp] Will retry on first tool call");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Start MCP server
|
|
371
|
-
const transport = new StdioServerTransport();
|
|
372
|
-
await server.connect(transport);
|
|
373
|
-
console.error("[browser-mcp] MCP server started");
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
main().catch((error) => {
|
|
377
|
-
console.error("[browser-mcp] Fatal error:", error);
|
|
378
|
-
process.exit(1);
|
|
379
|
-
});
|