@different-ai/opencode-browser 1.0.5 → 2.0.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/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
- });