@aikeymouse/chromelink-mcp 1.2.2 → 1.2.3

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.
@@ -8,4 +8,7 @@
8
8
  * Chrome automation capabilities via the Model Context Protocol.
9
9
  */
10
10
 
11
- require('../index.js');
11
+ import('../index.js').catch(err => {
12
+ console.error('Failed to start MCP server:', err);
13
+ process.exit(1);
14
+ });
package/index.js CHANGED
@@ -6,14 +6,14 @@
6
6
  * Model Context Protocol (MCP) server that exposes Chrome browser automation
7
7
  * capabilities to AI agents like Claude, GPT, etc.
8
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.
9
+ * This server uses the official @modelcontextprotocol/sdk package for
10
+ * standards-compliant MCP protocol implementation.
11
11
  *
12
12
  * Architecture:
13
13
  * AI Agent (Claude/GPT) <-> MCP Server (this file) <-> browser-link-server <-> Chrome Extension
14
14
  *
15
15
  * Communication:
16
- * - AI Agent ↔ MCP Server: stdio (MCP protocol)
16
+ * - AI Agent ↔ MCP Server: stdio (MCP protocol via SDK)
17
17
  * - MCP Server ↔ browser-link-server: WebSocket (ws://localhost:9000)
18
18
  *
19
19
  * Usage:
@@ -25,676 +25,634 @@
25
25
  * "mcpServers": {
26
26
  * "chrome-link": {
27
27
  * "command": "node",
28
- * "args": ["/path/to/chrome-driver-extension/native-host/mcp-server.js"]
28
+ * "args": ["/path/to/chrome-driver-extension/mcp-server/index.js"]
29
29
  * }
30
30
  * }
31
31
  * }
32
32
  */
33
33
 
34
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
35
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
36
+ import {
37
+ CallToolRequestSchema,
38
+ ListToolsRequestSchema
39
+ } from "@modelcontextprotocol/sdk/types.js";
40
+ import { createRequire } from 'module';
41
+
42
+ // Import CommonJS modules
43
+ const require = createRequire(import.meta.url);
34
44
  const ChromeLinkClient = require('@aikeymouse/chromelink-client');
35
45
  const packageJson = require('./package.json');
36
46
 
37
- // MCP Protocol Implementation
38
- class MCPServer {
39
- constructor() {
40
- this.client = null;
41
- this.requestId = 1;
42
- }
43
-
44
- /**
45
- * Log to stderr (stdout is reserved for MCP protocol)
46
- */
47
- log(message, ...args) {
48
- console.error(`[MCP Server] ${message}`, ...args);
49
- }
47
+ // Global ChromeLink client instance
48
+ let client = null;
50
49
 
51
- /**
52
- * Send MCP response to stdout
53
- */
54
- sendResponse(response) {
55
- const message = JSON.stringify(response);
56
- process.stdout.write(message + '\n');
57
- }
50
+ /**
51
+ * Log to stderr (stdout is reserved for MCP protocol)
52
+ */
53
+ function log(message, ...args) {
54
+ console.error(`[MCP Server] ${message}`, ...args);
55
+ }
58
56
 
59
- /**
60
- * Send error response
61
- */
62
- sendError(id, code, message) {
63
- this.sendResponse({
64
- jsonrpc: '2.0',
65
- id,
66
- error: {
67
- code,
68
- message
69
- }
57
+ /**
58
+ * Initialize connection to browser-link-server
59
+ */
60
+ async function initializeClient() {
61
+ try {
62
+ client = new ChromeLinkClient({ verbose: false });
63
+ await client.connect('ws://localhost:9000');
64
+ log('Connected to browser-link-server');
65
+
66
+ // Handle WebSocket disconnection - log but don't exit
67
+ // Server will attempt to reconnect on next tool call
68
+ client.ws.on('close', () => {
69
+ log('WebSocket connection closed (session may have expired)');
70
+ client = null;
71
+ });
72
+
73
+ client.ws.on('error', (err) => {
74
+ log('WebSocket error:', err.message);
70
75
  });
76
+ } catch (error) {
77
+ log('Failed to connect to browser-link-server:', error.message);
78
+ throw error;
71
79
  }
80
+ }
72
81
 
73
- /**
74
- * Initialize connection to browser-link-server
75
- */
76
- async initialize() {
77
- try {
78
- this.client = new ChromeLinkClient('ws://localhost:9000');
79
- await this.client.connect();
80
- this.log('Connected to browser-link-server');
81
-
82
- // Handle WebSocket disconnection - log but don't exit
83
- // Server will attempt to reconnect on next tool call
84
- this.client.ws.on('close', () => {
85
- this.log('WebSocket connection closed (session may have expired)');
86
- this.client = null;
87
- });
88
-
89
- this.client.ws.on('error', (err) => {
90
- this.log('WebSocket error:', err.message);
91
- });
92
- } catch (error) {
93
- this.log('Failed to connect to browser-link-server:', error.message);
94
- throw error;
95
- }
96
- }
97
-
98
- /**
99
- * Ensure connection is active, reconnect if needed
100
- */
101
- async ensureConnected() {
102
- if (!this.client || !this.client.ws || this.client.ws.readyState !== 1) {
103
- this.log('Reconnecting to browser-link-server...');
104
- await this.initialize();
105
- }
82
+ /**
83
+ * Ensure connection is active, reconnect if needed
84
+ */
85
+ async function ensureConnected() {
86
+ if (!client || !client.ws || client.ws.readyState !== 1) {
87
+ log('Reconnecting to browser-link-server...');
88
+ await initializeClient();
106
89
  }
90
+ }
107
91
 
108
- /**
109
- * Get list of available MCP tools
110
- */
111
- getTools() {
112
- return {
113
- tools: [
114
- // Tab Management
115
- {
116
- name: 'chrome_list_tabs',
117
- description: 'List all open tabs in the browser',
118
- inputSchema: {
119
- type: 'object',
120
- properties: {},
121
- required: []
92
+ /**
93
+ * Get list of available MCP tools
94
+ */
95
+ function getTools() {
96
+ return [
97
+ // Tab Management
98
+ {
99
+ name: 'chrome_list_tabs',
100
+ description: 'List all open tabs in the browser',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {},
104
+ required: []
105
+ }
106
+ },
107
+ {
108
+ name: 'chrome_open_tab',
109
+ description: 'Open a new tab with the specified URL',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ url: {
114
+ type: 'string',
115
+ description: 'URL to open in the new tab'
116
+ },
117
+ focus: {
118
+ type: 'boolean',
119
+ description: 'Whether to focus the new tab (default: true)',
120
+ default: true
122
121
  }
123
122
  },
124
- {
125
- name: 'chrome_open_tab',
126
- description: 'Open a new tab with the specified URL',
127
- inputSchema: {
128
- type: 'object',
129
- properties: {
130
- url: {
131
- type: 'string',
132
- description: 'URL to open in the new tab'
133
- },
134
- focus: {
135
- type: 'boolean',
136
- description: 'Whether to focus the new tab (default: true)',
137
- default: true
138
- }
139
- },
140
- required: ['url']
123
+ required: ['url']
124
+ }
125
+ },
126
+ {
127
+ name: 'chrome_navigate_tab',
128
+ description: 'Navigate an existing tab to a new URL',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ tabId: {
133
+ type: 'number',
134
+ description: 'ID of the tab to navigate'
135
+ },
136
+ url: {
137
+ type: 'string',
138
+ description: 'URL to navigate to'
139
+ },
140
+ focus: {
141
+ type: 'boolean',
142
+ description: 'Whether to focus the tab (default: true)',
143
+ default: true
141
144
  }
142
145
  },
143
- {
144
- name: 'chrome_navigate_tab',
145
- description: 'Navigate an existing tab to a new URL',
146
- inputSchema: {
147
- type: 'object',
148
- properties: {
149
- tabId: {
150
- type: 'number',
151
- description: 'ID of the tab to navigate'
152
- },
153
- url: {
154
- type: 'string',
155
- description: 'URL to navigate to'
156
- },
157
- focus: {
158
- type: 'boolean',
159
- description: 'Whether to focus the tab (default: true)',
160
- default: true
161
- }
162
- },
163
- required: ['tabId', 'url']
146
+ required: ['tabId', 'url']
147
+ }
148
+ },
149
+ {
150
+ name: 'chrome_switch_tab',
151
+ description: 'Switch to (focus) a specific tab',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: {
155
+ tabId: {
156
+ type: 'number',
157
+ description: 'ID of the tab to switch to'
164
158
  }
165
159
  },
166
- {
167
- name: 'chrome_switch_tab',
168
- description: 'Switch to (focus) a specific tab',
169
- inputSchema: {
170
- type: 'object',
171
- properties: {
172
- tabId: {
173
- type: 'number',
174
- description: 'ID of the tab to switch to'
175
- }
176
- },
177
- required: ['tabId']
160
+ required: ['tabId']
161
+ }
162
+ },
163
+ {
164
+ name: 'chrome_close_tab',
165
+ description: 'Close a specific tab',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ tabId: {
170
+ type: 'number',
171
+ description: 'ID of the tab to close'
178
172
  }
179
173
  },
180
- {
181
- name: 'chrome_close_tab',
182
- description: 'Close a specific tab',
183
- inputSchema: {
184
- type: 'object',
185
- properties: {
186
- tabId: {
187
- type: 'number',
188
- description: 'ID of the tab to close'
189
- }
190
- },
191
- required: ['tabId']
174
+ required: ['tabId']
175
+ }
176
+ },
177
+ {
178
+ name: 'chrome_get_active_tab',
179
+ description: 'Get information about the currently active tab',
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {},
183
+ required: []
184
+ }
185
+ },
186
+
187
+ // Navigation History
188
+ {
189
+ name: 'chrome_go_back',
190
+ description: 'Navigate back in tab history (only works after user navigation, not programmatic)',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ tabId: {
195
+ type: 'number',
196
+ description: 'ID of the tab to navigate back'
192
197
  }
193
198
  },
194
- {
195
- name: 'chrome_get_active_tab',
196
- description: 'Get information about the currently active tab',
197
- inputSchema: {
198
- type: 'object',
199
- properties: {},
200
- required: []
199
+ required: ['tabId']
200
+ }
201
+ },
202
+ {
203
+ name: 'chrome_go_forward',
204
+ description: 'Navigate forward in tab history (only works after user navigation, not programmatic)',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ tabId: {
209
+ type: 'number',
210
+ description: 'ID of the tab to navigate forward'
201
211
  }
202
212
  },
203
-
204
- // Navigation History
205
- {
206
- name: 'chrome_go_back',
207
- description: 'Navigate back in tab history (only works after user navigation, not programmatic)',
208
- inputSchema: {
209
- type: 'object',
210
- properties: {
211
- tabId: {
212
- type: 'number',
213
- description: 'ID of the tab to navigate back'
214
- }
215
- },
216
- required: ['tabId']
213
+ required: ['tabId']
214
+ }
215
+ },
216
+
217
+ // DOM Interaction
218
+ {
219
+ name: 'chrome_wait_for_element',
220
+ description: 'Wait for an element matching the CSS selector to appear on the page',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ selector: {
225
+ type: 'string',
226
+ description: 'CSS selector for the element'
227
+ },
228
+ tabId: {
229
+ type: 'number',
230
+ description: 'ID of the tab'
231
+ },
232
+ timeout: {
233
+ type: 'number',
234
+ description: 'Maximum time to wait in milliseconds (default: 5000)',
235
+ default: 5000
217
236
  }
218
237
  },
219
- {
220
- name: 'chrome_go_forward',
221
- description: 'Navigate forward in tab history (only works after user navigation, not programmatic)',
222
- inputSchema: {
223
- type: 'object',
224
- properties: {
225
- tabId: {
226
- type: 'number',
227
- description: 'ID of the tab to navigate forward'
228
- }
229
- },
230
- required: ['tabId']
238
+ required: ['selector', 'tabId']
239
+ }
240
+ },
241
+ {
242
+ name: 'chrome_get_text',
243
+ description: 'Get the text content of an element matching the CSS selector',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ selector: {
248
+ type: 'string',
249
+ description: 'CSS selector for the element'
250
+ },
251
+ tabId: {
252
+ type: 'number',
253
+ description: 'ID of the tab'
231
254
  }
232
255
  },
233
-
234
- // DOM Interaction
235
- {
236
- name: 'chrome_wait_for_element',
237
- description: 'Wait for an element matching the CSS selector to appear on the page',
238
- inputSchema: {
239
- type: 'object',
240
- properties: {
241
- selector: {
242
- type: 'string',
243
- description: 'CSS selector for the element'
244
- },
245
- tabId: {
246
- type: 'number',
247
- description: 'ID of the tab'
248
- },
249
- timeout: {
250
- type: 'number',
251
- description: 'Maximum time to wait in milliseconds (default: 5000)',
252
- default: 5000
253
- }
254
- },
255
- required: ['selector', 'tabId']
256
+ required: ['selector', 'tabId']
257
+ }
258
+ },
259
+ {
260
+ name: 'chrome_click',
261
+ description: 'Click an element matching the CSS selector',
262
+ inputSchema: {
263
+ type: 'object',
264
+ properties: {
265
+ selector: {
266
+ type: 'string',
267
+ description: 'CSS selector for the element to click'
268
+ },
269
+ tabId: {
270
+ type: 'number',
271
+ description: 'ID of the tab'
256
272
  }
257
273
  },
258
- {
259
- name: 'chrome_get_text',
260
- description: 'Get the text content of an element matching the CSS selector',
261
- inputSchema: {
262
- type: 'object',
263
- properties: {
264
- selector: {
265
- type: 'string',
266
- description: 'CSS selector for the element'
267
- },
268
- tabId: {
269
- type: 'number',
270
- description: 'ID of the tab'
271
- }
272
- },
273
- required: ['selector', 'tabId']
274
+ required: ['selector', 'tabId']
275
+ }
276
+ },
277
+ {
278
+ name: 'chrome_type',
279
+ description: 'Type text into an element matching the CSS selector',
280
+ inputSchema: {
281
+ type: 'object',
282
+ properties: {
283
+ selector: {
284
+ type: 'string',
285
+ description: 'CSS selector for the input element'
286
+ },
287
+ text: {
288
+ type: 'string',
289
+ description: 'Text to type into the element'
290
+ },
291
+ tabId: {
292
+ type: 'number',
293
+ description: 'ID of the tab'
274
294
  }
275
295
  },
276
- {
277
- name: 'chrome_click',
278
- description: 'Click an element matching the CSS selector',
279
- inputSchema: {
280
- type: 'object',
281
- properties: {
282
- selector: {
283
- type: 'string',
284
- description: 'CSS selector for the element to click'
285
- },
286
- tabId: {
287
- type: 'number',
288
- description: 'ID of the tab'
289
- }
290
- },
291
- required: ['selector', 'tabId']
296
+ required: ['selector', 'text', 'tabId']
297
+ }
298
+ },
299
+
300
+ // JavaScript Execution
301
+ {
302
+ name: 'chrome_execute_js',
303
+ description: 'Execute arbitrary JavaScript code in the page context',
304
+ inputSchema: {
305
+ type: 'object',
306
+ properties: {
307
+ code: {
308
+ type: 'string',
309
+ description: 'JavaScript code to execute'
310
+ },
311
+ tabId: {
312
+ type: 'number',
313
+ description: 'ID of the tab'
292
314
  }
293
315
  },
294
- {
295
- name: 'chrome_type',
296
- description: 'Type text into an element matching the CSS selector',
297
- inputSchema: {
298
- type: 'object',
299
- properties: {
300
- selector: {
301
- type: 'string',
302
- description: 'CSS selector for the input element'
303
- },
304
- text: {
305
- type: 'string',
306
- description: 'Text to type into the element'
307
- },
308
- tabId: {
309
- type: 'number',
310
- description: 'ID of the tab'
311
- }
312
- },
313
- required: ['selector', 'text', 'tabId']
316
+ required: ['code', 'tabId']
317
+ }
318
+ },
319
+ {
320
+ name: 'chrome_call_helper',
321
+ description: 'Call a predefined DOM helper functions that works for CSP-restricted pages too. 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, extractPageElements)',
322
+ inputSchema: {
323
+ type: 'object',
324
+ properties: {
325
+ functionName: {
326
+ type: 'string',
327
+ description: 'Name of the helper function',
328
+ enum: [
329
+ 'clickElement',
330
+ 'typeText',
331
+ 'appendChar',
332
+ 'clearContentEditable',
333
+ 'getText',
334
+ 'getHTML',
335
+ 'getLastHTML',
336
+ 'elementExists',
337
+ 'isVisible',
338
+ 'waitForElement',
339
+ 'highlightElement',
340
+ 'removeHighlights',
341
+ 'getElementBounds',
342
+ 'scrollElementIntoView',
343
+ 'inspectElement',
344
+ 'getContainerElements',
345
+ 'extractPageElements'
346
+ ]
347
+ },
348
+ args: {
349
+ type: 'array',
350
+ description: 'Arguments to pass to the helper function (as individual parameters, not objects)',
351
+ items: {
352
+ oneOf: [
353
+ { type: 'string' },
354
+ { type: 'number' },
355
+ { type: 'boolean' },
356
+ { type: 'null' }
357
+ ]
358
+ }
359
+ },
360
+ tabId: {
361
+ type: 'number',
362
+ description: 'ID of the tab'
314
363
  }
315
364
  },
316
-
317
- // JavaScript Execution
318
- {
319
- name: 'chrome_execute_js',
320
- description: 'Execute arbitrary JavaScript code in the page context',
321
- inputSchema: {
322
- type: 'object',
323
- properties: {
324
- code: {
325
- type: 'string',
326
- description: 'JavaScript code to execute'
327
- },
328
- tabId: {
329
- type: 'number',
330
- description: 'ID of the tab'
331
- }
332
- },
333
- required: ['code', 'tabId']
365
+ required: ['functionName', 'args', 'tabId']
366
+ }
367
+ },
368
+
369
+ // Screenshots
370
+ {
371
+ name: 'chrome_capture_screenshot',
372
+ description: 'Capture a screenshot of the current tab',
373
+ inputSchema: {
374
+ type: 'object',
375
+ properties: {
376
+ format: {
377
+ type: 'string',
378
+ description: 'Image format (png or jpeg)',
379
+ enum: ['png', 'jpeg'],
380
+ default: 'png'
381
+ },
382
+ quality: {
383
+ type: 'number',
384
+ description: 'JPEG quality (0-100, only for jpeg format)',
385
+ minimum: 0,
386
+ maximum: 100,
387
+ default: 90
334
388
  }
335
389
  },
336
- {
337
- name: 'chrome_call_helper',
338
- description: 'Call a predefined DOM helper functions that works for CSP-restricted pages too. 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, extractPageElements)',
339
- inputSchema: {
340
- type: 'object',
341
- properties: {
342
- functionName: {
343
- type: 'string',
344
- description: 'Name of the helper function',
345
- enum: [
346
- 'clickElement',
347
- 'typeText',
348
- 'appendChar',
349
- 'clearContentEditable',
350
- 'getText',
351
- 'getHTML',
352
- 'getLastHTML',
353
- 'elementExists',
354
- 'isVisible',
355
- 'waitForElement',
356
- 'highlightElement',
357
- 'removeHighlights',
358
- 'getElementBounds',
359
- 'scrollElementIntoView',
360
- 'inspectElement',
361
- 'getContainerElements',
362
- 'extractPageElements'
363
- ]
364
- },
365
- args: {
366
- type: 'array',
367
- description: 'Arguments to pass to the helper function (as individual parameters, not objects)',
368
- items: {
369
- oneOf: [
370
- { type: 'string' },
371
- { type: 'number' },
372
- { type: 'boolean' },
373
- { type: 'null' }
374
- ]
375
- }
376
- },
377
- tabId: {
378
- type: 'number',
379
- description: 'ID of the tab'
380
- }
381
- },
382
- required: ['functionName', 'args', 'tabId']
390
+ required: []
391
+ }
392
+ },
393
+
394
+ // Script Injection
395
+ {
396
+ name: 'chrome_register_injection',
397
+ description: 'Register a content script to be injected into matching pages',
398
+ inputSchema: {
399
+ type: 'object',
400
+ properties: {
401
+ id: {
402
+ type: 'string',
403
+ description: 'Unique identifier for this injection'
404
+ },
405
+ code: {
406
+ type: 'string',
407
+ description: 'JavaScript code to inject'
408
+ },
409
+ matches: {
410
+ type: 'array',
411
+ description: 'URL patterns to match (e.g., ["https://*.example.com/*"])',
412
+ items: {
413
+ type: 'string'
414
+ }
415
+ },
416
+ runAt: {
417
+ type: 'string',
418
+ description: 'When to inject the script',
419
+ enum: ['document_start', 'document_end', 'document_idle'],
420
+ default: 'document_idle'
383
421
  }
384
422
  },
385
-
386
- // Screenshots
387
- {
388
- name: 'chrome_capture_screenshot',
389
- description: 'Capture a screenshot of the current tab',
390
- inputSchema: {
391
- type: 'object',
392
- properties: {
393
- format: {
394
- type: 'string',
395
- description: 'Image format (png or jpeg)',
396
- enum: ['png', 'jpeg'],
397
- default: 'png'
398
- },
399
- quality: {
400
- type: 'number',
401
- description: 'JPEG quality (0-100, only for jpeg format)',
402
- minimum: 0,
403
- maximum: 100,
404
- default: 90
405
- }
406
- },
407
- required: []
423
+ required: ['id', 'code', 'matches']
424
+ }
425
+ },
426
+ {
427
+ name: 'chrome_unregister_injection',
428
+ description: 'Unregister a previously registered content script',
429
+ inputSchema: {
430
+ type: 'object',
431
+ properties: {
432
+ id: {
433
+ type: 'string',
434
+ description: 'ID of the injection to unregister'
408
435
  }
409
436
  },
437
+ required: ['id']
438
+ }
439
+ }
440
+ ];
441
+ }
410
442
 
411
- // Script Injection
412
- {
413
- name: 'chrome_register_injection',
414
- description: 'Register a content script to be injected into matching pages',
415
- inputSchema: {
416
- type: 'object',
417
- properties: {
418
- id: {
419
- type: 'string',
420
- description: 'Unique identifier for this injection'
421
- },
422
- code: {
423
- type: 'string',
424
- description: 'JavaScript code to inject'
425
- },
426
- matches: {
427
- type: 'array',
428
- description: 'URL patterns to match (e.g., ["https://*.example.com/*"])',
429
- items: {
430
- type: 'string'
431
- }
432
- },
433
- runAt: {
434
- type: 'string',
435
- description: 'When to inject the script',
436
- enum: ['document_start', 'document_end', 'document_idle'],
437
- default: 'document_idle'
438
- }
439
- },
440
- required: ['id', 'code', 'matches']
441
- }
442
- },
443
- {
444
- name: 'chrome_unregister_injection',
445
- description: 'Unregister a previously registered content script',
446
- inputSchema: {
447
- type: 'object',
448
- properties: {
449
- id: {
450
- type: 'string',
451
- description: 'ID of the injection to unregister'
452
- }
453
- },
454
- required: ['id']
443
+ /**
444
+ * Handle tool invocation
445
+ */
446
+ async function handleToolCall(name, args) {
447
+ log(`Tool call: ${name}`, args);
448
+
449
+ try {
450
+ // Ensure connection is active before executing tool
451
+ await ensureConnected();
452
+
453
+ let result;
454
+
455
+ switch (name) {
456
+ // Tab Management
457
+ case 'chrome_list_tabs':
458
+ result = await client.listTabs();
459
+ break;
460
+
461
+ case 'chrome_open_tab':
462
+ result = await client.openTab(args.url, args.focus);
463
+ break;
464
+
465
+ case 'chrome_navigate_tab':
466
+ result = await client.navigateTab(args.tabId, args.url, args.focus);
467
+ break;
468
+
469
+ case 'chrome_switch_tab':
470
+ result = await client.switchTab(args.tabId);
471
+ break;
472
+
473
+ case 'chrome_close_tab':
474
+ result = await client.closeTab(args.tabId);
475
+ break;
476
+
477
+ case 'chrome_get_active_tab':
478
+ result = await client.getActiveTab();
479
+ break;
480
+
481
+ // Navigation History
482
+ case 'chrome_go_back':
483
+ result = await client.goBack(args.tabId);
484
+ break;
485
+
486
+ case 'chrome_go_forward':
487
+ result = await client.goForward(args.tabId);
488
+ break;
489
+
490
+ // DOM Interaction
491
+ case 'chrome_wait_for_element':
492
+ result = await client.waitForElement(args.selector, args.timeout || 5000, args.tabId);
493
+ break;
494
+
495
+ case 'chrome_get_text':
496
+ result = await client.getText(args.selector, args.tabId);
497
+ break;
498
+
499
+ case 'chrome_click':
500
+ result = await client.click(args.selector, args.tabId);
501
+ break;
502
+
503
+ case 'chrome_type':
504
+ result = await client.type(args.selector, args.text, args.tabId);
505
+ break;
506
+
507
+ // JavaScript Execution
508
+ case 'chrome_execute_js':
509
+ result = await client.executeJS(args.code, args.tabId);
510
+ break;
511
+
512
+ case 'chrome_call_helper':
513
+ // Validate that args array contains only primitives, not objects
514
+ if (args.args && Array.isArray(args.args)) {
515
+ for (let i = 0; i < args.args.length; i++) {
516
+ const arg = args.args[i];
517
+ if (arg !== null && typeof arg === 'object') {
518
+ throw new Error(`Invalid argument at index ${i}: expected primitive type (string, number, boolean, null), got object. Use individual parameters like ["form", false], not [{"containerSelector": "form"}]`);
519
+ }
455
520
  }
456
521
  }
457
- ]
458
- };
459
- }
522
+ result = await client.callHelper(args.functionName, args.args, args.tabId);
523
+ break;
524
+
525
+ // Screenshots
526
+ case 'chrome_capture_screenshot':
527
+ result = await client.captureScreenshot({
528
+ format: args.format || 'png',
529
+ quality: args.quality || 90
530
+ });
531
+ break;
532
+
533
+ // Script Injection
534
+ case 'chrome_register_injection':
535
+ result = await client.registerInjection(
536
+ args.id,
537
+ args.code,
538
+ args.matches,
539
+ args.runAt || 'document_idle'
540
+ );
541
+ break;
542
+
543
+ case 'chrome_unregister_injection':
544
+ result = await client.unregisterInjection(args.id);
545
+ break;
546
+
547
+ default:
548
+ throw new Error(`Unknown tool: ${name}`);
549
+ }
460
550
 
461
- /**
462
- * Handle tool invocation
463
- */
464
- async handleToolCall(name, args) {
465
- this.log(`Tool call: ${name}`, args);
551
+ log(`Tool result for ${name}:`, result);
552
+ return result;
466
553
 
467
- try {
468
- // Ensure connection is active before executing tool
469
- await this.ensureConnected();
470
-
471
- let result;
472
-
473
- switch (name) {
474
- // Tab Management
475
- case 'chrome_list_tabs':
476
- result = await this.client.listTabs();
477
- break;
478
-
479
- case 'chrome_open_tab':
480
- result = await this.client.openTab(args.url, args.focus);
481
- break;
482
-
483
- case 'chrome_navigate_tab':
484
- result = await this.client.navigateTab(args.tabId, args.url, args.focus);
485
- break;
486
-
487
- case 'chrome_switch_tab':
488
- result = await this.client.switchTab(args.tabId);
489
- break;
490
-
491
- case 'chrome_close_tab':
492
- result = await this.client.closeTab(args.tabId);
493
- break;
494
-
495
- case 'chrome_get_active_tab':
496
- result = await this.client.getActiveTab();
497
- break;
498
-
499
- // Navigation History
500
- case 'chrome_go_back':
501
- result = await this.client.goBack(args.tabId);
502
- break;
503
-
504
- case 'chrome_go_forward':
505
- result = await this.client.goForward(args.tabId);
506
- break;
507
-
508
- // DOM Interaction
509
- case 'chrome_wait_for_element':
510
- result = await this.client.waitForElement(args.selector, args.timeout || 5000, args.tabId);
511
- break;
512
-
513
- case 'chrome_get_text':
514
- result = await this.client.getText(args.selector, args.tabId);
515
- break;
516
-
517
- case 'chrome_click':
518
- result = await this.client.click(args.selector, args.tabId);
519
- break;
520
-
521
- case 'chrome_type':
522
- result = await this.client.type(args.selector, args.text, args.tabId);
523
- break;
524
-
525
- // JavaScript Execution
526
- case 'chrome_execute_js':
527
- result = await this.client.executeJS(args.code, args.tabId);
528
- break;
529
-
530
- case 'chrome_call_helper':
531
- // Validate that args array contains only primitives, not objects
532
- if (args.args && Array.isArray(args.args)) {
533
- for (let i = 0; i < args.args.length; i++) {
534
- const arg = args.args[i];
535
- if (arg !== null && typeof arg === 'object') {
536
- throw new Error(`Invalid argument at index ${i}: expected primitive type (string, number, boolean, null), got object. Use individual parameters like ["form", false], not [{"containerSelector": "form"}]`);
537
- }
538
- }
539
- }
540
- result = await this.client.callHelper(args.functionName, args.args, args.tabId);
541
- break;
542
-
543
- // Screenshots
544
- case 'chrome_capture_screenshot':
545
- result = await this.client.captureScreenshot({
546
- format: args.format || 'png',
547
- quality: args.quality || 90
548
- });
549
- break;
550
-
551
- // Script Injection
552
- case 'chrome_register_injection':
553
- result = await this.client.registerInjection(
554
- args.id,
555
- args.code,
556
- args.matches,
557
- args.runAt || 'document_idle'
558
- );
559
- break;
560
-
561
- case 'chrome_unregister_injection':
562
- result = await this.client.unregisterInjection(args.id);
563
- break;
564
-
565
- default:
566
- throw new Error(`Unknown tool: ${name}`);
567
- }
568
-
569
- this.log(`Tool result for ${name}:`, result);
570
- return result;
554
+ } catch (error) {
555
+ log(`Tool error for ${name}:`, error.message);
556
+ throw error;
557
+ }
558
+ }
571
559
 
572
- } catch (error) {
573
- this.log(`Tool error for ${name}:`, error.message);
574
- throw error;
560
+ /**
561
+ * Main server initialization
562
+ */
563
+ async function main() {
564
+ log('Starting MCP server with official SDK...');
565
+
566
+ // Connect to browser-link-server
567
+ await initializeClient();
568
+
569
+ // Create MCP Server instance
570
+ const server = new Server({
571
+ name: 'chrome-link',
572
+ version: packageJson.version
573
+ }, {
574
+ capabilities: {
575
+ tools: {}
575
576
  }
576
- }
577
+ });
577
578
 
578
- /**
579
- * Handle incoming MCP request
580
- */
581
- async handleRequest(request) {
582
- const { id, method, params } = request;
579
+ // Register tools/list handler
580
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
581
+ return {
582
+ tools: getTools()
583
+ };
584
+ });
583
585
 
586
+ // Register tools/call handler
587
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
588
+ const { name, arguments: args } = request.params;
589
+
584
590
  try {
585
- switch (method) {
586
- case 'initialize':
587
- this.sendResponse({
588
- jsonrpc: '2.0',
589
- id,
590
- result: {
591
- protocolVersion: '2024-11-05',
592
- capabilities: {
593
- tools: {}
594
- },
595
- serverInfo: {
596
- name: 'chrome-link',
597
- version: packageJson.version
591
+ const result = await handleToolCall(name, args || {});
592
+
593
+ // Special handling for screenshots - return as image content
594
+ if (name === 'chrome_capture_screenshot' && result.dataUrl) {
595
+ // Extract base64 data and mime type from data URL
596
+ // Format: data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...
597
+ const match = result.dataUrl.match(/^data:([^;]+);base64,(.+)$/);
598
+ if (match) {
599
+ const [, mimeType, base64Data] = match;
600
+ return {
601
+ content: [
602
+ {
603
+ type: 'image',
604
+ data: base64Data,
605
+ mimeType: mimeType
598
606
  }
599
- }
600
- });
601
- break;
602
-
603
- case 'tools/list':
604
- this.sendResponse({
605
- jsonrpc: '2.0',
606
- id,
607
- result: this.getTools()
608
- });
609
- break;
610
-
611
- case 'tools/call':
612
- const result = await this.handleToolCall(params.name, params.arguments || {});
613
- this.sendResponse({
614
- jsonrpc: '2.0',
615
- id,
616
- result: {
617
- content: [
618
- {
619
- type: 'text',
620
- text: JSON.stringify(result, null, 2)
621
- }
622
- ]
623
- }
624
- });
625
- break;
626
-
627
- default:
628
- this.sendError(id, -32601, `Method not found: ${method}`);
607
+ ]
608
+ };
609
+ }
629
610
  }
630
- } catch (error) {
631
- this.sendError(id, -32603, error.message);
632
- }
633
- }
634
-
635
- /**
636
- * Start the MCP server
637
- */
638
- async start() {
639
- this.log('Starting MCP server...');
640
-
641
- // Connect to browser-link-server
642
- await this.initialize();
643
-
644
- // Handle stdin for MCP protocol
645
- let buffer = '';
646
- process.stdin.on('data', (chunk) => {
647
- buffer += chunk.toString();
648
611
 
649
- // Process complete JSON-RPC messages
650
- const lines = buffer.split('\n');
651
- buffer = lines.pop() || ''; // Keep incomplete line in buffer
652
-
653
- for (const line of lines) {
654
- if (line.trim()) {
655
- try {
656
- const request = JSON.parse(line);
657
- this.handleRequest(request);
658
- } catch (error) {
659
- this.log('Failed to parse request:', error.message);
612
+ // For all other tools, return as text (without pretty-printing to reduce size)
613
+ return {
614
+ content: [
615
+ {
616
+ type: 'text',
617
+ text: JSON.stringify(result)
660
618
  }
661
- }
662
- }
663
- });
664
-
665
- process.stdin.on('end', () => {
666
- this.log('Stdin closed, shutting down...');
667
- this.shutdown();
668
- });
669
-
670
- // Handle process signals
671
- process.on('SIGINT', () => {
672
- this.log('Received SIGINT, shutting down...');
673
- this.shutdown();
674
- });
675
-
676
- process.on('SIGTERM', () => {
677
- this.log('Received SIGTERM, shutting down...');
678
- this.shutdown();
679
- });
619
+ ]
620
+ };
621
+ } catch (error) {
622
+ log(`Error handling tool call ${name}:`, error.message);
623
+ throw error;
624
+ }
625
+ });
680
626
 
681
- this.log('MCP server started successfully');
682
- }
627
+ // Handle process signals for graceful shutdown
628
+ process.on('SIGINT', async () => {
629
+ log('Received SIGINT, shutting down...');
630
+ if (client) {
631
+ client.close();
632
+ }
633
+ await server.close();
634
+ process.exit(0);
635
+ });
683
636
 
684
- /**
685
- * Shutdown the server
686
- */
687
- shutdown() {
688
- if (this.client) {
689
- this.client.close();
637
+ process.on('SIGTERM', async () => {
638
+ log('Received SIGTERM, shutting down...');
639
+ if (client) {
640
+ client.close();
690
641
  }
642
+ await server.close();
691
643
  process.exit(0);
692
- }
644
+ });
645
+
646
+ // Connect transport and start server
647
+ const transport = new StdioServerTransport();
648
+ await server.connect(transport);
649
+
650
+ log('MCP server started successfully with SDK');
693
651
  }
694
652
 
695
- // Start the server
696
- const server = new MCPServer();
697
- server.start().catch((error) => {
653
+ // Run the server
654
+ main().catch((error) => {
698
655
  console.error('[MCP Server] Fatal error:', error);
699
656
  process.exit(1);
700
657
  });
658
+
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@aikeymouse/chromelink-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Model Context Protocol (MCP) server for Chrome browser automation via AI agents",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "bin": {
7
8
  "chromelink-mcp": "./bin/chromelink-mcp"
@@ -39,7 +40,8 @@
39
40
  "node": ">=16.0.0"
40
41
  },
41
42
  "dependencies": {
42
- "@aikeymouse/chromelink-client": "^1.2.2",
43
+ "@aikeymouse/chromelink-client": "^1.2.3",
44
+ "@modelcontextprotocol/sdk": "^0.5.0",
43
45
  "ws": "^8.16.0"
44
46
  },
45
47
  "peerDependencies": {},
@@ -12,8 +12,12 @@
12
12
  * where VERSION comes from the VERSION file in the project root.
13
13
  */
14
14
 
15
- const fs = require('fs');
16
- const path = require('path');
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
17
21
 
18
22
  const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
19
23
  const BACKUP_FILE = PACKAGE_JSON + '.bak';