@dacodedbeat/mcp-browser-buddy 1.0.0 → 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/README.md +6 -6
- package/dist/applescript.d.ts +45 -0
- package/dist/applescript.d.ts.map +1 -0
- package/dist/applescript.js +319 -0
- package/dist/applescript.js.map +1 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +127 -0
- package/dist/format.js.map +1 -0
- package/dist/index.js +148 -702
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,734 +1,180 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(
|
|
315
|
-
const
|
|
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("
|
|
319
|
-
const
|
|
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
|
|
328
|
-
const
|
|
329
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
346
|
-
|
|
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:
|
|
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
|
-
|
|
354
|
-
tabId:
|
|
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:
|
|
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
|
-
|
|
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) => {
|