@aikeymouse/chromelink-mcp 1.2.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 ADDED
@@ -0,0 +1,282 @@
1
+ # ChromeLink MCP Server
2
+
3
+ Model Context Protocol (MCP) server that exposes Chrome browser automation capabilities to AI agents like Claude, GPT, and other MCP-compatible tools.
4
+
5
+ ## Installation
6
+
7
+ ### Option 1: NPM (Recommended)
8
+
9
+ ```bash
10
+ npm install -g @aikeymouse/chromelink-mcp
11
+ ```
12
+
13
+ Then configure in your MCP client:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "chrome-link": {
19
+ "command": "chromelink-mcp"
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ### Option 2: From Source
26
+
27
+ Clone the repository and use the server directly:
28
+
29
+ ```bash
30
+ git clone https://github.com/aikeymouse/chrome-link.git
31
+ cd chrome-link/mcp-server
32
+ npm install
33
+ ```
34
+
35
+ Configure with absolute path:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "chrome-link": {
41
+ "command": "node",
42
+ "args": ["/absolute/path/to/chrome-link/mcp-server/index.js"]
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Prerequisites
49
+
50
+ 1. **Install ChromeLink extension** in Chrome
51
+ 2. **Start browser-link-server** (automatically started by extension on first connection)
52
+
53
+ ## Local Development
54
+
55
+ For local development with file dependencies:
56
+
57
+ ```bash
58
+ cd chrome-link/mcp-server
59
+ npm install # Creates symlink to ../clients/node
60
+ ```
61
+
62
+ The MCP server uses a local file dependency (`"@aikeymouse/chromelink-client": "file:../clients/node"`) for development. This is automatically converted to an npm dependency (`^1.2.0`) during publishing via the `prepublishOnly` script.
63
+
64
+ After version changes, update the symlink:
65
+ ```bash
66
+ cd mcp-server && npm install
67
+ ```
68
+
69
+ ## Running the MCP Server
70
+
71
+ ### From NPM Installation
72
+ ```bash
73
+ chromelink-mcp
74
+ ```
75
+
76
+ ### From Source
77
+ ```bash
78
+ node mcp-server/index.js
79
+ ```
80
+
81
+ The MCP server will:
82
+ - Connect to `ws://localhost:9000` (browser-link-server)
83
+ - Expose 18 Chrome automation tools via MCP protocol
84
+ - Communicate with AI agents via stdio
85
+
86
+ ## Available Tools
87
+
88
+ ### Tab Management
89
+ - `chrome_list_tabs` - List all open tabs
90
+ - `chrome_open_tab` - Open new tab with URL
91
+ - `chrome_navigate_tab` - Navigate tab to URL
92
+ - `chrome_switch_tab` - Switch to specific tab
93
+ - `chrome_close_tab` - Close a tab
94
+ - `chrome_get_active_tab` - Get active tab info
95
+
96
+ ### Navigation History
97
+ - `chrome_go_back` - Navigate back in history
98
+ - `chrome_go_forward` - Navigate forward in history
99
+
100
+ ### DOM Interaction
101
+ - `chrome_wait_for_element` - Wait for element to appear
102
+ - `chrome_get_text` - Get element text content
103
+ - `chrome_click` - Click an element
104
+ - `chrome_type` - Type text into element
105
+
106
+ ### JavaScript Execution
107
+ - `chrome_execute_js` - Execute arbitrary JavaScript
108
+ - `chrome_call_helper` - Call predefined helper functions:
109
+ - **Element Interaction**: clickElement, typeText, appendChar, clearContentEditable
110
+ - **Element Query**: getText, getHTML, getLastHTML, elementExists, isVisible, waitForElement
111
+ - **Element Highlighting**: highlightElement, removeHighlights
112
+ - **Element Positioning**: getElementBounds, scrollElementIntoView
113
+ - **Element Inspection**: inspectElement, getContainerElements
114
+
115
+ ### Screenshots
116
+ - `chrome_capture_screenshot` - Capture tab screenshot (PNG/JPEG)
117
+
118
+ ### Script Injection
119
+ - `chrome_register_injection` - Register content script
120
+ - `chrome_unregister_injection` - Unregister content script
121
+
122
+ ## Configuration for AI Agents
123
+
124
+ ### Claude Desktop
125
+
126
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "chrome-link": {
132
+ "command": "node",
133
+ "args": ["/absolute/path/to/chrome-driver-extension/mcp-server/index.js"]
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ **Important**: Use absolute path to `index.js`
140
+
141
+ After configuration:
142
+ 1. Restart Claude Desktop
143
+ 2. Start a conversation
144
+ 3. Ask Claude: "What Chrome tools do you have?"
145
+
146
+ ### Example Prompts
147
+
148
+ ```
149
+ Open https://github.com in a new Chrome tab
150
+
151
+ List all my open Chrome tabs
152
+
153
+ Go to https://example.com and get the text of the h1 element
154
+
155
+ Take a screenshot of the current tab in PNG format
156
+
157
+ Fill out the login form:
158
+ 1. Open https://the-internet.herokuapp.com/login
159
+ 2. Type "tomsmith" in #username
160
+ 3. Type "SuperSecretPassword!" in #password
161
+ 4. Click the login button
162
+ ```
163
+
164
+ ## Architecture
165
+
166
+ ```
167
+ AI Agent (Claude/GPT)
168
+ ↓ stdio (MCP protocol)
169
+ MCP Server (this file)
170
+ ↓ WebSocket (ws://localhost:9000)
171
+ browser-link-server
172
+ ↓ native messaging (stdin/stdout)
173
+ Chrome Extension
174
+ ```
175
+
176
+ **Key Design Points:**
177
+ - MCP server runs as **separate process** (avoids stdio conflict)
178
+ - Uses existing WebSocket client (thin wrapper, no code duplication)
179
+ - Zero changes needed to browser-link-server or extension
180
+ - Multiple MCP servers can connect simultaneously
181
+
182
+ ## Testing
183
+
184
+ ### Manual Test
185
+
186
+ ```bash
187
+ # Start browser-link-server (if not auto-started)
188
+ node native-host/browser-link-server.js
189
+
190
+ # In another terminal, test MCP server
191
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node mcp-server/index.js
192
+ ```
193
+
194
+ Expected output:
195
+ ```json
196
+ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"chrome-link","version":"1.0.0"}}}
197
+ ```
198
+
199
+ ### Test Tool Invocation
200
+
201
+ ```bash
202
+ # Create test file
203
+ cat > /tmp/test-mcp.json << 'EOF'
204
+ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}
205
+ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chrome_list_tabs","arguments":{}}}
206
+ EOF
207
+
208
+ # Run test
209
+ cat /tmp/test-mcp.json | node native-host/mcp-server.js 2>/dev/null
210
+ ```
211
+
212
+ ### Using MCP Inspector
213
+
214
+ ```bash
215
+ npm install -g @modelcontextprotocol/inspector
216
+ npx @modelcontextprotocol/inspector node mcp-server/index.js
217
+ ```
218
+
219
+ Opens web UI for testing tools interactively.
220
+
221
+ ## Troubleshooting
222
+
223
+ ### MCP Server Won't Connect
224
+
225
+ **Error**: `Failed to connect to browser-link-server`
226
+
227
+ **Solution**:
228
+ 1. Check Chrome extension is installed: `chrome://extensions/`
229
+ 2. Check browser-link-server is running: `curl http://localhost:9000/session` (expect `{"status":"ready","message":"Upgrade to WebSocket"}`)
230
+ 3. Test WebSocket manually: `wscat -c ws://localhost:9000`
231
+
232
+ ### Tools Not Showing in Claude
233
+
234
+ **Error**: Claude doesn't list Chrome tools
235
+
236
+ **Solution**:
237
+ 1. Verify config file path is absolute (not relative)
238
+ 2. Check JSON syntax: `python3 -m json.tool < ~/Library/Application\ Support/Claude/claude_desktop_config.json`
239
+ 3. Restart Claude Desktop
240
+ 4. Check Claude logs: `~/Library/Logs/Claude/`
241
+
242
+ ### Tool Execution Fails
243
+
244
+ **Error**: `TypeError: tabId is not a number`
245
+
246
+ **Solution**:
247
+ 1. Use `chrome_list_tabs` first to get valid tab IDs
248
+ 2. Ensure tab still exists (not closed)
249
+ 3. Check MCP server logs (stderr output)
250
+
251
+ ## Logging
252
+
253
+ MCP server logs to **stderr** (stdout is reserved for MCP protocol):
254
+ - Connection status
255
+ - Tool invocations
256
+ - Errors and warnings
257
+
258
+ To see logs:
259
+ ```bash
260
+ node native-host/mcp-server.js 2>&1 | grep "\[MCP Server\]"
261
+ ```
262
+
263
+ ## Security
264
+
265
+ ⚠️ **MCP server has full browser control** - use with trusted AI agents only
266
+
267
+ Recommendations:
268
+ - Run in separate browser profile for automation
269
+ - Monitor AI agent actions via logs
270
+ - Disable MCP server when not needed
271
+ - Consider adding authentication for production use
272
+
273
+ ## Documentation
274
+
275
+ - [MCP Server Architecture](../docs/dev/MCP_SERVER_ARCHITECTURE.md) - Detailed design docs
276
+ - [MCP Server Configuration](../docs/dev/MCP_SERVER_CONFIGURATION.md) - Setup guide for various clients
277
+ - [ChromeLink Protocol](../docs/PROTOCOL.md) - WebSocket protocol specification
278
+ - [Node.js Client](../clients/node/README.md) - Client library used by MCP server
279
+
280
+ ## License
281
+
282
+ MIT - See LICENSE file in repository root
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ChromeLink MCP Server - Executable
5
+ *
6
+ * This script launches the MCP server for Chrome browser automation.
7
+ * It connects to the browser-link-server via WebSocket and exposes
8
+ * Chrome automation capabilities via the Model Context Protocol.
9
+ */
10
+
11
+ require('../index.js');
package/index.js ADDED
@@ -0,0 +1,658 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ChromeLink MCP Server
5
+ *
6
+ * Model Context Protocol (MCP) server that exposes Chrome browser automation
7
+ * capabilities to AI agents like Claude, GPT, etc.
8
+ *
9
+ * This server acts as a thin wrapper around the ChromeLink WebSocket client,
10
+ * exposing browser automation as MCP tools that AI agents can discover and use.
11
+ *
12
+ * Architecture:
13
+ * AI Agent (Claude/GPT) <-> MCP Server (this file) <-> browser-link-server <-> Chrome Extension
14
+ *
15
+ * Communication:
16
+ * - AI Agent ↔ MCP Server: stdio (MCP protocol)
17
+ * - MCP Server ↔ browser-link-server: WebSocket (ws://localhost:9000)
18
+ *
19
+ * Usage:
20
+ * node mcp-server.js
21
+ *
22
+ * Configuration (Claude Desktop):
23
+ * Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
24
+ * {
25
+ * "mcpServers": {
26
+ * "chrome-link": {
27
+ * "command": "node",
28
+ * "args": ["/path/to/chrome-driver-extension/native-host/mcp-server.js"]
29
+ * }
30
+ * }
31
+ * }
32
+ */
33
+
34
+ const ChromeLinkClient = require('@aikeymouse/chromelink-client');
35
+
36
+ // MCP Protocol Implementation
37
+ class MCPServer {
38
+ constructor() {
39
+ this.client = null;
40
+ this.requestId = 1;
41
+ }
42
+
43
+ /**
44
+ * Log to stderr (stdout is reserved for MCP protocol)
45
+ */
46
+ log(message, ...args) {
47
+ console.error(`[MCP Server] ${message}`, ...args);
48
+ }
49
+
50
+ /**
51
+ * Send MCP response to stdout
52
+ */
53
+ sendResponse(response) {
54
+ const message = JSON.stringify(response);
55
+ process.stdout.write(message + '\n');
56
+ }
57
+
58
+ /**
59
+ * Send error response
60
+ */
61
+ sendError(id, code, message) {
62
+ this.sendResponse({
63
+ jsonrpc: '2.0',
64
+ id,
65
+ error: {
66
+ code,
67
+ message
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Initialize connection to browser-link-server
74
+ */
75
+ async initialize() {
76
+ try {
77
+ this.client = new ChromeLinkClient('ws://localhost:9000');
78
+ await this.client.connect();
79
+ this.log('Connected to browser-link-server');
80
+ } catch (error) {
81
+ this.log('Failed to connect to browser-link-server:', error.message);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get list of available MCP tools
88
+ */
89
+ getTools() {
90
+ return {
91
+ tools: [
92
+ // Tab Management
93
+ {
94
+ name: 'chrome_list_tabs',
95
+ description: 'List all open tabs in the browser',
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {},
99
+ required: []
100
+ }
101
+ },
102
+ {
103
+ name: 'chrome_open_tab',
104
+ description: 'Open a new tab with the specified URL',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ url: {
109
+ type: 'string',
110
+ description: 'URL to open in the new tab'
111
+ },
112
+ focus: {
113
+ type: 'boolean',
114
+ description: 'Whether to focus the new tab (default: true)',
115
+ default: true
116
+ }
117
+ },
118
+ required: ['url']
119
+ }
120
+ },
121
+ {
122
+ name: 'chrome_navigate_tab',
123
+ description: 'Navigate an existing tab to a new URL',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ tabId: {
128
+ type: 'number',
129
+ description: 'ID of the tab to navigate'
130
+ },
131
+ url: {
132
+ type: 'string',
133
+ description: 'URL to navigate to'
134
+ },
135
+ focus: {
136
+ type: 'boolean',
137
+ description: 'Whether to focus the tab (default: true)',
138
+ default: true
139
+ }
140
+ },
141
+ required: ['tabId', 'url']
142
+ }
143
+ },
144
+ {
145
+ name: 'chrome_switch_tab',
146
+ description: 'Switch to (focus) a specific tab',
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: {
150
+ tabId: {
151
+ type: 'number',
152
+ description: 'ID of the tab to switch to'
153
+ }
154
+ },
155
+ required: ['tabId']
156
+ }
157
+ },
158
+ {
159
+ name: 'chrome_close_tab',
160
+ description: 'Close a specific tab',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ tabId: {
165
+ type: 'number',
166
+ description: 'ID of the tab to close'
167
+ }
168
+ },
169
+ required: ['tabId']
170
+ }
171
+ },
172
+ {
173
+ name: 'chrome_get_active_tab',
174
+ description: 'Get information about the currently active tab',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {},
178
+ required: []
179
+ }
180
+ },
181
+
182
+ // Navigation History
183
+ {
184
+ name: 'chrome_go_back',
185
+ description: 'Navigate back in tab history (only works after user navigation, not programmatic)',
186
+ inputSchema: {
187
+ type: 'object',
188
+ properties: {
189
+ tabId: {
190
+ type: 'number',
191
+ description: 'ID of the tab to navigate back'
192
+ }
193
+ },
194
+ required: ['tabId']
195
+ }
196
+ },
197
+ {
198
+ name: 'chrome_go_forward',
199
+ description: 'Navigate forward in tab history (only works after user navigation, not programmatic)',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ tabId: {
204
+ type: 'number',
205
+ description: 'ID of the tab to navigate forward'
206
+ }
207
+ },
208
+ required: ['tabId']
209
+ }
210
+ },
211
+
212
+ // DOM Interaction
213
+ {
214
+ name: 'chrome_wait_for_element',
215
+ description: 'Wait for an element matching the CSS selector to appear on the page',
216
+ inputSchema: {
217
+ type: 'object',
218
+ properties: {
219
+ selector: {
220
+ type: 'string',
221
+ description: 'CSS selector for the element'
222
+ },
223
+ tabId: {
224
+ type: 'number',
225
+ description: 'ID of the tab'
226
+ },
227
+ timeout: {
228
+ type: 'number',
229
+ description: 'Maximum time to wait in milliseconds (default: 5000)',
230
+ default: 5000
231
+ }
232
+ },
233
+ required: ['selector', 'tabId']
234
+ }
235
+ },
236
+ {
237
+ name: 'chrome_get_text',
238
+ description: 'Get the text content of an element matching the CSS selector',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ selector: {
243
+ type: 'string',
244
+ description: 'CSS selector for the element'
245
+ },
246
+ tabId: {
247
+ type: 'number',
248
+ description: 'ID of the tab'
249
+ }
250
+ },
251
+ required: ['selector', 'tabId']
252
+ }
253
+ },
254
+ {
255
+ name: 'chrome_click',
256
+ description: 'Click an element matching the CSS selector',
257
+ inputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ selector: {
261
+ type: 'string',
262
+ description: 'CSS selector for the element to click'
263
+ },
264
+ tabId: {
265
+ type: 'number',
266
+ description: 'ID of the tab'
267
+ }
268
+ },
269
+ required: ['selector', 'tabId']
270
+ }
271
+ },
272
+ {
273
+ name: 'chrome_type',
274
+ description: 'Type text into an element matching the CSS selector',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: {
278
+ selector: {
279
+ type: 'string',
280
+ description: 'CSS selector for the input element'
281
+ },
282
+ text: {
283
+ type: 'string',
284
+ description: 'Text to type into the element'
285
+ },
286
+ tabId: {
287
+ type: 'number',
288
+ description: 'ID of the tab'
289
+ }
290
+ },
291
+ required: ['selector', 'text', 'tabId']
292
+ }
293
+ },
294
+
295
+ // JavaScript Execution
296
+ {
297
+ name: 'chrome_execute_js',
298
+ description: 'Execute arbitrary JavaScript code in the page context',
299
+ inputSchema: {
300
+ type: 'object',
301
+ properties: {
302
+ code: {
303
+ type: 'string',
304
+ description: 'JavaScript code to execute'
305
+ },
306
+ tabId: {
307
+ type: 'number',
308
+ description: 'ID of the tab'
309
+ }
310
+ },
311
+ required: ['code', 'tabId']
312
+ }
313
+ },
314
+ {
315
+ name: 'chrome_call_helper',
316
+ description: 'Call a predefined DOM helper function for CSP-restricted pages. Available helpers: Element Interaction (clickElement, typeText, appendChar, clearContentEditable), Element Query (getText, getHTML, getLastHTML, elementExists, isVisible, waitForElement), Element Highlighting (highlightElement, removeHighlights), Element Positioning (getElementBounds, scrollElementIntoView), Element Inspection (inspectElement, getContainerElements)',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ functionName: {
321
+ type: 'string',
322
+ description: 'Name of the helper function',
323
+ enum: [
324
+ 'clickElement',
325
+ 'typeText',
326
+ 'appendChar',
327
+ 'clearContentEditable',
328
+ 'getText',
329
+ 'getHTML',
330
+ 'getLastHTML',
331
+ 'elementExists',
332
+ 'isVisible',
333
+ 'waitForElement',
334
+ 'highlightElement',
335
+ 'removeHighlights',
336
+ 'getElementBounds',
337
+ 'scrollElementIntoView',
338
+ 'inspectElement',
339
+ 'getContainerElements'
340
+ ]
341
+ },
342
+ args: {
343
+ type: 'array',
344
+ description: 'Arguments to pass to the helper function',
345
+ items: {}
346
+ },
347
+ tabId: {
348
+ type: 'number',
349
+ description: 'ID of the tab'
350
+ }
351
+ },
352
+ required: ['functionName', 'args', 'tabId']
353
+ }
354
+ },
355
+
356
+ // Screenshots
357
+ {
358
+ name: 'chrome_capture_screenshot',
359
+ description: 'Capture a screenshot of the current tab',
360
+ inputSchema: {
361
+ type: 'object',
362
+ properties: {
363
+ format: {
364
+ type: 'string',
365
+ description: 'Image format (png or jpeg)',
366
+ enum: ['png', 'jpeg'],
367
+ default: 'png'
368
+ },
369
+ quality: {
370
+ type: 'number',
371
+ description: 'JPEG quality (0-100, only for jpeg format)',
372
+ minimum: 0,
373
+ maximum: 100,
374
+ default: 90
375
+ }
376
+ },
377
+ required: []
378
+ }
379
+ },
380
+
381
+ // Script Injection
382
+ {
383
+ name: 'chrome_register_injection',
384
+ description: 'Register a content script to be injected into matching pages',
385
+ inputSchema: {
386
+ type: 'object',
387
+ properties: {
388
+ id: {
389
+ type: 'string',
390
+ description: 'Unique identifier for this injection'
391
+ },
392
+ code: {
393
+ type: 'string',
394
+ description: 'JavaScript code to inject'
395
+ },
396
+ matches: {
397
+ type: 'array',
398
+ description: 'URL patterns to match (e.g., ["https://*.example.com/*"])',
399
+ items: {
400
+ type: 'string'
401
+ }
402
+ },
403
+ runAt: {
404
+ type: 'string',
405
+ description: 'When to inject the script',
406
+ enum: ['document_start', 'document_end', 'document_idle'],
407
+ default: 'document_idle'
408
+ }
409
+ },
410
+ required: ['id', 'code', 'matches']
411
+ }
412
+ },
413
+ {
414
+ name: 'chrome_unregister_injection',
415
+ description: 'Unregister a previously registered content script',
416
+ inputSchema: {
417
+ type: 'object',
418
+ properties: {
419
+ id: {
420
+ type: 'string',
421
+ description: 'ID of the injection to unregister'
422
+ }
423
+ },
424
+ required: ['id']
425
+ }
426
+ }
427
+ ]
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Handle tool invocation
433
+ */
434
+ async handleToolCall(name, args) {
435
+ this.log(`Tool call: ${name}`, args);
436
+
437
+ try {
438
+ let result;
439
+
440
+ switch (name) {
441
+ // Tab Management
442
+ case 'chrome_list_tabs':
443
+ result = await this.client.listTabs();
444
+ break;
445
+
446
+ case 'chrome_open_tab':
447
+ result = await this.client.openTab(args.url, args.focus);
448
+ break;
449
+
450
+ case 'chrome_navigate_tab':
451
+ result = await this.client.navigateTab(args.tabId, args.url, args.focus);
452
+ break;
453
+
454
+ case 'chrome_switch_tab':
455
+ result = await this.client.switchTab(args.tabId);
456
+ break;
457
+
458
+ case 'chrome_close_tab':
459
+ result = await this.client.closeTab(args.tabId);
460
+ break;
461
+
462
+ case 'chrome_get_active_tab':
463
+ result = await this.client.getActiveTab();
464
+ break;
465
+
466
+ // Navigation History
467
+ case 'chrome_go_back':
468
+ result = await this.client.goBack(args.tabId);
469
+ break;
470
+
471
+ case 'chrome_go_forward':
472
+ result = await this.client.goForward(args.tabId);
473
+ break;
474
+
475
+ // DOM Interaction
476
+ case 'chrome_wait_for_element':
477
+ result = await this.client.waitForElement(args.selector, args.timeout || 5000, args.tabId);
478
+ break;
479
+
480
+ case 'chrome_get_text':
481
+ result = await this.client.getText(args.selector, args.tabId);
482
+ break;
483
+
484
+ case 'chrome_click':
485
+ result = await this.client.click(args.selector, args.tabId);
486
+ break;
487
+
488
+ case 'chrome_type':
489
+ result = await this.client.type(args.selector, args.text, args.tabId);
490
+ break;
491
+
492
+ // JavaScript Execution
493
+ case 'chrome_execute_js':
494
+ result = await this.client.executeJS(args.code, args.tabId);
495
+ break;
496
+
497
+ case 'chrome_call_helper':
498
+ result = await this.client.callHelper(args.functionName, args.args, args.tabId);
499
+ break;
500
+
501
+ // Screenshots
502
+ case 'chrome_capture_screenshot':
503
+ result = await this.client.captureScreenshot({
504
+ format: args.format || 'png',
505
+ quality: args.quality || 90
506
+ });
507
+ break;
508
+
509
+ // Script Injection
510
+ case 'chrome_register_injection':
511
+ result = await this.client.registerInjection(
512
+ args.id,
513
+ args.code,
514
+ args.matches,
515
+ args.runAt || 'document_idle'
516
+ );
517
+ break;
518
+
519
+ case 'chrome_unregister_injection':
520
+ result = await this.client.unregisterInjection(args.id);
521
+ break;
522
+
523
+ default:
524
+ throw new Error(`Unknown tool: ${name}`);
525
+ }
526
+
527
+ this.log(`Tool result for ${name}:`, result);
528
+ return result;
529
+
530
+ } catch (error) {
531
+ this.log(`Tool error for ${name}:`, error.message);
532
+ throw error;
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Handle incoming MCP request
538
+ */
539
+ async handleRequest(request) {
540
+ const { id, method, params } = request;
541
+
542
+ try {
543
+ switch (method) {
544
+ case 'initialize':
545
+ this.sendResponse({
546
+ jsonrpc: '2.0',
547
+ id,
548
+ result: {
549
+ protocolVersion: '2024-11-05',
550
+ capabilities: {
551
+ tools: {}
552
+ },
553
+ serverInfo: {
554
+ name: 'chrome-link',
555
+ version: '1.2.0'
556
+ }
557
+ }
558
+ });
559
+ break;
560
+
561
+ case 'tools/list':
562
+ this.sendResponse({
563
+ jsonrpc: '2.0',
564
+ id,
565
+ result: this.getTools()
566
+ });
567
+ break;
568
+
569
+ case 'tools/call':
570
+ const result = await this.handleToolCall(params.name, params.arguments || {});
571
+ this.sendResponse({
572
+ jsonrpc: '2.0',
573
+ id,
574
+ result: {
575
+ content: [
576
+ {
577
+ type: 'text',
578
+ text: JSON.stringify(result, null, 2)
579
+ }
580
+ ]
581
+ }
582
+ });
583
+ break;
584
+
585
+ default:
586
+ this.sendError(id, -32601, `Method not found: ${method}`);
587
+ }
588
+ } catch (error) {
589
+ this.sendError(id, -32603, error.message);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Start the MCP server
595
+ */
596
+ async start() {
597
+ this.log('Starting MCP server...');
598
+
599
+ // Connect to browser-link-server
600
+ await this.initialize();
601
+
602
+ // Handle stdin for MCP protocol
603
+ let buffer = '';
604
+ process.stdin.on('data', (chunk) => {
605
+ buffer += chunk.toString();
606
+
607
+ // Process complete JSON-RPC messages
608
+ const lines = buffer.split('\n');
609
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
610
+
611
+ for (const line of lines) {
612
+ if (line.trim()) {
613
+ try {
614
+ const request = JSON.parse(line);
615
+ this.handleRequest(request);
616
+ } catch (error) {
617
+ this.log('Failed to parse request:', error.message);
618
+ }
619
+ }
620
+ }
621
+ });
622
+
623
+ process.stdin.on('end', () => {
624
+ this.log('Stdin closed, shutting down...');
625
+ this.shutdown();
626
+ });
627
+
628
+ // Handle process signals
629
+ process.on('SIGINT', () => {
630
+ this.log('Received SIGINT, shutting down...');
631
+ this.shutdown();
632
+ });
633
+
634
+ process.on('SIGTERM', () => {
635
+ this.log('Received SIGTERM, shutting down...');
636
+ this.shutdown();
637
+ });
638
+
639
+ this.log('MCP server started successfully');
640
+ }
641
+
642
+ /**
643
+ * Shutdown the server
644
+ */
645
+ shutdown() {
646
+ if (this.client) {
647
+ this.client.close();
648
+ }
649
+ process.exit(0);
650
+ }
651
+ }
652
+
653
+ // Start the server
654
+ const server = new MCPServer();
655
+ server.start().catch((error) => {
656
+ console.error('[MCP Server] Fatal error:', error);
657
+ process.exit(1);
658
+ });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@aikeymouse/chromelink-mcp",
3
+ "version": "1.2.0",
4
+ "description": "Model Context Protocol (MCP) server for Chrome browser automation via AI agents",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "chromelink-mcp": "./bin/chromelink-mcp"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js",
11
+ "test": "echo \"Error: no test specified\" && exit 1",
12
+ "prepublishOnly": "node scripts/prepare-publish.js",
13
+ "postpublish": "node scripts/restore-publish.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "chrome",
19
+ "browser-automation",
20
+ "ai",
21
+ "claude",
22
+ "gpt",
23
+ "automation",
24
+ "testing",
25
+ "websocket"
26
+ ],
27
+ "author": "aikeymouse",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/aikeymouse/chrome-link.git",
32
+ "directory": "mcp-server"
33
+ },
34
+ "homepage": "https://github.com/aikeymouse/chrome-link#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/aikeymouse/chrome-link/issues"
37
+ },
38
+ "engines": {
39
+ "node": ">=16.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@aikeymouse/chromelink-client": "^1.2.0",
43
+ "ws": "^8.16.0"
44
+ },
45
+ "peerDependencies": {},
46
+ "files": [
47
+ "index.js",
48
+ "bin/",
49
+ "scripts/",
50
+ "README.md",
51
+ "LICENSE"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "provenance": true
56
+ }
57
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Prepare package.json for publishing to npm
5
+ *
6
+ * This script runs before `npm publish` to:
7
+ * 1. Backup the current package.json
8
+ * 2. Replace local file dependency with npm version dependency
9
+ *
10
+ * The local dependency "@aikeymouse/chromelink-client": "file:../clients/node"
11
+ * is replaced with "@aikeymouse/chromelink-client": "^{VERSION}"
12
+ * where VERSION comes from the VERSION file in the project root.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
19
+ const BACKUP_FILE = PACKAGE_JSON + '.bak';
20
+ const VERSION_FILE = path.join(__dirname, '..', '..', 'VERSION');
21
+
22
+ try {
23
+ // Read VERSION file
24
+ if (!fs.existsSync(VERSION_FILE)) {
25
+ console.error('ERROR: VERSION file not found at', VERSION_FILE);
26
+ process.exit(1);
27
+ }
28
+
29
+ const version = fs.readFileSync(VERSION_FILE, 'utf8').trim();
30
+ if (!version.match(/^\d+\.\d+\.\d+$/)) {
31
+ console.error('ERROR: Invalid version format in VERSION file:', version);
32
+ process.exit(1);
33
+ }
34
+
35
+ console.log(`Preparing package.json for npm publish (version: ${version})`);
36
+
37
+ // Read package.json
38
+ const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8'));
39
+
40
+ // Backup original package.json
41
+ fs.writeFileSync(BACKUP_FILE, JSON.stringify(packageJson, null, 2) + '\n');
42
+ console.log('✓ Backed up package.json');
43
+
44
+ // Replace file dependency with npm version
45
+ if (packageJson.dependencies && packageJson.dependencies['@aikeymouse/chromelink-client']) {
46
+ const oldDep = packageJson.dependencies['@aikeymouse/chromelink-client'];
47
+ packageJson.dependencies['@aikeymouse/chromelink-client'] = `^${version}`;
48
+ console.log(`✓ Updated dependency: ${oldDep} → ^${version}`);
49
+ }
50
+
51
+ // Write updated package.json
52
+ fs.writeFileSync(PACKAGE_JSON, JSON.stringify(packageJson, null, 2) + '\n');
53
+ console.log('✓ Package.json ready for publishing');
54
+
55
+ } catch (error) {
56
+ console.error('ERROR preparing package.json:', error.message);
57
+
58
+ // Try to restore from backup if it exists
59
+ if (fs.existsSync(BACKUP_FILE)) {
60
+ try {
61
+ fs.copyFileSync(BACKUP_FILE, PACKAGE_JSON);
62
+ fs.unlinkSync(BACKUP_FILE);
63
+ console.log('✓ Restored package.json from backup');
64
+ } catch (restoreError) {
65
+ console.error('ERROR restoring backup:', restoreError.message);
66
+ }
67
+ }
68
+
69
+ process.exit(1);
70
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Restore package.json after publishing to npm
5
+ *
6
+ * This script runs after `npm publish` to:
7
+ * 1. Restore package.json from backup
8
+ * 2. Clean up backup file
9
+ *
10
+ * This ensures the local file dependency is restored for development.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
17
+ const BACKUP_FILE = PACKAGE_JSON + '.bak';
18
+
19
+ try {
20
+ if (!fs.existsSync(BACKUP_FILE)) {
21
+ console.log('No backup file found, skipping restore');
22
+ process.exit(0);
23
+ }
24
+
25
+ console.log('Restoring package.json from backup');
26
+
27
+ // Restore from backup
28
+ const backup = fs.readFileSync(BACKUP_FILE, 'utf8');
29
+ fs.writeFileSync(PACKAGE_JSON, backup);
30
+ console.log('✓ Restored package.json');
31
+
32
+ // Clean up backup file
33
+ fs.unlinkSync(BACKUP_FILE);
34
+ console.log('✓ Removed backup file');
35
+
36
+ console.log('✓ Package.json restored to development state');
37
+
38
+ } catch (error) {
39
+ console.error('ERROR restoring package.json:', error.message);
40
+ console.error('You may need to manually restore from', BACKUP_FILE);
41
+ process.exit(1);
42
+ }