playwright-mcp-tabbed 1.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/LICENSE +21 -0
- package/README.md +177 -0
- package/README.zh-CN.md +177 -0
- package/assets/readme-hero.html +376 -0
- package/assets/readme-hero.png +0 -0
- package/dist/index.js +46 -0
- package/dist/tabManager.js +102 -0
- package/dist/tools.js +556 -0
- package/package.json +47 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toolDefinitions = void 0;
|
|
4
|
+
exports.handleTool = handleTool;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_util_1 = require("node:util");
|
|
7
|
+
const tabManager_1 = require("./tabManager");
|
|
8
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
9
|
+
function ok(text) {
|
|
10
|
+
return { content: [{ type: 'text', text }] };
|
|
11
|
+
}
|
|
12
|
+
function err(text) {
|
|
13
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
14
|
+
}
|
|
15
|
+
function stringifyResult(value) {
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
return 'undefined';
|
|
21
|
+
}
|
|
22
|
+
return JSON.stringify(value, null, 2);
|
|
23
|
+
}
|
|
24
|
+
// ─── Tab index schema (shared across tools) ──────────────────────────────────
|
|
25
|
+
const tabIndexProp = {
|
|
26
|
+
tab_index: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Tab index to operate on. If omitted, uses tab 0. Use browser_tabs to create and list tabs.',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
// ─── Tool definitions ────────────────────────────────────────────────────────
|
|
32
|
+
exports.toolDefinitions = [
|
|
33
|
+
{
|
|
34
|
+
name: 'browser_tabs',
|
|
35
|
+
description: 'List, create, or close browser tabs.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
action: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['list', 'new', 'close'],
|
|
42
|
+
description: 'list: list all tabs with index/url/title. new: open a new blank tab and return its index. close: close a specific tab.',
|
|
43
|
+
},
|
|
44
|
+
index: { type: 'number', description: 'Tab index to close (required for close action).' },
|
|
45
|
+
},
|
|
46
|
+
required: ['action'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'browser_navigate',
|
|
51
|
+
description: 'Navigate to a URL in the specified tab.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
url: { type: 'string', description: 'URL to navigate to.' },
|
|
56
|
+
...tabIndexProp,
|
|
57
|
+
},
|
|
58
|
+
required: ['url'],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'browser_snapshot',
|
|
63
|
+
description: 'Take an accessibility snapshot (structured DOM text) of the page. Use this instead of screenshot for understanding page structure.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: { ...tabIndexProp },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'browser_take_screenshot',
|
|
71
|
+
description: 'Take a screenshot of the specified tab. Returns a base64 PNG image.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
full_page: { type: 'boolean', description: 'Capture full page (default: false).' },
|
|
76
|
+
...tabIndexProp,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'browser_run_code',
|
|
82
|
+
description: 'Run a Playwright JavaScript function against the specified tab.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
code: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'A JavaScript function body or arrow function. It will receive page as the first argument.',
|
|
89
|
+
},
|
|
90
|
+
...tabIndexProp,
|
|
91
|
+
},
|
|
92
|
+
required: ['code'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'browser_click',
|
|
97
|
+
description: 'Click an element on the page.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
selector: { type: 'string', description: 'CSS selector or text selector (e.g. "text=Submit").' },
|
|
102
|
+
...tabIndexProp,
|
|
103
|
+
},
|
|
104
|
+
required: ['selector'],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'browser_type',
|
|
109
|
+
description: 'Type text into a focused or selected input element.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
selector: { type: 'string', description: 'CSS selector of the input element.' },
|
|
114
|
+
text: { type: 'string', description: 'Text to type.' },
|
|
115
|
+
clear_first: { type: 'boolean', description: 'Clear existing text before typing (default: false).' },
|
|
116
|
+
...tabIndexProp,
|
|
117
|
+
},
|
|
118
|
+
required: ['selector', 'text'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'browser_fill_form',
|
|
123
|
+
description: 'Fill multiple form fields at once.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
fields: {
|
|
128
|
+
type: 'array',
|
|
129
|
+
description: 'List of {selector, value} pairs.',
|
|
130
|
+
items: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
selector: { type: 'string' },
|
|
134
|
+
value: { type: 'string' },
|
|
135
|
+
},
|
|
136
|
+
required: ['selector', 'value'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
...tabIndexProp,
|
|
140
|
+
},
|
|
141
|
+
required: ['fields'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'browser_file_upload',
|
|
146
|
+
description: 'Upload one or multiple files in the specified tab.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
paths: {
|
|
151
|
+
type: 'array',
|
|
152
|
+
description: 'Absolute paths to files. If omitted, cancels the file chooser or clears the input.',
|
|
153
|
+
items: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
...tabIndexProp,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'browser_hover',
|
|
163
|
+
description: 'Hover over an element.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
selector: { type: 'string', description: 'CSS selector of the element to hover.' },
|
|
168
|
+
...tabIndexProp,
|
|
169
|
+
},
|
|
170
|
+
required: ['selector'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'browser_select_option',
|
|
175
|
+
description: 'Select an option in a <select> element.',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
selector: { type: 'string', description: 'CSS selector of the <select> element.' },
|
|
180
|
+
value: { type: 'string', description: 'Option value or label to select.' },
|
|
181
|
+
...tabIndexProp,
|
|
182
|
+
},
|
|
183
|
+
required: ['selector', 'value'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'browser_press_key',
|
|
188
|
+
description: 'Press a keyboard key.',
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties: {
|
|
192
|
+
key: { type: 'string', description: 'Key to press, e.g. "Enter", "Escape", "Tab", "ArrowDown".' },
|
|
193
|
+
...tabIndexProp,
|
|
194
|
+
},
|
|
195
|
+
required: ['key'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'browser_wait_for',
|
|
200
|
+
description: 'Wait for a selector to appear or disappear, or wait for navigation.',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
selector: { type: 'string', description: 'CSS selector to wait for.' },
|
|
205
|
+
state: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
enum: ['visible', 'hidden', 'attached', 'detached'],
|
|
208
|
+
description: 'State to wait for (default: visible).',
|
|
209
|
+
},
|
|
210
|
+
timeout: { type: 'number', description: 'Max wait time in ms (default: 10000).' },
|
|
211
|
+
...tabIndexProp,
|
|
212
|
+
},
|
|
213
|
+
required: ['selector'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'browser_evaluate',
|
|
218
|
+
description: 'Execute JavaScript in the page context and return the result.',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
code: { type: 'string', description: 'JavaScript code to execute. Can return a value.' },
|
|
223
|
+
...tabIndexProp,
|
|
224
|
+
},
|
|
225
|
+
required: ['code'],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'browser_navigate_back',
|
|
230
|
+
description: 'Navigate back in history.',
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: { ...tabIndexProp },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'browser_network_requests',
|
|
238
|
+
description: 'List recent network requests made by the page.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: { ...tabIndexProp },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'browser_console_messages',
|
|
246
|
+
description: 'Get console messages (log, warn, error) from the page.',
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: { ...tabIndexProp },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'browser_resize',
|
|
254
|
+
description: 'Resize the browser viewport.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {
|
|
258
|
+
width: { type: 'number', description: 'Viewport width in pixels.' },
|
|
259
|
+
height: { type: 'number', description: 'Viewport height in pixels.' },
|
|
260
|
+
...tabIndexProp,
|
|
261
|
+
},
|
|
262
|
+
required: ['width', 'height'],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'browser_drag',
|
|
267
|
+
description: 'Drag from one element to another.',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
source_selector: { type: 'string', description: 'CSS selector of element to drag from.' },
|
|
272
|
+
target_selector: { type: 'string', description: 'CSS selector of element to drag to.' },
|
|
273
|
+
...tabIndexProp,
|
|
274
|
+
},
|
|
275
|
+
required: ['source_selector', 'target_selector'],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'browser_handle_dialog',
|
|
280
|
+
description: 'Set up handling for the next browser dialog (alert/confirm/prompt).',
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
action: {
|
|
285
|
+
type: 'string',
|
|
286
|
+
enum: ['accept', 'dismiss'],
|
|
287
|
+
description: 'Whether to accept or dismiss the dialog.',
|
|
288
|
+
},
|
|
289
|
+
prompt_text: { type: 'string', description: 'Text to enter if the dialog is a prompt.' },
|
|
290
|
+
...tabIndexProp,
|
|
291
|
+
},
|
|
292
|
+
required: ['action'],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'browser_close',
|
|
297
|
+
description: 'Close the browser entirely and clean up all resources.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'browser_install',
|
|
305
|
+
description: 'Install Chromium used by this MCP server.',
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
// ─── Tool handler ─────────────────────────────────────────────────────────────
|
|
313
|
+
// Per-tab network request and console message collectors
|
|
314
|
+
const networkRequests = new Map();
|
|
315
|
+
const consoleMessages = new Map();
|
|
316
|
+
async function ensureTabTracking(tabIndex) {
|
|
317
|
+
const page = await tabManager_1.tabManager.getPage(tabIndex);
|
|
318
|
+
if (!networkRequests.has(tabIndex)) {
|
|
319
|
+
networkRequests.set(tabIndex, []);
|
|
320
|
+
page.on('request', req => {
|
|
321
|
+
networkRequests.get(tabIndex)?.push({ method: req.method(), url: req.url(), status: null });
|
|
322
|
+
});
|
|
323
|
+
page.on('response', res => {
|
|
324
|
+
const requests = networkRequests.get(tabIndex);
|
|
325
|
+
if (!requests)
|
|
326
|
+
return;
|
|
327
|
+
const entry = [...requests].reverse().find(r => r.url === res.url() && r.status === null);
|
|
328
|
+
if (entry)
|
|
329
|
+
entry.status = res.status();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (!consoleMessages.has(tabIndex)) {
|
|
333
|
+
consoleMessages.set(tabIndex, []);
|
|
334
|
+
page.on('console', msg => {
|
|
335
|
+
consoleMessages.get(tabIndex)?.push({ type: msg.type(), text: msg.text() });
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function handleTool(name, args) {
|
|
340
|
+
try {
|
|
341
|
+
const requestedTabIndex = args.tab_index;
|
|
342
|
+
const resolvedTabIndex = name === 'browser_tabs' || name === 'browser_close'
|
|
343
|
+
? undefined
|
|
344
|
+
: await tabManager_1.tabManager.resolveTabIndex(requestedTabIndex);
|
|
345
|
+
switch (name) {
|
|
346
|
+
// ── browser_tabs ──────────────────────────────────────────────────────
|
|
347
|
+
case 'browser_tabs': {
|
|
348
|
+
const action = args.action;
|
|
349
|
+
if (action === 'list') {
|
|
350
|
+
const tabs = await tabManager_1.tabManager.listTabsAsync();
|
|
351
|
+
return ok(JSON.stringify(tabs, null, 2));
|
|
352
|
+
}
|
|
353
|
+
if (action === 'new') {
|
|
354
|
+
const { index } = await tabManager_1.tabManager.newTabAndGetIndex();
|
|
355
|
+
await ensureTabTracking(index);
|
|
356
|
+
return ok(`Created new tab with index ${index}`);
|
|
357
|
+
}
|
|
358
|
+
if (action === 'close') {
|
|
359
|
+
const index = args.index;
|
|
360
|
+
await tabManager_1.tabManager.closeTab(index);
|
|
361
|
+
networkRequests.delete(index);
|
|
362
|
+
consoleMessages.delete(index);
|
|
363
|
+
return ok(`Closed tab ${index}`);
|
|
364
|
+
}
|
|
365
|
+
return err(`Unknown browser_tabs action: ${action}`);
|
|
366
|
+
}
|
|
367
|
+
// ── browser_navigate ──────────────────────────────────────────────────
|
|
368
|
+
case 'browser_navigate': {
|
|
369
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
370
|
+
await ensureTabTracking(resolvedTabIndex);
|
|
371
|
+
const url = args.url;
|
|
372
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
373
|
+
return ok(`Navigated to ${url} (tab ${resolvedTabIndex})`);
|
|
374
|
+
}
|
|
375
|
+
// ── browser_snapshot ─────────────────────────────────────────────────
|
|
376
|
+
case 'browser_snapshot': {
|
|
377
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
378
|
+
const snapshot = await page.locator('body').ariaSnapshot();
|
|
379
|
+
return ok(snapshot);
|
|
380
|
+
}
|
|
381
|
+
// ── browser_take_screenshot ───────────────────────────────────────────
|
|
382
|
+
case 'browser_take_screenshot': {
|
|
383
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
384
|
+
const fullPage = args.full_page;
|
|
385
|
+
const buffer = await page.screenshot({ fullPage: fullPage ?? false });
|
|
386
|
+
const base64 = buffer.toString('base64');
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: 'image', data: base64, mimeType: 'image/png' }],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// ── browser_run_code ─────────────────────────────────────────────────
|
|
392
|
+
case 'browser_run_code': {
|
|
393
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
394
|
+
const code = args.code;
|
|
395
|
+
const runnable = code.includes('=>') || code.trim().startsWith('function')
|
|
396
|
+
? code
|
|
397
|
+
: `async (page) => { ${code} }`;
|
|
398
|
+
const fn = new Function(`return (${runnable});`)();
|
|
399
|
+
const result = await fn(page);
|
|
400
|
+
return ok(stringifyResult(result));
|
|
401
|
+
}
|
|
402
|
+
// ── browser_click ────────────────────────────────────────────────────
|
|
403
|
+
case 'browser_click': {
|
|
404
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
405
|
+
const selector = args.selector;
|
|
406
|
+
await page.click(selector);
|
|
407
|
+
return ok(`Clicked "${selector}" (tab ${resolvedTabIndex})`);
|
|
408
|
+
}
|
|
409
|
+
// ── browser_type ─────────────────────────────────────────────────────
|
|
410
|
+
case 'browser_type': {
|
|
411
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
412
|
+
const selector = args.selector;
|
|
413
|
+
const text = args.text;
|
|
414
|
+
const clearFirst = args.clear_first;
|
|
415
|
+
if (clearFirst) {
|
|
416
|
+
await page.fill(selector, '');
|
|
417
|
+
}
|
|
418
|
+
await page.type(selector, text);
|
|
419
|
+
return ok(`Typed into "${selector}" (tab ${resolvedTabIndex})`);
|
|
420
|
+
}
|
|
421
|
+
// ── browser_fill_form ────────────────────────────────────────────────
|
|
422
|
+
case 'browser_fill_form': {
|
|
423
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
424
|
+
const fields = args.fields;
|
|
425
|
+
for (const { selector, value } of fields) {
|
|
426
|
+
await page.fill(selector, value);
|
|
427
|
+
}
|
|
428
|
+
return ok(`Filled ${fields.length} form field(s) (tab ${resolvedTabIndex})`);
|
|
429
|
+
}
|
|
430
|
+
// ── browser_file_upload ──────────────────────────────────────────────
|
|
431
|
+
case 'browser_file_upload': {
|
|
432
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
433
|
+
const paths = args.paths;
|
|
434
|
+
const fileInputs = page.locator('input[type="file"]');
|
|
435
|
+
if (await fileInputs.count()) {
|
|
436
|
+
await fileInputs.last().setInputFiles(paths ?? []);
|
|
437
|
+
return ok(paths?.length
|
|
438
|
+
? `Uploaded ${paths.length} file(s) via file input (tab ${resolvedTabIndex})`
|
|
439
|
+
: `Cleared file input selection (tab ${resolvedTabIndex})`);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const chooser = await page.waitForEvent('filechooser', { timeout: 3000 });
|
|
443
|
+
await chooser.setFiles(paths ?? []);
|
|
444
|
+
return ok(paths?.length
|
|
445
|
+
? `Uploaded ${paths.length} file(s) via file chooser (tab ${resolvedTabIndex})`
|
|
446
|
+
: `Cancelled file chooser (tab ${resolvedTabIndex})`);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
throw new Error('No file input or pending file chooser found. Trigger the upload control first.');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ── browser_hover ────────────────────────────────────────────────────
|
|
453
|
+
case 'browser_hover': {
|
|
454
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
455
|
+
await page.hover(args.selector);
|
|
456
|
+
return ok(`Hovered "${args.selector}" (tab ${resolvedTabIndex})`);
|
|
457
|
+
}
|
|
458
|
+
// ── browser_select_option ────────────────────────────────────────────
|
|
459
|
+
case 'browser_select_option': {
|
|
460
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
461
|
+
const selector = args.selector;
|
|
462
|
+
const value = args.value;
|
|
463
|
+
await page.selectOption(selector, { label: value }).catch(() => page.selectOption(selector, { value }));
|
|
464
|
+
return ok(`Selected "${value}" in "${selector}" (tab ${resolvedTabIndex})`);
|
|
465
|
+
}
|
|
466
|
+
// ── browser_press_key ────────────────────────────────────────────────
|
|
467
|
+
case 'browser_press_key': {
|
|
468
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
469
|
+
await page.keyboard.press(args.key);
|
|
470
|
+
return ok(`Pressed key "${args.key}" (tab ${resolvedTabIndex})`);
|
|
471
|
+
}
|
|
472
|
+
// ── browser_wait_for ─────────────────────────────────────────────────
|
|
473
|
+
case 'browser_wait_for': {
|
|
474
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
475
|
+
const state = args.state ?? 'visible';
|
|
476
|
+
const timeout = args.timeout ?? 10000;
|
|
477
|
+
await page.waitForSelector(args.selector, { state, timeout });
|
|
478
|
+
return ok(`Selector "${args.selector}" is now ${state} (tab ${resolvedTabIndex})`);
|
|
479
|
+
}
|
|
480
|
+
// ── browser_evaluate ─────────────────────────────────────────────────
|
|
481
|
+
case 'browser_evaluate': {
|
|
482
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
483
|
+
const result = await page.evaluate(args.code);
|
|
484
|
+
return ok(JSON.stringify(result, null, 2));
|
|
485
|
+
}
|
|
486
|
+
// ── browser_navigate_back ────────────────────────────────────────────
|
|
487
|
+
case 'browser_navigate_back': {
|
|
488
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
489
|
+
await page.goBack();
|
|
490
|
+
return ok(`Navigated back (tab ${resolvedTabIndex})`);
|
|
491
|
+
}
|
|
492
|
+
// ── browser_network_requests ─────────────────────────────────────────
|
|
493
|
+
case 'browser_network_requests': {
|
|
494
|
+
const requests = networkRequests.get(resolvedTabIndex) ?? [];
|
|
495
|
+
return ok(JSON.stringify(requests.slice(-50), null, 2));
|
|
496
|
+
}
|
|
497
|
+
// ── browser_console_messages ─────────────────────────────────────────
|
|
498
|
+
case 'browser_console_messages': {
|
|
499
|
+
const messages = consoleMessages.get(resolvedTabIndex) ?? [];
|
|
500
|
+
return ok(JSON.stringify(messages.slice(-50), null, 2));
|
|
501
|
+
}
|
|
502
|
+
// ── browser_resize ───────────────────────────────────────────────────
|
|
503
|
+
case 'browser_resize': {
|
|
504
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
505
|
+
await page.setViewportSize({
|
|
506
|
+
width: args.width,
|
|
507
|
+
height: args.height,
|
|
508
|
+
});
|
|
509
|
+
return ok(`Resized viewport to ${args.width}x${args.height} (tab ${resolvedTabIndex})`);
|
|
510
|
+
}
|
|
511
|
+
// ── browser_drag ─────────────────────────────────────────────────────
|
|
512
|
+
case 'browser_drag': {
|
|
513
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
514
|
+
await page.dragAndDrop(args.source_selector, args.target_selector);
|
|
515
|
+
return ok(`Dragged from "${args.source_selector}" to "${args.target_selector}" (tab ${resolvedTabIndex})`);
|
|
516
|
+
}
|
|
517
|
+
// ── browser_handle_dialog ────────────────────────────────────────────
|
|
518
|
+
case 'browser_handle_dialog': {
|
|
519
|
+
const page = await tabManager_1.tabManager.getPage(resolvedTabIndex);
|
|
520
|
+
const action = args.action;
|
|
521
|
+
const promptText = args.prompt_text;
|
|
522
|
+
page.once('dialog', async (dialog) => {
|
|
523
|
+
if (action === 'accept') {
|
|
524
|
+
await dialog.accept(promptText);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
await dialog.dismiss();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return ok(`Dialog handler set to "${action}" (tab ${resolvedTabIndex})`);
|
|
531
|
+
}
|
|
532
|
+
// ── browser_close ────────────────────────────────────────────────────
|
|
533
|
+
case 'browser_close': {
|
|
534
|
+
await tabManager_1.tabManager.close();
|
|
535
|
+
networkRequests.clear();
|
|
536
|
+
consoleMessages.clear();
|
|
537
|
+
return ok('Browser closed.');
|
|
538
|
+
}
|
|
539
|
+
// ── browser_install ──────────────────────────────────────────────────
|
|
540
|
+
case 'browser_install': {
|
|
541
|
+
const cliPath = require.resolve('playwright/cli');
|
|
542
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [cliPath, 'install', 'chromium'], {
|
|
543
|
+
cwd: process.cwd(),
|
|
544
|
+
env: process.env,
|
|
545
|
+
});
|
|
546
|
+
return ok(`Installed Chromium.\n${[stdout, stderr].filter(Boolean).join('\n').trim()}`);
|
|
547
|
+
}
|
|
548
|
+
default:
|
|
549
|
+
return err(`Unknown tool: ${name}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
554
|
+
return err(`Tool "${name}" failed: ${message}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-mcp-tabbed",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Playwright MCP server with per-tool tab_index support for parallel agent use",
|
|
5
|
+
"author": "SongOfHawk",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/songofhawk/playwright-mcp-tabbed.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/songofhawk/playwright-mcp-tabbed#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/songofhawk/playwright-mcp-tabbed/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"playwright",
|
|
17
|
+
"mcp",
|
|
18
|
+
"cursor",
|
|
19
|
+
"browser-automation",
|
|
20
|
+
"parallel-agents"
|
|
21
|
+
],
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"playwright-mcp-tabbed": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"assets",
|
|
29
|
+
"README.md",
|
|
30
|
+
"README.zh-CN.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"start": "node dist/index.js",
|
|
36
|
+
"dev": "tsx src/index.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"playwright": "^1.49.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"tsx": "^4.19.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
}
|
|
47
|
+
}
|