@kaitranntt/ccs 7.68.1-dev.7 → 7.68.1-dev.8

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.
Files changed (111) hide show
  1. package/dist/auth/commands/create-command-env.d.ts +1 -0
  2. package/dist/auth/commands/create-command-env.d.ts.map +1 -1
  3. package/dist/ccs.js +55 -2
  4. package/dist/ccs.js.map +1 -1
  5. package/dist/channels/official-channels-runtime.d.ts +1 -0
  6. package/dist/channels/official-channels-runtime.d.ts.map +1 -1
  7. package/dist/channels/official-channels-store.d.ts +1 -0
  8. package/dist/channels/official-channels-store.d.ts.map +1 -1
  9. package/dist/cliproxy/base-config-loader.d.ts +1 -0
  10. package/dist/cliproxy/base-config-loader.d.ts.map +1 -1
  11. package/dist/cliproxy/config/env-builder.d.ts +1 -0
  12. package/dist/cliproxy/config/env-builder.d.ts.map +1 -1
  13. package/dist/cliproxy/config/extended-context-config.d.ts +1 -0
  14. package/dist/cliproxy/config/extended-context-config.d.ts.map +1 -1
  15. package/dist/cliproxy/config/thinking-config.d.ts +1 -0
  16. package/dist/cliproxy/config/thinking-config.d.ts.map +1 -1
  17. package/dist/cliproxy/executor/env-resolver.d.ts +2 -0
  18. package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
  19. package/dist/cliproxy/executor/env-resolver.js +2 -1
  20. package/dist/cliproxy/executor/env-resolver.js.map +1 -1
  21. package/dist/cliproxy/executor/index.d.ts.map +1 -1
  22. package/dist/cliproxy/executor/index.js +28 -1
  23. package/dist/cliproxy/executor/index.js.map +1 -1
  24. package/dist/cliproxy/model-id-normalizer.d.ts +1 -0
  25. package/dist/cliproxy/model-id-normalizer.d.ts.map +1 -1
  26. package/dist/cliproxy/routing-strategy-http.d.ts +1 -0
  27. package/dist/cliproxy/routing-strategy-http.d.ts.map +1 -1
  28. package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
  29. package/dist/cliproxy/tool-sanitization-proxy.js +0 -3
  30. package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
  31. package/dist/cliproxy/types.d.ts +2 -0
  32. package/dist/cliproxy/types.d.ts.map +1 -1
  33. package/dist/config/migration-manager.d.ts.map +1 -1
  34. package/dist/config/migration-manager.js +2 -1
  35. package/dist/config/migration-manager.js.map +1 -1
  36. package/dist/cursor/cursor-anthropic-response.d.ts +1 -0
  37. package/dist/cursor/cursor-anthropic-response.d.ts.map +1 -1
  38. package/dist/cursor/cursor-executor.d.ts +1 -0
  39. package/dist/cursor/cursor-executor.d.ts.map +1 -1
  40. package/dist/docker/docker-executor.d.ts +1 -0
  41. package/dist/docker/docker-executor.d.ts.map +1 -1
  42. package/dist/management/instance-manager.d.ts.map +1 -1
  43. package/dist/management/instance-manager.js +5 -2
  44. package/dist/management/instance-manager.js.map +1 -1
  45. package/dist/targets/claude-adapter.d.ts +6 -1
  46. package/dist/targets/claude-adapter.d.ts.map +1 -1
  47. package/dist/targets/claude-adapter.js +6 -2
  48. package/dist/targets/claude-adapter.js.map +1 -1
  49. package/dist/targets/codex-adapter.d.ts +1 -0
  50. package/dist/targets/codex-adapter.d.ts.map +1 -1
  51. package/dist/targets/codex-adapter.js +12 -8
  52. package/dist/targets/codex-adapter.js.map +1 -1
  53. package/dist/targets/droid-adapter.d.ts +3 -1
  54. package/dist/targets/droid-adapter.d.ts.map +1 -1
  55. package/dist/targets/droid-adapter.js +3 -2
  56. package/dist/targets/droid-adapter.js.map +1 -1
  57. package/dist/targets/target-adapter.d.ts +5 -0
  58. package/dist/targets/target-adapter.d.ts.map +1 -1
  59. package/dist/types/cli.d.ts +1 -0
  60. package/dist/types/cli.d.ts.map +1 -1
  61. package/dist/utils/browser/chrome-reuse.d.ts +17 -0
  62. package/dist/utils/browser/chrome-reuse.d.ts.map +1 -0
  63. package/dist/utils/browser/chrome-reuse.js +205 -0
  64. package/dist/utils/browser/chrome-reuse.js.map +1 -0
  65. package/dist/utils/browser/claude-tool-args.d.ts +2 -0
  66. package/dist/utils/browser/claude-tool-args.d.ts.map +1 -0
  67. package/dist/utils/browser/claude-tool-args.js +18 -0
  68. package/dist/utils/browser/claude-tool-args.js.map +1 -0
  69. package/dist/utils/browser/index.d.ts +8 -0
  70. package/dist/utils/browser/index.d.ts.map +1 -0
  71. package/dist/utils/browser/index.js +24 -0
  72. package/dist/utils/browser/index.js.map +1 -0
  73. package/dist/utils/browser/mcp-installer.d.ts +14 -0
  74. package/dist/utils/browser/mcp-installer.d.ts.map +1 -0
  75. package/dist/utils/browser/mcp-installer.js +356 -0
  76. package/dist/utils/browser/mcp-installer.js.map +1 -0
  77. package/dist/utils/browser-codex-overrides.d.ts +3 -0
  78. package/dist/utils/browser-codex-overrides.d.ts.map +1 -0
  79. package/dist/utils/browser-codex-overrides.js +29 -0
  80. package/dist/utils/browser-codex-overrides.js.map +1 -0
  81. package/dist/utils/claude-detector.d.ts +1 -0
  82. package/dist/utils/claude-detector.d.ts.map +1 -1
  83. package/dist/utils/claude-spawner.d.ts +1 -0
  84. package/dist/utils/claude-spawner.d.ts.map +1 -1
  85. package/dist/utils/claude-tool-args.d.ts +7 -0
  86. package/dist/utils/claude-tool-args.d.ts.map +1 -0
  87. package/dist/utils/claude-tool-args.js +43 -0
  88. package/dist/utils/claude-tool-args.js.map +1 -0
  89. package/dist/utils/image-analysis/claude-tool-args.d.ts.map +1 -1
  90. package/dist/utils/image-analysis/claude-tool-args.js +3 -38
  91. package/dist/utils/image-analysis/claude-tool-args.js.map +1 -1
  92. package/dist/utils/package-manager-detector.d.ts +1 -0
  93. package/dist/utils/package-manager-detector.d.ts.map +1 -1
  94. package/dist/utils/shell-executor.d.ts +1 -0
  95. package/dist/utils/shell-executor.d.ts.map +1 -1
  96. package/dist/utils/websearch/claude-tool-args.d.ts.map +1 -1
  97. package/dist/utils/websearch/claude-tool-args.js +6 -41
  98. package/dist/utils/websearch/claude-tool-args.js.map +1 -1
  99. package/dist/utils/websearch/trace.d.ts +1 -0
  100. package/dist/utils/websearch/trace.d.ts.map +1 -1
  101. package/dist/web-server/middleware/auth-middleware.d.ts +0 -1
  102. package/dist/web-server/middleware/auth-middleware.d.ts.map +1 -1
  103. package/dist/web-server/routes/cliproxy-local-proxy.d.ts.map +1 -1
  104. package/dist/web-server/routes/cliproxy-local-proxy.js +26 -6
  105. package/dist/web-server/routes/cliproxy-local-proxy.js.map +1 -1
  106. package/dist/web-server/services/codex-dashboard-service.d.ts +1 -0
  107. package/dist/web-server/services/codex-dashboard-service.d.ts.map +1 -1
  108. package/dist/web-server/services/droid-dashboard-service.d.ts +1 -0
  109. package/dist/web-server/services/droid-dashboard-service.d.ts.map +1 -1
  110. package/lib/mcp/ccs-browser-server.cjs +877 -0
  111. package/package.json +2 -1
@@ -0,0 +1,877 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { WebSocket } = require('ws');
4
+
5
+ const PROTOCOL_VERSION = '2024-11-05';
6
+ const SERVER_NAME = 'ccs-browser';
7
+ const SERVER_VERSION = '1.0.0';
8
+ const TOOL_SESSION_INFO = 'browser_get_session_info';
9
+ const TOOL_URL_TITLE = 'browser_get_url_and_title';
10
+ const TOOL_VISIBLE_TEXT = 'browser_get_visible_text';
11
+ const TOOL_DOM_SNAPSHOT = 'browser_get_dom_snapshot';
12
+ const TOOL_NAVIGATE = 'browser_navigate';
13
+ const TOOL_CLICK = 'browser_click';
14
+ const TOOL_TYPE = 'browser_type';
15
+ const TOOL_TAKE_SCREENSHOT = 'browser_take_screenshot';
16
+ const TOOL_NAMES = [
17
+ TOOL_SESSION_INFO,
18
+ TOOL_URL_TITLE,
19
+ TOOL_VISIBLE_TEXT,
20
+ TOOL_DOM_SNAPSHOT,
21
+ TOOL_NAVIGATE,
22
+ TOOL_CLICK,
23
+ TOOL_TYPE,
24
+ TOOL_TAKE_SCREENSHOT,
25
+ ];
26
+ const CDP_TIMEOUT_MS = 5000;
27
+ const NAVIGATION_POLL_INTERVAL_MS = 100;
28
+
29
+ let inputBuffer = Buffer.alloc(0);
30
+ let requestCounter = 0;
31
+
32
+ function shouldExposeTools() {
33
+ return Boolean(process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL);
34
+ }
35
+
36
+ function writeMessage(message) {
37
+ process.stdout.write(`${JSON.stringify(message)}\n`);
38
+ }
39
+
40
+ function writeResponse(id, result) {
41
+ writeMessage({ jsonrpc: '2.0', id, result });
42
+ }
43
+
44
+ function writeError(id, code, message) {
45
+ writeMessage({ jsonrpc: '2.0', id, error: { code, message } });
46
+ }
47
+
48
+ function getTools() {
49
+ if (!shouldExposeTools()) {
50
+ return [];
51
+ }
52
+
53
+ return [
54
+ {
55
+ name: TOOL_SESSION_INFO,
56
+ description:
57
+ 'List the current Chrome session pages available through the configured DevTools connection, including page ids, titles, URLs, and websocket endpoints.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {},
61
+ additionalProperties: false,
62
+ },
63
+ },
64
+ {
65
+ name: TOOL_URL_TITLE,
66
+ description:
67
+ 'Read the current page URL and title from the configured Chrome session. Optionally choose a page by index.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ pageIndex: {
72
+ type: 'integer',
73
+ minimum: 0,
74
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
75
+ },
76
+ },
77
+ additionalProperties: false,
78
+ },
79
+ },
80
+ {
81
+ name: TOOL_VISIBLE_TEXT,
82
+ description:
83
+ 'Read visible text from the current page via DOM evaluation in the configured Chrome session. Optionally choose a page by index.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ pageIndex: {
88
+ type: 'integer',
89
+ minimum: 0,
90
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
91
+ },
92
+ },
93
+ additionalProperties: false,
94
+ },
95
+ },
96
+ {
97
+ name: TOOL_DOM_SNAPSHOT,
98
+ description:
99
+ 'Read a DOM snapshot from the current page by returning the document outerHTML. Optionally choose a page by index.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ pageIndex: {
104
+ type: 'integer',
105
+ minimum: 0,
106
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
107
+ },
108
+ },
109
+ additionalProperties: false,
110
+ },
111
+ },
112
+ {
113
+ name: TOOL_NAVIGATE,
114
+ description:
115
+ 'Navigate the selected page to an absolute http or https URL and wait until navigation is ready. Optionally choose a page by index.',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ pageIndex: {
120
+ type: 'integer',
121
+ minimum: 0,
122
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
123
+ },
124
+ url: {
125
+ type: 'string',
126
+ description: 'Required absolute http or https URL to navigate to.',
127
+ },
128
+ },
129
+ required: ['url'],
130
+ additionalProperties: false,
131
+ },
132
+ },
133
+ {
134
+ name: TOOL_CLICK,
135
+ description:
136
+ 'Click the first element matching a CSS selector in the selected page using a minimal mouse event chain with click fallback. Optionally choose a page by index.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ pageIndex: {
141
+ type: 'integer',
142
+ minimum: 0,
143
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
144
+ },
145
+ selector: {
146
+ type: 'string',
147
+ description: 'Required CSS selector for the element to click.',
148
+ },
149
+ },
150
+ required: ['selector'],
151
+ additionalProperties: false,
152
+ },
153
+ },
154
+ {
155
+ name: TOOL_TYPE,
156
+ description:
157
+ 'Type text into the first element matching a CSS selector when it is a supported text-editable target. Optionally choose a page by index.',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {
161
+ pageIndex: {
162
+ type: 'integer',
163
+ minimum: 0,
164
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
165
+ },
166
+ selector: {
167
+ type: 'string',
168
+ description: 'Required CSS selector for the target element.',
169
+ },
170
+ text: {
171
+ type: 'string',
172
+ description: 'Required text to assign. May be an empty string.',
173
+ },
174
+ clearFirst: {
175
+ type: 'boolean',
176
+ description: 'When true, clear the current value or content before assigning text.',
177
+ },
178
+ },
179
+ required: ['selector', 'text'],
180
+ additionalProperties: false,
181
+ },
182
+ },
183
+ {
184
+ name: TOOL_TAKE_SCREENSHOT,
185
+ description:
186
+ 'Capture a PNG screenshot from the selected page. Optionally choose a page by index or request fullPage capture.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ pageIndex: {
191
+ type: 'integer',
192
+ minimum: 0,
193
+ description: 'Optional zero-based page index from browser_get_session_info. Defaults to the first page.',
194
+ },
195
+ fullPage: {
196
+ type: 'boolean',
197
+ description: 'Optional full-page capture flag.',
198
+ },
199
+ },
200
+ additionalProperties: false,
201
+ },
202
+ },
203
+ ];
204
+ }
205
+
206
+ async function fetchJson(url) {
207
+ const response = await fetch(url);
208
+ if (!response.ok) {
209
+ throw new Error(`HTTP ${response.status} for ${url}`);
210
+ }
211
+ return await response.json();
212
+ }
213
+
214
+ function getHttpUrl() {
215
+ const value = process.env.CCS_BROWSER_DEVTOOLS_HTTP_URL;
216
+ if (!value) {
217
+ throw new Error('Browser MCP is unavailable because CCS_BROWSER_DEVTOOLS_HTTP_URL is missing.');
218
+ }
219
+ return value.replace(/\/+$/, '');
220
+ }
221
+
222
+ async function listPageTargets() {
223
+ const targets = await fetchJson(`${getHttpUrl()}/json/list`);
224
+ if (!Array.isArray(targets)) {
225
+ throw new Error('Browser MCP received an invalid /json/list response.');
226
+ }
227
+
228
+ return targets
229
+ .filter((target) => target && typeof target === 'object' && target.type === 'page')
230
+ .map((target) => ({
231
+ id: typeof target.id === 'string' ? target.id : '',
232
+ title: typeof target.title === 'string' ? target.title : '',
233
+ url: typeof target.url === 'string' ? target.url : '',
234
+ type: typeof target.type === 'string' ? target.type : 'page',
235
+ webSocketDebuggerUrl:
236
+ typeof target.webSocketDebuggerUrl === 'string' ? target.webSocketDebuggerUrl : '',
237
+ }));
238
+ }
239
+
240
+ function parsePageIndex(toolArgs) {
241
+ if (!toolArgs || !Object.prototype.hasOwnProperty.call(toolArgs, 'pageIndex')) {
242
+ return 0;
243
+ }
244
+
245
+ if (!Number.isInteger(toolArgs.pageIndex) || toolArgs.pageIndex < 0) {
246
+ throw new Error('pageIndex must be a non-negative integer');
247
+ }
248
+
249
+ return toolArgs.pageIndex;
250
+ }
251
+
252
+ async function getSelectedPage(toolArgs) {
253
+ const pages = await listPageTargets();
254
+ if (pages.length === 0) {
255
+ throw new Error('Browser MCP did not find any page targets in the current Chrome session.');
256
+ }
257
+
258
+ const pageIndex = parsePageIndex(toolArgs);
259
+
260
+ const page = pages[pageIndex];
261
+ if (!page) {
262
+ throw new Error(`Browser MCP page index ${pageIndex} is out of range (found ${pages.length} pages).`);
263
+ }
264
+ if (!page.webSocketDebuggerUrl) {
265
+ throw new Error(`Browser MCP page ${pageIndex} does not expose a websocket debugger URL.`);
266
+ }
267
+
268
+ return { page, pageIndex, pages };
269
+ }
270
+
271
+ function formatSessionInfo(pages) {
272
+ return [
273
+ '[CCS Browser Session]',
274
+ '',
275
+ ...pages.map((page, index) => `${index}. ${page.title || '<untitled>'} | ${page.url || '<empty>'}`),
276
+ ].join('\n');
277
+ }
278
+
279
+ function createEvaluateExpression(kind) {
280
+ switch (kind) {
281
+ case 'url-title':
282
+ return `JSON.stringify({ title: document.title, url: location.href })`;
283
+ case 'visible-text':
284
+ return `document.body ? document.body.innerText : ''`;
285
+ case 'dom-snapshot':
286
+ return `document.documentElement ? document.documentElement.outerHTML : ''`;
287
+ default:
288
+ throw new Error(`Unknown browser evaluation kind: ${kind}`);
289
+ }
290
+ }
291
+
292
+ async function sendCdpCommand(page, method, params = {}) {
293
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
294
+ const requestId = ++requestCounter;
295
+
296
+ return await new Promise((resolve, reject) => {
297
+ let settled = false;
298
+ const timer = setTimeout(() => {
299
+ if (!settled) {
300
+ settled = true;
301
+ ws.terminate();
302
+ reject(new Error('Browser MCP timed out waiting for a DevTools response.'));
303
+ }
304
+ }, CDP_TIMEOUT_MS);
305
+
306
+ function settleError(error) {
307
+ if (settled) {
308
+ return;
309
+ }
310
+ clearTimeout(timer);
311
+ settled = true;
312
+ reject(error);
313
+ }
314
+
315
+ ws.on('open', () => {
316
+ ws.send(
317
+ JSON.stringify({
318
+ id: requestId,
319
+ method,
320
+ params,
321
+ })
322
+ );
323
+ });
324
+
325
+ ws.on('message', (data) => {
326
+ if (settled) {
327
+ return;
328
+ }
329
+
330
+ let message;
331
+ try {
332
+ message = JSON.parse(data.toString());
333
+ } catch {
334
+ return;
335
+ }
336
+
337
+ if (message.id !== requestId) {
338
+ return;
339
+ }
340
+
341
+ clearTimeout(timer);
342
+ settled = true;
343
+ ws.close();
344
+
345
+ if (message.error) {
346
+ reject(new Error(message.error.message || 'DevTools request failed.'));
347
+ return;
348
+ }
349
+
350
+ resolve(message.result || null);
351
+ });
352
+
353
+ ws.on('error', (error) => {
354
+ settleError(error);
355
+ });
356
+
357
+ ws.on('close', () => {
358
+ if (settled) {
359
+ return;
360
+ }
361
+ settleError(new Error('Browser MCP lost the DevTools websocket connection.'));
362
+ });
363
+ });
364
+ }
365
+
366
+ async function evaluateInPage(page, kind) {
367
+ const response = await sendCdpCommand(page, 'Runtime.evaluate', {
368
+ expression: createEvaluateExpression(kind),
369
+ returnByValue: true,
370
+ });
371
+
372
+ const result = response && response.result ? response.result : null;
373
+ if (!result) {
374
+ throw new Error('Browser MCP received an invalid DevTools evaluation response.');
375
+ }
376
+
377
+ if (result.subtype === 'error') {
378
+ throw new Error(result.description || 'DevTools evaluation returned an error.');
379
+ }
380
+
381
+ return typeof result.value === 'string' ? result.value : result.value ?? '';
382
+ }
383
+
384
+ async function evaluateExpression(page, expression) {
385
+ const response = await sendCdpCommand(page, 'Runtime.evaluate', {
386
+ expression,
387
+ returnByValue: true,
388
+ });
389
+
390
+ const result = response && response.result ? response.result : null;
391
+ if (!result) {
392
+ throw new Error('Browser MCP received an invalid DevTools evaluation response.');
393
+ }
394
+
395
+ if (result.subtype === 'error') {
396
+ throw new Error(result.description || 'DevTools evaluation returned an error.');
397
+ }
398
+
399
+ return typeof result.value === 'string' ? result.value : result.value ?? '';
400
+ }
401
+
402
+ function requireNonEmptyString(value, label) {
403
+ if (typeof value !== 'string' || value.trim() === '') {
404
+ throw new Error(`${label} is required`);
405
+ }
406
+ return value.trim();
407
+ }
408
+
409
+ function requireString(value, label) {
410
+ if (typeof value !== 'string') {
411
+ throw new Error(`${label} is required`);
412
+ }
413
+ return value;
414
+ }
415
+
416
+ function requireValidHttpUrl(value) {
417
+ const raw = requireNonEmptyString(value, 'url');
418
+ let parsed;
419
+ try {
420
+ parsed = new URL(raw);
421
+ } catch {
422
+ throw new Error('url must be an absolute http or https URL');
423
+ }
424
+
425
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
426
+ throw new Error('url must be an absolute http or https URL');
427
+ }
428
+
429
+ return parsed.toString();
430
+ }
431
+
432
+ function sleep(ms) {
433
+ return new Promise((resolve) => setTimeout(resolve, ms));
434
+ }
435
+
436
+ async function getNavigationState(page) {
437
+ const raw = await evaluateExpression(page, `JSON.stringify({ href: location.href, readyState: document.readyState })`);
438
+ const parsed = JSON.parse(raw);
439
+ return {
440
+ href: typeof parsed.href === 'string' ? parsed.href : '',
441
+ readyState: typeof parsed.readyState === 'string' ? parsed.readyState : '',
442
+ };
443
+ }
444
+
445
+ function isSameDocumentHashNavigation(beforeHref, requestedUrl) {
446
+ try {
447
+ const before = new URL(beforeHref);
448
+ const requested = new URL(requestedUrl);
449
+ return (
450
+ before.origin === requested.origin &&
451
+ before.pathname === requested.pathname &&
452
+ before.search === requested.search &&
453
+ before.hash !== requested.hash
454
+ );
455
+ } catch {
456
+ return false;
457
+ }
458
+ }
459
+
460
+ function isNavigationReady(state, beforeHref, requestedUrl) {
461
+ if (state.readyState !== 'interactive' && state.readyState !== 'complete') {
462
+ return false;
463
+ }
464
+
465
+ if (state.href === requestedUrl) {
466
+ return true;
467
+ }
468
+
469
+ if (state.href && state.href !== beforeHref) {
470
+ return true;
471
+ }
472
+
473
+ if (isSameDocumentHashNavigation(beforeHref, requestedUrl) && state.href === requestedUrl) {
474
+ return true;
475
+ }
476
+
477
+ return false;
478
+ }
479
+
480
+ async function waitForNavigationReady(page, beforeHref, requestedUrl) {
481
+ const deadline = Date.now() + CDP_TIMEOUT_MS;
482
+
483
+ while (Date.now() <= deadline) {
484
+ const state = await getNavigationState(page);
485
+ if (isNavigationReady(state, beforeHref, requestedUrl)) {
486
+ return state.href;
487
+ }
488
+ if (Date.now() + NAVIGATION_POLL_INTERVAL_MS > deadline) {
489
+ break;
490
+ }
491
+ await sleep(NAVIGATION_POLL_INTERVAL_MS);
492
+ }
493
+
494
+ throw new Error(`navigation did not complete for URL: ${requestedUrl}`);
495
+ }
496
+
497
+ async function handleNavigate(toolArgs) {
498
+ const { page, pageIndex } = await getSelectedPage(toolArgs);
499
+ const url = requireValidHttpUrl(toolArgs.url);
500
+ const before = await getNavigationState(page);
501
+ const navigateResult = await sendCdpCommand(page, 'Page.navigate', { url });
502
+ if (navigateResult && typeof navigateResult.errorText === 'string' && navigateResult.errorText) {
503
+ throw new Error(`navigation failed for URL: ${url}: ${navigateResult.errorText}`);
504
+ }
505
+ const finalUrl = await waitForNavigationReady(page, before.href, url);
506
+ return `pageIndex: ${pageIndex}\nurl: ${finalUrl}\nstatus: navigated`;
507
+ }
508
+
509
+ async function handleClick(toolArgs) {
510
+ const { page, pageIndex } = await getSelectedPage(toolArgs);
511
+ const selector = requireNonEmptyString(toolArgs.selector, 'selector');
512
+
513
+ const expression = `(() => {
514
+ const selector = JSON.parse(${JSON.stringify(JSON.stringify(selector))});
515
+ const element = document.querySelector(selector);
516
+ if (!element) {
517
+ throw new Error('element not found for selector: ' + selector);
518
+ }
519
+ if (!element.isConnected) {
520
+ throw new Error('element is detached for selector: ' + selector);
521
+ }
522
+ if ('disabled' in element && element.disabled) {
523
+ throw new Error('element is disabled for selector: ' + selector);
524
+ }
525
+ const style = window.getComputedStyle(element);
526
+ const rect = element.getBoundingClientRect();
527
+ if (
528
+ style.display === 'none' ||
529
+ style.visibility === 'hidden' ||
530
+ rect.width <= 0 ||
531
+ rect.height <= 0
532
+ ) {
533
+ throw new Error('element is hidden or not interactable for selector: ' + selector);
534
+ }
535
+ element.scrollIntoView({ block: 'center', inline: 'center' });
536
+
537
+ const dispatchMouseEvent = (type, init) => {
538
+ const event = new MouseEvent(type, {
539
+ bubbles: true,
540
+ cancelable: true,
541
+ composed: true,
542
+ view: window,
543
+ detail: 1,
544
+ ...init,
545
+ });
546
+ return element.dispatchEvent(event);
547
+ };
548
+
549
+ try {
550
+ const dispatchResult = {
551
+ shouldActivate:
552
+ dispatchMouseEvent('mousedown', { button: 0, buttons: 1 }) &&
553
+ dispatchMouseEvent('mouseup', { button: 0, buttons: 0 }),
554
+ };
555
+ if (!dispatchResult.shouldActivate) {
556
+ return 'ok';
557
+ }
558
+ if (!element.isConnected) {
559
+ return 'ok';
560
+ }
561
+ } catch (mouseError) {
562
+ // Fall through to the native activation path below.
563
+ }
564
+
565
+ element.click();
566
+
567
+ return 'ok';
568
+ })()`;
569
+
570
+ await evaluateExpression(page, expression);
571
+ return `pageIndex: ${pageIndex}\nselector: ${selector}\nstatus: clicked`;
572
+ }
573
+
574
+ async function handleType(toolArgs) {
575
+ const { page, pageIndex } = await getSelectedPage(toolArgs);
576
+ const selector = requireNonEmptyString(toolArgs.selector, 'selector');
577
+ const text = requireString(toolArgs.text, 'text');
578
+ const clearFirst = toolArgs.clearFirst === true;
579
+
580
+ const expression = `(() => {
581
+ const selector = JSON.parse(${JSON.stringify(JSON.stringify(selector))});
582
+ const text = JSON.parse(${JSON.stringify(JSON.stringify(text))});
583
+ const clearFirst = ${clearFirst ? 'true' : 'false'};
584
+ const element = document.querySelector(selector);
585
+ if (!element) {
586
+ throw new Error('element not found for selector: ' + selector);
587
+ }
588
+
589
+ const dispatchEvents = (target) => {
590
+ target.dispatchEvent(new Event('input', { bubbles: true }));
591
+ target.dispatchEvent(new Event('change', { bubbles: true }));
592
+ };
593
+
594
+ const focusTarget = (target) => {
595
+ if (typeof target.focus === 'function') {
596
+ target.focus();
597
+ }
598
+ };
599
+
600
+ let readback = '';
601
+ let expectedValue = '';
602
+
603
+ if (element instanceof HTMLTextAreaElement) {
604
+ focusTarget(element);
605
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
606
+ expectedValue = (clearFirst ? '' : element.value) + text;
607
+ if (setter) {
608
+ setter.call(element, expectedValue);
609
+ } else {
610
+ element.value = expectedValue;
611
+ }
612
+ dispatchEvents(element);
613
+ readback = element.value;
614
+ } else if (element instanceof HTMLInputElement) {
615
+ const supportedTypes = new Set(['', 'text', 'search', 'email', 'url', 'tel', 'password']);
616
+ const normalizedType = (element.getAttribute('type') || '').toLowerCase();
617
+ if (!supportedTypes.has(normalizedType)) {
618
+ throw new Error('element is not text-editable for selector: ' + selector);
619
+ }
620
+ focusTarget(element);
621
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
622
+ expectedValue = (clearFirst ? '' : element.value) + text;
623
+ if (setter) {
624
+ setter.call(element, expectedValue);
625
+ } else {
626
+ element.value = expectedValue;
627
+ }
628
+ dispatchEvents(element);
629
+ readback = element.value;
630
+ } else if (element.isContentEditable === true) {
631
+ focusTarget(element);
632
+ expectedValue = (clearFirst ? '' : (element.textContent || '')) + text;
633
+ element.textContent = expectedValue;
634
+ dispatchEvents(element);
635
+ readback = element.textContent || '';
636
+ } else {
637
+ throw new Error('element is not text-editable for selector: ' + selector);
638
+ }
639
+
640
+ if (readback !== expectedValue) {
641
+ throw new Error('typed text verification failed for selector: ' + selector);
642
+ }
643
+
644
+ return JSON.stringify({ value: readback, typedLength: readback.length });
645
+ })()`;
646
+
647
+ const raw = await evaluateExpression(page, expression);
648
+ const parsed = JSON.parse(raw);
649
+ const typedLength = typeof parsed.typedLength === 'number' ? parsed.typedLength : String(parsed.value || '').length;
650
+ return `pageIndex: ${pageIndex}\nselector: ${selector}\ntypedLength: ${typedLength}\nstatus: typed`;
651
+ }
652
+
653
+ async function handleScreenshot(toolArgs) {
654
+ const { page, pageIndex } = await getSelectedPage(toolArgs);
655
+ const fullPage = toolArgs.fullPage === true;
656
+ const response = await sendCdpCommand(page, 'Page.captureScreenshot', {
657
+ format: 'png',
658
+ captureBeyondViewport: fullPage,
659
+ });
660
+
661
+ const data = response && typeof response.data === 'string' ? response.data : '';
662
+ if (!data) {
663
+ throw new Error('screenshot capture failed');
664
+ }
665
+
666
+ return `pageIndex: ${pageIndex}\nformat: png\nfullPage: ${fullPage ? 'true' : 'false'}\ndata: ${data}`;
667
+ }
668
+
669
+ async function handleToolCall(message) {
670
+ const id = message.id;
671
+ const params = message.params || {};
672
+ const toolName = params.name || '<missing>';
673
+ const toolArgs = params.arguments || {};
674
+
675
+ if (!TOOL_NAMES.includes(toolName)) {
676
+ writeError(id, -32602, `Unknown tool: ${toolName}`);
677
+ return;
678
+ }
679
+
680
+ if (!shouldExposeTools()) {
681
+ writeResponse(id, {
682
+ content: [
683
+ {
684
+ type: 'text',
685
+ text: 'Browser MCP is unavailable because browser reuse is not configured for this Claude session.',
686
+ },
687
+ ],
688
+ isError: true,
689
+ });
690
+ return;
691
+ }
692
+
693
+ try {
694
+ if (toolName === TOOL_SESSION_INFO) {
695
+ const pages = await listPageTargets();
696
+ writeResponse(id, {
697
+ content: [{ type: 'text', text: formatSessionInfo(pages) }],
698
+ });
699
+ return;
700
+ }
701
+
702
+ if (toolName === TOOL_NAVIGATE) {
703
+ const text = await handleNavigate(toolArgs);
704
+ writeResponse(id, {
705
+ content: [{ type: 'text', text }],
706
+ });
707
+ return;
708
+ }
709
+
710
+ if (toolName === TOOL_CLICK) {
711
+ const text = await handleClick(toolArgs);
712
+ writeResponse(id, {
713
+ content: [{ type: 'text', text }],
714
+ });
715
+ return;
716
+ }
717
+
718
+ if (toolName === TOOL_TYPE) {
719
+ const text = await handleType(toolArgs);
720
+ writeResponse(id, {
721
+ content: [{ type: 'text', text }],
722
+ });
723
+ return;
724
+ }
725
+
726
+ if (toolName === TOOL_TAKE_SCREENSHOT) {
727
+ const text = await handleScreenshot(toolArgs);
728
+ writeResponse(id, {
729
+ content: [{ type: 'text', text }],
730
+ });
731
+ return;
732
+ }
733
+
734
+ const { page, pageIndex } = await getSelectedPage(toolArgs);
735
+
736
+ if (toolName === TOOL_URL_TITLE) {
737
+ const raw = await evaluateInPage(page, 'url-title');
738
+ const parsed = JSON.parse(raw);
739
+ writeResponse(id, {
740
+ content: [
741
+ {
742
+ type: 'text',
743
+ text: `pageIndex: ${pageIndex}\ntitle: ${parsed.title || ''}\nurl: ${parsed.url || ''}`,
744
+ },
745
+ ],
746
+ });
747
+ return;
748
+ }
749
+
750
+ if (toolName === TOOL_VISIBLE_TEXT) {
751
+ const text = await evaluateInPage(page, 'visible-text');
752
+ writeResponse(id, {
753
+ content: [{ type: 'text', text: text || '' }],
754
+ });
755
+ return;
756
+ }
757
+
758
+ const html = await evaluateInPage(page, 'dom-snapshot');
759
+ writeResponse(id, {
760
+ content: [{ type: 'text', text: html || '' }],
761
+ });
762
+ } catch (error) {
763
+ writeResponse(id, {
764
+ content: [
765
+ {
766
+ type: 'text',
767
+ text: `Browser MCP failed: ${(error && error.message) || String(error)}`,
768
+ },
769
+ ],
770
+ isError: true,
771
+ });
772
+ }
773
+ }
774
+
775
+ async function handleMessage(message) {
776
+ if (!message || message.jsonrpc !== '2.0' || typeof message.method !== 'string') {
777
+ return;
778
+ }
779
+
780
+ switch (message.method) {
781
+ case 'initialize':
782
+ writeResponse(message.id, {
783
+ protocolVersion: PROTOCOL_VERSION,
784
+ capabilities: { tools: {} },
785
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
786
+ });
787
+ return;
788
+ case 'notifications/initialized':
789
+ return;
790
+ case 'ping':
791
+ writeResponse(message.id, {});
792
+ return;
793
+ case 'tools/list':
794
+ writeResponse(message.id, { tools: getTools() });
795
+ return;
796
+ case 'tools/call':
797
+ await handleToolCall(message);
798
+ return;
799
+ default:
800
+ if (message.id !== undefined) {
801
+ writeError(message.id, -32601, `Method not found: ${message.method}`);
802
+ }
803
+ }
804
+ }
805
+
806
+ function parseMessages() {
807
+ while (true) {
808
+ let body;
809
+ const startsWithLegacyHeaders = inputBuffer
810
+ .subarray(0, Math.min(inputBuffer.length, 32))
811
+ .toString('utf8')
812
+ .toLowerCase()
813
+ .startsWith('content-length:');
814
+
815
+ if (startsWithLegacyHeaders) {
816
+ const headerEnd = inputBuffer.indexOf('\r\n\r\n');
817
+ if (headerEnd === -1) {
818
+ return;
819
+ }
820
+
821
+ const headerText = inputBuffer.subarray(0, headerEnd).toString('utf8');
822
+ const match = headerText.match(/content-length:\s*(\d+)/i);
823
+ if (!match) {
824
+ inputBuffer = Buffer.alloc(0);
825
+ return;
826
+ }
827
+
828
+ const contentLength = Number.parseInt(match[1], 10);
829
+ const messageEnd = headerEnd + 4 + contentLength;
830
+ if (inputBuffer.length < messageEnd) {
831
+ return;
832
+ }
833
+
834
+ body = inputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
835
+ inputBuffer = inputBuffer.subarray(messageEnd);
836
+ } else {
837
+ const newlineIndex = inputBuffer.indexOf('\n');
838
+ if (newlineIndex === -1) {
839
+ return;
840
+ }
841
+
842
+ body = inputBuffer.subarray(0, newlineIndex).toString('utf8').replace(/\r$/, '').trim();
843
+ inputBuffer = inputBuffer.subarray(newlineIndex + 1);
844
+ if (!body) {
845
+ continue;
846
+ }
847
+ }
848
+
849
+ let message;
850
+ try {
851
+ message = JSON.parse(body);
852
+ } catch {
853
+ continue;
854
+ }
855
+
856
+ Promise.resolve(handleMessage(message)).catch((error) => {
857
+ if (message && message.id !== undefined) {
858
+ writeError(message.id, -32603, (error && error.message) || 'Internal error');
859
+ }
860
+ });
861
+ }
862
+ }
863
+
864
+ process.stdin.on('data', (chunk) => {
865
+ inputBuffer = Buffer.concat([inputBuffer, chunk]);
866
+ parseMessages();
867
+ });
868
+
869
+ process.stdin.on('error', () => {
870
+ process.exit(0);
871
+ });
872
+
873
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => {
874
+ process.on(signal, () => process.exit(0));
875
+ });
876
+
877
+ process.stdin.resume();