Skip to main content
Glama

@pokutuna/mcp-chrome-tabs

by pokutuna
mcp.ts8.11 kB
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { Browser, Tab, TabRef, TabContent, getInterface, } from "./browser/browser.js"; import { readFile } from "fs/promises"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { createHash } from "crypto"; import * as view from "./view.js"; import { runDefuddleInWorker } from "./util.js"; export type McpServerOptions = { applicationName: string; excludeHosts: string[]; checkInterval: number; browser: Browser; maxContentChars: number; extractionTimeout: number; }; function isExcludedHost(url: string, excludeHosts: string[]): boolean { const u = new URL(url); return excludeHosts.some( (d) => u.hostname === d || u.hostname.endsWith("." + d) ); } async function listTabs(opts: McpServerOptions): Promise<Tab[]> { const browser = getInterface(opts.browser); const tabs = await browser.getTabList(opts.applicationName); return tabs.filter((t) => !isExcludedHost(t.url, opts.excludeHosts)); } async function getTab( tabRef: TabRef | null, opts: McpServerOptions ): Promise<TabContent> { const browser = getInterface(opts.browser); const raw = await browser.getPageContent(opts.applicationName, tabRef); if (isExcludedHost(raw.url, opts.excludeHosts)) { throw new Error("Content not available for excluded host"); } try { const content = await runDefuddleInWorker( raw.content, raw.url, opts.extractionTimeout ); return { title: raw.title, url: raw.url, content, }; } catch (error) { throw new Error( `Failed to extract content: ${error instanceof Error ? error.message : String(error)}` ); } } async function packageVersion(): Promise<string> { const packageJsonText = await readFile( join(dirname(fileURLToPath(import.meta.url)), "../package.json"), "utf8" ); const packageJson = JSON.parse(packageJsonText); return packageJson.version; } function hashTabList(tabs: Tab[]): string { const sortedTabs = tabs.slice().sort((a, b) => { if (a.windowId !== b.windowId) return a.windowId < b.windowId ? -1 : 1; if (a.tabId !== b.tabId) return a.tabId < b.tabId ? -1 : 1; return 0; }); const dump = sortedTabs .map((tab) => `${tab.windowId}:${tab.tabId}:${tab.title}:${tab.url}`) .join("|"); return createHash("sha256").update(dump, "utf8").digest("hex"); } export async function createMcpServer( options: McpServerOptions ): Promise<McpServer> { const server = new McpServer( { name: "chrome-tabs", title: "Chrome Tabs", version: await packageVersion(), }, { instructions: "Use this server to access the user's open browser tabs.", capabilities: { resources: { listChanged: true, }, }, debouncedNotificationMethods: ["notifications/resources/list_changed"], } ); server.registerTool( "list_tabs", { title: "List Tabs", description: "List all open tabs in the user's browser with their titles and tab references.", inputSchema: { includeUrl: z .boolean() .optional() .default(false) .describe( "Include URLs in the output. Enable only when you need to reference specific URLs. (default: false, hostnames always included)" ), }, }, async (args) => { const { includeUrl } = args; const tabs = await listTabs(options); return { content: [ { type: "text", text: view.formatList(tabs, includeUrl), }, ], }; } ); server.registerTool( "read_tab_content", { title: "Read Tab Content", description: "Get readable content from a tab in the user's browser. Provide ID (from list_tabs output) to read a specific tab, or omit for the active tab.", inputSchema: { id: z .string() .optional() .describe( "Tab reference from list_tabs output (e.g: ID:12345:67890). If omitted, uses the currently active tab." ), startIndex: z .number() .int() .nonnegative() .optional() .default(0) .describe( "Starting character position for content extraction (default: 0)" ), }, }, async (args) => { const { id, startIndex } = args; const tab = await getTab(id ? view.parseTabRef(id) : null, options); return { content: [ { type: "text", text: view.formatTabContent( tab, startIndex, options.maxContentChars ), }, ], }; } ); server.registerTool( "open_in_new_tab", { title: "Open in New Tab", description: "Open a URL in a new tab to present content or enable user interaction with webpages", inputSchema: { url: z.string().url().describe("URL to open in the browser"), }, }, async (args) => { const { url } = args; const browser = getInterface(options.browser); const tabRef = await browser.openURL(options.applicationName, url); const tabId = `ID:${tabRef.windowId}:${tabRef.tabId}`; return { content: [ { type: "text", text: `Successfully opened URL in new tab. Tab: \`${tabId}\``, }, ], }; } ); server.registerResource( "current_tab", "tab://current", { title: "Active Browser Tab", description: "Content of the currently active tab in the user's browser", mimeType: "text/markdown", }, async (uri) => { const tab = await getTab(null, options); // TODO: Add pagination support for resources (startIndex parameter) const text = view.formatTabContent(tab, 0, undefined); return { contents: [ { uri: uri.href, text, mimeType: "text/markdown", }, ], }; } ); // Register tab://{windowId}/{tabId} resources only when checkInterval > 0 // In 2025-10, few MCP clients support resource subscriptions, so this is disabled by default if (options.checkInterval > 0) { server.registerResource( "tabs", new ResourceTemplate(view.uriTemplate, { list: async () => { const tabs = await listTabs(options); return { resources: tabs.map((tab) => ({ uri: view.formatUri(tab), name: view.formatTabName(tab), mimeType: "text/markdown", })), }; }, }), { title: "Browser Tabs", description: "Content of a specific tab in the user's browser", mimeType: "text/markdown", }, async (uri, { windowId, tabId }) => { const tabRef: TabRef = { windowId: String(windowId), tabId: String(tabId), }; const tab = await getTab(tabRef, options); // TODO: Add pagination support for resources (startIndex parameter) const text = view.formatTabContent(tab, 0, undefined); return { contents: [ { uri: uri.href, mimeType: "text/markdown", text, }, ], }; } ); } if (options.checkInterval > 0) { let lastHash: string = hashTabList(await listTabs(options)); const check = async () => { try { const hash = hashTabList(await listTabs(options)); if (hash !== lastHash) { server.sendResourceListChanged(); lastHash = hash; } } catch (error) { console.error("Error during periodic tab list update:", error); } // Use setTimeout instead of setInterval to avoid overlapping calls setTimeout(check, options.checkInterval); }; check(); } return server; }

Latest Blog Posts

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/pokutuna/mcp-chrome-tabs'

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