chrometools-mcp 3.2.10 → 3.3.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.
- package/CHANGELOG.md +169 -0
- package/README.md +14 -5
- package/angular-tools.js +9 -3
- package/bridge/bridge-client.js +62 -7
- package/bridge/bridge-service.js +80 -2
- package/browser/page-manager.js +83 -0
- package/extension/background.js +117 -0
- package/extension/content.js +3 -1
- package/extension/manifest.json +2 -1
- package/index.js +284 -48
- package/models/TextInputModel.js +56 -5
- package/models/index.js +20 -6
- package/nul +0 -0
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +308 -39
- package/server/tool-definitions.js +3 -1
- package/server/tool-schemas.js +5 -0
- package/utils/hints-generator.js +46 -4
- package/utils/post-click-diagnostics.js +146 -47
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,175 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.3.8] - 2026-02-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **analyzePage: addEventListener tracking for Angular/React/Vue** — Monkey-patch detection
|
|
9
|
+
- Injects tracker via `evaluateOnNewDocument` before page load
|
|
10
|
+
- Catches `addEventListener('click', ...)` calls from any framework
|
|
11
|
+
- Solves Angular detection (compiled `(click)` bindings now visible)
|
|
12
|
+
- WeakMap storage prevents memory leaks
|
|
13
|
+
- `hasExplicitClickBinding()` now checks `window.__hasClickListener()`
|
|
14
|
+
|
|
15
|
+
- **analyzePage: viewportOnly flag** — Filter to visible elements only
|
|
16
|
+
- Reduces output by 30-56% on large pages
|
|
17
|
+
- Useful for data-heavy pages with content below fold
|
|
18
|
+
|
|
19
|
+
- **analyzePage: diff mode** — Show only changes since last analysis
|
|
20
|
+
- Returns `{added, removed, changed}` structure
|
|
21
|
+
- ~80-90% size reduction for incremental updates
|
|
22
|
+
|
|
23
|
+
- **analyzePage: clickTarget legend** — Clarified in tool description
|
|
24
|
+
- Format: `"tag:id"` (e.g., `"div:container_19"`)
|
|
25
|
+
- No clickTarget = element handles its own click
|
|
26
|
+
|
|
27
|
+
## [3.3.7] - 2026-02-03
|
|
28
|
+
|
|
29
|
+
### Performance
|
|
30
|
+
- **analyzePage: 26% output size reduction** — Optimized JSON structure
|
|
31
|
+
- Removed `position` for static elements (default, no need to include)
|
|
32
|
+
- Removed `zIndex: "auto"` (default value)
|
|
33
|
+
- Removed `isStacking`, `hasBackdrop`, `isFullscreen` from position object
|
|
34
|
+
- Removed empty `children: []` arrays
|
|
35
|
+
- Filtered out `null`, `undefined`, empty string, and `false` values from metadata
|
|
36
|
+
- Google Search benchmark: ~38 KB → ~28 KB
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **CLAUDE.md: analyzePage benchmark requirement** — Mandatory performance check
|
|
40
|
+
- Test URL: `https://www.google.com/search?q=puppeteer+mcp+server`
|
|
41
|
+
- Baseline: ~28 KB, threshold: < 40 KB
|
|
42
|
+
- Must run after any changes to analyzePage tool
|
|
43
|
+
|
|
44
|
+
## [3.3.6] - 2026-02-03
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- **Chrome Extension: POST request tracking** — Track POST requests via webRequest API
|
|
48
|
+
- Extension now captures POST/PUT/PATCH requests that Puppeteer may miss
|
|
49
|
+
- Shows in "Browser-level requests (via Extension)" section
|
|
50
|
+
- Useful for SPA apps with complex async request patterns
|
|
51
|
+
|
|
52
|
+
## [3.3.5] - 2026-01-30
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- **type/click: Intermittent timeout fix** — Added timeouts to internal operations
|
|
56
|
+
- `resolveSelector` now has 5s timeout (was unbounded)
|
|
57
|
+
- `getElementInfo` now has 5s timeout (was unbounded)
|
|
58
|
+
- `TextInputModel.setValue` operations have individual 5s timeouts
|
|
59
|
+
- Prevents indefinite hanging on unstable CDP sessions
|
|
60
|
+
- Error messages now show which specific operation timed out
|
|
61
|
+
|
|
62
|
+
## [3.3.4] - 2026-01-30
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- **click: Form submit detection for non-SPA apps** — Detect classic form submissions with page reload
|
|
66
|
+
- Tracks URL before and after click to detect page navigation
|
|
67
|
+
- Reports "Page navigation detected (form submit)" when URL changes
|
|
68
|
+
- Solves issue where POST requests were lost during page reload
|
|
69
|
+
- Works for Django, Rails, PHP and other server-rendered apps
|
|
70
|
+
- Example output: `🔄 Page navigation detected (form submit): /form → /success`
|
|
71
|
+
|
|
72
|
+
## [3.3.3] - 2026-01-30
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
- **click: Post-click diagnostics timeout** — Fixed 30s timeout caused by network wait logic
|
|
76
|
+
- Now waits only for mutation requests (POST/PUT/PATCH) started within 200ms after click
|
|
77
|
+
- Ignores GET requests completely (were causing unnecessary waits)
|
|
78
|
+
- Hard 10s timeout limit enforced (never hangs indefinitely)
|
|
79
|
+
- Always returns success even if requests still pending after 10s
|
|
80
|
+
- Shows pending requests status instead of reporting timeout
|
|
81
|
+
- Changed output from "Form submission" to "Mutation requests"
|
|
82
|
+
- Example output:
|
|
83
|
+
```
|
|
84
|
+
📡 Mutation requests detected (2 POST/PUT/PATCH):
|
|
85
|
+
1. ✓ POST /admin/tenant/.../change/ → 302 Found
|
|
86
|
+
2. ⏳ POST /api/slow-endpoint/ → pending
|
|
87
|
+
|
|
88
|
+
⏳ 1 request(s) still pending after 10000ms timeout
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Performance
|
|
92
|
+
- **click: Removed unnecessary delays** — Removed 100ms waits after scrollIntoView and retry clicks
|
|
93
|
+
- Click operations now complete instantly when no mutation requests detected
|
|
94
|
+
- Typical click time: <50ms (was 500-10000ms with old diagnostics)
|
|
95
|
+
|
|
96
|
+
## [3.3.2] - 2026-01-30
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- **click/type: Viewport scrolling** — Always scrolls to element before interaction
|
|
100
|
+
- Prevents 30s timeout when elements are outside viewport
|
|
101
|
+
- Moves scrollIntoView BEFORE first click/type attempt (was in fallback)
|
|
102
|
+
- Fixes Django admin forms hanging after multiple navigations
|
|
103
|
+
- Pattern: scroll → wait 100ms → interact
|
|
104
|
+
- Applied to both click and type tools
|
|
105
|
+
|
|
106
|
+
## [3.3.1] - 2026-01-30
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
- **click: All network requests visibility** — Shows ALL requests started within 200ms after click
|
|
110
|
+
- AI agent now sees complete network activity picture after every click
|
|
111
|
+
- Shows: method, URL, and status code for each request
|
|
112
|
+
- Example output:
|
|
113
|
+
```
|
|
114
|
+
📡 Network requests started within 200ms after click (15 total):
|
|
115
|
+
1. ✓ GET /api/auth/ws_token/ → 200 OK
|
|
116
|
+
2. ✓ GET /static/css/app.css → 200 OK
|
|
117
|
+
3. ✓ POST /admin/tenant/.../change/ → 302 Found
|
|
118
|
+
```
|
|
119
|
+
- Helps AI understand what happened after click (form submission, navigation, polling, etc.)
|
|
120
|
+
- 200ms window captures both mutation requests and side-effect requests
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
- **Enhanced diagnostics output** — Restructured to show all requests first, then mutation tracking
|
|
124
|
+
- Section 1: All requests in 200ms window (complete visibility)
|
|
125
|
+
- Section 2: Form submission status (POST/PATCH/PUT tracking)
|
|
126
|
+
- Clear icons: ✓ (success), ✗ (error), ⏳ (pending)
|
|
127
|
+
- Agent can immediately see if any requests occurred or not
|
|
128
|
+
|
|
129
|
+
## [3.3.0] - 2026-01-30
|
|
130
|
+
|
|
131
|
+
### Added
|
|
132
|
+
- **click: skipNetworkWait parameter** — Skip network wait for forms with long-polling/WebSockets
|
|
133
|
+
- New parameter: `skipNetworkWait: boolean` (default: false)
|
|
134
|
+
- Use case: Pages with continuous long-polling to get instant response
|
|
135
|
+
- Example: `click({ selector: 'button[type="submit"]', skipNetworkWait: true })`
|
|
136
|
+
- **click: networkWaitTimeout parameter** — Custom network wait timeout
|
|
137
|
+
- New parameter: `networkWaitTimeout: number` (default: 10000ms)
|
|
138
|
+
- Configurable per-click timeout for network requests
|
|
139
|
+
- Example: `click({ selector: '.save-btn', networkWaitTimeout: 5000 })`
|
|
140
|
+
- **type: timeout parameter** — Explicit timeout for type operations
|
|
141
|
+
- New parameter: `timeout: number` (default: 30000ms)
|
|
142
|
+
- Prevents infinite hangs on input fields
|
|
143
|
+
- Example: `type({ selector: 'input[name="url"]', text: 'value', timeout: 10000 })`
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
- **Smart form submission tracking** — Only tracks POST/PATCH/PUT requests in 100ms window
|
|
147
|
+
- Filters mutation requests (POST/PATCH/PUT) that started within 100ms after click
|
|
148
|
+
- Waits ONLY for these mutation requests (up to 10s)
|
|
149
|
+
- Ignores all other requests (GET, polling, analytics, etc.)
|
|
150
|
+
- Shows form submission status: `✓ POST /admin/tenant/.../change/ → 302 Found`
|
|
151
|
+
- Example: Django form with 50 polling requests → only 1 POST tracked
|
|
152
|
+
- **Type operation timeout protection** — Wrapped in Promise.race with configurable timeout
|
|
153
|
+
- Prevents 120s hangs on problematic input fields
|
|
154
|
+
- Returns clear error message: "Type operation timed out after Xms"
|
|
155
|
+
- Django forms: type now fails fast instead of hanging
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
- **Django form timeout issues** — Fixed 30s click timeout and 120s type timeout
|
|
159
|
+
- Root cause: Long-polling/WebSockets created noise in network tracking
|
|
160
|
+
- Solution: Track only POST/PATCH/PUT requests within 100ms detection window
|
|
161
|
+
- Type operations now have explicit timeout protection
|
|
162
|
+
- Example: Django Admin forms now show reliable form submission status
|
|
163
|
+
|
|
164
|
+
## [3.2.11] - 2026-01-30
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
- **Tailwind CSS selector escape** — Fixed `analyzePage` failing on pages with Tailwind CSS
|
|
168
|
+
- Tailwind classes with colons (e.g., `hover:text-gray-800`) broke `querySelectorAll`
|
|
169
|
+
- Added filtering for classes containing special characters (`:`, `/`, `[`, `]`)
|
|
170
|
+
- Applied `CSS.escape()` to class names when building CSS selectors
|
|
171
|
+
- Fixed in: APOM tree builder, Angular tools, hints generator, content script
|
|
172
|
+
- Example: Page with Tailwind → no more "not a valid selector" errors
|
|
173
|
+
|
|
5
174
|
## [3.2.10] - 2026-01-29
|
|
6
175
|
|
|
7
176
|
### Fixed
|
package/README.md
CHANGED
|
@@ -494,9 +494,11 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
|
|
|
494
494
|
- `waitAfter` (optional): Wait time in ms (default: 1500)
|
|
495
495
|
- `screenshot` (optional): Capture screenshot (default: false for performance) ⚡
|
|
496
496
|
- `timeout` (optional): Max operation time in ms (default: 30000)
|
|
497
|
-
- **Use
|
|
498
|
-
-
|
|
499
|
-
- **
|
|
497
|
+
- `skipNetworkWait` (optional): Skip waiting for network requests (default: false). **Use for pages with continuous long-polling to get instant response.**
|
|
498
|
+
- `networkWaitTimeout` (optional): Custom network wait timeout in ms (default: 10000). Only used if skipNetworkWait is false.
|
|
499
|
+
- **Use case**: Buttons, links, form submissions, Django admin forms
|
|
500
|
+
- **Returns**: Confirmation text + optional screenshot + network diagnostics
|
|
501
|
+
- **Performance**: 2-10x faster without screenshot, instant with skipNetworkWait
|
|
500
502
|
- **Example**:
|
|
501
503
|
```javascript
|
|
502
504
|
// PREFERRED: Using APOM ID
|
|
@@ -504,6 +506,12 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
|
|
|
504
506
|
|
|
505
507
|
// Alternative: Using CSS selector
|
|
506
508
|
click({ selector: "button[type='submit']" })
|
|
509
|
+
|
|
510
|
+
// Django forms with WebSockets (prevents timeout)
|
|
511
|
+
click({ selector: ".submit-row input[type='submit']", skipNetworkWait: true })
|
|
512
|
+
|
|
513
|
+
// Custom network timeout for slow APIs
|
|
514
|
+
click({ id: "save_btn", networkWaitTimeout: 10000 })
|
|
507
515
|
```
|
|
508
516
|
|
|
509
517
|
#### type
|
|
@@ -513,9 +521,10 @@ Type text into input fields with optional clearing and typing delay. **PREFERRED
|
|
|
513
521
|
- `selector` (optional): CSS selector. Use when APOM ID is not available.
|
|
514
522
|
- ⚠️ Either `id` OR `selector` required (mutually exclusive)
|
|
515
523
|
- `text` (required): Text to type
|
|
516
|
-
- `delay` (optional): Delay between keystrokes in ms
|
|
524
|
+
- `delay` (optional): Delay between keystrokes in ms (default: 30)
|
|
517
525
|
- `clearFirst` (optional): Clear field first (default: true)
|
|
518
|
-
-
|
|
526
|
+
- `timeout` (optional): Max operation time in ms (default: 30000). **Prevents infinite hangs on Django forms.**
|
|
527
|
+
- **Use case**: Filling forms, search boxes, text inputs, Django admin forms
|
|
519
528
|
- **Returns**: Confirmation text
|
|
520
529
|
- **Example**:
|
|
521
530
|
```javascript
|
package/angular-tools.js
CHANGED
|
@@ -29,10 +29,16 @@ export async function listAngularComponents(page, includeHidden = false) {
|
|
|
29
29
|
|
|
30
30
|
const component = ng.getComponent(el);
|
|
31
31
|
if (component && component.constructor && component.constructor.name !== 'Object') {
|
|
32
|
-
// Get selector
|
|
32
|
+
// Get selector (filter out Tailwind classes with special characters)
|
|
33
33
|
const tagName = el.tagName.toLowerCase();
|
|
34
|
-
const id = el.id ? `#${el.id}` : '';
|
|
35
|
-
const
|
|
34
|
+
const id = el.id ? `#${CSS.escape(el.id)}` : '';
|
|
35
|
+
const stableClasses = el.className
|
|
36
|
+
? el.className.split(' ')
|
|
37
|
+
.filter(c => c && !/[:\/\[\]]/.test(c))
|
|
38
|
+
.slice(0, 3)
|
|
39
|
+
.map(c => CSS.escape(c))
|
|
40
|
+
: [];
|
|
41
|
+
const classes = stableClasses.length > 0 ? `.${stableClasses.join('.')}` : '';
|
|
36
42
|
const selector = id || `${tagName}${classes}` || tagName;
|
|
37
43
|
|
|
38
44
|
// Get methods (public only by default)
|
package/bridge/bridge-client.js
CHANGED
|
@@ -12,8 +12,17 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import WebSocket from 'ws';
|
|
15
|
+
import { appendFileSync } from 'fs';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { join } from 'path';
|
|
15
18
|
|
|
16
19
|
const BRIDGE_PORT = 9223;
|
|
20
|
+
const LOG_FILE = join(homedir(), 'chrometools-bridge.log');
|
|
21
|
+
|
|
22
|
+
function logToFile(message) {
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
appendFileSync(LOG_FILE, `${timestamp} ${message}\n`);
|
|
25
|
+
}
|
|
17
26
|
const RECONNECT_DELAY = 2000;
|
|
18
27
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
19
28
|
|
|
@@ -64,19 +73,24 @@ export function setActiveTabSyncHandler(handler) {
|
|
|
64
73
|
*/
|
|
65
74
|
export async function connectToBridge() {
|
|
66
75
|
return new Promise((resolve) => {
|
|
76
|
+
logToFile(`connectToBridge called, isConnected=${isConnected}, wsState=${ws?.readyState}`);
|
|
77
|
+
|
|
67
78
|
if (isConnected && ws?.readyState === WebSocket.OPEN) {
|
|
68
79
|
debugLog('Already connected to Bridge');
|
|
80
|
+
logToFile('Already connected, skipping');
|
|
69
81
|
resolve(true);
|
|
70
82
|
return;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
debugLog(`Connecting to Bridge on port ${BRIDGE_PORT}...`);
|
|
86
|
+
logToFile(`Attempting connection to ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
74
87
|
|
|
75
88
|
try {
|
|
76
89
|
ws = new WebSocket(`ws://127.0.0.1:${BRIDGE_PORT}`);
|
|
77
90
|
|
|
78
91
|
const connectTimeout = setTimeout(() => {
|
|
79
|
-
|
|
92
|
+
console.error('[chrometools-mcp] Bridge connection timeout (5s)');
|
|
93
|
+
logToFile('TIMEOUT: Connection timeout after 5s');
|
|
80
94
|
ws?.close();
|
|
81
95
|
resolve(false);
|
|
82
96
|
}, 5000);
|
|
@@ -86,7 +100,8 @@ export async function connectToBridge() {
|
|
|
86
100
|
isConnected = true;
|
|
87
101
|
reconnectAttempts = 0;
|
|
88
102
|
debugLog('Connected to Bridge');
|
|
89
|
-
console.error('[chrometools-mcp] Connected to Bridge Service');
|
|
103
|
+
console.error('[chrometools-mcp] Connected to Bridge Service (isConnected=true)');
|
|
104
|
+
logToFile('SUCCESS: WebSocket connected, isConnected=true');
|
|
90
105
|
resolve(true);
|
|
91
106
|
});
|
|
92
107
|
|
|
@@ -99,8 +114,9 @@ export async function connectToBridge() {
|
|
|
99
114
|
}
|
|
100
115
|
});
|
|
101
116
|
|
|
102
|
-
ws.on('close', () => {
|
|
103
|
-
|
|
117
|
+
ws.on('close', (code, reason) => {
|
|
118
|
+
console.error(`[chrometools-mcp] Bridge disconnected (code=${code}, reason=${reason || 'none'})`);
|
|
119
|
+
logToFile(`CLOSE: WebSocket closed, code=${code}, reason=${reason || 'none'}`);
|
|
104
120
|
isConnected = false;
|
|
105
121
|
ws = null;
|
|
106
122
|
scheduleReconnect();
|
|
@@ -108,12 +124,14 @@ export async function connectToBridge() {
|
|
|
108
124
|
|
|
109
125
|
ws.on('error', (error) => {
|
|
110
126
|
clearTimeout(connectTimeout);
|
|
111
|
-
|
|
127
|
+
console.error(`[chrometools-mcp] Bridge connection error: ${error.message}`);
|
|
128
|
+
logToFile(`ERROR: WebSocket error: ${error.message}`);
|
|
112
129
|
resolve(false);
|
|
113
130
|
});
|
|
114
131
|
|
|
115
132
|
} catch (error) {
|
|
116
|
-
|
|
133
|
+
console.error(`[chrometools-mcp] Failed to create WebSocket: ${error.message}`);
|
|
134
|
+
logToFile(`EXCEPTION: Failed to create WebSocket: ${error.message}`);
|
|
117
135
|
resolve(false);
|
|
118
136
|
}
|
|
119
137
|
});
|
|
@@ -125,7 +143,7 @@ export async function connectToBridge() {
|
|
|
125
143
|
function scheduleReconnect() {
|
|
126
144
|
if (reconnectTimer) return;
|
|
127
145
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
128
|
-
|
|
146
|
+
console.error(`[chrometools-mcp] Bridge: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached, giving up`);
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
131
149
|
|
|
@@ -360,6 +378,7 @@ export function isExtensionConnected() {
|
|
|
360
378
|
* Check if connected to Bridge
|
|
361
379
|
*/
|
|
362
380
|
export function isBridgeConnected() {
|
|
381
|
+
logToFile(`isBridgeConnected called, returning ${isConnected}`);
|
|
363
382
|
return isConnected;
|
|
364
383
|
}
|
|
365
384
|
|
|
@@ -470,3 +489,39 @@ export function getRecorderState() {
|
|
|
470
489
|
export function getRecordings() {
|
|
471
490
|
return bridgeState.recordings;
|
|
472
491
|
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get network requests from Bridge (tracked via Extension webRequest API)
|
|
495
|
+
* These persist across page navigations unlike CDP-based tracking
|
|
496
|
+
*/
|
|
497
|
+
export async function getNetworkRequestsFromBridge(options = {}) {
|
|
498
|
+
if (!isConnected) {
|
|
499
|
+
debugLog('Cannot get network requests: not connected to Bridge');
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const response = await sendCommand('get_network_requests', {}, options.timeout || 5000);
|
|
505
|
+
return response.payload?.requests || [];
|
|
506
|
+
} catch (error) {
|
|
507
|
+
debugLog('Failed to get network requests:', error.message);
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Clear network requests in Bridge
|
|
514
|
+
*/
|
|
515
|
+
export async function clearNetworkRequestsFromBridge() {
|
|
516
|
+
if (!isConnected) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
await sendCommand('clear_network_requests', {}, 5000);
|
|
522
|
+
return true;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
debugLog('Failed to clear network requests:', error.message);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
package/bridge/bridge-service.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - Launched by Chrome when Extension starts
|
|
9
9
|
* - Maintains state (tabs, recordings, events)
|
|
10
|
-
* - Accepts
|
|
10
|
+
* - Accepts up to 20 WebSocket clients (with dead connection cleanup)
|
|
11
11
|
* - Clients get full state on connect + real-time updates
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ const require = createRequire(import.meta.url);
|
|
|
24
24
|
// ============================================
|
|
25
25
|
|
|
26
26
|
const WS_PORT = 9223;
|
|
27
|
-
const MAX_CLIENTS = 8
|
|
27
|
+
const MAX_CLIENTS = 20; // Increased from 8 to handle multiple MCP restarts
|
|
28
28
|
const HOST_NAME = 'com.chrometools.bridge';
|
|
29
29
|
|
|
30
30
|
// ============================================
|
|
@@ -40,11 +40,13 @@ const state = {
|
|
|
40
40
|
metadata: null,
|
|
41
41
|
secrets: {}
|
|
42
42
|
},
|
|
43
|
+
networkRequests: new Map(), // requestId -> request info (from Extension webRequest API)
|
|
43
44
|
eventLog: [], // Ring buffer of recent events (max 1000)
|
|
44
45
|
extensionConnected: false
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const MAX_EVENT_LOG = 1000;
|
|
49
|
+
const MAX_NETWORK_REQUESTS = 500;
|
|
48
50
|
|
|
49
51
|
// ============================================
|
|
50
52
|
// WebSocket Server for Claude/MCP Clients
|
|
@@ -61,6 +63,14 @@ function startWebSocketServer() {
|
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
wss.on('connection', (ws) => {
|
|
66
|
+
// Clean up dead connections before checking limit
|
|
67
|
+
for (const client of clients) {
|
|
68
|
+
if (client.readyState !== 1) { // Not OPEN
|
|
69
|
+
clients.delete(client);
|
|
70
|
+
log(`Cleaned up dead client (readyState=${client.readyState})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
if (clients.size >= MAX_CLIENTS) {
|
|
65
75
|
log(`Max clients (${MAX_CLIENTS}) reached, rejecting connection`);
|
|
66
76
|
ws.close(1013, 'Max clients reached');
|
|
@@ -105,6 +115,7 @@ function getFullState() {
|
|
|
105
115
|
tabs: Array.from(state.tabs.values()),
|
|
106
116
|
recordings: state.recordings,
|
|
107
117
|
recorderState: state.recorderState,
|
|
118
|
+
networkRequests: Array.from(state.networkRequests.values()),
|
|
108
119
|
extensionConnected: state.extensionConnected,
|
|
109
120
|
timestamp: Date.now()
|
|
110
121
|
};
|
|
@@ -153,6 +164,25 @@ function handleClientMessage(ws, message) {
|
|
|
153
164
|
});
|
|
154
165
|
break;
|
|
155
166
|
|
|
167
|
+
case 'get_network_requests':
|
|
168
|
+
// Return network requests from state (collected via Extension webRequest API)
|
|
169
|
+
sendToClient(ws, {
|
|
170
|
+
type: 'network_requests',
|
|
171
|
+
payload: {
|
|
172
|
+
requests: Array.from(state.networkRequests.values())
|
|
173
|
+
},
|
|
174
|
+
requestId: message.requestId
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'clear_network_requests':
|
|
179
|
+
state.networkRequests.clear();
|
|
180
|
+
sendToClient(ws, {
|
|
181
|
+
type: 'network_requests_cleared',
|
|
182
|
+
requestId: message.requestId
|
|
183
|
+
});
|
|
184
|
+
break;
|
|
185
|
+
|
|
156
186
|
// Commands to forward to extension
|
|
157
187
|
case 'start_recording':
|
|
158
188
|
case 'stop_recording':
|
|
@@ -295,6 +325,54 @@ function handleExtensionMessage(message) {
|
|
|
295
325
|
broadcastToClients({ type: 'pong' });
|
|
296
326
|
break;
|
|
297
327
|
|
|
328
|
+
// Network request events (from Extension webRequest API)
|
|
329
|
+
case 'network_request_started':
|
|
330
|
+
state.networkRequests.set(message.payload.requestId, message.payload);
|
|
331
|
+
// Clean old requests
|
|
332
|
+
if (state.networkRequests.size > MAX_NETWORK_REQUESTS) {
|
|
333
|
+
const entries = Array.from(state.networkRequests.entries());
|
|
334
|
+
const removeCount = entries.length - MAX_NETWORK_REQUESTS;
|
|
335
|
+
for (let i = 0; i < removeCount; i++) {
|
|
336
|
+
state.networkRequests.delete(entries[i][0]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
broadcastToClients({ type: 'network_request_started', payload: message.payload });
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case 'network_request_completed':
|
|
343
|
+
if (state.networkRequests.has(message.payload.requestId)) {
|
|
344
|
+
const req = state.networkRequests.get(message.payload.requestId);
|
|
345
|
+
req.status = message.payload.status;
|
|
346
|
+
req.statusText = message.payload.statusText;
|
|
347
|
+
req.completedAt = Date.now();
|
|
348
|
+
}
|
|
349
|
+
broadcastToClients({ type: 'network_request_completed', payload: message.payload });
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case 'network_request_failed':
|
|
353
|
+
if (state.networkRequests.has(message.payload.requestId)) {
|
|
354
|
+
const req = state.networkRequests.get(message.payload.requestId);
|
|
355
|
+
req.status = 'failed';
|
|
356
|
+
req.error = message.payload.error;
|
|
357
|
+
req.completedAt = Date.now();
|
|
358
|
+
}
|
|
359
|
+
broadcastToClients({ type: 'network_request_failed', payload: message.payload });
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'network_requests':
|
|
363
|
+
// Response from extension with all network requests
|
|
364
|
+
broadcastToClients({
|
|
365
|
+
type: 'network_requests',
|
|
366
|
+
payload: message.payload,
|
|
367
|
+
requestId: message.requestId
|
|
368
|
+
});
|
|
369
|
+
break;
|
|
370
|
+
|
|
371
|
+
case 'network_requests_cleared':
|
|
372
|
+
state.networkRequests.clear();
|
|
373
|
+
broadcastToClients({ type: 'network_requests_cleared' });
|
|
374
|
+
break;
|
|
375
|
+
|
|
298
376
|
default:
|
|
299
377
|
// Forward unknown messages to clients
|
|
300
378
|
broadcastToClients(message);
|
package/browser/page-manager.js
CHANGED
|
@@ -7,6 +7,59 @@
|
|
|
7
7
|
import { getBrowser } from './browser-manager.js';
|
|
8
8
|
// Note: injectRecorder removed - now using Chrome Extension for recording
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Click listener tracking script - injected via evaluateOnNewDocument
|
|
12
|
+
* Monkey-patches addEventListener to track which elements have click listeners
|
|
13
|
+
* This enables detection of click handlers added by frameworks like Angular
|
|
14
|
+
*/
|
|
15
|
+
const CLICK_LISTENER_TRACKER_SCRIPT = `
|
|
16
|
+
(function() {
|
|
17
|
+
// Only inject once
|
|
18
|
+
if (window.__clickListenerTracker) return;
|
|
19
|
+
window.__clickListenerTracker = true;
|
|
20
|
+
|
|
21
|
+
// WeakMap to track elements with click listeners (prevents memory leaks)
|
|
22
|
+
const clickListeners = new WeakMap();
|
|
23
|
+
|
|
24
|
+
// Store original methods
|
|
25
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
|
26
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
|
27
|
+
|
|
28
|
+
// Patch addEventListener
|
|
29
|
+
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
|
30
|
+
if (type === 'click' && this instanceof Element) {
|
|
31
|
+
// Track this element has click listener
|
|
32
|
+
const count = clickListeners.get(this) || 0;
|
|
33
|
+
clickListeners.set(this, count + 1);
|
|
34
|
+
}
|
|
35
|
+
return originalAddEventListener.call(this, type, listener, options);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Patch removeEventListener for accuracy
|
|
39
|
+
EventTarget.prototype.removeEventListener = function(type, listener, options) {
|
|
40
|
+
if (type === 'click' && this instanceof Element) {
|
|
41
|
+
const count = clickListeners.get(this) || 0;
|
|
42
|
+
if (count > 0) {
|
|
43
|
+
clickListeners.set(this, count - 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return originalRemoveEventListener.call(this, type, listener, options);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Global function to check if element has click listener
|
|
50
|
+
window.__hasClickListener = function(element) {
|
|
51
|
+
if (!element) return false;
|
|
52
|
+
const count = clickListeners.get(element);
|
|
53
|
+
return count && count > 0;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Also expose for debugging
|
|
57
|
+
window.__getClickListenerCount = function(element) {
|
|
58
|
+
return clickListeners.get(element) || 0;
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
`;
|
|
62
|
+
|
|
10
63
|
/**
|
|
11
64
|
* Debug log helper (only logs to stderr when DEBUG=1)
|
|
12
65
|
* @param {...any} args - Arguments to log
|
|
@@ -159,6 +212,20 @@ export async function setupNetworkMonitoring(page) {
|
|
|
159
212
|
|
|
160
213
|
// Note: setupRecorderAutoReinjection removed - Chrome Extension handles recording now
|
|
161
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Inject click listener tracker into page
|
|
217
|
+
* Must be called BEFORE page.goto() to catch all listeners
|
|
218
|
+
* @param {Page} page - Puppeteer page instance
|
|
219
|
+
*/
|
|
220
|
+
async function injectClickListenerTracker(page) {
|
|
221
|
+
try {
|
|
222
|
+
await page.evaluateOnNewDocument(CLICK_LISTENER_TRACKER_SCRIPT);
|
|
223
|
+
debugLog('Click listener tracker injected');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
debugLog('Failed to inject click listener tracker:', error.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
162
229
|
/**
|
|
163
230
|
* Get or create page for URL
|
|
164
231
|
* @param {string} url - URL to navigate to
|
|
@@ -180,6 +247,9 @@ export async function getOrCreatePage(url) {
|
|
|
180
247
|
// Create new page
|
|
181
248
|
const page = await browser.newPage();
|
|
182
249
|
|
|
250
|
+
// Inject click listener tracker BEFORE navigation
|
|
251
|
+
await injectClickListenerTracker(page);
|
|
252
|
+
|
|
183
253
|
// Set up console log capture
|
|
184
254
|
const client = await page.target().createCDPSession();
|
|
185
255
|
await client.send('Runtime.enable');
|
|
@@ -277,6 +347,19 @@ export function setLastPage(page) {
|
|
|
277
347
|
* @returns {Promise<void>}
|
|
278
348
|
*/
|
|
279
349
|
export async function setupNewPage(page, source = 'manual') {
|
|
350
|
+
// Inject click listener tracker
|
|
351
|
+
// For new tabs, we need both:
|
|
352
|
+
// 1. evaluateOnNewDocument for future navigations
|
|
353
|
+
// 2. evaluate for already-loaded content (won't catch existing listeners, but will catch new ones)
|
|
354
|
+
try {
|
|
355
|
+
await page.evaluateOnNewDocument(CLICK_LISTENER_TRACKER_SCRIPT);
|
|
356
|
+
// Also inject immediately for current page (in case content already loaded)
|
|
357
|
+
await page.evaluate(CLICK_LISTENER_TRACKER_SCRIPT);
|
|
358
|
+
debugLog(`Click listener tracker injected for ${source} page`);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
debugLog('Failed to inject click listener tracker:', error.message);
|
|
361
|
+
}
|
|
362
|
+
|
|
280
363
|
// Set up console log capture
|
|
281
364
|
try {
|
|
282
365
|
const client = await page.target().createCDPSession();
|