Skip to main content
Glama

Chromium ARM64 Browser

by nfodor
index-browser-only.js12 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { spawn, execSync } from 'child_process'; import { WebSocket } from 'ws'; import http from 'http'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Global browser instance let chromiumProcess = null; let wsConnection = null; let currentTabId = null; let debuggingPort = 9222; // Helper function to find Chromium executable function getChromiumPath() { const platform = os.platform(); if (platform === 'linux') { // Try common Linux paths const linuxPaths = [ '/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable' ]; for (const chromePath of linuxPaths) { if (fs.existsSync(chromePath)) { return chromePath; } } } else if (platform === 'darwin') { // macOS paths const macPaths = [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/opt/homebrew/bin/chromium' ]; for (const chromePath of macPaths) { if (fs.existsSync(chromePath)) { return chromePath; } } } else if (platform === 'win32') { // Windows paths const winPaths = [ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Chromium\\Application\\chrome.exe' ]; for (const chromePath of winPaths) { if (fs.existsSync(chromePath)) { return chromePath; } } } // Try to find via which command try { const result = execSync('which chromium-browser || which chromium || which google-chrome', { encoding: 'utf8' }).trim(); if (result) { return result.split('\n')[0]; } } catch (error) { // Ignore error, will throw below } throw new Error(`Could not find Chromium browser. Please install it for your platform.`); } class BrowserOnlyMCPServer { constructor() { this.server = new Server( { name: 'chromium-browser-only', version: '1.3.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } setupToolHandlers() { // Only expose browsing-related tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'navigate', description: 'Navigate to a URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The URL to navigate to', }, }, required: ['url'], }, }, { name: 'get_content', description: 'Get page content (HTML or text)', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['html', 'text'], description: 'Type of content to get', default: 'text', }, }, }, }, { name: 'screenshot', description: 'Take a screenshot of the current page', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name for the screenshot file', default: 'screenshot.png', }, fullPage: { type: 'boolean', description: 'Capture full page', default: false, }, }, }, }, { name: 'evaluate', description: 'Execute JavaScript in the browser (read-only operations)', inputSchema: { type: 'object', properties: { script: { type: 'string', description: 'JavaScript code to execute (for reading page info)', }, }, required: ['script'], }, }, { name: 'close_browser', description: 'Close the browser instance', inputSchema: { type: 'object', properties: {}, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'navigate': return await this.navigate(args.url); case 'get_content': return await this.getContent(args.type || 'text'); case 'screenshot': return await this.screenshot(args.name, args.fullPage); case 'evaluate': return await this.evaluate(args.script); case 'close_browser': return await this.closeBrowser(); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } }); } setupErrorHandling() { this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.closeBrowser(); await this.server.close(); process.exit(0); }); } async sendCDPCommand(method, params = {}) { return new Promise((resolve, reject) => { if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) { reject(new Error('No browser connection available')); return; } const commandId = Date.now(); const command = { id: commandId, method, params }; const timeout = setTimeout(() => { reject(new Error(`CDP command timeout: ${method}`)); }, 10000); const messageHandler = (data) => { try { const response = JSON.parse(data.toString()); if (response.id === commandId) { clearTimeout(timeout); wsConnection.off('message', messageHandler); if (response.error) { reject(new Error(`CDP error: ${response.error.message}`)); } else { resolve(response.result); } } } catch (error) { // Ignore parsing errors for non-matching messages } }; wsConnection.on('message', messageHandler); wsConnection.send(JSON.stringify(command)); }); } async httpRequest(url) { return new Promise((resolve, reject) => { const request = http.get(url, (response) => { let data = ''; response.on('data', chunk => data += chunk); response.on('end', () => resolve(JSON.parse(data))); }); request.on('error', reject); request.setTimeout(5000, () => { request.destroy(); reject(new Error('HTTP request timeout')); }); }); } async ensureChromium() { if (chromiumProcess && wsConnection) { return; } return new Promise((resolve, reject) => { const chromiumPath = getChromiumPath(); chromiumProcess = spawn(chromiumPath, [ '--headless', '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-web-security', '--disable-features=VizDisplayCompositor', '--disable-extensions', '--disable-plugins', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding', '--disable-field-trial-config', '--disable-back-forward-cache', '--disable-ipc-flooding-protection', '--enable-logging', '--log-level=0', `--remote-debugging-port=${debuggingPort}`, '--no-first-run', ]); chromiumProcess.on('error', reject); // Wait for browser to be ready const checkReady = async () => { try { const response = await this.httpRequest(`http://localhost:${debuggingPort}/json`); if (!currentTabId && response.length > 0) { currentTabId = response[0].id; } if (!currentTabId) { const newTabResponse = await this.httpRequest(`http://localhost:${debuggingPort}/json/new`); currentTabId = newTabResponse.id; } wsConnection = new WebSocket(`ws://localhost:${debuggingPort}/devtools/page/${currentTabId}`); wsConnection.on('open', () => resolve()); wsConnection.on('error', reject); } catch (error) { setTimeout(checkReady, 500); } }; setTimeout(checkReady, 1000); }); } async navigate(url) { await this.ensureChromium(); await this.sendCDPCommand('Page.navigate', { url }); return { content: [{ type: 'text', text: `Successfully navigated to ${url}` }], }; } async getContent(type = 'text') { await this.ensureChromium(); if (type === 'html') { const result = await this.sendCDPCommand('DOM.getDocument'); const html = await this.sendCDPCommand('DOM.getOuterHTML', { nodeId: result.root.nodeId, }); return { content: [{ type: 'text', text: html.outerHTML }], }; } else { const result = await this.sendCDPCommand('Runtime.evaluate', { expression: 'document.body.innerText', returnByValue: true, }); return { content: [{ type: 'text', text: result.result.value || '' }], }; } } async screenshot(name = 'screenshot.png', fullPage = false) { await this.ensureChromium(); const screenshotParams = { format: 'png' }; if (fullPage) { const metrics = await this.sendCDPCommand('Page.getLayoutMetrics'); await this.sendCDPCommand('Emulation.setDeviceMetricsOverride', { width: metrics.contentSize.width, height: metrics.contentSize.height, deviceScaleFactor: 1, mobile: false, }); screenshotParams.clip = { x: 0, y: 0, width: metrics.contentSize.width, height: metrics.contentSize.height, scale: 1, }; } const screenshot = await this.sendCDPCommand('Page.captureScreenshot', screenshotParams); const screenshotPath = `/tmp/${name}`; fs.writeFileSync(screenshotPath, screenshot.data, 'base64'); return { content: [{ type: 'text', text: `Screenshot saved to ${screenshotPath}` }], }; } async evaluate(script) { await this.ensureChromium(); const result = await this.sendCDPCommand('Runtime.evaluate', { expression: script, returnByValue: true, }); return { content: [{ type: 'text', text: `Result: ${JSON.stringify(result.result.value)}` }], }; } async closeBrowser() { if (wsConnection) { wsConnection.close(); wsConnection = null; } if (chromiumProcess) { chromiumProcess.kill('SIGTERM'); chromiumProcess = null; } currentTabId = null; return { content: [{ type: 'text', text: 'Browser closed successfully' }], }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new BrowserOnlyMCPServer(); server.run().catch(error => { process.stderr.write(`Failed to start browser-only MCP server: ${error.message}\n`); 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/nfodor/mcp-chromium-arm64'

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