Skip to main content
Glama

Browser Control MCP

by eyalzh
message-handler.ts9.85 kB
import type { ServerMessageRequest } from "@browser-control-mcp/common"; import { WebsocketClient } from "./client"; import { isCommandAllowed, isDomainInDenyList, COMMAND_TO_TOOL_ID, addAuditLogEntry } from "./extension-config"; export class MessageHandler { private client: WebsocketClient; constructor(client: WebsocketClient) { this.client = client; } public async handleDecodedMessage(req: ServerMessageRequest): Promise<void> { const isAllowed = await isCommandAllowed(req.cmd); if (!isAllowed) { throw new Error(`Command '${req.cmd}' is disabled in extension settings`); } this.addAuditLogForReq(req).catch((error) => { console.error("Failed to add audit log entry:", error); }); switch (req.cmd) { case "open-tab": await this.openUrl(req.correlationId, req.url); break; case "close-tabs": await this.closeTabs(req.correlationId, req.tabIds); break; case "get-tab-list": await this.sendTabs(req.correlationId); break; case "get-browser-recent-history": await this.sendRecentHistory(req.correlationId, req.searchQuery); break; case "get-tab-content": await this.sendTabsContent(req.correlationId, req.tabId, req.offset); break; case "reorder-tabs": await this.reorderTabs(req.correlationId, req.tabOrder); break; case "find-highlight": await this.findAndHighlightText( req.correlationId, req.tabId, req.queryPhrase ); break; case "group-tabs": await this.groupTabs( req.correlationId, req.tabIds, req.isCollapsed, req.groupColor as browser.tabGroups.Color, req.groupTitle ); break; default: const _exhaustiveCheck: never = req; console.error("Invalid message received:", req); } } private async addAuditLogForReq(req: ServerMessageRequest) { // Get the URL in context (either from param or from the tab) let contextUrl: string | undefined; if ("url" in req && req.url) { contextUrl = req.url; } if ("tabId" in req) { try { const tab = await browser.tabs.get(req.tabId); contextUrl = tab.url; } catch (error) { console.error("Failed to get tab URL for audit log:", error); } } const toolId = COMMAND_TO_TOOL_ID[req.cmd]; const auditEntry = { toolId, command: req.cmd, timestamp: Date.now(), url: contextUrl }; await addAuditLogEntry(auditEntry); } private async openUrl(correlationId: string, url: string): Promise<void> { if (!url.startsWith("https://")) { console.error("Invalid URL:", url); throw new Error("Invalid URL"); } if (await isDomainInDenyList(url)) { throw new Error("Domain in user defined deny list"); } const tab = await browser.tabs.create({ url, }); await this.client.sendResourceToServer({ resource: "opened-tab-id", correlationId, tabId: tab.id, }); } private async closeTabs( correlationId: string, tabIds: number[] ): Promise<void> { await browser.tabs.remove(tabIds); await this.client.sendResourceToServer({ resource: "tabs-closed", correlationId, }); } private async sendTabs(correlationId: string): Promise<void> { const tabs = await browser.tabs.query({}); await this.client.sendResourceToServer({ resource: "tabs", correlationId, tabs, }); } private async sendRecentHistory( correlationId: string, searchQuery: string | null = null ): Promise<void> { const historyItems = await browser.history.search({ text: searchQuery ?? "", // Search for all URLs (empty string matches everything) maxResults: 200, // Limit to 200 results startTime: 0, // Search from the beginning of time }); const filteredHistoryItems = historyItems.filter((item) => { return !!item.url; }); await this.client.sendResourceToServer({ resource: "history", correlationId, historyItems: filteredHistoryItems, }); } // Check that the user has granted permission to access the URL's domain. // This will open the options page with a URL parameter to request permission // and throw an error to indicate that the request cannot proceed until permission is granted. private async checkForUrlPermission(url: string | undefined): Promise<void> { if (url) { const origin = new URL(url).origin; const granted = await browser.permissions.contains({ origins: [`${origin}/*`], }); if (!granted) { // Open the options page with a URL parameter to request permission: const optionsUrl = browser.runtime.getURL("options.html"); const urlWithParams = `${optionsUrl}?requestUrl=${encodeURIComponent( url )}`; await browser.tabs.create({ url: urlWithParams }); throw new Error( `The user has not yet granted permission to access the domain "${origin}". A dialog is now being opened to request permission. If the user grants permission, you can try the request again.` ); } } } private async checkForGlobalPermission(permissions: string[]): Promise<void> { const granted = await browser.permissions.contains({ permissions, }); if (!granted) { // Open the options page with a URL parameter to request permission: const optionsUrl = browser.runtime.getURL("options.html"); const urlWithParams = `${optionsUrl}?requestPermissions=${encodeURIComponent( JSON.stringify(permissions) )}`; await browser.tabs.create({ url: urlWithParams }); throw new Error( `The user has not yet granted permission for the following operations: ${permissions.join( ", " )}. A dialog is now being opened to request permission. If the user grants permission, you can try the request again.` ); } } private async sendTabsContent( correlationId: string, tabId: number, offset?: number ): Promise<void> { const tab = await browser.tabs.get(tabId); if (tab.url && (await isDomainInDenyList(tab.url))) { throw new Error(`Domain in tab URL is in the deny list`); } await this.checkForUrlPermission(tab.url); const MAX_CONTENT_LENGTH = 50_000; const results = await browser.tabs.executeScript(tabId, { code: ` (function () { function getLinks() { const linkElements = document.querySelectorAll('a[href]'); return Array.from(linkElements).map(el => ({ url: el.href, text: el.innerText.trim() || el.getAttribute('aria-label') || el.getAttribute('title') || '' })).filter(link => link.text !== '' && link.url.startsWith('https://') && !link.url.includes('#')); } function getTextContent() { let isTruncated = false; let text = document.body.innerText.substring(${Number(offset) || 0}); if (text.length > ${MAX_CONTENT_LENGTH}) { text = text.substring(0, ${MAX_CONTENT_LENGTH}); isTruncated = true; } return { text, isTruncated } } const textContent = getTextContent(); return { links: getLinks(), fullText: textContent.text, isTruncated: textContent.isTruncated, totalLength: document.body.innerText.length }; })(); `, }); const { isTruncated, fullText, links, totalLength } = results[0]; await this.client.sendResourceToServer({ resource: "tab-content", tabId, correlationId, isTruncated, fullText, links, totalLength, }); } private async reorderTabs( correlationId: string, tabOrder: number[] ): Promise<void> { // Reorder the tabs sequentially for (let newIndex = 0; newIndex < tabOrder.length; newIndex++) { const tabId = tabOrder[newIndex]; await browser.tabs.move(tabId, { index: newIndex }); } await this.client.sendResourceToServer({ resource: "tabs-reordered", correlationId, tabOrder, }); } private async findAndHighlightText( correlationId: string, tabId: number, queryPhrase: string ): Promise<void> { const tab = await browser.tabs.get(tabId); if (tab.url && (await isDomainInDenyList(tab.url))) { throw new Error(`Domain in tab URL is in the deny list`); } await this.checkForGlobalPermission(["find"]); const findResults = await browser.find.find(queryPhrase, { tabId, caseSensitive: true, }); // If there are results, highlight them if (findResults.count > 0) { // But first, activate the tab. In firefox, this would also enable // auto-scrolling to the highlighted result. await browser.tabs.update(tabId, { active: true }); browser.find.highlightResults({ tabId, }); } await this.client.sendResourceToServer({ resource: "find-highlight-result", correlationId, noOfResults: findResults.count, }); } private async groupTabs( correlationId: string, tabIds: number[], isCollapsed: boolean, groupColor: browser.tabGroups.Color, groupTitle: string ): Promise<void> { const groupId = await browser.tabs.group({ tabIds, }); let tabGroup = await browser.tabGroups.update(groupId, { collapsed: isCollapsed, color: groupColor, title: groupTitle, }); await this.client.sendResourceToServer({ resource: "new-tab-group", correlationId, groupId: tabGroup.id, }); } }

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/eyalzh/browser-control-mcp'

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