Skip to main content
Glama
mcp-browser.js11.4 kB
#!/usr/bin/env node import dotenv from "dotenv"; import puppeteer from "puppeteer-core"; import { existsSync } from "fs"; import os from "os"; import path from "path"; import { spawn } from "child_process"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; dotenv.config(); const chromeHost = process.env.CHROME_REMOTE_DEBUG_HOST || "127.0.0.1"; const chromePort = Number(process.env.CHROME_REMOTE_DEBUG_PORT || 9222); const explicitWSEndpoint = process.env.CHROME_WS_ENDPOINT; // Use default Chrome profile if not explicitly set function getDefaultUserDataDir() { const platform = os.platform(); const home = os.homedir(); // Use a dedicated debugging profile directory if (platform === "win32") { return path.join(home, "AppData/Local/MCPBrowser/ChromeDebug"); } else if (platform === "darwin") { return path.join(home, "Library/Application Support/MCPBrowser/ChromeDebug"); } else { return path.join(home, ".config/MCPBrowser/ChromeDebug"); } } const userDataDir = process.env.CHROME_USER_DATA_DIR || getDefaultUserDataDir(); const chromePathEnv = process.env.CHROME_PATH; function getDefaultChromePaths() { const platform = os.platform(); if (platform === "win32") { return [ "C:/Program Files/Google/Chrome/Application/chrome.exe", "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", "C:/Program Files/Microsoft/Edge/Application/msedge.exe", ]; } else if (platform === "darwin") { return [ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Chromium.app/Contents/MacOS/Chromium", ]; } else { return [ "/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium", ]; } } const defaultChromePaths = getDefaultChromePaths(); let cachedBrowser = null; let lastKeptPage = null; // reuse the same tab when requested let chromeLaunchPromise = null; // prevent multiple simultaneous launches async function devtoolsAvailable() { try { const url = `http://${chromeHost}:${chromePort}/json/version`; const res = await fetch(url, { method: "GET" }); if (!res.ok) return false; const data = await res.json(); return Boolean(data.webSocketDebuggerUrl); } catch { return false; } } function findChromePath() { if (chromePathEnv && existsSync(chromePathEnv)) return chromePathEnv; return defaultChromePaths.find((p) => existsSync(p)); } async function launchChromeIfNeeded() { if (explicitWSEndpoint) return; // user provided explicit endpoint; assume managed externally // If Chrome is already available, don't launch if (await devtoolsAvailable()) return; // If a launch is already in progress, wait for it if (chromeLaunchPromise) { return await chromeLaunchPromise; } // Create a new launch promise to prevent multiple simultaneous launches chromeLaunchPromise = (async () => { try { // Double-check after acquiring the launch lock if (await devtoolsAvailable()) return; const chromePath = findChromePath(); if (!chromePath) { throw new Error("Chrome/Edge not found. Set CHROME_PATH to your browser executable."); } const args = [ `--remote-debugging-port=${chromePort}`, `--user-data-dir=${userDataDir}`, '--no-first-run', // Skip first run experience '--no-default-browser-check', // Skip default browser check '--disable-sync', // Disable Chrome sync prompts 'about:blank' // Open with a blank page ]; const child = spawn(chromePath, args, { detached: true, stdio: "ignore" }); child.unref(); // Wait for DevTools to come up const deadline = Date.now() + 20000; while (Date.now() < deadline) { if (await devtoolsAvailable()) return; await new Promise((r) => setTimeout(r, 500)); } throw new Error("Chrome did not become available on DevTools port; check CHROME_PATH/port/profile."); } finally { chromeLaunchPromise = null; } })(); return await chromeLaunchPromise; } async function resolveWSEndpoint() { if (explicitWSEndpoint) return explicitWSEndpoint; const url = `http://${chromeHost}:${chromePort}/json/version`; const res = await fetch(url); if (!res.ok) { throw new Error(`Unable to reach Chrome devtools at ${url}: ${res.status}`); } const data = await res.json(); if (!data.webSocketDebuggerUrl) { throw new Error("No webSocketDebuggerUrl in /json/version response"); } return data.webSocketDebuggerUrl; } async function getBrowser() { await launchChromeIfNeeded(); if (cachedBrowser && cachedBrowser.isConnected()) return cachedBrowser; const wsEndpoint = await resolveWSEndpoint(); cachedBrowser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, defaultViewport: null, }); cachedBrowser.on("disconnected", () => { cachedBrowser = null; lastKeptPage = null; }); return cachedBrowser; } async function fetchPage({ url, keepPageOpen = true, }) { // Hardcoded smart defaults const waitUntil = "networkidle0"; const timeoutMs = 60000; const reuseLastKeptPage = true; if (!url) { throw new Error("url parameter is required"); } const browser = await getBrowser(); let page = null; // Smart tab reuse: only reuse if same domain (preserves auth within domain) if (reuseLastKeptPage && lastKeptPage && !lastKeptPage.isClosed()) { let newHostname; try { newHostname = new URL(url).hostname; } catch { throw new Error(`Invalid URL: ${url}`); } const currentUrl = lastKeptPage.url(); if (currentUrl) { try { const currentHostname = new URL(currentUrl).hostname; // Reuse tab only if same domain (keeps auth session alive) if (currentHostname === newHostname) { page = lastKeptPage; await page.bringToFront().catch(() => {}); } else { // Different domain - close old tab and create new one await lastKeptPage.close().catch(() => {}); lastKeptPage = null; } } catch { // If URL parsing fails, create new tab } } } // Create new tab if no reuse if (!page) { try { page = await browser.newPage(); } catch (error) { // If newPage() fails (can happen with some profiles), try to reuse existing page const pages = await browser.pages(); for (const p of pages) { try { const pageUrl = p.url(); // Skip chrome:// pages and other internal pages if (!pageUrl.startsWith('chrome://') && !pageUrl.startsWith('chrome-extension://')) { page = p; break; } } catch { // Skip pages we can't access } } if (!page) { throw new Error('Unable to create or find a controllable page'); } } } let shouldKeepOpen = keepPageOpen || page === lastKeptPage; let wasSuccess = false; try { console.error(`[MCPBrowser] Navigating to: ${url}`); await page.goto(url, { waitUntil, timeout: timeoutMs }); console.error(`[MCPBrowser] Navigation completed: ${page.url()}`); // Extract content const text = await page.evaluate(() => document.body?.innerText || ""); const html = await page.evaluate(() => document.documentElement?.outerHTML || ""); wasSuccess = true; if (keepPageOpen && lastKeptPage !== page) { // Close old kept page if we're keeping a different one if (lastKeptPage && !lastKeptPage.isClosed()) { await lastKeptPage.close().catch(() => {}); } lastKeptPage = page; } return { success: true, url: page.url(), text: truncate(text, 2000000), html: truncate(html, 2000000), }; } catch (err) { shouldKeepOpen = shouldKeepOpen || keepPageOpen; const hint = shouldKeepOpen ? "Tab is left open. Complete sign-in there, then call fetch_webpage_protected again with just the URL." : undefined; return { success: false, error: err.message || String(err), pageKeptOpen: shouldKeepOpen, hint }; } finally { if (!shouldKeepOpen && lastKeptPage === page) { lastKeptPage = null; } if (!shouldKeepOpen) { await page.close().catch(() => {}); } } } function truncate(str, max) { if (!str) return ""; return str.length > max ? `${str.slice(0, max)}... [truncated]` : str; } async function main() { const server = new Server({ name: "MCPBrowser", version: "0.2.15" }, { capabilities: { tools: {} } }); const tools = [ { name: "fetch_webpage_protected", description: "🌐 PRIMARY BROWSER TOOL for authenticated/protected websites: Opens pages in your Chrome browser with your actual user profile (saved passwords, extensions, active sessions). **USE THIS FIRST** for: internal/corporate sites (*.microsoft.com, *.eng.ms, etc.), login-required pages, SSO/OAuth protected content, paywalled sites, anti-bot protected pages, or any 401/403/authentication errors. **AUTHENTICATION FLOW**: First call with keepPageOpen=true may return login page (EXPECTED - user is authenticating in browser). WAIT 10-30 seconds, then RETRY the same URL - authentication completes in background. DO NOT give up after seeing login page - retry 2-3 times with delays. Returns both plain text and HTML. Tab reuse preserves sessions across requests. Always prefer this over generic URL fetchers for authenticated content.", inputSchema: { type: "object", properties: { url: { type: "string", description: "The URL to fetch" }, keepPageOpen: { type: "boolean", description: "Keep the tab open after fetching for manual auth or reuse (default: true)" }, }, required: ["url"], additionalProperties: false, }, annotations: { title: "Access Authenticated Web Page" } }, ]; server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name !== "fetch_webpage_protected") { throw new Error(`Unknown tool: ${name}`); } const safeArgs = args || {}; const fallbackUrl = process.env.DEFAULT_FETCH_URL || process.env.MCP_DEFAULT_FETCH_URL; if (!safeArgs.url) { if (fallbackUrl) { safeArgs.url = fallbackUrl; } else { return { content: [ { type: "text", text: JSON.stringify({ success: false, error: "Missing url and no DEFAULT_FETCH_URL/MCP_DEFAULT_FETCH_URL configured" }), }, ], }; } } const result = await fetchPage(safeArgs); return { content: [ { type: "text", text: JSON.stringify(result), }, ], }; }); const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((err) => { console.error(err); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cherchyk/MCPBrowser'

If you have feedback or need assistance with the MCP directory API, please join our Discord server