mcpbrowser 0.3.28 → 0.3.30
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/package.json +1 -1
- package/src/actions/click-element.js +21 -45
- package/src/actions/fetch-page.js +63 -101
- package/src/actions/type-text.js +20 -44
- package/src/browsers/ChromiumBrowser.js +59 -2
- package/src/browsers/brave.js +11 -1
- package/src/browsers/chrome.js +11 -1
- package/src/browsers/edge.js +11 -1
- package/src/core/auth.js +115 -208
- package/src/core/html.js +3 -3
- package/src/core/logger.js +57 -21
- package/src/core/page.js +230 -77
- package/src/mcp-browser.js +12 -8
- package/src/utils.js +59 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpbrowser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.30",
|
|
4
4
|
"mcpName": "io.github.cherchyk/mcpbrowser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP browser server - fetch web pages using real Chrome/Edge/Brave browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { getBrowser, getValidatedPage } from '../core/browser.js';
|
|
28
|
-
import { extractAndProcessHtml,
|
|
28
|
+
import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
|
|
29
29
|
import { MCPResponse, InformationalResponse } from '../core/responses.js';
|
|
30
30
|
import logger from '../core/logger.js';
|
|
31
31
|
|
|
@@ -262,60 +262,36 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
262
262
|
logger.debug(`Clicking: ${selector || `text="${text}"`}`);
|
|
263
263
|
await elementHandle.click();
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logger.info(`click_element completed: ${selector || `text="${text}"`}`);
|
|
280
|
-
|
|
281
|
-
return new ClickElementSuccessResponse(
|
|
282
|
-
currentUrl,
|
|
283
|
-
selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
|
|
284
|
-
html,
|
|
285
|
-
[
|
|
265
|
+
// Wait for page to stabilize (handles both navigation and SPA content updates)
|
|
266
|
+
logger.debug(`Waiting for page to be ready${returnHtml ? '' : ' (fast mode)'}...`);
|
|
267
|
+
await waitForPageReady(page, { afterInteraction: true });
|
|
268
|
+
|
|
269
|
+
// Wait for SPAs to render dynamic content after click
|
|
270
|
+
if (postClickWait > 0) {
|
|
271
|
+
await new Promise(resolve => setTimeout(resolve, postClickWait));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const currentUrl = page.url();
|
|
275
|
+
const clickMessage = selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`;
|
|
276
|
+
const html = returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
|
|
277
|
+
const nextSteps = returnHtml
|
|
278
|
+
? [
|
|
286
279
|
"Use MCPBrowser's click_element again to navigate further",
|
|
287
280
|
"Use MCPBrowser's type_text to fill forms if needed",
|
|
288
281
|
"Use MCPBrowser's get_current_html to refresh page state",
|
|
289
282
|
"Use MCPBrowser's take_screenshot if page has popups or visual content that's hard to parse from HTML",
|
|
290
283
|
"Use MCPBrowser's close_tab when finished"
|
|
291
284
|
]
|
|
292
|
-
|
|
293
|
-
} else {
|
|
294
|
-
// Wait for page to stabilize even for fast clicks (ensures JS has finished)
|
|
295
|
-
logger.debug('Waiting for page stability (fast mode)...');
|
|
296
|
-
await waitForPageStability(page);
|
|
297
|
-
|
|
298
|
-
// Wait for SPAs to render dynamic content after click
|
|
299
|
-
if (postClickWait > 0) {
|
|
300
|
-
await new Promise(resolve => setTimeout(resolve, postClickWait));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const currentUrl = page.url();
|
|
304
|
-
|
|
305
|
-
logger.info(`click_element completed: ${selector || `text="${text}"`}`);
|
|
306
|
-
|
|
307
|
-
return new ClickElementSuccessResponse(
|
|
308
|
-
currentUrl,
|
|
309
|
-
selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
|
|
310
|
-
null,
|
|
311
|
-
[
|
|
285
|
+
: [
|
|
312
286
|
"Use MCPBrowser's get_current_html to see updated page state",
|
|
313
287
|
"Use MCPBrowser's take_screenshot if the page has popups, modals, or visual content",
|
|
314
288
|
"Use MCPBrowser's click_element or MCPBrowser's type_text for more interactions",
|
|
315
289
|
"Use MCPBrowser's close_tab when finished"
|
|
316
|
-
]
|
|
317
|
-
|
|
318
|
-
}
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
logger.info(`click_element completed: ${selector || `text="${text}"`}`);
|
|
293
|
+
|
|
294
|
+
return new ClickElementSuccessResponse(currentUrl, clickMessage, html, nextSteps);
|
|
319
295
|
} catch (err) {
|
|
320
296
|
logger.error(`click_element failed: ${err.message}`);
|
|
321
297
|
return new InformationalResponse(
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { getBrowser, domainPages } from '../core/browser.js';
|
|
7
|
-
import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extractAndProcessHtml
|
|
8
|
-
import {
|
|
7
|
+
import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extractAndProcessHtml } from '../core/page.js';
|
|
8
|
+
import { isLikelyAuthUrl, waitForAuth } from '../core/auth.js';
|
|
9
9
|
import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
|
|
10
10
|
import logger from '../core/logger.js';
|
|
11
11
|
|
|
@@ -141,7 +141,7 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
|
|
|
141
141
|
|
|
142
142
|
// Queue this request - processed sequentially, one at a time
|
|
143
143
|
return queueRequest(async () => {
|
|
144
|
-
return await doFetchPage({ url,
|
|
144
|
+
return await doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait });
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -149,12 +149,8 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
|
|
|
149
149
|
* Internal function that does the actual page fetching.
|
|
150
150
|
* Called by the queue processor - only one runs at a time.
|
|
151
151
|
*/
|
|
152
|
-
async function doFetchPage({ url,
|
|
153
|
-
|
|
154
|
-
const waitUntil = "domcontentloaded";
|
|
155
|
-
const navigationTimeout = 30000;
|
|
156
|
-
const authCompletionTimeout = 600000;
|
|
157
|
-
const reuseLastKeptPage = true;
|
|
152
|
+
async function doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait }) {
|
|
153
|
+
const originalHostname = new URL(url).hostname;
|
|
158
154
|
|
|
159
155
|
// Ensure browser connection
|
|
160
156
|
let browserInstance;
|
|
@@ -174,94 +170,36 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
174
170
|
}
|
|
175
171
|
|
|
176
172
|
try {
|
|
177
|
-
|
|
178
|
-
let
|
|
179
|
-
|
|
180
|
-
// Navigate to URL (pure navigation)
|
|
181
|
-
const { statusCode, statusText } = await navigateToUrl(page, url, waitUntil, navigationTimeout);
|
|
182
|
-
|
|
183
|
-
// Wait for page content to be ready (handles SPAs automatically)
|
|
173
|
+
let page = await getOrCreatePage(browserInstance, originalHostname);
|
|
174
|
+
let { statusCode, statusText } = await navigateToUrl(page, url, 'domcontentloaded', 30000);
|
|
184
175
|
await waitForPageReady(page);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Check if we already have a tab for the redirected hostname
|
|
204
|
-
// (can happen after reconnect - we mapped mail.google.com but not gmail.com)
|
|
205
|
-
const existingPage = domainPages.get(redirectInfo.currentHostname);
|
|
206
|
-
if (existingPage && existingPage !== page && !existingPage.isClosed()) {
|
|
207
|
-
logger.debug(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
|
|
208
|
-
// Close the new tab we just opened, use the existing one
|
|
209
|
-
await page.close().catch(() => {});
|
|
210
|
-
domainPages.delete(hostname);
|
|
211
|
-
page = existingPage;
|
|
212
|
-
// Map original hostname to existing page
|
|
213
|
-
domainPages.set(hostname, existingPage);
|
|
214
|
-
} else {
|
|
215
|
-
// Map both original and final hostname to the same page
|
|
216
|
-
domainPages.set(hostname, page);
|
|
217
|
-
domainPages.set(redirectInfo.currentHostname, page);
|
|
218
|
-
}
|
|
219
|
-
hostname = redirectInfo.currentHostname;
|
|
220
|
-
} else if (redirectInfo.type === 'auth') {
|
|
221
|
-
logger.info(`Authentication required: ${redirectInfo.flowType}`);
|
|
222
|
-
logger.info(`Auth URL: ${redirectInfo.currentUrl}`);
|
|
223
|
-
|
|
224
|
-
// Try auto-auth first (check if existing session works)
|
|
225
|
-
const autoAuthResult = await waitForAutoAuth(page);
|
|
226
|
-
|
|
227
|
-
if (autoAuthResult.success) {
|
|
228
|
-
logger.info(`Auto-auth successful, now at: ${page.url()}`);
|
|
229
|
-
// Update hostname to where we landed
|
|
230
|
-
if (autoAuthResult.hostname !== hostname) {
|
|
231
|
-
domainPages.delete(hostname);
|
|
232
|
-
domainPages.set(autoAuthResult.hostname, page);
|
|
233
|
-
hostname = autoAuthResult.hostname;
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
// Wait for manual auth
|
|
237
|
-
const manualAuthResult = await waitForManualAuth(page, authCompletionTimeout);
|
|
238
|
-
|
|
239
|
-
if (!manualAuthResult.success) {
|
|
240
|
-
logger.error(`Authentication failed: ${manualAuthResult.error}`);
|
|
241
|
-
return new ErrorResponse(
|
|
242
|
-
manualAuthResult.error,
|
|
243
|
-
[
|
|
244
|
-
"Complete authentication in the browser window",
|
|
245
|
-
"Call MCPBrowser's fetch_webpage again with the same URL to retry",
|
|
246
|
-
"Use MCPBrowser's close_tab to reset the session if authentication fails"
|
|
247
|
-
]
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Update hostname to where we landed
|
|
252
|
-
if (manualAuthResult.hostname !== hostname) {
|
|
253
|
-
domainPages.delete(hostname);
|
|
254
|
-
domainPages.set(manualAuthResult.hostname, page);
|
|
255
|
-
hostname = manualAuthResult.hostname;
|
|
256
|
-
}
|
|
257
|
-
logger.info(`Authentication successful, now at: ${page.url()}`);
|
|
176
|
+
|
|
177
|
+
// Auth: handle multi-step auth flows (e.g., server OIDC → client MSAL)
|
|
178
|
+
// Loop because some sites bounce through auth multiple times before landing.
|
|
179
|
+
let authAttempts = 0;
|
|
180
|
+
while (isLikelyAuthUrl(page.url()) && !isLikelyAuthUrl(url) && authAttempts < 3) {
|
|
181
|
+
authAttempts++;
|
|
182
|
+
logger.info(`Authentication required (attempt ${authAttempts}): ${page.url()}`);
|
|
183
|
+
const authResult = await waitForAuth(page);
|
|
184
|
+
if (!authResult.success) {
|
|
185
|
+
return new ErrorResponse(
|
|
186
|
+
authResult.error,
|
|
187
|
+
[
|
|
188
|
+
"Complete authentication in the browser window",
|
|
189
|
+
"Call MCPBrowser's fetch_webpage again with the same URL to retry",
|
|
190
|
+
"Use MCPBrowser's close_tab to reset the session if authentication fails"
|
|
191
|
+
]
|
|
192
|
+
);
|
|
258
193
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
194
|
+
await waitForPageReady(page, { afterInteraction: true });
|
|
195
|
+
statusCode = null;
|
|
196
|
+
statusText = '';
|
|
262
197
|
}
|
|
263
|
-
|
|
264
|
-
//
|
|
198
|
+
|
|
199
|
+
// Reconcile domain mappings after any redirect (permanent, auth, or none)
|
|
200
|
+
page = reconcileDomainMapping(page, originalHostname);
|
|
201
|
+
|
|
202
|
+
// Additional wait if requested
|
|
265
203
|
if (postLoadWait > 0) {
|
|
266
204
|
logger.debug(`Waiting ${postLoadWait}ms (postLoadWait)...`);
|
|
267
205
|
await new Promise(resolve => setTimeout(resolve, postLoadWait));
|
|
@@ -272,15 +210,10 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
272
210
|
|
|
273
211
|
logger.info(`fetch_webpage completed: ${page.url()}`);
|
|
274
212
|
|
|
275
|
-
// Check for non-2xx HTTP status codes
|
|
213
|
+
// Check for non-2xx HTTP status codes
|
|
276
214
|
if (statusCode && (statusCode >= 400 && statusCode < 600)) {
|
|
277
215
|
logger.debug(`HTTP ${statusCode} ${statusText} - returning as informational response`);
|
|
278
|
-
return new HttpStatusResponse(
|
|
279
|
-
page.url(),
|
|
280
|
-
statusCode,
|
|
281
|
-
statusText,
|
|
282
|
-
processedHtml
|
|
283
|
-
);
|
|
216
|
+
return new HttpStatusResponse(page.url(), statusCode, statusText, processedHtml);
|
|
284
217
|
}
|
|
285
218
|
|
|
286
219
|
return new FetchPageSuccessResponse(
|
|
@@ -306,3 +239,32 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
306
239
|
);
|
|
307
240
|
}
|
|
308
241
|
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reconcile domain mappings after navigation/redirect.
|
|
245
|
+
* If the page ended up on a different hostname, map both to the same tab.
|
|
246
|
+
* If another tab already exists for the new hostname, reuse it.
|
|
247
|
+
* @param {Page} page - The current Puppeteer page
|
|
248
|
+
* @param {string} originalHostname - The hostname from the original URL
|
|
249
|
+
* @returns {Page} The page to use (may differ if an existing tab was found)
|
|
250
|
+
*/
|
|
251
|
+
function reconcileDomainMapping(page, originalHostname) {
|
|
252
|
+
const currentHostname = new URL(page.url()).hostname;
|
|
253
|
+
if (currentHostname === originalHostname) return page;
|
|
254
|
+
|
|
255
|
+
logger.debug(`Redirect: ${originalHostname} → ${currentHostname}`);
|
|
256
|
+
|
|
257
|
+
const existing = domainPages.get(currentHostname);
|
|
258
|
+
if (existing && existing !== page && !existing.isClosed()) {
|
|
259
|
+
// Another tab already owns this hostname — close ours, reuse existing
|
|
260
|
+
logger.debug(`Found existing tab for ${currentHostname}, reusing it`);
|
|
261
|
+
page.close().catch(() => {});
|
|
262
|
+
domainPages.set(originalHostname, existing);
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Map both hostnames to same page
|
|
267
|
+
domainPages.set(originalHostname, page);
|
|
268
|
+
domainPages.set(currentHostname, page);
|
|
269
|
+
return page;
|
|
270
|
+
}
|
package/src/actions/type-text.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getBrowser, getValidatedPage } from '../core/browser.js';
|
|
6
|
-
import { extractAndProcessHtml,
|
|
6
|
+
import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
|
|
7
7
|
import { MCPResponse, ErrorResponse, InformationalResponse } from '../core/responses.js';
|
|
8
8
|
import logger from '../core/logger.js';
|
|
9
9
|
|
|
@@ -226,59 +226,35 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
|
|
|
226
226
|
? filledSelectors[0]
|
|
227
227
|
: `${filledSelectors.length} fields (${filledSelectors.join(', ')})`;
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
logger.info(`type_text completed: typed into ${fieldsSummary}`);
|
|
243
|
-
|
|
244
|
-
return new TypeTextSuccessResponse(
|
|
245
|
-
currentUrl,
|
|
246
|
-
`Typed text into: ${fieldsSummary}`,
|
|
247
|
-
html,
|
|
248
|
-
[
|
|
229
|
+
// Wait for page to stabilize (handles form validation, autocomplete, etc.)
|
|
230
|
+
logger.debug(`Waiting for page to be ready after typing${returnHtml ? '' : ' (fast mode)'}...`);
|
|
231
|
+
await waitForPageReady(page, { afterInteraction: true });
|
|
232
|
+
|
|
233
|
+
// Wait for SPAs to render dynamic content after typing
|
|
234
|
+
if (postTypeWait > 0) {
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, postTypeWait));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const currentUrl = page.url();
|
|
239
|
+
const html = returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
|
|
240
|
+
const nextSteps = returnHtml
|
|
241
|
+
? [
|
|
249
242
|
"Use MCPBrowser's type_text to fill additional fields",
|
|
250
243
|
"Use MCPBrowser's click_element to submit the form or navigate",
|
|
251
244
|
"Use MCPBrowser's get_current_html to check for validation messages",
|
|
252
245
|
"Use MCPBrowser's take_screenshot if form has visual feedback or validation that's hard to parse from HTML",
|
|
253
246
|
"Use MCPBrowser's close_tab when finished"
|
|
254
247
|
]
|
|
255
|
-
|
|
256
|
-
} else {
|
|
257
|
-
// Wait for page to stabilize even without returning HTML
|
|
258
|
-
logger.debug('Waiting for page stability after typing (fast mode)...');
|
|
259
|
-
await waitForPageStability(page);
|
|
260
|
-
|
|
261
|
-
// Wait for SPAs to render dynamic content after typing
|
|
262
|
-
if (postTypeWait > 0) {
|
|
263
|
-
await new Promise(resolve => setTimeout(resolve, postTypeWait));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const currentUrl = page.url();
|
|
267
|
-
|
|
268
|
-
logger.info(`type_text completed: typed into ${fieldsSummary} (no HTML)`);
|
|
269
|
-
|
|
270
|
-
return new TypeTextSuccessResponse(
|
|
271
|
-
currentUrl,
|
|
272
|
-
`Typed text into: ${fieldsSummary}`,
|
|
273
|
-
null,
|
|
274
|
-
[
|
|
248
|
+
: [
|
|
275
249
|
"Use MCPBrowser's get_current_html to see updated page state",
|
|
276
250
|
"Use MCPBrowser's take_screenshot if the page has visual feedback that's hard to parse",
|
|
277
251
|
"Use MCPBrowser's type_text for additional fields or MCPBrowser's click_element to submit",
|
|
278
252
|
"Use MCPBrowser's close_tab when finished"
|
|
279
|
-
]
|
|
280
|
-
|
|
281
|
-
}
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
logger.info(`type_text completed: typed into ${fieldsSummary}${returnHtml ? '' : ' (no HTML)'}`);
|
|
256
|
+
|
|
257
|
+
return new TypeTextSuccessResponse(currentUrl, `Typed text into: ${fieldsSummary}`, html, nextSteps);
|
|
282
258
|
} catch (err) {
|
|
283
259
|
// Build informative error message for agent
|
|
284
260
|
const totalFields = fields.length;
|
|
@@ -9,8 +9,9 @@ import puppeteer from 'puppeteer-core';
|
|
|
9
9
|
import { existsSync } from "fs";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import path from "path";
|
|
12
|
-
import { spawn } from "child_process";
|
|
12
|
+
import { spawn, execSync } from "child_process";
|
|
13
13
|
import logger from '../core/logger.js';
|
|
14
|
+
import { isWSL, wslToWindowsPath, windowsPathToWSL } from '../utils.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Base class for all Chromium-based browsers
|
|
@@ -49,6 +50,18 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
49
50
|
const platform = os.platform();
|
|
50
51
|
const home = os.homedir();
|
|
51
52
|
|
|
53
|
+
// In WSL the browser is a Windows process, so the user-data-dir
|
|
54
|
+
// must reside on a Windows-accessible filesystem (e.g. /mnt/c/…).
|
|
55
|
+
if (isWSL()) {
|
|
56
|
+
const windowsLocalAppData = this._getWindowsLocalAppData();
|
|
57
|
+
if (windowsLocalAppData) {
|
|
58
|
+
// windowsLocalAppData is already a WSL path like /mnt/c/Users/.../AppData/Local
|
|
59
|
+
return path.join(windowsLocalAppData, `MCPBrowser/${this.config.userDataDirName}`);
|
|
60
|
+
}
|
|
61
|
+
// Fallback: predictable location on C: drive
|
|
62
|
+
return `/mnt/c/MCPBrowserData/${this.config.userDataDirName}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
if (platform === "win32") {
|
|
53
66
|
return path.join(home, `AppData/Local/MCPBrowser/${this.config.userDataDirName}`);
|
|
54
67
|
} else if (platform === "darwin") {
|
|
@@ -74,6 +87,37 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the Windows %LOCALAPPDATA% directory as a WSL path.
|
|
92
|
+
* Cached per-process. Returns null on failure.
|
|
93
|
+
* @returns {string|null} WSL-style path (e.g. /mnt/c/Users/.../AppData/Local)
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
_getWindowsLocalAppData() {
|
|
97
|
+
// Use a class-level cache so we only shell out once
|
|
98
|
+
if (ChromiumBrowser._wslLocalAppData !== undefined) {
|
|
99
|
+
return ChromiumBrowser._wslLocalAppData;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const raw = execSync('cmd.exe /c echo %LOCALAPPDATA%', {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
timeout: 5000,
|
|
106
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
|
+
}).trim();
|
|
108
|
+
|
|
109
|
+
if (raw && !raw.includes('%LOCALAPPDATA%')) {
|
|
110
|
+
ChromiumBrowser._wslLocalAppData = windowsPathToWSL(raw);
|
|
111
|
+
return ChromiumBrowser._wslLocalAppData;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// cmd.exe not available or timed out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ChromiumBrowser._wslLocalAppData = null;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
/**
|
|
78
122
|
* Find the browser executable path
|
|
79
123
|
* @returns {string|undefined}
|
|
@@ -109,14 +153,24 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
const userDataDir = this.getDefaultUserDataDir();
|
|
156
|
+
|
|
157
|
+
// When running inside WSL the browser executable is a Windows process.
|
|
158
|
+
// It does not understand WSL paths (/mnt/c/…), so convert to a native
|
|
159
|
+
// Windows path for the --user-data-dir argument.
|
|
160
|
+
const userDataDirArg = isWSL() ? wslToWindowsPath(userDataDir) : userDataDir;
|
|
161
|
+
|
|
112
162
|
const args = [
|
|
113
163
|
`--remote-debugging-port=${this.config.port}`,
|
|
114
|
-
`--user-data-dir=${
|
|
164
|
+
`--user-data-dir=${userDataDirArg}`,
|
|
115
165
|
'--no-first-run',
|
|
116
166
|
'--no-default-browser-check'
|
|
117
167
|
];
|
|
118
168
|
|
|
119
169
|
logger.info(`Launching ${this.config.name} with remote debugging on port ${this.config.port}...`);
|
|
170
|
+
if (isWSL()) {
|
|
171
|
+
logger.info(`WSL detected – launching Windows browser at ${execPath}`);
|
|
172
|
+
logger.info(` user-data-dir (Windows): ${userDataDirArg}`);
|
|
173
|
+
}
|
|
120
174
|
|
|
121
175
|
const child = spawn(execPath, args, {
|
|
122
176
|
detached: true,
|
|
@@ -181,3 +235,6 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
181
235
|
this.browser = null;
|
|
182
236
|
}
|
|
183
237
|
}
|
|
238
|
+
|
|
239
|
+
// Static cache for WSL %LOCALAPPDATA% lookup (undefined = not yet resolved)
|
|
240
|
+
ChromiumBrowser._wslLocalAppData = undefined;
|
package/src/browsers/brave.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { ChromiumBrowser } from './ChromiumBrowser.js';
|
|
7
7
|
import os from "os";
|
|
8
|
+
import { isWSL } from '../utils.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get platform-specific default paths where Brave browser is typically installed.
|
|
12
|
+
* When running under WSL, Windows-side paths (via /mnt/c/) are also included.
|
|
11
13
|
* @returns {string[]} Array of possible Brave executable paths for the current platform
|
|
12
14
|
*/
|
|
13
15
|
function getDefaultBravePaths() {
|
|
@@ -24,13 +26,21 @@ function getDefaultBravePaths() {
|
|
|
24
26
|
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
25
27
|
];
|
|
26
28
|
} else {
|
|
27
|
-
|
|
29
|
+
const paths = [
|
|
28
30
|
"/usr/bin/brave",
|
|
29
31
|
"/usr/bin/brave-browser",
|
|
30
32
|
"/usr/bin/brave-browser-stable",
|
|
31
33
|
"/opt/brave.com/brave/brave-browser",
|
|
32
34
|
"/snap/bin/brave",
|
|
33
35
|
];
|
|
36
|
+
// In WSL, also look for Windows-side Brave via /mnt/c/
|
|
37
|
+
if (isWSL()) {
|
|
38
|
+
paths.push(
|
|
39
|
+
"/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
40
|
+
"/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return paths;
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
|
package/src/browsers/chrome.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { ChromiumBrowser } from './ChromiumBrowser.js';
|
|
7
7
|
import os from "os";
|
|
8
|
+
import { isWSL } from '../utils.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get platform-specific default paths where Chrome is typically installed.
|
|
12
|
+
* When running under WSL, Windows-side paths (via /mnt/c/) are also included.
|
|
11
13
|
* @returns {string[]} Array of possible Chrome executable paths for the current platform
|
|
12
14
|
*/
|
|
13
15
|
function getDefaultChromePaths() {
|
|
@@ -24,11 +26,19 @@ function getDefaultChromePaths() {
|
|
|
24
26
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
25
27
|
];
|
|
26
28
|
} else {
|
|
27
|
-
|
|
29
|
+
const paths = [
|
|
28
30
|
"/usr/bin/google-chrome",
|
|
29
31
|
"/usr/bin/chromium-browser",
|
|
30
32
|
"/usr/bin/chromium",
|
|
31
33
|
];
|
|
34
|
+
// In WSL, also look for Windows-side Chrome via /mnt/c/
|
|
35
|
+
if (isWSL()) {
|
|
36
|
+
paths.push(
|
|
37
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
38
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return paths;
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
44
|
|
package/src/browsers/edge.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { ChromiumBrowser } from './ChromiumBrowser.js';
|
|
7
7
|
import os from "os";
|
|
8
|
+
import { isWSL } from '../utils.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get platform-specific default paths where Edge browser is typically installed.
|
|
12
|
+
* When running under WSL, Windows-side paths (via /mnt/c/) are also included.
|
|
11
13
|
* @returns {string[]} Array of possible Edge executable paths for the current platform
|
|
12
14
|
*/
|
|
13
15
|
function getDefaultEdgePaths() {
|
|
@@ -23,13 +25,21 @@ function getDefaultEdgePaths() {
|
|
|
23
25
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
24
26
|
];
|
|
25
27
|
} else {
|
|
26
|
-
|
|
28
|
+
const paths = [
|
|
27
29
|
"/usr/bin/microsoft-edge",
|
|
28
30
|
"/usr/bin/microsoft-edge-stable",
|
|
29
31
|
"/usr/bin/microsoft-edge-beta",
|
|
30
32
|
"/usr/bin/microsoft-edge-dev",
|
|
31
33
|
"/opt/microsoft/msedge/msedge",
|
|
32
34
|
];
|
|
35
|
+
// In WSL, also look for Windows-side Edge via /mnt/c/
|
|
36
|
+
if (isWSL()) {
|
|
37
|
+
paths.push(
|
|
38
|
+
"/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe",
|
|
39
|
+
"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return paths;
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
|