chrometools-mcp 3.2.6 → 3.3.6
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 +212 -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 +31 -0
- package/extension/background.js +117 -0
- package/extension/content.js +3 -1
- package/extension/manifest.json +2 -1
- package/index.js +213 -45
- package/models/TextInputModel.js +56 -5
- package/models/index.js +20 -6
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +19 -7
- package/server/tool-schemas.js +3 -0
- package/utils/hints-generator.js +46 -4
- package/utils/post-click-diagnostics.js +376 -0
- /package/{publish_output.txt → nul} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,218 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.3.5] - 2026-01-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **type/click: Intermittent timeout fix** — Added timeouts to internal operations
|
|
9
|
+
- `resolveSelector` now has 5s timeout (was unbounded)
|
|
10
|
+
- `getElementInfo` now has 5s timeout (was unbounded)
|
|
11
|
+
- `TextInputModel.setValue` operations have individual 5s timeouts
|
|
12
|
+
- Prevents indefinite hanging on unstable CDP sessions
|
|
13
|
+
- Error messages now show which specific operation timed out
|
|
14
|
+
|
|
15
|
+
## [3.3.4] - 2026-01-30
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **click: Form submit detection for non-SPA apps** — Detect classic form submissions with page reload
|
|
19
|
+
- Tracks URL before and after click to detect page navigation
|
|
20
|
+
- Reports "Page navigation detected (form submit)" when URL changes
|
|
21
|
+
- Solves issue where POST requests were lost during page reload
|
|
22
|
+
- Works for Django, Rails, PHP and other server-rendered apps
|
|
23
|
+
- Example output: `🔄 Page navigation detected (form submit): /form → /success`
|
|
24
|
+
|
|
25
|
+
## [3.3.3] - 2026-01-30
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **click: Post-click diagnostics timeout** — Fixed 30s timeout caused by network wait logic
|
|
29
|
+
- Now waits only for mutation requests (POST/PUT/PATCH) started within 200ms after click
|
|
30
|
+
- Ignores GET requests completely (were causing unnecessary waits)
|
|
31
|
+
- Hard 10s timeout limit enforced (never hangs indefinitely)
|
|
32
|
+
- Always returns success even if requests still pending after 10s
|
|
33
|
+
- Shows pending requests status instead of reporting timeout
|
|
34
|
+
- Changed output from "Form submission" to "Mutation requests"
|
|
35
|
+
- Example output:
|
|
36
|
+
```
|
|
37
|
+
📡 Mutation requests detected (2 POST/PUT/PATCH):
|
|
38
|
+
1. ✓ POST /admin/tenant/.../change/ → 302 Found
|
|
39
|
+
2. ⏳ POST /api/slow-endpoint/ → pending
|
|
40
|
+
|
|
41
|
+
⏳ 1 request(s) still pending after 10000ms timeout
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Performance
|
|
45
|
+
- **click: Removed unnecessary delays** — Removed 100ms waits after scrollIntoView and retry clicks
|
|
46
|
+
- Click operations now complete instantly when no mutation requests detected
|
|
47
|
+
- Typical click time: <50ms (was 500-10000ms with old diagnostics)
|
|
48
|
+
|
|
49
|
+
## [3.3.2] - 2026-01-30
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- **click/type: Viewport scrolling** — Always scrolls to element before interaction
|
|
53
|
+
- Prevents 30s timeout when elements are outside viewport
|
|
54
|
+
- Moves scrollIntoView BEFORE first click/type attempt (was in fallback)
|
|
55
|
+
- Fixes Django admin forms hanging after multiple navigations
|
|
56
|
+
- Pattern: scroll → wait 100ms → interact
|
|
57
|
+
- Applied to both click and type tools
|
|
58
|
+
|
|
59
|
+
## [3.3.1] - 2026-01-30
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- **click: All network requests visibility** — Shows ALL requests started within 200ms after click
|
|
63
|
+
- AI agent now sees complete network activity picture after every click
|
|
64
|
+
- Shows: method, URL, and status code for each request
|
|
65
|
+
- Example output:
|
|
66
|
+
```
|
|
67
|
+
📡 Network requests started within 200ms after click (15 total):
|
|
68
|
+
1. ✓ GET /api/auth/ws_token/ → 200 OK
|
|
69
|
+
2. ✓ GET /static/css/app.css → 200 OK
|
|
70
|
+
3. ✓ POST /admin/tenant/.../change/ → 302 Found
|
|
71
|
+
```
|
|
72
|
+
- Helps AI understand what happened after click (form submission, navigation, polling, etc.)
|
|
73
|
+
- 200ms window captures both mutation requests and side-effect requests
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- **Enhanced diagnostics output** — Restructured to show all requests first, then mutation tracking
|
|
77
|
+
- Section 1: All requests in 200ms window (complete visibility)
|
|
78
|
+
- Section 2: Form submission status (POST/PATCH/PUT tracking)
|
|
79
|
+
- Clear icons: ✓ (success), ✗ (error), ⏳ (pending)
|
|
80
|
+
- Agent can immediately see if any requests occurred or not
|
|
81
|
+
|
|
82
|
+
## [3.3.0] - 2026-01-30
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
- **click: skipNetworkWait parameter** — Skip network wait for forms with long-polling/WebSockets
|
|
86
|
+
- New parameter: `skipNetworkWait: boolean` (default: false)
|
|
87
|
+
- Use case: Pages with continuous long-polling to get instant response
|
|
88
|
+
- Example: `click({ selector: 'button[type="submit"]', skipNetworkWait: true })`
|
|
89
|
+
- **click: networkWaitTimeout parameter** — Custom network wait timeout
|
|
90
|
+
- New parameter: `networkWaitTimeout: number` (default: 10000ms)
|
|
91
|
+
- Configurable per-click timeout for network requests
|
|
92
|
+
- Example: `click({ selector: '.save-btn', networkWaitTimeout: 5000 })`
|
|
93
|
+
- **type: timeout parameter** — Explicit timeout for type operations
|
|
94
|
+
- New parameter: `timeout: number` (default: 30000ms)
|
|
95
|
+
- Prevents infinite hangs on input fields
|
|
96
|
+
- Example: `type({ selector: 'input[name="url"]', text: 'value', timeout: 10000 })`
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
- **Smart form submission tracking** — Only tracks POST/PATCH/PUT requests in 100ms window
|
|
100
|
+
- Filters mutation requests (POST/PATCH/PUT) that started within 100ms after click
|
|
101
|
+
- Waits ONLY for these mutation requests (up to 10s)
|
|
102
|
+
- Ignores all other requests (GET, polling, analytics, etc.)
|
|
103
|
+
- Shows form submission status: `✓ POST /admin/tenant/.../change/ → 302 Found`
|
|
104
|
+
- Example: Django form with 50 polling requests → only 1 POST tracked
|
|
105
|
+
- **Type operation timeout protection** — Wrapped in Promise.race with configurable timeout
|
|
106
|
+
- Prevents 120s hangs on problematic input fields
|
|
107
|
+
- Returns clear error message: "Type operation timed out after Xms"
|
|
108
|
+
- Django forms: type now fails fast instead of hanging
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
- **Django form timeout issues** — Fixed 30s click timeout and 120s type timeout
|
|
112
|
+
- Root cause: Long-polling/WebSockets created noise in network tracking
|
|
113
|
+
- Solution: Track only POST/PATCH/PUT requests within 100ms detection window
|
|
114
|
+
- Type operations now have explicit timeout protection
|
|
115
|
+
- Example: Django Admin forms now show reliable form submission status
|
|
116
|
+
|
|
117
|
+
## [3.2.11] - 2026-01-30
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
- **Tailwind CSS selector escape** — Fixed `analyzePage` failing on pages with Tailwind CSS
|
|
121
|
+
- Tailwind classes with colons (e.g., `hover:text-gray-800`) broke `querySelectorAll`
|
|
122
|
+
- Added filtering for classes containing special characters (`:`, `/`, `[`, `]`)
|
|
123
|
+
- Applied `CSS.escape()` to class names when building CSS selectors
|
|
124
|
+
- Fixed in: APOM tree builder, Angular tools, hints generator, content script
|
|
125
|
+
- Example: Page with Tailwind → no more "not a valid selector" errors
|
|
126
|
+
|
|
127
|
+
## [3.2.10] - 2026-01-29
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
- **Network request deduplication** — Fixed duplicate pending requests in diagnostics
|
|
131
|
+
- Prevented same requestId from being added multiple times during redirects/retries
|
|
132
|
+
- Added deduplication check in Network.requestWillBeSent event handler
|
|
133
|
+
- Updates existing request instead of creating duplicate entry
|
|
134
|
+
- Example: example.com showed 4 pending (2 URLs × 2 duplicates) → now shows 2 pending (correct count)
|
|
135
|
+
- **Memory leak prevention** — Limited networkRequests array growth
|
|
136
|
+
- Keeps maximum 500 most recent network requests in memory
|
|
137
|
+
- Automatically removes oldest requests when limit exceeded
|
|
138
|
+
- Prevents unbounded memory growth during long browser sessions
|
|
139
|
+
- Example: After 100 page navigations, memory stays bounded
|
|
140
|
+
|
|
141
|
+
## [3.2.9] - 2026-01-29
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
- **navigateTo diagnostics** — Post-navigation diagnostics for navigateTo tool
|
|
145
|
+
- Detects chrome-error:// pages (unreachable servers, DNS failures)
|
|
146
|
+
- Waits 20s for slow page loads and network requests
|
|
147
|
+
- Reports JS console errors and network errors after navigation
|
|
148
|
+
- Shows pending requests if page loads slowly
|
|
149
|
+
- Same comprehensive diagnostics as click tool
|
|
150
|
+
- Example: Navigate to offline backend → instant error report instead of silent failure
|
|
151
|
+
- **openBrowser diagnostics** — Post-navigation diagnostics for openBrowser tool
|
|
152
|
+
- Same comprehensive diagnostics as navigateTo and click
|
|
153
|
+
- Critical for first action in session - shows errors immediately
|
|
154
|
+
- Detects chrome-error:// pages on initial load
|
|
155
|
+
- Reports network errors, console errors, pending requests
|
|
156
|
+
- Example: Open unreachable backend → instant error report with details
|
|
157
|
+
|
|
158
|
+
### Changed
|
|
159
|
+
- **Diagnostics naming** — Renamed for clarity and universal use
|
|
160
|
+
- "POST-CLICK DIAGNOSTICS" → "POST-ACTION DIAGNOSTICS"
|
|
161
|
+
- Function parameters: beforeClickTimestamp → beforeActionTimestamp
|
|
162
|
+
- Comments updated to reflect use in both click and navigate actions
|
|
163
|
+
- File remains post-click-diagnostics.js for backward compatibility
|
|
164
|
+
|
|
165
|
+
## [3.2.8] - 2026-01-29
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
- **Network wait timeout** — Increased from 5s to 20s for slow APIs
|
|
169
|
+
- Gives slow backend APIs time to complete before timeout
|
|
170
|
+
- AI gets complete success/error status instead of "pending unknown"
|
|
171
|
+
- Pending requests after 20s are reported with details (URL, method, runtime)
|
|
172
|
+
- Clear warning: "Status unknown - may complete successfully or fail"
|
|
173
|
+
|
|
174
|
+
### Fixed
|
|
175
|
+
- **Click timeout on network errors** — No more 30s timeout when backend unreachable
|
|
176
|
+
- Detects chrome-error:// pages (ERR_CONNECTION_REFUSED, DNS_PROBE_FINISHED_NXDOMAIN, etc.)
|
|
177
|
+
- Returns error details immediately after 500ms diagnostic wait
|
|
178
|
+
- Shows error code and suggestion: "Backend likely not running or unreachable"
|
|
179
|
+
- Reduces diagnosis from 3 API calls to 1
|
|
180
|
+
- Example: Form submits to localhost:8001 (not running) → instant error report instead of 30s timeout
|
|
181
|
+
- **Network request tracking** — Now tracks ALL requests triggered by click, not just pending at 500ms
|
|
182
|
+
- Filters requests by timestamp (only those started AFTER click)
|
|
183
|
+
- Catches slow-starting requests that begin after initial 500ms wait
|
|
184
|
+
- Shows accurate count: completed/pending/total requests
|
|
185
|
+
- Prevents false "No network requests triggered" when requests start late
|
|
186
|
+
- **Delayed error collection** — Errors from requests that complete during maxWait are now captured
|
|
187
|
+
- Added 100ms delay after network wait before collecting errors
|
|
188
|
+
- Catches errors from requests that finish right as timeout expires
|
|
189
|
+
- Network summary shows: "⚠️ Network: 2 OK, 1 failed" when errors present
|
|
190
|
+
- Ensures AI sees errors even if request completes at edge of timeout window
|
|
191
|
+
- **Pending request reporting** — AI now sees details about slow/hanging requests
|
|
192
|
+
- Lists pending requests with URL, method, and elapsed time
|
|
193
|
+
- Suggests backend performance check or network connectivity issues
|
|
194
|
+
- Example: "POST /api/slow - Running for: 20145ms"
|
|
195
|
+
|
|
196
|
+
## [3.2.7] - 2026-01-29
|
|
197
|
+
|
|
198
|
+
### Added
|
|
199
|
+
- **Post-click diagnostics** — Click tool now automatically detects and reports errors
|
|
200
|
+
- Waits 500ms after click to capture async events
|
|
201
|
+
- Detects pending network requests and waits for completion (up to 5s)
|
|
202
|
+
- Collects JavaScript console errors and network errors
|
|
203
|
+
- **Error limit**: Max 15 console errors + 15 network errors to prevent spam
|
|
204
|
+
- Shows omitted error count if limit exceeded
|
|
205
|
+
- Returns diagnostics in click response for immediate AI feedback
|
|
206
|
+
- Prevents AI from making blind follow-up requests when errors occur
|
|
207
|
+
- New module: `utils/post-click-diagnostics.js`
|
|
208
|
+
|
|
209
|
+
### Changed
|
|
210
|
+
- **Click behavior** — Enhanced UX for AI agents
|
|
211
|
+
- Click now includes network activity summary (requests completed, timing)
|
|
212
|
+
- Errors displayed immediately in click response
|
|
213
|
+
- AI can see what broke without additional tool calls
|
|
214
|
+
- Better error context: timestamp, location, status codes
|
|
215
|
+
- Smart error limiting prevents overwhelming AI with hundreds of errors
|
|
216
|
+
|
|
5
217
|
## [3.2.6] - 2026-01-28
|
|
6
218
|
|
|
7
219
|
### Removed
|
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
|
@@ -30,6 +30,21 @@ export const consoleLogs = [];
|
|
|
30
30
|
// Network requests storage
|
|
31
31
|
export const networkRequests = [];
|
|
32
32
|
|
|
33
|
+
// Maximum number of network requests to keep in memory (prevent unbounded growth)
|
|
34
|
+
const MAX_NETWORK_REQUESTS = 500;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clean old network requests to prevent memory leak
|
|
38
|
+
* Keeps only the most recent MAX_NETWORK_REQUESTS requests
|
|
39
|
+
*/
|
|
40
|
+
function cleanOldNetworkRequests() {
|
|
41
|
+
if (networkRequests.length > MAX_NETWORK_REQUESTS) {
|
|
42
|
+
// Remove oldest requests (keep most recent)
|
|
43
|
+
const removeCount = networkRequests.length - MAX_NETWORK_REQUESTS;
|
|
44
|
+
networkRequests.splice(0, removeCount);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
// Page analysis cache
|
|
34
49
|
export const pageAnalysisCache = new Map();
|
|
35
50
|
|
|
@@ -52,6 +67,19 @@ export async function setupNetworkMonitoring(page) {
|
|
|
52
67
|
|
|
53
68
|
client.on('Network.requestWillBeSent', (event) => {
|
|
54
69
|
const timestamp = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
// Check if request already exists (prevent duplicates from redirects/retries)
|
|
72
|
+
const existingReq = networkRequests.find(r => r.requestId === event.requestId);
|
|
73
|
+
if (existingReq) {
|
|
74
|
+
// Update existing request instead of creating duplicate
|
|
75
|
+
existingReq.url = event.request.url;
|
|
76
|
+
existingReq.method = event.request.method;
|
|
77
|
+
existingReq.headers = event.request.headers;
|
|
78
|
+
existingReq.postData = event.request.postData;
|
|
79
|
+
existingReq.timestamp = timestamp;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
networkRequests.push({
|
|
56
84
|
requestId: event.requestId,
|
|
57
85
|
url: event.request.url,
|
|
@@ -64,6 +92,9 @@ export async function setupNetworkMonitoring(page) {
|
|
|
64
92
|
status: 'pending',
|
|
65
93
|
documentURL: event.documentURL
|
|
66
94
|
});
|
|
95
|
+
|
|
96
|
+
// Clean old requests to prevent unbounded memory growth
|
|
97
|
+
cleanOldNetworkRequests();
|
|
67
98
|
});
|
|
68
99
|
|
|
69
100
|
client.on('Network.responseReceived', (event) => {
|