UNPKG

aironin-browse-mcp

Version:

aiRonin Browse MCP server with headed Chrome support

549 lines • 21.8 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Try to import browser dependencies, but don't fail if they're not available let BrowserSession = null; let discoverChromeHostUrl = null; try { const browserModule = await import("aironin-browse-core"); BrowserSession = browserModule.BrowserSession; discoverChromeHostUrl = browserModule.discoverChromeHostUrl; } catch (error) { // Browser dependencies not available, but we can still provide the MCP server console.error("Browser dependencies not available. Install with: npm install aironin-browse-core@^1.2.0"); } // Create the server const server = new McpServer({ name: "aironin-browse-mcp", version: "1.0.0", }); // Browser session instance let browserSession = null; // Configuration helper function function getConfigValue(key, defaultValue) { return process.env[key] || defaultValue; } // Helper function to ensure browser is launched with remote detection async function ensureBrowserLaunched(forceRemote = false) { if (!BrowserSession) { throw new Error("Browser dependencies not available. Please install: npm install aironin-browse-core@^1.2.0"); } if (!browserSession) { browserSession = new BrowserSession(); // Set default configuration values if not provided if (!process.env.SCREENSHOT_QUALITY) { process.env.SCREENSHOT_QUALITY = getConfigValue("SCREENSHOT_QUALITY", "75"); } if (!process.env.BROWSER_VIEWPORT_SIZE) { process.env.BROWSER_VIEWPORT_SIZE = getConfigValue("BROWSER_VIEWPORT_SIZE", "900x600"); } if (!process.env.BROWSER_NAVIGATION_TIMEOUT) { process.env.BROWSER_NAVIGATION_TIMEOUT = getConfigValue("BROWSER_NAVIGATION_TIMEOUT", "15000"); } // Try remote browser detection first (unless explicitly disabled) const remoteBrowserEnabled = getConfigValue("REMOTE_BROWSER_ENABLED", "true"); if (forceRemote || remoteBrowserEnabled !== "false") { const remoteHostUrl = await discoverChromeHostUrl(9222); if (remoteHostUrl) { process.env.REMOTE_BROWSER_ENABLED = "true"; process.env.REMOTE_BROWSER_HOST = remoteHostUrl; } else { process.env.REMOTE_BROWSER_ENABLED = "false"; } } else { process.env.REMOTE_BROWSER_ENABLED = "false"; } await browserSession.launchBrowser(); } return browserSession; } // Register tools server.registerTool("launch_browser", { title: "Launch Browser", description: "Launch browser and navigate to URL", inputSchema: { url: z.string().describe("URL to navigate to"), remote: z.boolean().optional().describe("Force remote browser connection (default: auto-detect)") } }, async ({ url, remote }) => { const forceRemote = remote || false; const browser = await ensureBrowserLaunched(forceRemote); const result = await browser.navigateToUrl(url); return { content: [ { type: "text", text: `šŸš€ Browser launched and navigated to ${url}\n\n` + `Current URL: ${result.currentUrl}\n` + `Browser Mode: ${process.env.REMOTE_BROWSER_ENABLED === "true" ? "Remote" : "Local"}\n` + `Screenshot captured: ${result.screenshot ? "Yes" : "No"}\n` + `Page loaded successfully.` } ] }; }); server.registerTool("click_element", { title: "Click Element", description: "Click at specified coordinates", inputSchema: { coordinates: z.string().describe("Coordinates in format 'x,y'") } }, async ({ coordinates }) => { const browser = await ensureBrowserLaunched(); await browser.click(coordinates); return { content: [ { type: "text", text: `šŸ–±ļø Clicked at coordinates ${coordinates}` } ] }; }); server.registerTool("type_text", { title: "Type Text", description: "Type text into the browser", inputSchema: { text: z.string().describe("Text to type") } }, async ({ text }) => { const browser = await ensureBrowserLaunched(); await browser.type(text); return { content: [ { type: "text", text: `āŒØļø Typed text: "${text}"` } ] }; }); server.registerTool("scroll_page", { title: "Scroll Page", description: "Scroll the page up or down", inputSchema: { direction: z.enum(["up", "down"]).optional().describe("Scroll direction") } }, async ({ direction }) => { const scrollDirection = direction || "down"; const browser = await ensureBrowserLaunched(); if (scrollDirection === "up") { await browser.scrollUp(); } else { await browser.scrollDown(); } return { content: [ { type: "text", text: `šŸ“œ Scrolled page ${scrollDirection}` } ] }; }); server.registerTool("hover_element", { title: "Hover Element", description: "Hover at specified coordinates", inputSchema: { coordinates: z.string().describe("Coordinates in format 'x,y'") } }, async ({ coordinates }) => { const browser = await ensureBrowserLaunched(); await browser.hover(coordinates); return { content: [ { type: "text", text: `šŸ–±ļø Hovered at coordinates ${coordinates}` } ] }; }); server.registerTool("resize_browser", { title: "Resize Browser", description: "Resize browser window", inputSchema: { size: z.string().describe("Size in format 'width,height'") } }, async ({ size }) => { const browser = await ensureBrowserLaunched(); await browser.resize(size); return { content: [ { type: "text", text: `šŸ“ Resized browser to ${size}` } ] }; }); server.registerTool("take_screenshot", { title: "Take Screenshot", description: "Take a screenshot for AI agent analysis of the current page", inputSchema: { quality: z.number().optional().describe("Screenshot quality (1-100)"), fullPage: z.boolean().optional().describe("Capture full page (default: false)") } }, async ({ quality, fullPage = false }) => { const browser = await ensureBrowserLaunched(); // Set screenshot quality if provided if (quality) { process.env.SCREENSHOT_QUALITY = quality.toString(); } // Take screenshot using the browser session's doActionWithOptions method const result = await browser.doActionWithOptions(async (page) => { // Just wait a moment to ensure page is stable, screenshot is taken automatically await new Promise(resolve => setTimeout(resolve, 100)); }, { fullPage }); const defaultQuality = parseInt(getConfigValue("SCREENSHOT_QUALITY", "75")); const actualQuality = quality || defaultQuality; return { content: [ { type: "text", text: `šŸ“ø Screenshot captured for analysis!\n\n` + `Current URL: ${result.currentUrl}\n` + `Image size: ${Math.round((result.screenshot?.length || 0) / 1024)}KB\n` + `Quality: ${actualQuality}%\n` + `Full page capture: ${fullPage ? "Yes" : "No"}\n` + `Screenshot data available for AI analysis` } ] }; }); server.registerTool("capture_page", { title: "Capture Full Page", description: "Capture a full page screenshot (entire page, not just viewport) for AI agent analysis", inputSchema: { quality: z.number().optional().describe("Screenshot quality (1-100)"), fullPage: z.boolean().optional().describe("Capture full page (default: true)") } }, async ({ quality, fullPage = true }) => { const browser = await ensureBrowserLaunched(); // Set screenshot quality if provided if (quality) { process.env.SCREENSHOT_QUALITY = quality.toString(); } // Take full page screenshot using the browser session's doActionWithOptions method const result = await browser.doActionWithOptions(async (page) => { // Just wait a moment to ensure page is stable, screenshot is taken automatically await new Promise(resolve => setTimeout(resolve, 100)); }, { fullPage }); const defaultQuality = parseInt(getConfigValue("SCREENSHOT_QUALITY", "75")); const actualQuality = quality || defaultQuality; return { content: [ { type: "text", text: `šŸ“ø Full page screenshot captured for analysis!\n\n` + `Current URL: ${result.currentUrl}\n` + `Image size: ${Math.round((result.screenshot?.length || 0) / 1024)}KB\n` + `Quality: ${actualQuality}%\n` + `Full page capture: ${fullPage ? "Yes" : "No"}\n` + `Screenshot data available for AI analysis` } ] }; }); server.registerTool("save_screenshot", { title: "Save Screenshot", description: "Take and save a screenshot of the current page to disk", inputSchema: { quality: z.number().optional().describe("Screenshot quality (1-100)"), filename: z.string().optional().describe("Custom filename (without extension)"), fullPage: z.boolean().optional().describe("Capture full page (default: false)") } }, async ({ quality, filename, fullPage = false }) => { const browser = await ensureBrowserLaunched(); // Set screenshot quality if provided if (quality) { process.env.SCREENSHOT_QUALITY = quality.toString(); } // Take screenshot using the browser session's doActionWithOptions method const result = await browser.doActionWithOptions(async (page) => { // Just wait a moment to ensure page is stable, screenshot is taken automatically await new Promise(resolve => setTimeout(resolve, 100)); }, { fullPage }); // Save screenshot to file if available let filePath = ""; if (result.screenshot) { const fs = await import("node:fs/promises"); const path = await import("node:path"); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const customName = filename ? filename.replace(/[^a-zA-Z0-9-_]/g, '_') : 'screenshot'; const finalFilename = `${customName}-${timestamp}.webp`; filePath = path.join(process.cwd(), finalFilename); // Extract base64 data and save to file const base64Data = result.screenshot.replace(/^data:image\/[^;]+;base64,/, ''); await fs.writeFile(filePath, Buffer.from(base64Data, 'base64')); } const defaultQuality = parseInt(getConfigValue("SCREENSHOT_QUALITY", "75")); const actualQuality = quality || defaultQuality; return { content: [ { type: "text", text: `šŸ“ø Screenshot saved successfully!\n\n` + `Image size: ${Math.round((result.screenshot?.length || 0) / 1024)}KB\n` + `Quality: ${actualQuality}%\n` + `Full page capture: ${fullPage ? "Yes" : "No"}\n` + `Current URL: ${result.currentUrl}\n` + `${filePath ? `Saved to: ${filePath}` : 'Failed to save screenshot'}` } ] }; }); server.registerTool("analyze_page", { title: "Analyze Page", description: "Analyze the current page content for AI agent understanding", inputSchema: { includeScreenshot: z.boolean().optional().describe("Include screenshot data for visual analysis") } }, async ({ includeScreenshot }) => { const browser = await ensureBrowserLaunched(); // Take screenshot and get page info const result = await browser.doAction(async (page) => { // Just wait a moment to ensure page is stable, screenshot is taken automatically await new Promise(resolve => setTimeout(resolve, 100)); }); // Build analysis text let analysis = `šŸ” Page Analysis\n\n`; analysis += `šŸ“„ URL: ${result.currentUrl}\n`; analysis += `šŸ“Š Screenshot: ${result.screenshot ? "Available" : "Not available"}\n`; analysis += `šŸ“ Console Logs: ${result.logs ? "Available" : "None"}\n\n`; analysis += `šŸ’” AI Agent Notes:\n`; analysis += `- Screenshot data available for visual analysis\n`; analysis += `- Use this data to understand the page structure\n`; analysis += `- Plan next actions based on available elements\n`; if (includeScreenshot) { analysis += `- Visual analysis enabled\n`; } return { content: [ { type: "text", text: analysis } ] }; }); server.registerTool("get_page_source", { title: "Get Page Source", description: "Get the full HTML source code of the current page", inputSchema: { includeComments: z.boolean().optional().describe("Include HTML comments in the output (default: true)") } }, async ({ includeComments = true }) => { const browser = await ensureBrowserLaunched(); const result = await browser.getPageSource(includeComments); const sourceLength = result.htmlSource?.length || 0; const originalLength = result.originalLength || sourceLength; const processedLength = result.processedLength || sourceLength; return { content: [ { type: "text", text: `šŸ“„ Page Source Retrieved\n\n` + `šŸ“Š Source Statistics:\n` + `- Original length: ${originalLength} characters\n` + `- Processed length: ${processedLength} characters\n` + `- Comments included: ${includeComments ? "Yes" : "No"}\n` + `- Current URL: ${result.currentUrl}\n\n` + `šŸ“ HTML Source:\n\`\`\`html\n${result.htmlSource || "No source available"}\n\`\`\`` } ] }; }); server.registerTool("get_page_title", { title: "Get Page Title", description: "Get the title of the current page", inputSchema: {} }, async () => { const browser = await ensureBrowserLaunched(); const result = await browser.getPageTitle(); return { content: [ { type: "text", text: `šŸ“‹ Page Title\n\n` + `šŸ“„ Title: "${result.pageTitle || "No title found"}"\n` + `🌐 URL: ${result.currentUrl}\n` + `šŸ“ Console Logs: ${result.logs ? "Available" : "None"}` } ] }; }); server.registerTool("get_page_meta", { title: "Get Page Meta Tags", description: "Extract meta tags and other page metadata from the current page", inputSchema: { includeAllMeta: z.boolean().optional().describe("Include all meta tags, not just common ones (default: false)") } }, async ({ includeAllMeta = false }) => { const browser = await ensureBrowserLaunched(); const result = await browser.getPageMeta(includeAllMeta); const meta = result.metaData || {}; let metaText = `šŸ“‹ Page Meta Tags\n\n`; metaText += `šŸ“„ Title: "${meta.title || "No title"}"\n`; metaText += `šŸ“ Description: "${meta.description || "No description"}"\n`; metaText += `šŸ·ļø Keywords: "${meta.keywords || "No keywords"}"\n`; metaText += `šŸ‘¤ Author: "${meta.author || "No author"}"\n`; metaText += `šŸ“± Viewport: "${meta.viewport || "No viewport"}"\n`; metaText += `šŸ¤– Robots: "${meta.robots || "No robots"}"\n\n`; if (Object.keys(meta.ogTags).length > 0) { metaText += `šŸ“˜ Open Graph Tags:\n`; Object.entries(meta.ogTags).forEach(([key, value]) => { metaText += `- ${key}: "${value}"\n`; }); metaText += `\n`; } if (Object.keys(meta.twitterTags).length > 0) { metaText += `🐦 Twitter Tags:\n`; Object.entries(meta.twitterTags).forEach(([key, value]) => { metaText += `- ${key}: "${value}"\n`; }); metaText += `\n`; } if (includeAllMeta && meta.allMeta.length > 0) { metaText += `šŸ“‹ All Meta Tags:\n`; meta.allMeta.forEach((tag) => { metaText += `- ${tag.name}: "${tag.content}"\n`; }); } metaText += `🌐 URL: ${result.currentUrl}\n`; metaText += `šŸ“ Console Logs: ${result.logs ? "Available" : "None"}`; return { content: [ { type: "text", text: metaText } ] }; }); server.registerTool("inspect_element", { title: "Inspect Element", description: "Get HTML and properties of an element at specified coordinates", inputSchema: { coordinates: z.string().describe("Coordinates in format 'x,y'"), includeChildren: z.boolean().optional().describe("Include child elements in the output (default: false)") } }, async ({ coordinates, includeChildren = false }) => { const browser = await ensureBrowserLaunched(); const result = await browser.inspectElement(coordinates, includeChildren); const element = result.elementInfo; if (!element) { return { content: [ { type: "text", text: `āŒ No element found at coordinates ${coordinates}` } ] }; } let elementText = `šŸ” Element Inspection\n\n`; elementText += `šŸ“ Coordinates: ${coordinates}\n`; elementText += `šŸ·ļø Tag: <${element.tagName}>\n`; elementText += `šŸ†” ID: "${element.id || "No ID"}"\n`; elementText += `šŸŽØ Classes: "${element.className || "No classes"}"\n`; elementText += `šŸ“ Position: x=${element.rect.x}, y=${element.rect.y}\n`; elementText += `šŸ“ Size: ${element.rect.width}x${element.rect.height}\n\n`; elementText += `šŸ“ Text Content: "${element.textContent || "No text"}"\n\n`; elementText += `šŸŽØ Computed Styles:\n`; elementText += `- Display: ${element.computedStyle.display}\n`; elementText += `- Visibility: ${element.computedStyle.visibility}\n`; elementText += `- Opacity: ${element.computedStyle.opacity}\n`; elementText += `- Background: ${element.computedStyle.backgroundColor}\n`; elementText += `- Color: ${element.computedStyle.color}\n`; elementText += `- Font Size: ${element.computedStyle.fontSize}\n`; elementText += `- Font Weight: ${element.computedStyle.fontWeight}\n\n`; elementText += `šŸ·ļø Attributes:\n`; element.attributes.forEach((attr) => { elementText += `- ${attr.name}: "${attr.value}"\n`; }); if (includeChildren) { elementText += `\nšŸ“¦ Inner HTML:\n\`\`\`html\n${element.innerHTML || "No inner HTML"}\n\`\`\`\n`; } elementText += `\nšŸ“„ Outer HTML:\n\`\`\`html\n${element.outerHTML}\n\`\`\`\n`; elementText += `🌐 URL: ${result.currentUrl}\n`; elementText += `šŸ“ Console Logs: ${result.logs ? "Available" : "None"}`; return { content: [ { type: "text", text: elementText } ] }; }); server.registerTool("get_console_logs", { title: "Get Console Logs", description: "Retrieve JavaScript console logs from the current page", inputSchema: { clearLogs: z.boolean().optional().describe("Clear logs after retrieving them (default: false)") } }, async ({ clearLogs = false }) => { const browser = await ensureBrowserLaunched(); // Simply get the current state without performing any action // This will return the last captured logs from navigation const result = { logs: browser.getLastCapturedLogs() || "", currentUrl: browser.getCurrentUrl() || "" }; const logs = result.logs || ""; const logLines = logs.split('\n').filter((line) => line.trim()); let logText = `šŸ“ JavaScript Console Logs\n\n`; logText += `🌐 URL: ${result.currentUrl}\n`; logText += `šŸ“Š Total log entries: ${logLines.length}\n\n`; if (logLines.length > 0) { logText += `šŸ“‹ Log Entries:\n`; logLines.forEach((line, index) => { logText += `${index + 1}. ${line}\n`; }); } else { logText += `šŸ“­ No console logs captured\n`; logText += `šŸ’” This could mean:\n`; logText += `- No JavaScript errors or logs occurred\n`; logText += `- Page is static or has minimal JavaScript\n`; logText += `- Logs were cleared recently\n`; } return { content: [ { type: "text", text: logText } ] }; }); server.registerTool("close_browser", { title: "Close Browser", description: "Close the browser", inputSchema: {} }, async () => { if (browserSession) { await browserSession.closeBrowser(); browserSession = null; } return { content: [ { type: "text", text: `šŸ”’ Browser closed successfully` } ] }; }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch(console.error); //# sourceMappingURL=server.js.map