@dacodedbeat/mcp-browser-buddy 0.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,734 +1,180 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
4
  import { z } from "zod";
6
- import { spawn } from "node:child_process";
7
- // ─── Constants ────────────────────────────────────────────────────────────────
8
- const ALLOWED_BROWSERS = [
9
- "Google Chrome",
10
- "Brave Browser",
11
- "Chromium",
12
- "Google Chrome Canary",
13
- ];
14
- // ─── Logger ───────────────────────────────────────────────────────────────────
15
- function log(level, message) {
16
- const timestamp = new Date().toISOString();
17
- process.stderr.write(`[${timestamp}] [${level}] ${message}\n`);
18
- }
19
- // ASCII control chars — never appear in URLs or page titles
20
- const FIELD_SEP = "\x1f";
21
- const RECORD_SEP = "\x1e";
22
- // ─── AppleScript helpers ──────────────────────────────────────────────────────
23
- // Safe AppleScript string literal from arbitrary JS string — no shell quoting needed
24
- function asAS(value) {
25
- if (value === "")
26
- return '""';
27
- const tokens = [];
28
- let buf = "";
29
- for (const ch of value) {
30
- const code = ch.charCodeAt(0);
31
- if (ch === '"') {
32
- if (buf) {
33
- tokens.push(`"${buf}"`);
34
- buf = "";
35
- }
36
- tokens.push("quote");
37
- }
38
- else if (code < 32 || code === 127) {
39
- if (buf) {
40
- tokens.push(`"${buf}"`);
41
- buf = "";
42
- }
43
- tokens.push(`(ASCII character ${code})`);
44
- }
45
- else {
46
- buf += ch;
47
- }
48
- }
49
- if (buf)
50
- tokens.push(`"${buf}"`);
51
- return tokens.length > 0 ? tokens.join(" & ") : '""';
52
- }
53
- function runAppleScript(script, timeoutMs = 30000) {
54
- return new Promise((resolve, reject) => {
55
- log("INFO", `Executing AppleScript: ${script.substring(0, 50)}...`);
56
- const proc = spawn("osascript", ["-"]);
57
- let stdout = "";
58
- let stderr = "";
59
- const timer = setTimeout(() => {
60
- proc.kill();
61
- log("ERROR", `AppleScript timed out after ${timeoutMs}ms`);
62
- reject(new Error(`osascript timed out after ${timeoutMs}ms (browser may be showing a dialog)`));
63
- }, timeoutMs);
64
- proc.stdout.on("data", (d) => (stdout += d.toString("utf8")));
65
- proc.stderr.on("data", (d) => (stderr += d.toString("utf8")));
66
- proc.on("close", (code) => {
67
- clearTimeout(timer);
68
- if (code === 0) {
69
- resolve(stdout);
70
- }
71
- else {
72
- const err = stderr.trim() || `osascript exited with code ${code}`;
73
- log("ERROR", `AppleScript failed: ${err}`);
74
- reject(new Error(err));
75
- }
76
- });
77
- proc.stdin.write(script, "utf8");
78
- proc.stdin.end();
79
- });
80
- }
81
- // AppleScript body: find tab by ID across all windows, run `action`, error if not found.
82
- // `action` has access to: t (tab), w (window), tIdx (1-based position).
83
- function findTabScript(tabId, action) {
84
- return `
85
- set tid to ${asAS(tabId)}
86
- repeat with wIdx from 1 to (count of windows)
87
- set w to window wIdx
88
- repeat with tIdx from 1 to (count of tabs of w)
89
- set t to tab tIdx of w
90
- if (id of t) as string = tid then
91
- ${action}
92
- return
93
- end if
94
- end repeat
95
- end repeat
96
- error "Tab " & tid & " not found"`;
97
- }
98
- // ─── AppleScript operations ───────────────────────────────────────────────────
99
- async function getTabs(browser) {
100
- const script = `
101
- tell application "${browser}"
102
- set fs to (ASCII character 31)
103
- set rs to (ASCII character 30)
104
- set output to ""
105
- set winCount to count of windows
106
- repeat with wIdx from 1 to winCount
107
- set w to window wIdx
108
- set wID to id of w
109
- set wName to name of w
110
- set wMode to mode of w
111
- set activeIdx to active tab index of w
112
- set tabCount to count of tabs of w
113
- repeat with tIdx from 1 to tabCount
114
- set t to tab tIdx of w
115
- set tID to (id of t) as string
116
- set isActive to (tIdx = activeIdx)
117
- set isLoading to loading of t
118
- set output to output & wID & fs & wIdx & fs & wName & fs & wMode & fs & tID & fs & tIdx & fs & isActive & fs & isLoading & fs & (title of t) & fs & (URL of t) & rs
119
- end repeat
120
- end repeat
121
- return output
122
- end tell
123
- `;
124
- const raw = await runAppleScript(script);
125
- const records = raw.split(RECORD_SEP).filter((r) => r.length > 0);
126
- const windowMap = new Map();
127
- for (const record of records) {
128
- const [wId, wIdx, wName, wMode, tId, tIdx, isActive, isLoading, title, url] = record.split(FIELD_SEP);
129
- if (!windowMap.has(wId)) {
130
- windowMap.set(wId, {
131
- windowId: wId,
132
- windowIndex: parseInt(wIdx, 10),
133
- windowName: wName ?? "",
134
- mode: wMode ?? "normal",
135
- tabs: [],
136
- });
137
- }
138
- windowMap.get(wId).tabs.push({
139
- id: tId,
140
- windowId: wId,
141
- title: title ?? "",
142
- url: url ?? "",
143
- isActive: isActive === "true",
144
- loading: isLoading === "true",
145
- windowIndex: parseInt(wIdx, 10),
146
- tabIndex: parseInt(tIdx, 10),
147
- });
148
- }
149
- return Array.from(windowMap.values());
150
- }
151
- async function getActiveTab(browser) {
152
- const script = `
153
- tell application "${browser}"
154
- if (count of windows) = 0 then return ""
155
- set w to window 1
156
- set wID to id of w
157
- set wIdx to index of w
158
- set t to active tab of w
159
- set tID to (id of t) as string
160
- set tIdx to active tab index of w
161
- set fs to (ASCII character 31)
162
- return wID & fs & wIdx & fs & tID & fs & tIdx & fs & (loading of t) & fs & (title of t) & fs & (URL of t)
163
- end tell
164
- `;
165
- const raw = await runAppleScript(script);
166
- if (!raw.trim())
167
- return null;
168
- const [wId, wIdx, tId, tIdx, isLoading, title, url] = raw.trim().split(FIELD_SEP);
169
- return {
170
- id: tId,
171
- windowId: wId,
172
- title: title ?? "",
173
- url: url ?? "",
174
- isActive: true,
175
- loading: isLoading === "true",
176
- windowIndex: parseInt(wIdx, 10),
177
- tabIndex: parseInt(tIdx, 10),
178
- };
179
- }
180
- async function activateTab(browser, tabId) {
181
- const action = ["set active tab index of w to tIdx", "set index of w to 1", "activate"].join("\n ");
182
- await runAppleScript(`tell application "${browser}"\n${findTabScript(tabId, action)}\nend tell`);
183
- }
184
- async function goBack(browser, tabId) {
185
- await runAppleScript(`tell application "${browser}"\n${findTabScript(tabId, "go back t")}\nend tell`);
186
- }
187
- async function goForward(browser, tabId) {
188
- await runAppleScript(`tell application "${browser}"\n${findTabScript(tabId, "go forward t")}\nend tell`);
189
- }
190
- async function navigateTab(browser, tabId, url) {
191
- await runAppleScript(`tell application "${browser}"\n${findTabScript(tabId, `set URL of t to ${asAS(url)}`)}\nend tell`);
192
- }
193
- async function executeScript(browser, tabId, javascript) {
194
- const action = [
195
- `set jsCode to ${asAS(javascript)}`,
196
- "set jsResult to execute t javascript jsCode",
197
- "if jsResult is missing value then",
198
- ' return "null"',
199
- "end if",
200
- "return jsResult as string",
201
- ].join("\n ");
202
- const result = await runAppleScript(`tell application "${browser}"\n${findTabScript(tabId, action)}\nend tell`);
203
- return result.trim();
204
- }
205
- async function closeTabs(browser, tabIds) {
206
- const script = `
207
- tell application "${browser}"
208
- set tids to {${tabIds.map(asAS).join(", ")}}
209
- set closedCount to 0
210
- set winCount to count of windows
211
- repeat with wIdx from 1 to winCount
212
- -- Guard: closing the last tab in a window auto-closes it, shrinking the list
213
- if wIdx > (count of windows) then exit repeat
214
- set w to window wIdx
215
- set tabCount to count of tabs of w
216
- -- Backwards: closing tab N shifts indices N+1… down, never N-1…1
217
- repeat with tIdx from tabCount to 1 by -1
218
- try
219
- set t to tab tIdx of w
220
- if (id of t) as string is in tids then
221
- close t
222
- set closedCount to closedCount + 1
223
- end if
224
- end try
225
- end repeat
226
- end repeat
227
- return closedCount
228
- end tell
229
- `;
230
- const result = await runAppleScript(script);
231
- return { count: parseInt(result.trim(), 10) || 0 };
232
- }
233
- async function closeWindow(browser, windowId) {
234
- await runAppleScript(`
235
- tell application "${browser}"
236
- set wid to ${asAS(windowId)}
237
- repeat with i from 1 to (count of windows)
238
- set w to window i
239
- if (id of w) as string = wid then
240
- close w
241
- return
242
- end if
243
- end repeat
244
- error "Window " & wid & " not found"
245
- end tell`);
246
- }
247
- // Runs a single AppleScript command verb on all tabs matching the given IDs
248
- async function bulkTabCommand(browser, tabIds, command) {
249
- await runAppleScript(`
250
- tell application "${browser}"
251
- set tids to {${tabIds.map(asAS).join(", ")}}
252
- repeat with wIdx from 1 to (count of windows)
253
- set w to window wIdx
254
- repeat with tIdx from 1 to (count of tabs of w)
255
- set t to tab tIdx of w
256
- if (id of t) as string is in tids then
257
- ${command} t
258
- end if
259
- end repeat
260
- end repeat
261
- end tell`);
262
- }
263
- const reloadTabs = (b, ids) => bulkTabCommand(b, ids, "reload");
264
- const stopTabs = (b, ids) => bulkTabCommand(b, ids, "stop");
265
- async function createWindow(browser, mode) {
266
- const result = await runAppleScript(`
267
- tell application "${browser}"
268
- set newWin to make new window with properties {mode: ${asAS(mode)}}
269
- return (id of newWin) as string
270
- end tell`);
271
- return result.trim();
272
- }
273
- async function openTabs(browser, urls, windowId, activate = false) {
274
- const windowSetup = windowId
275
- ? `set wid to ${asAS(windowId)}
276
- repeat with i from 1 to (count of windows)
277
- set w to window i
278
- if (id of w) as string = wid then
279
- set targetWin to w
280
- exit repeat
281
- end if
282
- end repeat
283
- if targetWin is missing value then error "Window " & wid & " not found"`
284
- : `if (count of windows) = 0 then make new window
285
- set targetWin to window 1`;
286
- const activateBlock = activate
287
- ? `set active tab index of targetWin to (count of tabs of targetWin)
288
- set index of targetWin to 1
289
- activate`
290
- : "";
291
- const result = await runAppleScript(`
292
- tell application "${browser}"
293
- set targetWin to missing value
294
- ${windowSetup}
295
- set newIds to {}
296
- repeat with uRef in {${urls.map(asAS).join(", ")}}
297
- set u to contents of uRef
298
- set newTab to make new tab at end of tabs of targetWin with properties {URL:u}
299
- copy (id of newTab) as string to end of newIds
300
- end repeat
301
- ${activateBlock}
302
- set AppleScript's text item delimiters to ","
303
- return newIds as string
304
- end tell`);
305
- return result
306
- .trim()
307
- .split(",")
308
- .filter((id) => id.length > 0);
309
- }
310
- // ─── Zod schemas ──────────────────────────────────────────────────────────────
311
- const BrowserParam = z
5
+ import { createRequire } from "node:module";
6
+ import * as chrome from "./applescript.js";
7
+ import { ALLOWED_BROWSERS } from "./applescript.js";
8
+ import { DEFAULT_LIMIT, MAX_LIMIT, TAB_LINE_LEGEND, countTabs, filterTabs, formatClosed, formatDuplicates, formatTabs, summarizeWindows, tabLine, } from "./format.js";
9
+ // Keep serverInfo in sync with the published package version (dist/ -> ../package.json)
10
+ const PKG_VERSION = createRequire(import.meta.url)("../package.json").version;
11
+ // ─── Shared schema pieces ─────────────────────────────────────────────────────
12
+ const browserField = z
312
13
  .enum(ALLOWED_BROWSERS)
313
14
  .default("Google Chrome")
314
- .describe('Browser to control. One of: "Google Chrome", "Brave Browser", "Chromium", "Google Chrome Canary". Defaults to "Google Chrome".');
315
- const TabIdParam = z
15
+ .describe(`Browser to control. One of: ${ALLOWED_BROWSERS.join(", ")}. Defaults to Google Chrome.`);
16
+ const tabIdField = z
316
17
  .string()
317
18
  .regex(/^\d+$/, "Tab ID must be digits only")
318
- .describe("Unique numeric string identifying a tab. Extract from [Tab ID: XXXXX] in get_tabs output, or from the tabs_json block.");
319
- const TabIdSchema = z.object({ tabId: TabIdParam, browser: BrowserParam });
320
- const BulkTabSchema = z.object({
321
- tabIds: z.array(TabIdParam).min(1).describe("List of Tab IDs to operate on."),
322
- browser: BrowserParam,
323
- });
324
- const WindowIdParam = z
19
+ .describe("Numeric tab ID: the first token of each line in get_tabs / search_tabs output.");
20
+ const windowIdField = z
325
21
  .string()
326
22
  .regex(/^\d+$/)
327
- .describe("Window ID. Get from tabs_json in get_tabs output.");
328
- const CloseWindowSchema = z.object({ windowId: WindowIdParam, browser: BrowserParam });
329
- const OpenTabsSchema = z.object({
23
+ .describe("Window ID: the `[ID: …]` in a window header of get_tabs output.");
24
+ const paging = {
25
+ windowId: windowIdField.optional().describe("Only include tabs in this window."),
26
+ limit: z
27
+ .number()
28
+ .int()
29
+ .min(1)
30
+ .max(MAX_LIMIT)
31
+ .default(DEFAULT_LIMIT)
32
+ .describe(`Max tabs to return (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}).`),
33
+ offset: z.number().int().min(0).default(0).describe("Tabs to skip, for paging."),
34
+ };
35
+ const tabIdsField = z.array(tabIdField).min(1).describe("List of Tab IDs to operate on.");
36
+ // ─── Tool plumbing ────────────────────────────────────────────────────────────
37
+ // Explicit values: MCP defaults destructiveHint/openWorldHint to true when omitted.
38
+ const READ = {
39
+ readOnlyHint: true,
40
+ destructiveHint: false,
41
+ idempotentHint: true,
42
+ openWorldHint: false,
43
+ };
44
+ const WRITE = {
45
+ readOnlyHint: false,
46
+ destructiveHint: false,
47
+ idempotentHint: false,
48
+ openWorldHint: false,
49
+ };
50
+ const DESTRUCTIVE = { ...WRITE, destructiveHint: true };
51
+ const text = (s, isError = false) => ({
52
+ content: [{ type: "text", text: s }],
53
+ ...(isError && { isError }),
54
+ });
55
+ const server = new McpServer({ name: "mcp-browser-buddy", version: PKG_VERSION });
56
+ // Adding a tool = one registerTool call: schema, annotations and handler live together.
57
+ function tool(name, title, description, annotations, inputSchema, handler) {
58
+ server.registerTool(name, {
59
+ title,
60
+ description,
61
+ inputSchema: inputSchema,
62
+ annotations: { title, ...annotations },
63
+ }, async (args) => {
64
+ try {
65
+ return text(await handler(args));
66
+ }
67
+ catch (error) {
68
+ return text(`Error: ${error instanceof Error ? error.message : String(error)}`, true);
69
+ }
70
+ });
71
+ }
72
+ // ─── Tools ────────────────────────────────────────────────────────────────────
73
+ tool("get_tabs", "Get Tabs", "List open tabs grouped by window, one line each (Tab ID first). Paged. With many tabs, call with summary=true first, then drill into a window with windowId.", READ, {
74
+ summary: z
75
+ .boolean()
76
+ .default(false)
77
+ .describe("If true, return one block per window (id, tab count, active tab, top domains) instead of every tab."),
78
+ ...paging,
79
+ browser: browserField,
80
+ }, async ({ browser, windowId, summary, limit, offset }) => {
81
+ const { windows, skipped } = await chrome.getTabs(browser, windowId);
82
+ const head = `${countTabs(windows)} tab(s) in ${windows.length} window(s) of ${browser}.`;
83
+ const warn = skipped ? ` (${skipped} window(s) could not be read and were skipped)` : "";
84
+ const body = summary ? summarizeWindows(windows) : formatTabs(windows, limit, offset);
85
+ return `${head}${warn}\n${TAB_LINE_LEGEND}\n\n${body}`;
86
+ });
87
+ tool("search_tabs", "Search Tabs", "Find tabs whose title or URL contains every word of the query (case-insensitive). Paged; same line format as get_tabs.", READ, {
88
+ query: z.string().describe("Words to search for in tab titles and URLs; all must match."),
89
+ ...paging,
90
+ browser: browserField,
91
+ }, async ({ browser, query, windowId, limit, offset }) => {
92
+ const { windows } = await chrome.getTabs(browser, windowId);
93
+ const matched = filterTabs(windows, query);
94
+ return `Found ${countTabs(matched)} tab(s) matching "${query}".\n${TAB_LINE_LEGEND}\n\n${formatTabs(matched, limit, offset, "matching tab")}`;
95
+ });
96
+ tool("find_duplicates", "Find Duplicate Tabs", "Group tabs that have the same URL (ignoring #fragment and trailing slash). Cheaper than listing every tab; follow with close_tabs on the redundant IDs.", READ, {
97
+ windowId: windowIdField.optional().describe("Only look in this window."),
98
+ browser: browserField,
99
+ }, async ({ browser, windowId }) => formatDuplicates((await chrome.getTabs(browser, windowId)).windows));
100
+ tool("get_active_tab", "Get Active Tab", "Get details about the currently focused tab in the frontmost window.", READ, { browser: browserField }, async ({ browser }) => {
101
+ const tab = await chrome.getActiveTab(browser);
102
+ return tab
103
+ ? `Active tab in frontmost window:\n${tabLine(tab)}\n[Window ID: ${tab.windowId}]`
104
+ : "No active window found.";
105
+ });
106
+ tool("open_tabs", "Open Tabs", "Open one or more URLs in new tabs.", { ...WRITE, openWorldHint: true }, {
330
107
  urls: z.array(z.string().url()).min(1).describe("List of fully-qualified URLs to open."),
331
- windowId: WindowIdParam.optional().describe("Window ID to open the tabs in. Omit to use the frontmost window."),
108
+ windowId: windowIdField
109
+ .optional()
110
+ .describe("Window to open the tabs in. Omit to use the frontmost window."),
332
111
  activate: z
333
112
  .boolean()
334
113
  .default(false)
335
114
  .describe("If true, focus the last opened tab and bring its window to the front."),
336
- browser: BrowserParam,
115
+ browser: browserField,
116
+ }, async ({ browser, urls, windowId, activate }) => {
117
+ const ids = await chrome.openTabs(browser, urls, windowId, activate);
118
+ return `Opened ${ids.length} tab(s). New Tab IDs: ${ids.join(", ")}`;
337
119
  });
338
- const CreateWindowSchema = z.object({
120
+ tool("close_tabs", "Close Tabs", "Close one or more tabs by Tab ID. Returns the title and URL of every closed tab (reopen with open_tabs) and any IDs that were not found (e.g. stale after a listing).", DESTRUCTIVE, { tabIds: tabIdsField, browser: browserField }, async ({ browser, tabIds }) => {
121
+ const closed = await chrome.closeTabs(browser, tabIds);
122
+ const missing = tabIds.filter((id) => !closed.some((t) => t.id === id));
123
+ return `Closed ${closed.length} tab(s).${missing.length ? ` Not found: ${missing.join(", ")}.` : ""}${formatClosed(closed)}`;
124
+ });
125
+ tool("close_window", "Close Window", "Close an entire window and all its tabs. Returns the title and URL of every tab that was in it (reopen with open_tabs).", DESTRUCTIVE, { windowId: windowIdField, browser: browserField }, async ({ browser, windowId }) => {
126
+ const { windows } = await chrome.getTabs(browser, windowId);
127
+ await chrome.closeWindow(browser, windowId);
128
+ return `Closed window [ID: ${windowId}]${formatClosed(windows.flatMap((w) => w.tabs))}`;
129
+ });
130
+ tool("create_window", "Create Window", "Open a new browser window, optionally in incognito mode.", WRITE, {
339
131
  mode: z
340
132
  .enum(["normal", "incognito"])
341
133
  .default("normal")
342
134
  .describe('Window mode: "normal" or "incognito".'),
343
- browser: BrowserParam,
135
+ browser: browserField,
136
+ }, async ({ browser, mode }) => `Created ${mode} window [ID: ${await chrome.createWindow(browser, mode)}]`);
137
+ tool("activate_tab", "Activate Tab", "Bring a tab into focus.", { ...WRITE, idempotentHint: true }, { tabId: tabIdField, browser: browserField }, async ({ browser, tabId }) => {
138
+ await chrome.activateTab(browser, tabId);
139
+ return `Activated tab [Tab ID: ${tabId}]`;
140
+ });
141
+ // Reload can resubmit a POST or drop unsaved form input, so it is not a harmless read.
142
+ tool("reload_tabs", "Reload Tabs", "Reload one or more tabs by Tab ID.", { ...DESTRUCTIVE, idempotentHint: true }, { tabIds: tabIdsField, browser: browserField }, async ({ browser, tabIds }) => {
143
+ await chrome.reloadTabs(browser, tabIds);
144
+ return `Reloaded ${tabIds.length} tab(s).`;
344
145
  });
345
- const NavigateTabSchema = z.object({
346
- tabId: TabIdParam,
146
+ tool("stop_tabs", "Stop Tabs", "Stop loading one or more tabs by Tab ID.", { ...WRITE, idempotentHint: true }, { tabIds: tabIdsField, browser: browserField }, async ({ browser, tabIds }) => {
147
+ await chrome.stopTabs(browser, tabIds);
148
+ return `Stopped ${tabIds.length} tab(s).`;
149
+ });
150
+ tool("go_back", "Go Back", "Navigate back in the tab's history.", WRITE, { tabId: tabIdField, browser: browserField }, async ({ browser, tabId }) => {
151
+ await chrome.goBack(browser, tabId);
152
+ return `Navigated back in tab [Tab ID: ${tabId}]`;
153
+ });
154
+ tool("go_forward", "Go Forward", "Navigate forward in the tab's history.", WRITE, { tabId: tabIdField, browser: browserField }, async ({ browser, tabId }) => {
155
+ await chrome.goForward(browser, tabId);
156
+ return `Navigated forward in tab [Tab ID: ${tabId}]`;
157
+ });
158
+ tool("navigate_tab", "Navigate Tab", "Change the URL of an existing tab.", { ...DESTRUCTIVE, openWorldHint: true }, {
159
+ tabId: tabIdField,
347
160
  url: z
348
161
  .string()
349
162
  .url()
350
163
  .describe("Fully-qualified URL to navigate the tab to, e.g. https://example.com"),
351
- browser: BrowserParam,
164
+ browser: browserField,
165
+ }, async ({ browser, tabId, url }) => {
166
+ await chrome.navigateTab(browser, tabId, url);
167
+ return `Navigated tab [Tab ID: ${tabId}] to ${url}`;
352
168
  });
353
- const ExecuteScriptSchema = z.object({
354
- tabId: TabIdParam,
169
+ tool("execute_script", "Execute Script", "Run JavaScript in a tab's page context. Requires Chrome's View > Developer > Allow JavaScript from Apple Events to be enabled.", { ...DESTRUCTIVE, openWorldHint: true }, {
170
+ tabId: tabIdField,
355
171
  javascript: z
356
172
  .string()
357
173
  .describe('JavaScript expression or statement(s) to run in the tab\'s page context. The return value is coerced to string via AppleScript. Returns "null" if the result is undefined or null. For complex return types (arrays, objects), wrap in JSON.stringify(...) to get valid JSON back.'),
358
- browser: BrowserParam,
359
- });
360
- const SearchTabsSchema = z.object({
361
- query: z.string().describe("Keyword to search for in tab titles and URLs."),
362
- browser: BrowserParam,
363
- });
364
- const GetTabsSchema = z.object({ browser: BrowserParam });
365
- // ─── MCP server ───────────────────────────────────────────────────────────────
366
- const server = new Server({ name: "mcp-browser-buddy", version: "0.0.1" }, { capabilities: { tools: {} } });
367
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
368
- tools: [
369
- {
370
- name: "get_tabs",
371
- description: "List every open tab across all windows.",
372
- inputSchema: z.toJSONSchema(GetTabsSchema),
373
- annotations: {
374
- title: "Get Tabs",
375
- readOnlyHint: true,
376
- destructiveHint: false,
377
- idempotentHint: true,
378
- openWorldHint: false,
379
- },
380
- },
381
- {
382
- name: "search_tabs",
383
- description: "Search for tabs by keyword in title or URL.",
384
- inputSchema: z.toJSONSchema(SearchTabsSchema),
385
- annotations: {
386
- title: "Search Tabs",
387
- readOnlyHint: true,
388
- destructiveHint: false,
389
- idempotentHint: true,
390
- openWorldHint: false,
391
- },
392
- },
393
- {
394
- name: "get_active_tab",
395
- description: "Get details about the currently focused tab in the frontmost window.",
396
- inputSchema: z.toJSONSchema(GetTabsSchema),
397
- annotations: {
398
- title: "Get Active Tab",
399
- readOnlyHint: true,
400
- destructiveHint: false,
401
- idempotentHint: true,
402
- openWorldHint: false,
403
- },
404
- },
405
- {
406
- name: "open_tabs",
407
- description: "Open one or more URLs in new tabs.",
408
- inputSchema: z.toJSONSchema(OpenTabsSchema),
409
- annotations: {
410
- title: "Open Tabs",
411
- readOnlyHint: false,
412
- destructiveHint: false,
413
- idempotentHint: false,
414
- openWorldHint: true,
415
- },
416
- },
417
- {
418
- name: "close_tabs",
419
- description: "Permanently close one or more tabs by Tab ID.",
420
- inputSchema: z.toJSONSchema(BulkTabSchema),
421
- annotations: {
422
- title: "Close Tabs",
423
- readOnlyHint: false,
424
- destructiveHint: true,
425
- idempotentHint: false,
426
- openWorldHint: false,
427
- },
428
- },
429
- {
430
- name: "close_window",
431
- description: "Close an entire window and all its tabs.",
432
- inputSchema: z.toJSONSchema(CloseWindowSchema),
433
- annotations: {
434
- title: "Close Window",
435
- readOnlyHint: false,
436
- destructiveHint: true,
437
- idempotentHint: false,
438
- openWorldHint: false,
439
- },
440
- },
441
- {
442
- name: "create_window",
443
- description: "Open a new browser window, optionally in incognito mode.",
444
- inputSchema: z.toJSONSchema(CreateWindowSchema),
445
- annotations: {
446
- title: "Create Window",
447
- readOnlyHint: false,
448
- destructiveHint: false,
449
- idempotentHint: false,
450
- openWorldHint: false,
451
- },
452
- },
453
- {
454
- name: "activate_tab",
455
- description: "Bring a tab into focus.",
456
- inputSchema: z.toJSONSchema(TabIdSchema),
457
- annotations: {
458
- title: "Activate Tab",
459
- readOnlyHint: false,
460
- destructiveHint: false,
461
- idempotentHint: true,
462
- openWorldHint: false,
463
- },
464
- },
465
- {
466
- name: "reload_tabs",
467
- description: "Reload one or more tabs by Tab ID.",
468
- inputSchema: z.toJSONSchema(BulkTabSchema),
469
- annotations: {
470
- title: "Reload Tabs",
471
- readOnlyHint: false,
472
- destructiveHint: false,
473
- idempotentHint: true,
474
- openWorldHint: false,
475
- },
476
- },
477
- {
478
- name: "stop_tabs",
479
- description: "Stop loading one or more tabs by Tab ID.",
480
- inputSchema: z.toJSONSchema(BulkTabSchema),
481
- annotations: {
482
- title: "Stop Tabs",
483
- readOnlyHint: false,
484
- destructiveHint: false,
485
- idempotentHint: true,
486
- openWorldHint: false,
487
- },
488
- },
489
- {
490
- name: "go_back",
491
- description: "Navigate back in the tab's history.",
492
- inputSchema: z.toJSONSchema(TabIdSchema),
493
- annotations: {
494
- title: "Go Back",
495
- readOnlyHint: false,
496
- destructiveHint: false,
497
- idempotentHint: false,
498
- openWorldHint: false,
499
- },
500
- },
501
- {
502
- name: "go_forward",
503
- description: "Navigate forward in the tab's history.",
504
- inputSchema: z.toJSONSchema(TabIdSchema),
505
- annotations: {
506
- title: "Go Forward",
507
- readOnlyHint: false,
508
- destructiveHint: false,
509
- idempotentHint: false,
510
- openWorldHint: false,
511
- },
512
- },
513
- {
514
- name: "navigate_tab",
515
- description: "Change the URL of an existing tab.",
516
- inputSchema: z.toJSONSchema(NavigateTabSchema),
517
- annotations: {
518
- title: "Navigate Tab",
519
- readOnlyHint: false,
520
- destructiveHint: false,
521
- idempotentHint: false,
522
- openWorldHint: true,
523
- },
524
- },
525
- {
526
- name: "execute_script",
527
- description: "Run JavaScript in a tab's page context.",
528
- inputSchema: z.toJSONSchema(ExecuteScriptSchema),
529
- annotations: {
530
- title: "Execute Script",
531
- readOnlyHint: false,
532
- destructiveHint: true,
533
- idempotentHint: false,
534
- openWorldHint: true,
535
- },
536
- },
537
- ],
538
- }));
539
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
540
- try {
541
- const { name, arguments: args = {} } = request.params;
542
- if (name === "get_tabs") {
543
- const { browser } = GetTabsSchema.parse(args);
544
- const windows = await getTabs(browser);
545
- const totalTabs = windows.reduce((n, w) => n + w.tabs.length, 0);
546
- const humanReadable = windows
547
- .map((w) => {
548
- const incognito = w.mode === "incognito" ? " [incognito]" : "";
549
- const nameStr = w.windowName ? ` (${w.windowName})` : "";
550
- const header = `Window ${w.windowIndex}${nameStr}${incognito} [ID: ${w.windowId}]:`;
551
- const tabLines = w.tabs
552
- .map((t) => {
553
- const active = t.isActive ? " ★" : "";
554
- const loading = t.loading ? " [loading]" : "";
555
- return [
556
- ` ${w.windowIndex}-${t.tabIndex}.${active} ${t.title}${loading}`,
557
- ` ${t.url}`,
558
- ` [Tab ID: ${t.id}]`,
559
- ].join("\n");
560
- })
561
- .join("\n");
562
- return `${header}\n${tabLines}`;
563
- })
564
- .join("\n\n");
565
- const tabsJson = windows.flatMap((w) => w.tabs.map((t) => ({
566
- tabId: t.id,
567
- windowId: w.windowId,
568
- windowIndex: w.windowIndex,
569
- windowName: w.windowName,
570
- tabIndex: t.tabIndex,
571
- windowMode: w.mode,
572
- title: t.title,
573
- url: t.url,
574
- active: t.isActive,
575
- loading: t.loading,
576
- })));
577
- return {
578
- content: [
579
- {
580
- type: "text",
581
- text: [
582
- `${totalTabs} tab(s) open in ${browser}:`,
583
- "",
584
- humanReadable,
585
- "",
586
- "---",
587
- "Use tabId from tabs_json for all tab operations.",
588
- "<tabs_json>",
589
- JSON.stringify(tabsJson, null, 2),
590
- "</tabs_json>",
591
- ].join("\n"),
592
- },
593
- ],
594
- };
595
- }
596
- if (name === "search_tabs") {
597
- const { query, browser } = SearchTabsSchema.parse(args);
598
- const windows = await getTabs(browser);
599
- const q = query.toLowerCase();
600
- const results = windows
601
- .flatMap((w) => w.tabs.map((t) => ({ ...t, windowName: w.windowName, windowMode: w.mode })))
602
- .filter((t) => t.title.toLowerCase().includes(q) || t.url.toLowerCase().includes(q));
603
- return {
604
- content: [
605
- {
606
- type: "text",
607
- text: [
608
- `Found ${results.length} tab(s) matching "${query}":`,
609
- "",
610
- results
611
- .map((t) => ` - ${t.title}\n ${t.url}\n [Tab ID: ${t.id}]`)
612
- .join("\n\n"),
613
- "",
614
- "<tabs_json>",
615
- JSON.stringify(results, null, 2),
616
- "</tabs_json>",
617
- ].join("\n"),
618
- },
619
- ],
620
- };
621
- }
622
- if (name === "get_active_tab") {
623
- const { browser } = GetTabsSchema.parse(args);
624
- const tab = await getActiveTab(browser);
625
- if (!tab)
626
- return { content: [{ type: "text", text: "No active window found." }] };
627
- return {
628
- content: [
629
- {
630
- type: "text",
631
- text: [
632
- "Active tab in frontmost window:",
633
- ` ${tab.title}`,
634
- ` ${tab.url}`,
635
- ` [Tab ID: ${tab.id}] [Window ID: ${tab.windowId}]`,
636
- "",
637
- "<tabs_json>",
638
- JSON.stringify([tab], null, 2),
639
- "</tabs_json>",
640
- ].join("\n"),
641
- },
642
- ],
643
- };
644
- }
645
- if (name === "open_tabs") {
646
- const { urls, windowId, activate, browser } = OpenTabsSchema.parse(args);
647
- const newTabIds = await openTabs(browser, urls, windowId, activate);
648
- return {
649
- content: [
650
- {
651
- type: "text",
652
- text: `Opened ${newTabIds.length} tab(s). New Tab IDs: ${newTabIds.join(", ")}`,
653
- },
654
- ],
655
- };
656
- }
657
- if (name === "close_tabs") {
658
- const { tabIds, browser } = BulkTabSchema.parse(args);
659
- const { count } = await closeTabs(browser, tabIds);
660
- return { content: [{ type: "text", text: `Closed ${count} tab(s).` }] };
661
- }
662
- if (name === "close_window") {
663
- const { windowId, browser } = CloseWindowSchema.parse(args);
664
- await closeWindow(browser, windowId);
665
- return { content: [{ type: "text", text: `Closed window [ID: ${windowId}]` }] };
666
- }
667
- if (name === "create_window") {
668
- const { mode, browser } = CreateWindowSchema.parse(args);
669
- const windowId = await createWindow(browser, mode);
670
- return {
671
- content: [{ type: "text", text: `Created ${mode} window [ID: ${windowId}]` }],
672
- };
673
- }
674
- if (name === "activate_tab") {
675
- const { tabId, browser } = TabIdSchema.parse(args);
676
- await activateTab(browser, tabId);
677
- return { content: [{ type: "text", text: `Activated tab [Tab ID: ${tabId}]` }] };
678
- }
679
- if (name === "reload_tabs") {
680
- const { tabIds, browser } = BulkTabSchema.parse(args);
681
- await reloadTabs(browser, tabIds);
682
- return { content: [{ type: "text", text: `Reloaded ${tabIds.length} tab(s).` }] };
683
- }
684
- if (name === "stop_tabs") {
685
- const { tabIds, browser } = BulkTabSchema.parse(args);
686
- await stopTabs(browser, tabIds);
687
- return { content: [{ type: "text", text: `Stopped ${tabIds.length} tab(s).` }] };
688
- }
689
- if (name === "go_back") {
690
- const { tabId, browser } = TabIdSchema.parse(args);
691
- await goBack(browser, tabId);
692
- return {
693
- content: [{ type: "text", text: `Navigated back in tab [Tab ID: ${tabId}]` }],
694
- };
695
- }
696
- if (name === "go_forward") {
697
- const { tabId, browser } = TabIdSchema.parse(args);
698
- await goForward(browser, tabId);
699
- return {
700
- content: [{ type: "text", text: `Navigated forward in tab [Tab ID: ${tabId}]` }],
701
- };
702
- }
703
- if (name === "navigate_tab") {
704
- const { tabId, url, browser } = NavigateTabSchema.parse(args);
705
- await navigateTab(browser, tabId, url);
706
- return {
707
- content: [{ type: "text", text: `Navigated tab [Tab ID: ${tabId}] to ${url}` }],
708
- };
709
- }
710
- if (name === "execute_script") {
711
- const { tabId, javascript, browser } = ExecuteScriptSchema.parse(args);
712
- const result = await executeScript(browser, tabId, javascript);
713
- return { content: [{ type: "text", text: result }] };
714
- }
715
- throw new Error(`Unknown tool: ${name}`);
716
- }
717
- catch (error) {
718
- return {
719
- content: [
720
- {
721
- type: "text",
722
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
723
- },
724
- ],
725
- isError: true,
726
- };
727
- }
728
- });
174
+ browser: browserField,
175
+ }, async ({ browser, tabId, javascript }) => chrome.executeScript(browser, tabId, javascript));
729
176
  async function main() {
730
- const transport = new StdioServerTransport();
731
- await server.connect(transport);
177
+ await server.connect(new StdioServerTransport());
732
178
  console.error("mcp-browser-buddy running on stdio");
733
179
  }
734
180
  main().catch((err) => {