mcpbrowser 0.2.35 → 0.2.37

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 cherchyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -20,7 +20,7 @@ Or search "MCPBrowser" in VS Code Extensions view.
20
20
  **From GitHub Release:**
21
21
  Download from [GitHub Releases](https://github.com/cherchyk/MCPBrowser/releases):
22
22
  ```bash
23
- code --install-extension mcpbrowser-0.2.32.vsix
23
+ code --install-extension mcpbrowser-0.2.37.vsix
24
24
  ```
25
25
 
26
26
  The extension automatically:
@@ -31,7 +31,7 @@ The extension automatically:
31
31
  📦 [View on Marketplace](https://marketplace.visualstudio.com/items?itemName=cherchyk.mcpbrowser)
32
32
 
33
33
  ### Option 2: npm Package (Recommended for Manual Setup)
34
- Published on npm as [mcpbrowser](https://www.npmjs.com/package/mcpbrowser) v0.2.32.
34
+ Published on npm as [mcpbrowser](https://www.npmjs.com/package/mcpbrowser) v0.2.37.
35
35
 
36
36
  Add to your `mcp.json`:
37
37
  ```jsonc
@@ -48,7 +48,7 @@ Add to your `mcp.json`:
48
48
  - Mac/Linux: `~/.config/Code/User/mcp.json`
49
49
 
50
50
  ### Option 3: MCP Registry
51
- Available in the [MCP Registry](https://registry.modelcontextprotocol.io/) as `io.github.cherchyk/browser` v0.2.32.
51
+ Available in the [MCP Registry](https://registry.modelcontextprotocol.io/) as `io.github.cherchyk/browser` v0.2.37.
52
52
 
53
53
  Search for "browser" in the registry to find configuration instructions.
54
54
 
@@ -132,7 +132,7 @@ Restart VS Code or reload the window for the changes to take effect.
132
132
  In Claude Code or Copilot Chat, you should see the `MCPBrowser` server listed. Ask it to fetch an authenticated URL and it will drive your signed-in Chrome session.
133
133
 
134
134
  ## How it works
135
- - Tool `fetch_webpage_protected` (inside the MCP server) drives your live Chrome/Edge (DevTools Protocol) so it inherits your auth cookies, returning `html` (truncated up to 2M chars) for analysis.
135
+ - Tool `fetch_webpage` (inside the MCP server) drives your live Chrome/Edge (DevTools Protocol) so it inherits your auth cookies, returning `html` (truncated up to 2M chars) for analysis.
136
136
  - **Smart confirmation**: AI assistant asks for confirmation ONLY on first request to a new domain - explains browser will open for authentication. Subsequent requests to same domain work automatically (session preserved).
137
137
  - **Domain-aware tab reuse**: Automatically reuses the same tab for URLs on the same domain, preserving authentication session. Different domains open new tabs.
138
138
  - **Automatic page loading**: Waits for network idle (`networkidle0`) by default, ensuring JavaScript-heavy pages (SPAs, dashboards) fully load before returning content.
@@ -141,6 +141,151 @@ In Claude Code or Copilot Chat, you should see the `MCPBrowser` server listed. A
141
141
  - **Smart timeouts**: 60s default for page fetch, 10 min for auth redirects. Tabs stay open indefinitely for reuse (no auto-close).
142
142
  - The AI assistant's LLM invokes this tool via MCP; this repo itself does not run an LLM.
143
143
 
144
+ ## 🎯 Interactive Features (NEW!)
145
+
146
+ MCPBrowser now supports **human-like interaction** with web pages! You can click buttons, fill forms, and interact with dynamic content just like a human would.
147
+
148
+ ### Available Tools
149
+
150
+ #### 1. `click_element` - Click on page elements
151
+ Click on any element - buttons, links, divs with onclick handlers, or any clickable element.
152
+
153
+ **Parameters:**
154
+ - `url` (required): URL of the page (must be already loaded via `fetch_webpage`)
155
+ - `selector` (optional): CSS selector for the element (e.g., `#submit-btn`, `.login-button`)
156
+ - `text` (optional): Text content to search for if selector not provided (e.g., "Sign In", "Submit")
157
+ - `timeout` (optional): Maximum wait time in milliseconds (default: 30000)
158
+
159
+ **Example:**
160
+ ```javascript
161
+ // Click by selector
162
+ { url: "https://example.com", selector: "#login-button" }
163
+
164
+ // Click by text content
165
+ { url: "https://example.com", text: "Sign In" }
166
+ ```
167
+
168
+ #### 2. `type_text` - Type text into input fields
169
+ Type text into input fields, textareas, or any editable element with human-like typing simulation.
170
+
171
+ **Parameters:**
172
+ - `url` (required): URL of the page
173
+ - `selector` (required): CSS selector for the input element
174
+ - `text` (required): Text to type
175
+ - `clear` (optional): Clear existing text first (default: true)
176
+ - `delay` (optional): Delay between keystrokes in ms (default: 50)
177
+ - `timeout` (optional): Maximum wait time in milliseconds (default: 30000)
178
+
179
+ **Example:**
180
+ ```javascript
181
+ {
182
+ url: "https://example.com",
183
+ selector: "#username",
184
+ text: "myuser@example.com"
185
+ }
186
+ ```
187
+
188
+ #### 3. `get_interactive_elements` - List all interactive elements
189
+ Discover all clickable and interactive elements on the page - links, buttons, inputs, elements with onclick handlers.
190
+
191
+ **Parameters:**
192
+ - `url` (required): URL of the page
193
+ - `limit` (optional): Maximum number of elements to return (default: 50)
194
+
195
+ **Returns:** Array of elements with details (tag, text, selector, href, type, id, hasOnClick, role)
196
+
197
+ **Example:**
198
+ ```javascript
199
+ { url: "https://example.com", limit: 20 }
200
+ ```
201
+
202
+ #### 4. `wait_for_element` - Wait for element to appear
203
+ Wait for an element to appear on the page (useful after clicking something that triggers dynamic content).
204
+
205
+ **Parameters:**
206
+ - `url` (required): URL of the page
207
+ - `selector` (optional): CSS selector to wait for
208
+ - `text` (optional): Text content to wait for if selector not provided
209
+ - `timeout` (optional): Maximum wait time in milliseconds (default: 30000)
210
+
211
+ **Example:**
212
+ ```javascript
213
+ { url: "https://example.com", selector: ".success-message" }
214
+ ```
215
+
216
+ #### 5. `get_current_html` - Get updated HTML without reloading (NEW! 🚀)
217
+ **Efficiently** get the current HTML state from an already-loaded page **without navigation or reloading**. Use this after interactions (`click_element`, `type_text`, `wait_for_element`) to get the updated DOM state.
218
+
219
+ **Why use this instead of `fetch_webpage`?**
220
+ - ⚡ **Much faster** - no page reload, just extracts current DOM
221
+ - 🎯 **More accurate** - captures exact state after interaction
222
+ - 💾 **Preserves state** - doesn't lose dynamic JavaScript state
223
+ - 🔄 **Efficient** - perfect for interactive workflows
224
+
225
+ **Parameters:**
226
+ - `url` (required): URL of the page (must be already loaded via `fetch_webpage`)
227
+ - `removeUnnecessaryHTML` (optional): Clean HTML for size reduction (default: true, ~90% smaller)
228
+
229
+ **Example:**
230
+ ```javascript
231
+ { url: "https://example.com", removeUnnecessaryHTML: true }
232
+ ```
233
+
234
+ **Performance comparison:**
235
+ ```
236
+ fetch_webpage after interaction: 2-5 seconds (reloads page)
237
+ get_current_html after interaction: 0.1-0.3 seconds (just extracts HTML) ✅
238
+ ```
239
+
240
+ ### Usage Workflow
241
+
242
+ **Efficient interactive workflow (NEW!):**
243
+
244
+ 1. **First, fetch the page:**
245
+ ```javascript
246
+ fetch_webpage({ url: "https://example.com/login" })
247
+ ```
248
+
249
+ 2. **Discover interactive elements:**
250
+ ```javascript
251
+ get_interactive_elements({ url: "https://example.com/login" })
252
+ ```
253
+
254
+ 3. **Fill in the form:**
255
+ ```javascript
256
+ type_text({ url: "https://example.com/login", selector: "#username", text: "user@example.com" })
257
+ type_text({ url: "https://example.com/login", selector: "#password", text: "mypassword" })
258
+ ```
259
+
260
+ 4. **Click the submit button:**
261
+ ```javascript
262
+ click_element({ url: "https://example.com/login", selector: "#submit" })
263
+ // or by text: click_element({ url: "https://example.com/login", text: "Sign In" })
264
+ ```
265
+
266
+ 5. **Wait for content to load:**
267
+ ```javascript
268
+ wait_for_element({ url: "https://example.com/dashboard", selector: ".dashboard-content" })
269
+ ```
270
+
271
+ 6. **Get updated HTML efficiently (no reload!):**
272
+ ```javascript
273
+ get_current_html({ url: "https://example.com/dashboard" })
274
+ // ✅ Fast! Just extracts current DOM without reloading the page
275
+ ```
276
+
277
+ **When to use `get_current_html` vs `fetch_webpage`:**
278
+ - Use `fetch_webpage`: Initial page load, navigation to new URLs, auth flows
279
+ - Use `get_current_html`: After clicks, form fills, waits - when page is already loaded ✅
280
+
281
+ ### Key Features
282
+ - ✅ Works with **any clickable element** - not just `<a>` tags (buttons, divs with onclick, etc.)
283
+ - ✅ **Text-based selection** - click elements by their visible text
284
+ - ✅ **Human-like typing** - simulates natural keystroke delays
285
+ - ✅ **Automatic scrolling** - scrolls elements into view before interaction
286
+ - ✅ **Smart element detection** - finds the most specific match when searching by text
287
+ - ✅ **Session preservation** - all interactions happen in the same browser tab
288
+
144
289
  ## Auth-assisted fetch flow
145
290
  - AI assistant can call with just the URL, or with no params if you set an env default (`DEFAULT_FETCH_URL` or `MCP_DEFAULT_FETCH_URL`). By default tabs stay open indefinitely for reuse (domain-aware).
146
291
  - First call opens the tab and leaves it open so you can sign in. No extra params needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbrowser",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "mcpName": "io.github.cherchyk/mcpbrowser",
5
5
  "type": "module",
6
6
  "description": "MCP server for in-browser web page fetching using Chrome DevTools Protocol",
@@ -10,7 +10,9 @@
10
10
  },
11
11
  "scripts": {
12
12
  "mcp": "node src/mcp-browser.js",
13
- "test": "node --test tests/*.test.js"
13
+ "test": "node tests/run-all.js",
14
+ "test:unit": "node tests/run-unit.js",
15
+ "test:ci": "node tests/run-unit.js"
14
16
  },
15
17
  "keywords": [
16
18
  "mcp",
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/cherchyk/MCPBrowser",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.35",
9
+ "version": "0.2.37",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
@@ -0,0 +1,176 @@
1
+ /**
2
+ * click.js - Click element action
3
+ *
4
+ * This function handles two distinct use cases:
5
+ *
6
+ * 1. NAVIGATION/CONTENT UPDATES (returnHtml: true, default):
7
+ * - Clicks the element
8
+ * - Waits for page stability (network idle, DOM updates)
9
+ * - Returns the updated HTML content
10
+ * - Use for: Links, navigation buttons, SPA route changes (e.g., Gmail folders)
11
+ * - Takes 3-8 seconds due to stability wait
12
+ *
13
+ * 2. FAST FORM INTERACTIONS (returnHtml: false):
14
+ * - Clicks the element
15
+ * - Minimal 300ms wait
16
+ * - Returns success without HTML
17
+ * - Use for: Checkboxes, radio buttons, form fields that don't navigate
18
+ * - Takes <1 second
19
+ *
20
+ * Why this design?
21
+ * - Solves SPA navigation issue: URL hash changes instantly (#inbox → #trash),
22
+ * but content loads asynchronously. Without waiting, we'd return old content.
23
+ * - Consistent with fetch_webpage: Both wait for stability and return HTML
24
+ * - Flexible: Can disable waiting for fast form interactions
25
+ */
26
+
27
+ import { getBrowser, domainPages } from '../core/browser.js';
28
+ import { extractAndProcessHtml, waitForPageStability } from '../core/page.js';
29
+
30
+ /**
31
+ * Click on an element on the page
32
+ *
33
+ * @param {Object} params - Click parameters
34
+ * @param {string} params.url - The URL of the page to interact with
35
+ * @param {string} [params.selector] - CSS selector for the element to click
36
+ * @param {string} [params.text] - Text content to search for (alternative to selector)
37
+ * @param {number} [params.waitForElementTimeout=30000] - Maximum time (ms) to wait for element to appear before failing
38
+ * @param {boolean} [params.returnHtml=true] - Whether to wait for stability and return HTML
39
+ * @param {boolean} [params.removeUnnecessaryHTML=true] - Whether to clean HTML (only if returnHtml is true)
40
+ * @param {number} [params.postClickWait=1000] - Milliseconds to wait after click for SPAs to render dynamic content
41
+ * @returns {Promise<Object>} Result object with success status and details
42
+ *
43
+ * @example
44
+ * // Navigate to Gmail Bin folder (waits for emails to load, returns HTML)
45
+ * const result = await clickElement({ url: gmailUrl, text: "Bin" });
46
+ * console.log(result.html); // Contains bin emails
47
+ *
48
+ * @example
49
+ * // Fast checkbox click (no wait, no HTML)
50
+ * const result = await clickElement({
51
+ * url: formUrl,
52
+ * selector: "#agree-checkbox",
53
+ * returnHtml: false
54
+ * });
55
+ */
56
+ export async function clickElement({ url, selector, text, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postClickWait = 1000 }) {
57
+ if (!url) {
58
+ throw new Error("url parameter is required");
59
+ }
60
+
61
+ if (!selector && !text) {
62
+ throw new Error("Either selector or text parameter is required");
63
+ }
64
+
65
+ let hostname;
66
+ try {
67
+ hostname = new URL(url).hostname;
68
+ } catch {
69
+ throw new Error(`Invalid URL: ${url}`);
70
+ }
71
+
72
+ const browser = await getBrowser();
73
+ let page = domainPages.get(hostname);
74
+
75
+ if (!page || page.isClosed()) {
76
+ return {
77
+ success: false,
78
+ error: `No open page found for ${hostname}. Please fetch the page first using fetch_webpage.`
79
+ };
80
+ }
81
+
82
+ try {
83
+ let elementHandle;
84
+
85
+ if (selector) {
86
+ // Use CSS selector
87
+ await page.waitForSelector(selector, { timeout: waitForElementTimeout, visible: true });
88
+ elementHandle = await page.$(selector);
89
+ } else {
90
+ // Search by text content
91
+ await page.waitForFunction(
92
+ (searchText) => {
93
+ const elements = Array.from(document.querySelectorAll('*'));
94
+ return elements.some(el => {
95
+ const text = el.textContent?.trim();
96
+ return text && text.includes(searchText) && el.offsetParent !== null;
97
+ });
98
+ },
99
+ { timeout: waitForElementTimeout },
100
+ text
101
+ );
102
+
103
+ elementHandle = await page.evaluateHandle((searchText) => {
104
+ const elements = Array.from(document.querySelectorAll('*'));
105
+ // Prioritize smaller elements (more specific matches)
106
+ const matches = elements.filter(el => {
107
+ const elText = el.textContent?.trim();
108
+ return elText && elText.includes(searchText) && el.offsetParent !== null;
109
+ });
110
+ matches.sort((a, b) => a.textContent.length - b.textContent.length);
111
+ return matches[0];
112
+ }, text);
113
+ }
114
+
115
+ if (!elementHandle || !elementHandle.asElement()) {
116
+ return {
117
+ success: false,
118
+ error: selector ? `Element not found: ${selector}` : `Element with text "${text}" not found`
119
+ };
120
+ }
121
+
122
+ // Scroll element into view and click
123
+ // For automation, use instant scroll instead of smooth animation to avoid delays
124
+ await page.evaluate(el => el.scrollIntoView({ behavior: 'auto', block: 'center' }), elementHandle);
125
+ // original:
126
+ // Smooth scroll (commented out for performance):
127
+ // await page.evaluate(el => el.scrollIntoView({ behavior: 'smooth', block: 'center' }), elementHandle);
128
+ // await new Promise(r => setTimeout(r, 300)); // Brief delay after scroll
129
+
130
+ await elementHandle.click();
131
+
132
+ if (returnHtml) {
133
+ // Wait for page to stabilize (handles both navigation and SPA content updates)
134
+ // This ensures content is fully loaded before returning, just like fetch_webpage does
135
+ await waitForPageStability(page);
136
+
137
+ // Wait for SPAs to render dynamic content after click
138
+ if (postClickWait > 0) {
139
+ await new Promise(resolve => setTimeout(resolve, postClickWait));
140
+ }
141
+
142
+ const currentUrl = page.url();
143
+ const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
144
+
145
+ return {
146
+ success: true,
147
+ message: selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
148
+ currentUrl,
149
+ html,
150
+ clicked: selector || `text:"${text}"`
151
+ };
152
+ } else {
153
+ // Wait for page to stabilize even for fast clicks (ensures JS has finished)
154
+ await waitForPageStability(page);
155
+
156
+ // Wait for SPAs to render dynamic content after click
157
+ if (postClickWait > 0) {
158
+ await new Promise(resolve => setTimeout(resolve, postClickWait));
159
+ }
160
+
161
+ const currentUrl = page.url();
162
+
163
+ return {
164
+ success: true,
165
+ message: selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
166
+ currentUrl,
167
+ clicked: selector || `text:"${text}"`
168
+ };
169
+ }
170
+ } catch (err) {
171
+ return {
172
+ success: false,
173
+ error: `Failed to click element: ${err.message}`
174
+ };
175
+ }
176
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Close a tab for a specific domain
3
+ */
4
+
5
+ import { domainPages } from '../core/browser.js';
6
+
7
+ /**
8
+ * Closes the browser tab for the given URL's hostname and removes it from the tab pool.
9
+ * This forces a fresh session on the next visit to that hostname.
10
+ * @param {object} params - Parameters
11
+ * @param {string} params.url - The URL whose hostname tab should be closed
12
+ * @returns {Promise<object>} Result indicating success or failure
13
+ */
14
+ export async function closeTab({ url }) {
15
+ try {
16
+ // Validate URL
17
+ if (!url || typeof url !== 'string') {
18
+ return {
19
+ success: false,
20
+ error: 'Invalid or missing URL parameter'
21
+ };
22
+ }
23
+
24
+ // Extract hostname from URL
25
+ let hostname;
26
+ try {
27
+ hostname = new URL(url).hostname;
28
+ } catch {
29
+ return {
30
+ success: false,
31
+ error: 'Invalid URL format'
32
+ };
33
+ }
34
+
35
+ // Check if we have a tab for this hostname
36
+ if (!domainPages.has(hostname)) {
37
+ // Hostname not found - try to find by actual page URL
38
+ // This handles redirects where tab was created with original hostname
39
+ // but now the page URL is different
40
+ let foundHostname = null;
41
+ for (const [key, page] of domainPages.entries()) {
42
+ try {
43
+ if (!page.isClosed() && page.url() === url) {
44
+ foundHostname = key;
45
+ break;
46
+ }
47
+ } catch {
48
+ // Skip pages we can't access
49
+ }
50
+ }
51
+
52
+ if (!foundHostname) {
53
+ return {
54
+ success: true,
55
+ hostname,
56
+ message: 'No open tab found for this hostname',
57
+ alreadyClosed: true
58
+ };
59
+ }
60
+
61
+ // Found the page by URL - use that hostname
62
+ hostname = foundHostname;
63
+ }
64
+
65
+ // Get and close the page
66
+ const page = domainPages.get(hostname);
67
+
68
+ // Check if page is already closed
69
+ if (page.isClosed()) {
70
+ domainPages.delete(hostname);
71
+ return {
72
+ success: true,
73
+ hostname,
74
+ message: 'Tab was already closed',
75
+ alreadyClosed: true
76
+ };
77
+ }
78
+
79
+ // Close the page
80
+ await page.close();
81
+
82
+ // Remove from domain pool
83
+ domainPages.delete(hostname);
84
+
85
+ console.error(`[MCPBrowser] Closed tab for hostname: ${hostname}`);
86
+
87
+ return {
88
+ success: true,
89
+ hostname,
90
+ message: `Successfully closed tab for ${hostname}`
91
+ };
92
+
93
+ } catch (error) {
94
+ console.error(`[MCPBrowser] Error closing tab:`, error);
95
+ return {
96
+ success: false,
97
+ error: error.message
98
+ };
99
+ }
100
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * fetch.js - Main page fetching functionality
3
+ * Handles web page fetching with authentication flows and tab reuse
4
+ */
5
+
6
+ import { getBrowser, domainPages } from '../core/browser.js';
7
+ import { getOrCreatePage, navigateToUrl, extractAndProcessHtml, waitForPageStability } from '../core/page.js';
8
+ import { detectRedirectType, waitForAutoAuth, waitForManualAuth } from '../core/auth.js';
9
+
10
+ /**
11
+ * Fetch a web page using Chrome browser, with support for authentication flows and tab reuse.
12
+ * Reuses existing tabs per domain when possible. Handles authentication redirects by waiting
13
+ * for user to complete login (up to 10 minutes). Processes HTML to remove unnecessary elements
14
+ * and convert relative URLs to absolute.
15
+ * @param {Object} params - Fetch parameters
16
+ * @param {string} params.url - The URL to fetch
17
+ * @param {boolean} [params.removeUnnecessaryHTML=true] - Whether to clean HTML (removes scripts, styles, etc.)
18
+ * @param {number} [params.postLoadWait=1000] - Milliseconds to wait after page load for SPAs to render
19
+ * @returns {Promise<Object>} Result object with success status, URL, HTML content, or error details
20
+ */
21
+ export async function fetchPage({ url, removeUnnecessaryHTML = true, postLoadWait = 1000 }) {
22
+ // Hardcoded smart defaults - use 'domcontentloaded' for fastest loading
23
+ // (waits for HTML parsed, not all resources loaded - much faster for SPAs)
24
+ const waitUntil = "domcontentloaded";
25
+ const navigationTimeout = 30000;
26
+ const authCompletionTimeout = 600000;
27
+ const reuseLastKeptPage = true;
28
+
29
+ if (!url) {
30
+ throw new Error("url parameter is required");
31
+ }
32
+
33
+ // Parse hostname for domain-based tab reuse
34
+ let hostname;
35
+ try {
36
+ hostname = new URL(url).hostname;
37
+ } catch {
38
+ throw new Error(`Invalid URL: ${url}`);
39
+ }
40
+
41
+ const browser = await getBrowser();
42
+ let page = null;
43
+
44
+ try {
45
+ // Get or create page for this domain
46
+ page = await getOrCreatePage(browser, hostname, reuseLastKeptPage);
47
+
48
+ // Navigate to URL with fallback strategy
49
+ await navigateToUrl(page, url, waitUntil, navigationTimeout);
50
+
51
+ const currentUrl = page.url();
52
+ const currentHostname = new URL(currentUrl).hostname;
53
+ console.error(`[MCPBrowser] Navigation completed: ${currentUrl}`);
54
+
55
+ // Detect redirect type and handle accordingly
56
+ const redirectInfo = detectRedirectType(url, hostname, currentUrl, currentHostname);
57
+
58
+ if (redirectInfo.type === 'requested_auth') {
59
+ console.error(`[MCPBrowser] User requested auth page directly, returning content`);
60
+ // Update domain mapping if needed
61
+ if (redirectInfo.currentHostname !== hostname) {
62
+ domainPages.delete(hostname);
63
+ domainPages.set(redirectInfo.currentHostname, page);
64
+ hostname = redirectInfo.currentHostname;
65
+ }
66
+ } else if (redirectInfo.type === 'permanent') {
67
+ console.error(`[MCPBrowser] Permanent redirect detected: ${hostname} → ${redirectInfo.currentHostname}`);
68
+ console.error(`[MCPBrowser] Accepting redirect and updating domain mapping`);
69
+ domainPages.delete(hostname);
70
+ domainPages.set(redirectInfo.currentHostname, page);
71
+ hostname = redirectInfo.currentHostname;
72
+ } else if (redirectInfo.type === 'auth') {
73
+ console.error(`[MCPBrowser] Authentication flow detected (${redirectInfo.flowType})`);
74
+ console.error(`[MCPBrowser] Current location: ${redirectInfo.currentUrl}`);
75
+
76
+ // Try auto-auth first
77
+ const autoAuthResult = await waitForAutoAuth(page, redirectInfo.hostname, redirectInfo.originalBase);
78
+
79
+ if (autoAuthResult.success) {
80
+ // Update hostname if changed
81
+ if (autoAuthResult.hostname !== hostname) {
82
+ domainPages.delete(hostname);
83
+ domainPages.set(autoAuthResult.hostname, page);
84
+ hostname = autoAuthResult.hostname;
85
+ }
86
+ } else {
87
+ // Wait for manual auth
88
+ const manualAuthResult = await waitForManualAuth(page, redirectInfo.hostname, redirectInfo.originalBase, authCompletionTimeout);
89
+
90
+ if (!manualAuthResult.success) {
91
+ return {
92
+ success: false,
93
+ error: manualAuthResult.error,
94
+ pageKeptOpen: true,
95
+ hint: manualAuthResult.hint
96
+ };
97
+ }
98
+
99
+ // Update hostname if changed
100
+ if (manualAuthResult.hostname !== hostname) {
101
+ domainPages.delete(hostname);
102
+ domainPages.set(manualAuthResult.hostname, page);
103
+ hostname = manualAuthResult.hostname;
104
+ }
105
+ }
106
+
107
+ // Wait for page stability after auth
108
+ await waitForPageStability(page);
109
+ }
110
+
111
+ // Wait for SPAs to render dynamic content after page load
112
+ if (postLoadWait > 0) {
113
+ await new Promise(resolve => setTimeout(resolve, postLoadWait));
114
+ }
115
+
116
+ // Extract and process HTML
117
+ const processedHtml = await extractAndProcessHtml(page, removeUnnecessaryHTML);
118
+
119
+ return {
120
+ success: true,
121
+ currentUrl: page.url(),
122
+ html: processedHtml
123
+ };
124
+ } catch (err) {
125
+ const hint = "Tab is left open. Complete sign-in there, then call fetch_webpage again with just the URL.";
126
+ return { success: false, error: err.message || String(err), pageKeptOpen: true, hint };
127
+ } finally {
128
+ // Tab always stays open - domain-aware reuse handles cleanup
129
+ }
130
+ }