chrometools-mcp 3.5.5 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.6.0] - 2026-06-25
6
+
7
+ ### Added
8
+ - **iframe automation (`switchFrame` / `listFrames`)** — Automate pages whose UI lives in an `<iframe>`, including **cross-origin** ones (resolved over CDP, bypassing Same-Origin Policy). `switchFrame({ frameUrl | frameSelector })` sets an active frame so `click`, `type`, `hover`, `selectOption`, `pressKey`, `scrollTo`, `waitForElement`, `analyzePage`, `findElementsByText`, `smartFindElement`, `executeScript` all run inside it; call `switchFrame()` with no args to reset. `listFrames()` lists frames; `analyzePage` also reports a `frames` array when >1 frame exists. Active frame auto-resets on `navigateTo`
9
+ - **Browser connection env vars** — `CHROMETOOLS_BROWSER_WS_ENDPOINT` (connect directly to a CDP WebSocket), `CHROMETOOLS_DEBUG_PORT` (default 9222), `CHROMETOOLS_USER_DATA_DIR` (use a real/logged-in Chrome profile instead of a blank temp one), `CHROMETOOLS_CHROME_PATH`. Lets the server attach to your authenticated Chrome session — required for sites that need real cookies/cross-subdomain session
10
+ - **`smartFindElement({ minConfidence })`** — Confidence threshold (default 0.6) gating auto-`action`. If the best match is below it or too close to the runner-up, the action is skipped and candidates are returned with an `actionSkipped` reason. Candidate coverage broadened (`[onclick]`, `[role=menuitem]`, `[role=tab]`, nav links); scoring penalizes text that doesn't match the description and rewards nav/menu context
11
+ - **`click({ waitForRouteChange })`** — For SPAs: after click, waits for `location.pathname+search` to change and reports `routeChanged:true/false` (never fails the click on timeout)
12
+
13
+ ### Changed
14
+ - **`executeScript` top-level `return`** — Now runs the snippet as-is first and only re-wraps in an async IIFE if the engine reports an "Illegal return statement". Handles `const x=…; return x`, callbacks containing `function`, and bare expressions — replaces the fragile `^return` + no-`function` heuristic
15
+ - **`navigateTo` network summary** — Lists only XHR/Fetch requests (static assets hidden and counted), capped at 12 with a `… N more` note, instead of dumping every chunk/CSS/font
16
+
17
+ ### Fixed
18
+ - **`findElementsByText({ action })` in iframes** — The action path resolved the selector against the main frame even after `switchFrame`, causing `Element not found for action`. Now resolves inside the active frame
19
+
20
+ ## [3.5.6] - 2026-05-28
21
+
22
+ ### Added
23
+ - **`analyzePage({ includePortals, portalSelectors })`** — Generic React Portal scan beyond framework modals. Default selectors `['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]']`, opt-out via `includePortals: false`. Without this, action menus / tooltips / popovers rendered outside `#root` are invisible to APOM
24
+ - **In-tree popup detection** — `analyzePage` now also force-includes Popper/Tippy/FloatingUI-style popups: positioned (absolute/fixed) descendants inside a 0-height inline wrapper. Same opt-out flag (`includePortals`). Covers custom contextMenu implementations that don't use real React Portals
25
+ - **`click({ waitForSelector, waitTimeoutMs })`** — Atomic click + wait. After click, waits for a CSS selector to appear (visible). On timeout the click still succeeds but the result text reports `⚠️ WAIT_TIMEOUT`. Designed for dropdowns/popups that race against the next MCP call
26
+ - **`click({ autoAnalyzeAfter })`** — After click, diffs APOM state and appends `+N appeared: id:"text"` / `-N disappeared` delta. New element ids are pre-registered for follow-up `click`/`type` calls — opens a dropdown and clicks one of its items in two MCP calls instead of three
27
+ - **`screenshot()` viewport mode** — Both `id` and `selector` are now optional; without either, captures the viewport (same compression pipeline as element screenshots)
28
+ - **`executeScript` auto-IIFE** — Snippets starting with `return ...` are now auto-wrapped in `(async () => { ... })()`. Skipped when the snippet declares a `function`, to preserve implicit-return behavior
29
+
30
+ ### Fixed
31
+ - **`ModelRegistry is not defined` after navigation** — Root cause: `quickRegisterElements` (called from `resolveSelector` auto-refresh) didn't inject models code, so `buildAPOMTree` inside it failed with `ReferenceError` when the browser context had been wiped. Models are now always re-injected. If the error still surfaces, it's mapped to a clear `APOM registry stale, call analyzePage()` message instead of leaking the raw ReferenceError
32
+
5
33
  ## [3.5.5] - 2026-03-27
6
34
 
7
35
  ### Added
package/README.md CHANGED
@@ -219,13 +219,14 @@ The Chrome Extension is **required** for scenario recording and other advanced f
219
219
  - [Installation](#installation)
220
220
  - [Chrome Extension Setup](#chrome-extension-setup)
221
221
  - [AI Optimization Features](#ai-optimization-features)- [Scenario Recorder](#scenario-recorder) - Visual UI-based recording with smart optimization
222
- - [Available Tools](#available-tools) - **49+ Tools Total**
222
+ - [Available Tools](#available-tools) - **51+ Tools Total**
223
223
  - [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails, findElementsByText
224
224
  - [Core Tools](#1-core-tools) - ping, openBrowser
225
225
  - [Interaction Tools](#2-interaction-tools) - click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
226
226
  - [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
227
227
  - [Advanced Tools](#4-advanced-tools) - executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, pressKey, setStyles, setViewport, getViewport, navigateTo
228
228
  - [Tab Management Tools](#5-tab-management-tools) - listTabs, switchTab
229
+ - [Frame Tools](#5a-frame-tools-iframe-automation) - listFrames, switchFrame (cross-origin iframe automation)
229
230
  - [Recorder Tools](#7-recorder-tools) - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
230
231
  - [API / Swagger Tools](#8-api--swagger-tools) - loadSwagger, generateApiModels
231
232
  - [Typical Workflow Example](#typical-workflow-example)
@@ -357,6 +358,8 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
357
358
  - **Parameters**:
358
359
  - `description` (required): Natural language (e.g., "login button", "email field")
359
360
  - `maxResults` (optional): Max candidates to return (default: 5)
361
+ - `minConfidence` (optional): Confidence threshold (0–1, default **0.6**) for auto-executing `action`. If the best match scores below this — or is too close to the runner-up — the action is **skipped** and candidates are returned with an `actionSkipped` reason instead. Prevents auto-clicking the wrong control (e.g. a primary form submit when you asked for a menu item). Lower it to act on weaker matches.
362
+ - **Candidate coverage**: besides `button`/`input`/`a`/`[role=button]`, also considers `[onclick]`, `[role=menuitem]`, `[role=tab]`, and links inside `nav`/`[role=navigation]`/`[role=menu]` — so menu items rendered as `div`/`span[onclick]` are found. Scoring penalizes candidates whose text doesn't match the description and rewards navigation/menu context.
360
363
  - **Use case**: When you don't know the exact selector
361
364
  - **Returns**: Ranked candidates with confidence scores, selectors, and reasoning
362
365
  - **Example**:
@@ -403,7 +406,10 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
403
406
  - `refresh` (optional): Force refresh cache to get CURRENT state after changes (default: false)
404
407
  - `includeAll` (optional): Include ALL page elements, not just interactive ones (default: false). Useful for layout work - find any element, get its selector, then use `getComputedCss` or `setStyles` on it.
405
408
  - `useLegacyFormat` (optional): Return legacy format instead of APOM (default: false - APOM is the default)
406
- - `registerElements` (optional): Auto-register elements for ID-based usage (default: true) - `groupBy` (optional): 'type' or 'flat' - how to group elements (default: 'type') - **Why better than screenshot**:
409
+ - `registerElements` (optional): Auto-register elements for ID-based usage (default: true) - `groupBy` (optional): 'type' or 'flat' - how to group elements (default: 'type')
410
+ - `includePortals` (optional): Include contents of React Portal containers — menus, tooltips, popovers rendered outside the main React root (default: `true`). Without this, items inside dropdown popups (e.g. action menus in MTS-like apps) are invisible to `analyzePage`.
411
+ - `portalSelectors` (optional): Array of CSS selectors for portal root containers. Default: `['#modal-root', '#menu-popup-root', '#tooltip-root', '#popover-root', '[data-portal]']`. Override when the app uses different portal element ids.
412
+ - **In-tree popup heuristic**: when `includePortals` is enabled (default), `analyzePage` also detects "in-tree portal" patterns — popups rendered inside a 0-height inline wrapper and absolute-positioned out of it (Popper, Tippy, FloatingUI, custom contextMenu implementations). Without this, popup items live inside an `offsetHeight: 0` wrapper that `isVisible` drops, making the whole popup subtree invisible to `analyzePage`. - **Why better than screenshot**:
407
413
  - Shows actual data (form values, validation errors) not just visual
408
414
  - Uses 2-5k tokens vs screenshot 5-10k tokens
409
415
  - Returns structured data with **unique element IDs** for easy interaction
@@ -514,6 +520,10 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
514
520
  - `timeout` (optional): Max operation time in ms (default: 30000)
515
521
  - `skipNetworkWait` (optional): Skip waiting for network requests (default: false). **Use for pages with continuous long-polling to get instant response.**
516
522
  - `networkWaitTimeout` (optional): Custom network wait timeout in ms (default: 10000). Only used if skipNetworkWait is false.
523
+ - `waitForSelector` (optional): CSS selector to wait for **after** the click — atomic click+wait. Use for dropdowns/popups that render into a React Portal and otherwise race with the next MCP call. Example: `click({ id: 'button_47', waitForSelector: '#menu-popup-root > div' })`.
524
+ - `waitTimeoutMs` (optional): Timeout for `waitForSelector` in ms (default: 2000). On timeout the click still succeeds but the result text reports `⚠️ WAIT_TIMEOUT`.
525
+ - `waitForRouteChange` (optional): For SPAs (React Router etc.). After the click, waits for `location.pathname + location.search` to change relative to before, then reports `Route changed: "/a" → "/b"` or `⚠️ ROUTE_UNCHANGED`. Because SPAs navigate via `history.pushState`, plain network diagnostics may not register the navigation — this makes "success" mean the view actually changed, not just that the click was delivered. Never fails the click on timeout. For view changes that don't alter the URL, use `waitForSelector` instead.
526
+ - `autoAnalyzeAfter` (optional): After click, automatically diff APOM and append the delta to the result text (e.g. `+3 appeared: button_42:"Статистика", button_43:"Настройки", link_44:"Удалить"`). New element ids are pre-registered so the next `click({ id })`/`type({ id })` call works **without an extra `analyzePage`**. Designed for the dropdown/menu pattern: one MCP call instead of three.
517
527
  - **Use case**: Buttons, links, form submissions, Django admin forms
518
528
  - **Returns**: Confirmation text + optional screenshot + network diagnostics
519
529
  - **Performance**: 2-10x faster without screenshot, instant with skipNetworkWait
@@ -676,10 +686,12 @@ Get precise dimensions, positioning, margins, padding, and borders.
676
686
  - **Returns**: Box model data + metrics
677
687
 
678
688
  #### screenshot
679
- Capture optimized screenshot of specific element with smart compression and automatic 3 MB limit.
689
+ Capture optimized screenshot of a specific element, or the full viewport when no `id`/`selector` is given. Smart compression with a 3 MB hard limit.
680
690
  - **Parameters**:
681
- - `selector` (required)
682
- - `padding` (optional): Padding in pixels (default: 0)
691
+ - `id` (optional): APOM element ID from `analyzePage`. Mutually exclusive with `selector`.
692
+ - `selector` (optional): CSS selector. Mutually exclusive with `id`.
693
+ - Omit both `id` and `selector` to capture the **full viewport** (no element resolution needed).
694
+ - `padding` (optional): Padding in pixels (default: 0). Ignored for viewport screenshots.
683
695
  - `maxWidth` (optional): Max width for auto-scaling (default: 1024, null for original size)
684
696
  - `maxHeight` (optional): Max height for auto-scaling (default: 8000, null for original size)
685
697
  - `quality` (optional): JPEG quality 1-100 (default: 40)
@@ -717,6 +729,8 @@ Execute arbitrary JavaScript in page context with optional screenshot.
717
729
  - **Use case**: Complex interactions, custom manipulations
718
730
  - **Returns**: Execution result + optional screenshot
719
731
  - **Performance**: 2-10x faster without screenshot
732
+ - **Top-level `return`**: any snippet using a top-level `return` (e.g. `return document.title`, `const x = 1; return x`, or code that contains a `function` in a callback) just works — it's run as-is first, and only re-wrapped in an async IIFE if the engine reports an "Illegal return statement". Bare expressions like `document.title` still return their value. No manual `(() => { ... })()` wrapping needed.
733
+ - **Frames**: runs inside the active frame after `switchFrame` (see [Frame Tools](#5a-frame-tools-iframe-automation)); defaults to the main frame.
720
734
 
721
735
  #### getConsoleLogs
722
736
  Retrieve browser console logs (log, warn, error, etc.).
@@ -839,7 +853,8 @@ Navigate to different URL while keeping browser instance.
839
853
  - `url` (required)
840
854
  - `waitUntil` (optional): load event type
841
855
  - **Use case**: Moving between pages in workflow
842
- - **Returns**: New page title
856
+ - **Returns**: New page title. The post-navigation network summary lists only **XHR/Fetch** requests (static assets — JS chunks, CSS, fonts, images — are hidden and counted) and is capped at the first 12 with a `… N more` note, so a heavy SPA load doesn't bury the signal.
857
+ - **Note**: resets the active frame back to the main frame (see [Frame Tools](#5a-frame-tools-iframe-automation)).
843
858
 
844
859
  ### 5. Tab Management Tools
845
860
  Tools for managing multiple browser tabs. New tabs opened via `window.open()`, `target="_blank"`, or user actions are automatically detected and tracked.
@@ -882,6 +897,45 @@ switchTab({ tab: 0 })
882
897
  switchTab({ tab: "google.com" })
883
898
  ```
884
899
 
900
+ ### 5a. Frame Tools (iframe automation)
901
+
902
+ Tools for automating pages whose UI lives inside an `<iframe>` — including **cross-origin** iframes (e.g. a widget hosted on a different subdomain). Cross-origin frames can't be reached from page JavaScript (Same-Origin Policy blocks `iframe.contentDocument`), but ChromeTools resolves them over CDP, so the SOP doesn't apply.
903
+
904
+ By default all tools operate on the **main frame**. After `switchFrame`, the element tools — `click`, `type`, `hover`, `selectOption`, `pressKey`, `scrollTo`, `waitForElement`, `analyzePage`, `findElementsByText`, `smartFindElement`, `executeScript` — run **inside the selected frame** until you reset. The active frame is reset automatically on `navigateTo`.
905
+
906
+ #### listFrames
907
+ List all frames on the current page so you can discover which one to switch into.
908
+ - **Parameters**: None
909
+ - **Returns**: `{ count, activeFrame, frames: [{ url, name, isMain }], hint }`
910
+ - **Use case**: Find a cross-origin iframe (e.g. `app.example.com`) before switching into it
911
+
912
+ #### switchFrame
913
+ Set the active frame for subsequent element tools. Call with **no arguments** to reset back to the main frame.
914
+ - **Parameters** (provide one, or none to reset):
915
+ - `frameUrl` (optional): substring matched against each frame's URL (e.g. `"app.example.com"`). Selects the first match.
916
+ - `frameSelector` (optional): CSS selector of the `<iframe>` element; its content frame becomes active.
917
+ - **Returns**: `{ active: <frame url>, matcher, frames }` (or `{ active: null }` on reset)
918
+ - **Use case**: Enter a cross-origin iframe to click/fill a form rendered there
919
+
920
+ ```javascript
921
+ // Discover frames
922
+ listFrames()
923
+ // → { count: 2, frames: [ {url:".../app", isMain:true}, {url:"https://app.example.com/...", isMain:false} ] }
924
+
925
+ // Enter the cross-origin iframe
926
+ switchFrame({ frameUrl: "app.example.com" })
927
+
928
+ // Now element tools target the iframe
929
+ analyzePage() // returns the iframe's APOM tree
930
+ findElementsByText({ text: "Save" }) // searches inside the iframe
931
+ click({ id: "button_3" }) // clicks inside the iframe
932
+
933
+ // Back to the main document
934
+ switchFrame()
935
+ ```
936
+
937
+ > `analyzePage` also includes a `frames` array in its output whenever the page has more than one frame, so the agent can discover iframes without a separate `listFrames` call.
938
+
885
939
  ### 6. Figma Tools
886
940
  Design-to-code validation, file browsing, design system extraction, and comparison tools with automatic 3 MB compression.
887
941
 
@@ -1605,14 +1659,14 @@ Each tool definition is sent to the AI in every request, consuming context token
1605
1659
  | Group | Description | Tools (count) |
1606
1660
  |-------|-------------|---------------|
1607
1661
  | `core` | Basic tools | `ping`, `openBrowser` (2) |
1608
- | `interaction` | User interaction | `click`, `type`, `scrollTo`, `waitForElement`, `hover` (5) |
1662
+ | `interaction` | User interaction & frames | `click`, `type`, `scrollTo`, `waitForElement`, `hover`, `selectOption`, `selectFromGroup`, `drag`, `scrollHorizontal`, `switchFrame`, `listFrames` (11) |
1609
1663
  | `inspection` | Page inspection | `getComputedCss`, `getBoxModel`, `screenshot`, `saveScreenshot` (4) |
1610
1664
  | `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
1611
1665
  | `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `findElementsByText` (8) |
1612
1666
  | `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
1613
1667
  | `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette`, `convertFigmaToCode` (10) |
1614
1668
 
1615
- **Total:** 42 tools across 7 groups
1669
+ **Total:** 48 tools across 7 groups
1616
1670
 
1617
1671
  **Configuration:**
1618
1672
 
@@ -1737,6 +1791,48 @@ To use Figma tools, you need to configure your Figma Personal Access Token.
1737
1791
 
1738
1792
  ---
1739
1793
 
1794
+ ## Browser Connection (use your real, logged-in Chrome)
1795
+
1796
+ By default ChromeTools connects to a Chrome with remote debugging on port **9222**, and if none is found it launches a fresh Chrome with a **temporary profile** (no cookies, no logins). To automate a site that needs your **authenticated session** (and to reach cross-origin iframes that depend on it), point ChromeTools at your own Chrome via environment variables — all optional:
1797
+
1798
+ | Variable | Default | Purpose |
1799
+ |----------|---------|---------|
1800
+ | `CHROMETOOLS_BROWSER_WS_ENDPOINT` | _(unset)_ | Direct CDP WebSocket URL (e.g. `ws://127.0.0.1:9222/devtools/browser/<id>`). When set, ChromeTools connects straight to it, skipping port discovery and launch. |
1801
+ | `CHROMETOOLS_DEBUG_PORT` | `9222` | Remote-debugging port to discover/connect to (and to launch with). |
1802
+ | `CHROMETOOLS_USER_DATA_DIR` | `<temp>/chrome-mcp-profile` | Chrome profile used when launching a new instance. Point at a real (or cloned) profile to reuse its cookies/logins. |
1803
+ | `CHROMETOOLS_CHROME_PATH` | platform default | Path to the Chrome executable. |
1804
+
1805
+ **Quick path — attach to your already-logged-in Chrome:**
1806
+
1807
+ 1. Fully close Chrome (a profile can't open a debug port while another Chrome holds it).
1808
+ 2. Launch it with remote debugging on your normal profile:
1809
+ ```bash
1810
+ # Windows
1811
+ chrome.exe --remote-debugging-port=9222
1812
+ # macOS
1813
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
1814
+ ```
1815
+ 3. Log into the target site in that Chrome. ChromeTools will `connect()` to it — cross-origin iframes load authenticated, and `listTabs` sees your real tabs.
1816
+
1817
+ ```json
1818
+ {
1819
+ "mcpServers": {
1820
+ "chrometools": {
1821
+ "type": "stdio",
1822
+ "command": "npx",
1823
+ "args": ["chrometools-mcp"],
1824
+ "env": {
1825
+ "CHROMETOOLS_USER_DATA_DIR": "C:/Users/you/chrome-mcp-real-profile"
1826
+ }
1827
+ }
1828
+ }
1829
+ }
1830
+ ```
1831
+
1832
+ Without any of these variables, behavior is unchanged.
1833
+
1834
+ ---
1835
+
1740
1836
  ## WSL Setup Guide
1741
1837
 
1742
1838
  If you're using **Windows Subsystem for Linux (WSL)**, special configuration is required to display Chrome GUI windows.
@@ -9,7 +9,7 @@ import { spawn } from 'child_process';
9
9
  import http from 'http';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
- import { getChromePath, getTempDir, isWSL, CHROME_DEBUG_PORT } from '../utils/platform-utils.js';
12
+ import { getChromePath, getUserDataDir, isWSL, CHROME_DEBUG_PORT } from '../utils/platform-utils.js';
13
13
  import { handleNewTab, openPages, lastPage } from './page-manager.js';
14
14
 
15
15
  // Get extension path (use forward slashes for Chrome on Windows)
@@ -97,6 +97,30 @@ export async function getBrowser() {
97
97
  let browser;
98
98
  let endpoint;
99
99
 
100
+ // Highest priority: explicit CDP WebSocket endpoint (e.g. a real
101
+ // logged-in Chrome the user already launched). Skips port-discovery
102
+ // and spawning entirely — connect straight to the live session.
103
+ const wsEndpoint = process.env.CHROMETOOLS_BROWSER_WS_ENDPOINT;
104
+ if (wsEndpoint) {
105
+ browser = await puppeteer.connect({
106
+ browserWSEndpoint: wsEndpoint,
107
+ defaultViewport: null,
108
+ });
109
+ debugLog("Connected via CHROMETOOLS_BROWSER_WS_ENDPOINT:", wsEndpoint);
110
+ connectedToExistingChrome = true;
111
+ console.error("[chrometools-mcp] Connected to Chrome via CHROMETOOLS_BROWSER_WS_ENDPOINT.");
112
+
113
+ browser.on('disconnected', () => {
114
+ debugLog("Browser disconnected");
115
+ browserPromise = null;
116
+ connectedToExistingChrome = false;
117
+ });
118
+
119
+ setupNewTabTracking(browser);
120
+ scheduleBridgeReconnect();
121
+ return browser;
122
+ }
123
+
100
124
  // Try to connect to existing Chrome with remote debugging
101
125
  try {
102
126
  endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 2);
@@ -132,7 +156,7 @@ export async function getBrowser() {
132
156
 
133
157
  // Launch new Chrome with remote debugging enabled
134
158
  const chromePath = getChromePath();
135
- const userDataDir = `${getTempDir()}/chrome-mcp-profile`;
159
+ const userDataDir = getUserDataDir();
136
160
 
137
161
  debugLog("Chrome path:", chromePath);
138
162
  debugLog("User data dir:", userDataDir);
@@ -346,6 +346,95 @@ export function setLastPage(page) {
346
346
  lastPage = page;
347
347
  }
348
348
 
349
+ // ─────────────────────────────────────────────────────────────────────────────
350
+ // Active frame state (P0-1: cross-origin iframe support)
351
+ //
352
+ // Tools default to the main frame. switchFrame() stores a *matcher* (not a Frame
353
+ // object — Frames get invalidated when the iframe re-navigates) keyed by Page.
354
+ // getTargetFrame() re-resolves the matcher against page.frames() on every call,
355
+ // so it survives iframe reloads. Cleared on top-level navigation.
356
+ // ─────────────────────────────────────────────────────────────────────────────
357
+ const activeFrames = new WeakMap(); // Page -> { frameUrl?, frameSelector? }
358
+
359
+ /**
360
+ * Set (or clear) the active frame matcher for a page.
361
+ * @param {Page} page
362
+ * @param {{frameUrl?: string, frameSelector?: string}|null} matcher - null resets to main frame
363
+ */
364
+ export function setActiveFrame(page, matcher) {
365
+ if (!matcher || (!matcher.frameUrl && !matcher.frameSelector)) {
366
+ activeFrames.delete(page);
367
+ } else {
368
+ activeFrames.set(page, matcher);
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Clear the active frame for a page (back to main frame).
374
+ * @param {Page} page
375
+ */
376
+ export function clearActiveFrame(page) {
377
+ activeFrames.delete(page);
378
+ }
379
+
380
+ /**
381
+ * Get the matcher currently stored for a page (or null).
382
+ * @param {Page} page
383
+ * @returns {{frameUrl?: string, frameSelector?: string}|null}
384
+ */
385
+ export function getActiveFrameMatcher(page) {
386
+ return activeFrames.get(page) || null;
387
+ }
388
+
389
+ /**
390
+ * Resolve the execution context (Frame) for a page based on its active matcher.
391
+ * Returns the main frame when no active frame is set. A Frame exposes the same
392
+ * surface tools rely on (.evaluate, .$, .$$, .click, .type, .waitForSelector),
393
+ * so handlers can use the returned context in place of `page` for DOM work.
394
+ * @param {Page} page
395
+ * @returns {Promise<Frame>} - Target Puppeteer Frame
396
+ */
397
+ export async function getTargetFrame(page) {
398
+ const matcher = activeFrames.get(page);
399
+ if (!matcher) {
400
+ return page.mainFrame();
401
+ }
402
+
403
+ if (matcher.frameSelector) {
404
+ const handle = await page.$(matcher.frameSelector);
405
+ const frame = handle ? await handle.contentFrame() : null;
406
+ if (!frame) {
407
+ throw new Error(
408
+ `Active frame (selector "${matcher.frameSelector}") not found. Call listFrames() and switchFrame() again, or switchFrame() with no args to reset.`
409
+ );
410
+ }
411
+ return frame;
412
+ }
413
+
414
+ // frameUrl: substring match against all frames
415
+ const frame = page.frames().find(f => f.url().includes(matcher.frameUrl));
416
+ if (!frame) {
417
+ throw new Error(
418
+ `Active frame (url contains "${matcher.frameUrl}") not found. Call listFrames() and switchFrame() again, or switchFrame() with no args to reset.`
419
+ );
420
+ }
421
+ return frame;
422
+ }
423
+
424
+ /**
425
+ * List all frames on a page with metadata (for listFrames tool / analyzePage output).
426
+ * @param {Page} page
427
+ * @returns {Array<{url: string, name: string, isMain: boolean}>}
428
+ */
429
+ export function listFramesForPage(page) {
430
+ const main = page.mainFrame();
431
+ return page.frames().map(f => ({
432
+ url: f.url(),
433
+ name: f.name() || '',
434
+ isMain: f === main,
435
+ }));
436
+ }
437
+
349
438
  /**
350
439
  * Setup a new page with all monitoring (console, network, recorder)
351
440
  * Used for both explicitly created pages and auto-detected new tabs
@@ -177,6 +177,12 @@ function analyzeButtonContextInPage(element) {
177
177
 
178
178
  // Primary button indicators
179
179
  isPrimary: /primary|main|btn-primary|button-primary/i.test(element.className || ''),
180
+
181
+ // Navigation/menu context (P1-3): menu items and nav links must be able to
182
+ // out-score a form's primary submit button when the user asked for navigation.
183
+ hasMenuRole: element.getAttribute('role') === 'menuitem' || element.getAttribute('role') === 'tab',
184
+ hasOnClick: element.hasAttribute('onclick'),
185
+ isNavContext: element.closest('nav, [role="navigation"], [role="menu"], [role="menubar"], [role="tablist"]') !== null,
180
186
  };
181
187
 
182
188
  // Check if it's the last button in form
@@ -217,6 +223,10 @@ function scoreSubmitButton(element, context, description) {
217
223
  if (context.hasSubmitIcon) score += 5; // submit icon
218
224
  if (context.isPrimary) score += 15; // primary button style
219
225
 
226
+ // Navigation/menu context bonus (P1-3)
227
+ if (context.hasMenuRole) score += 15; // role=menuitem/tab
228
+ if (context.isNavContext) score += 10; // inside nav/menu container
229
+
220
230
  // Visibility bonus
221
231
  if (context.isVisible) score += 10;
222
232
 
@@ -235,6 +245,22 @@ function scoreSubmitButton(element, context, description) {
235
245
  score -= 20;
236
246
  }
237
247
 
248
+ // Text-mismatch penalty (P1-3): when the description carries actual words but
249
+ // the element's text neither contains nor is contained by it AND it doesn't hit
250
+ // a submit keyword, it's almost certainly the wrong control (e.g. asked
251
+ // "Настройки", element says "Пополнить"). Without this, a primary form submit
252
+ // wins on structural points alone and gets auto-clicked.
253
+ if (descLower.trim().length > 0 && text.length > 0) {
254
+ const textMatchesDesc = text.includes(descLower) || descLower.includes(text);
255
+ if (!textMatchesDesc && !matchesSubmitKeyword(context.text, description)) {
256
+ // Strong enough to overcome a full house of structural submit points
257
+ // (type=submit + in-form + last + primary + visible ≈ +115), so a button
258
+ // whose text is unrelated to the request can't be auto-clicked. Submit
259
+ // keywords (Save/Войти/…) are exempt above, so real submits aren't harmed.
260
+ score -= 60;
261
+ }
262
+ }
263
+
238
264
  return score;
239
265
  }
240
266
 
@@ -572,6 +598,18 @@ function explainScore(element, context, description, score) {
572
598
  if (context.isLink && !matchesSubmitKeyword(context.text, description)) {
573
599
  reasons.push('link without submit keyword (-20)');
574
600
  }
601
+ if (context.hasMenuRole) reasons.push('menu/tab role (+15)');
602
+ if (context.isNavContext) reasons.push('nav/menu context (+10)');
603
+ {
604
+ const t = (context.text || '').toLowerCase();
605
+ const d = description.toLowerCase();
606
+ if (d.trim().length > 0 && t.length > 0) {
607
+ const m = t.includes(d) || d.includes(t);
608
+ if (!m && !matchesSubmitKeyword(context.text, description)) {
609
+ reasons.push('text ≠ description (-60)');
610
+ }
611
+ }
612
+ }
575
613
 
576
614
  // Common reasons
577
615
  if (context.inForm) reasons.push('in form (+20)');