aironin-browse-mcp
Version:
aiRonin Browse MCP server with headed Chrome support
549 lines ⢠21.8 kB
JavaScript
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