Skip to main content
Glama

Patchright Lite MCP Server

by dylangroos
index.ts10.8 kB
// src/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { randomUUID } from "crypto"; import { promises as fs } from "fs"; import * as path from "path"; import * as os from "os"; // Import the Patchright library // This is the Node.js version of Patchright which is a stealth browser automation tool // based on Playwright but with anti-detection features import { chromium, Browser, Page } from "patchright"; // Create temp directory for screenshots const TEMP_DIR = path.join(os.tmpdir(), 'patchright-mcp'); // Ensure temp directory exists (async () => { try { await fs.mkdir(TEMP_DIR, { recursive: true }); console.error(`Temp directory created at: ${TEMP_DIR}`); } catch (error) { console.error(`Error creating temp directory: ${error}`); } })(); // Keep track of browser instances and pages interface BrowserInstance { browser: Browser; pages: Map<string, Page>; } const browserInstances = new Map<string, BrowserInstance>(); const server = new McpServer({ name: "patchright-lite", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }); // Tool 1: Browse - Launch browser and visit a URL server.tool( "browse", "Browse to a URL and return the page title and visible text", { url: z.string().url().describe("The URL to navigate to"), headless: z.boolean().default(false).describe("Whether to run the browser in headless mode"), waitFor: z.number().default(1000).describe("Time to wait after page load (milliseconds)") }, async ({ url, headless, waitFor }: { url: string; headless: boolean; waitFor: number }) => { try { // Generate unique IDs for tracking const browserId = randomUUID(); const pageId = randomUUID(); // Launch browser with stealth settings const browser = await chromium.launch({ headless: headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu' ] }); // Create a context and page const context = await browser.newContext({ viewport: null // Avoid detection by not using default viewport }); const page = await context.newPage(); // Store browser and page references browserInstances.set(browserId, { browser, pages: new Map([[pageId, page]]) }); // Navigate to the URL using isolated context for stealth await page.goto(url); await page.waitForTimeout(waitFor); // Get page title const title = await page.title(); // Extract visible text with stealth (isolated context) // This ensures the page doesn't detect us using Runtime.evaluate const visibleText = await page.evaluate(` Array.from(document.querySelectorAll('body, body *')) .filter(element => { const style = window.getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; }) .map(element => element.textContent) .filter(text => text && text.trim().length > 0) .join('\\n') `) as string; // Take a screenshot const screenshotPath = path.join(TEMP_DIR, `screenshot-${pageId}.png`); await page.screenshot({ path: screenshotPath }); // Return a formatted response for the AI model return { content: [ { type: "text", text: `Successfully browsed to: ${url}\n\nPage Title: ${title}\n\nVisible Text Preview:\n${visibleText.substring(0, 1500)}${visibleText.length > 1500 ? '...' : ''}\n\nBrowser ID: ${browserId}\nPage ID: ${pageId}\nScreenshot saved to: ${screenshotPath}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to browse: ${error}` } ] }; } } ); // Tool 2: Interact - Perform simple interactions on a page server.tool( "interact", "Perform simple interactions on a page", { browserId: z.string().describe("Browser ID from a previous browse operation"), pageId: z.string().describe("Page ID from a previous browse operation"), action: z.enum(["click", "fill", "select"]).describe("The type of interaction to perform"), selector: z.string().describe("CSS selector for the element to interact with"), value: z.string().optional().describe("Value for fill/select actions") }, async ({ browserId, pageId, action, selector, value }: { browserId: string; pageId: string; action: "click" | "fill" | "select"; selector: string; value?: string }) => { try { // Get the browser instance and page const instance = browserInstances.get(browserId); if (!instance) { throw new Error(`Browser instance not found: ${browserId}`); } const page = instance.pages.get(pageId); if (!page) { throw new Error(`Page not found: ${pageId}`); } // Perform the requested action let actionResult = ''; switch (action) { case "click": await page.click(selector); actionResult = `Clicked on element: ${selector}`; break; case "fill": if (!value) { throw new Error("Value is required for fill action"); } await page.fill(selector, value); actionResult = `Filled element ${selector} with value: ${value}`; break; case "select": if (!value) { throw new Error("Value is required for select action"); } await page.selectOption(selector, value); actionResult = `Selected option ${value} in element: ${selector}`; break; } // Wait a moment for any results of the interaction await page.waitForTimeout(1000); // Take a screenshot of the result const screenshotPath = path.join(TEMP_DIR, `screenshot-${pageId}-${Date.now()}.png`); await page.screenshot({ path: screenshotPath }); // Get current URL after interaction const currentUrl = page.url(); return { content: [ { type: "text", text: `Successfully performed action.\n\n${actionResult}\n\nCurrent URL: ${currentUrl}\n\nScreenshot saved to: ${screenshotPath}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to interact with page: ${error}` } ] }; } } ); // Tool 3: Extract - Get information from the current page server.tool( "extract", "Extract information from the current page as text, html, or screenshot", { browserId: z.string().describe("Browser ID from a previous browse operation"), pageId: z.string().describe("Page ID from a previous browse operation"), type: z.enum(["text", "html", "screenshot"]).describe("Type of content to extract") }, async ({ browserId, pageId, type }: { browserId: string; pageId: string; type: "text" | "html" | "screenshot" }) => { try { // Get the browser instance and page const instance = browserInstances.get(browserId); if (!instance) { throw new Error(`Browser instance not found: ${browserId}`); } const page = instance.pages.get(pageId); if (!page) { throw new Error(`Page not found: ${pageId}`); } let extractedContent = ''; let screenshotPath = ''; // Extract content based on requested type switch (type) { case "text": // Get visible text with stealth isolation extractedContent = await page.evaluate(` Array.from(document.querySelectorAll('body, body *')) .filter(element => { const style = window.getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; }) .map(element => element.textContent) .filter(text => text && text.trim().length > 0) .join('\\n') `) as string; break; case "html": // Get HTML content extractedContent = await page.content(); break; case "screenshot": // Take a screenshot screenshotPath = path.join(TEMP_DIR, `screenshot-${pageId}-${Date.now()}.png`); await page.screenshot({ path: screenshotPath }); extractedContent = `Screenshot saved to: ${screenshotPath}`; break; } return { content: [ { type: "text", text: type === "text" || type === "screenshot" ? extractedContent.substring(0, 2000) + (extractedContent.length > 2000 ? '...' : '') : `Extracted HTML content (${extractedContent.length} characters). First 100 characters:\n${extractedContent.substring(0, 100)}...` } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to extract content: ${error}` } ] }; } } ); // Tool 4: Close - Close browser to free resources server.tool( "close", "Close browser to free resources", { browserId: z.string().describe("Browser ID to close") }, async ({ browserId }: { browserId: string }) => { try { // Get the browser instance const instance = browserInstances.get(browserId); if (!instance) { throw new Error(`Browser instance not found: ${browserId}`); } // Close the browser await instance.browser.close(); // Remove from the map browserInstances.delete(browserId); return { content: [ { type: "text", text: `Successfully closed browser: ${browserId}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Failed to close browser: ${error}` } ] }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Weather MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

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/dylangroos/patchright-mcp-lite'

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