@kazuph/mcp-browser-tabs
Version:
MCP server for retrieving Chrome browser tabs information
239 lines (237 loc) ⢠8.69 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
// Get Chrome tabs using legacy window/tab index method
async function getChromeTabsLegacy() {
const script = `
tell application "Google Chrome"
set windowList to windows
set output to ""
repeat with windowIndex from 1 to count of windowList
set theWindow to item windowIndex of windowList
set activeTabIndex to active tab index of theWindow
set tabList to tabs of theWindow
repeat with tabIndexInWindow from 1 to count of tabList
set theTab to item tabIndexInWindow of tabList
set isActive to (tabIndexInWindow = activeTabIndex)
set output to output & windowIndex & "|||" & tabIndexInWindow & "|||" & isActive & "|||" & (title of theTab) & "|||" & (URL of theTab) & "\\n"
end repeat
end repeat
return output
end tell
`;
try {
const { stdout } = await execAsync(`osascript -e '${script}'`);
const tabsData = stdout
.trim()
.split("\n")
.filter((line) => line.length > 0)
.map((line) => {
const [windowIndex, tabIndex, isActive, title, url] = line.split("|||");
return {
windowIndex: Number.parseInt(windowIndex, 10),
tabIndex: Number.parseInt(tabIndex, 10),
isActive: isActive === "true",
title: title || "",
url: url || "",
};
});
// Group by windows
const windowMap = new Map();
for (const tabData of tabsData) {
if (!windowMap.has(tabData.windowIndex)) {
windowMap.set(tabData.windowIndex, {
windowIndex: tabData.windowIndex,
tabs: [],
});
}
const window = windowMap.get(tabData.windowIndex);
window.tabs.push({
windowIndex: tabData.windowIndex,
tabIndex: tabData.tabIndex,
title: tabData.title,
url: tabData.url,
isActive: tabData.isActive,
});
}
return Array.from(windowMap.values());
}
catch (error) {
throw new Error(`Failed to get Chrome tabs: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Close tab by window/tab index
async function closeChromeTabByIndex(windowIndex, tabIndex) {
const script = `
tell application "Google Chrome"
try
set targetWindow to window ${windowIndex}
set targetTab to tab ${tabIndex} of targetWindow
close targetTab
on error errMsg
return "Error: " & errMsg
end try
end tell
`;
try {
await execAsync(`osascript -e '${script}'`);
}
catch (error) {
throw new Error(`Failed to close Chrome tab: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Activate tab by window/tab index
async function activateChromeTabByIndex(windowIndex, tabIndex) {
const script = `
tell application "Google Chrome"
try
set targetWindow to window ${windowIndex}
set (active tab index of targetWindow) to ${tabIndex}
set index of targetWindow to 1
on error errMsg
return "Error: " & errMsg
end try
end tell
`;
try {
await execAsync(`osascript -e '${script}'`);
}
catch (error) {
throw new Error(`Failed to activate Chrome tab: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Schema definitions
const ListToolsSchema = z.object({
method: z.literal("tools/list"),
});
const CallToolSchema = z.object({
method: z.literal("tools/call"),
params: z.object({
name: z.string(),
arguments: z.record(z.unknown()).optional(),
}),
});
// Server setup
const server = new Server({
name: "mcp-browser-tabs-legacy",
version: "2.0.0-legacy",
}, {
capabilities: {
tools: {},
},
});
// Tools list handler
server.setRequestHandler(ListToolsSchema, async (request, extra) => {
const tools = [
{
name: "get_tabs",
description: "Get all open tabs from Google Chrome browser using window/tab index method. Shows tab information with window and tab indices for reliable operations.",
inputSchema: zodToJsonSchema(z.object({})),
},
{
name: "close_tab",
description: "Close a specific tab using window/tab index. Uses 1-based indexing (window 1, tab 1, etc.).",
inputSchema: zodToJsonSchema(z.object({
windowIndex: z.number().int().positive().describe("Window index (1-based)"),
tabIndex: z.number().int().positive().describe("Tab index (1-based)"),
})),
},
{
name: "activate_tab",
description: "Activate (focus) a specific tab using window/tab index. Brings the tab to the front and makes it active.",
inputSchema: zodToJsonSchema(z.object({
windowIndex: z.number().int().positive().describe("Window index (1-based)"),
tabIndex: z.number().int().positive().describe("Tab index (1-based)"),
})),
},
];
return { tools };
});
// Tool execution handler
server.setRequestHandler(CallToolSchema, async (request, extra) => {
try {
const { name } = request.params;
if (name === "get_tabs") {
const windows = await getChromeTabsLegacy();
const formattedOutput = windows
.map((window) => {
const activeTab = window.tabs.find(tab => tab.isActive);
const activeIndicator = activeTab ? ` (Active: ${activeTab.windowIndex}-${activeTab.tabIndex})` : "";
return `Window ${window.windowIndex}${activeIndicator}:
${window.tabs
.map((tab) => {
const activeMarker = tab.isActive ? " ā
" : "";
return ` ${tab.windowIndex}-${tab.tabIndex}. ${tab.title}${activeMarker}
${tab.url}`;
})
.join("\n")}`;
})
.join("\n\n");
const totalTabs = windows.reduce((sum, window) => sum + window.tabs.length, 0);
return {
content: [
{
type: "text",
text: `Found ${totalTabs} open tabs in Chrome:
${formattedOutput}
š LEGACY MODE: Using window/tab index method
- Use close_tab or activate_tab with windowIndex and tabIndex
- Example: window 1, tab 3 = windowIndex: 1, tabIndex: 3`,
},
],
};
}
if (name === "close_tab") {
const { windowIndex, tabIndex } = request.params.arguments;
await closeChromeTabByIndex(windowIndex, tabIndex);
return {
content: [
{
type: "text",
text: `ā
Successfully closed tab at window ${windowIndex}, tab ${tabIndex}`,
},
],
};
}
if (name === "activate_tab") {
const { windowIndex, tabIndex } = request.params.arguments;
await activateChromeTabByIndex(windowIndex, tabIndex);
return {
content: [
{
type: "text",
text: `ā
Successfully activated tab at window ${windowIndex}, tab ${tabIndex}`,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
}
catch (error) {
return {
content: [
{
type: "text",
text: `ā Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// Server startup
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Browser Tabs MCP server (Legacy Mode) running on stdio");
}
runServer().catch((error) => {
process.stderr.write(`Fatal error running server: ${error}\n`);
process.exit(1);
});
//# sourceMappingURL=index-legacy-only.js.map