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.
@@ -0,0 +1,376 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>playwright-mcp-tabbed</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg-1: #0b1020;
11
+ --bg-2: #10172a;
12
+ --card: rgba(17, 24, 39, 0.82);
13
+ --border: rgba(148, 163, 184, 0.18);
14
+ --text: #e5eefb;
15
+ --muted: #8ea0bd;
16
+ --accent: #60a5fa;
17
+ --accent-2: #22c55e;
18
+ --accent-3: #f59e0b;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ min-height: 100vh;
28
+ display: grid;
29
+ place-items: center;
30
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
31
+ background:
32
+ radial-gradient(circle at top left, rgba(96, 165, 250, 0.22), transparent 30%),
33
+ radial-gradient(circle at bottom right, rgba(34, 197, 94, 0.16), transparent 28%),
34
+ linear-gradient(135deg, var(--bg-1), var(--bg-2));
35
+ color: var(--text);
36
+ }
37
+
38
+ .frame {
39
+ width: 1440px;
40
+ height: 900px;
41
+ padding: 42px;
42
+ }
43
+
44
+ .hero {
45
+ height: 100%;
46
+ display: grid;
47
+ grid-template-columns: 1.15fr 0.85fr;
48
+ gap: 28px;
49
+ }
50
+
51
+ .panel {
52
+ background: var(--card);
53
+ border: 1px solid var(--border);
54
+ border-radius: 28px;
55
+ backdrop-filter: blur(14px);
56
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
57
+ }
58
+
59
+ .left {
60
+ padding: 40px;
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: space-between;
64
+ }
65
+
66
+ .eyebrow {
67
+ display: inline-flex;
68
+ gap: 8px;
69
+ align-items: center;
70
+ color: var(--accent);
71
+ font-size: 18px;
72
+ font-weight: 700;
73
+ letter-spacing: 0.04em;
74
+ text-transform: uppercase;
75
+ }
76
+
77
+ .dot {
78
+ width: 10px;
79
+ height: 10px;
80
+ border-radius: 999px;
81
+ background: var(--accent);
82
+ box-shadow: 0 0 20px rgba(96, 165, 250, 0.9);
83
+ }
84
+
85
+ h1 {
86
+ margin: 18px 0 14px;
87
+ font-size: 68px;
88
+ line-height: 1.02;
89
+ letter-spacing: -0.04em;
90
+ }
91
+
92
+ .subtitle {
93
+ max-width: 740px;
94
+ font-size: 23px;
95
+ line-height: 1.55;
96
+ color: var(--muted);
97
+ }
98
+
99
+ .highlights {
100
+ display: grid;
101
+ grid-template-columns: repeat(3, minmax(0, 1fr));
102
+ gap: 16px;
103
+ margin-top: 32px;
104
+ }
105
+
106
+ .highlight {
107
+ padding: 18px;
108
+ border-radius: 20px;
109
+ background: rgba(15, 23, 42, 0.78);
110
+ border: 1px solid rgba(148, 163, 184, 0.15);
111
+ }
112
+
113
+ .highlight strong {
114
+ display: block;
115
+ margin-bottom: 8px;
116
+ font-size: 18px;
117
+ }
118
+
119
+ .highlight span {
120
+ color: var(--muted);
121
+ line-height: 1.5;
122
+ font-size: 15px;
123
+ }
124
+
125
+ .flow {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 14px;
129
+ margin-top: 28px;
130
+ padding: 18px 22px;
131
+ border-radius: 20px;
132
+ background: rgba(2, 6, 23, 0.55);
133
+ border: 1px solid rgba(96, 165, 250, 0.16);
134
+ }
135
+
136
+ .flow .step {
137
+ padding: 10px 14px;
138
+ border-radius: 14px;
139
+ background: rgba(30, 41, 59, 0.85);
140
+ color: #dbeafe;
141
+ font-weight: 600;
142
+ font-size: 15px;
143
+ }
144
+
145
+ .arrow {
146
+ color: #5ea4ff;
147
+ font-size: 24px;
148
+ }
149
+
150
+ .right {
151
+ padding: 30px;
152
+ display: grid;
153
+ grid-template-rows: auto 1fr;
154
+ gap: 18px;
155
+ }
156
+
157
+ .browser {
158
+ padding: 18px;
159
+ border-radius: 24px;
160
+ background: rgba(2, 6, 23, 0.64);
161
+ border: 1px solid rgba(148, 163, 184, 0.12);
162
+ }
163
+
164
+ .browser-top {
165
+ display: flex;
166
+ gap: 10px;
167
+ align-items: center;
168
+ margin-bottom: 18px;
169
+ }
170
+
171
+ .window-dot {
172
+ width: 12px;
173
+ height: 12px;
174
+ border-radius: 999px;
175
+ }
176
+
177
+ .window-dot.red { background: #fb7185; }
178
+ .window-dot.yellow { background: #fbbf24; }
179
+ .window-dot.green { background: #34d399; }
180
+
181
+ .tabs {
182
+ display: grid;
183
+ gap: 12px;
184
+ }
185
+
186
+ .tab {
187
+ border-radius: 18px;
188
+ border: 1px solid rgba(148, 163, 184, 0.16);
189
+ background: rgba(15, 23, 42, 0.82);
190
+ padding: 16px 18px;
191
+ }
192
+
193
+ .tab-top {
194
+ display: flex;
195
+ justify-content: space-between;
196
+ align-items: center;
197
+ margin-bottom: 10px;
198
+ }
199
+
200
+ .tab-title {
201
+ font-size: 17px;
202
+ font-weight: 700;
203
+ }
204
+
205
+ .pill {
206
+ padding: 6px 10px;
207
+ border-radius: 999px;
208
+ font-size: 12px;
209
+ font-weight: 700;
210
+ }
211
+
212
+ .blue { background: rgba(96, 165, 250, 0.18); color: #93c5fd; }
213
+ .green-pill { background: rgba(34, 197, 94, 0.18); color: #86efac; }
214
+ .amber { background: rgba(245, 158, 11, 0.18); color: #fcd34d; }
215
+
216
+ .tab-lines {
217
+ display: grid;
218
+ gap: 8px;
219
+ }
220
+
221
+ .line {
222
+ height: 10px;
223
+ border-radius: 999px;
224
+ background: rgba(148, 163, 184, 0.18);
225
+ }
226
+
227
+ .line.short { width: 42%; }
228
+ .line.mid { width: 64%; }
229
+ .line.long { width: 100%; }
230
+
231
+ .legend {
232
+ display: grid;
233
+ gap: 12px;
234
+ }
235
+
236
+ .legend-card {
237
+ padding: 18px;
238
+ border-radius: 18px;
239
+ background: rgba(2, 6, 23, 0.58);
240
+ border: 1px solid rgba(148, 163, 184, 0.12);
241
+ }
242
+
243
+ .legend-card h3 {
244
+ margin: 0 0 10px;
245
+ font-size: 17px;
246
+ }
247
+
248
+ .legend-card p {
249
+ margin: 0;
250
+ color: var(--muted);
251
+ line-height: 1.55;
252
+ font-size: 15px;
253
+ }
254
+
255
+ .footer {
256
+ margin-top: 20px;
257
+ display: flex;
258
+ justify-content: space-between;
259
+ align-items: center;
260
+ color: var(--muted);
261
+ font-size: 15px;
262
+ }
263
+
264
+ .brand {
265
+ color: #dbeafe;
266
+ font-weight: 700;
267
+ }
268
+ </style>
269
+ </head>
270
+ <body>
271
+ <div class="frame">
272
+ <div class="hero">
273
+ <section class="panel left">
274
+ <div>
275
+ <div class="eyebrow"><span class="dot"></span> Parallel Browser Automation For MCP</div>
276
+ <h1>playwright-mcp-tabbed</h1>
277
+ <div class="subtitle">
278
+ Give every agent its own browser tab while keeping one shared login session. Built for Cursor and other MCP clients that need reliable parallel browser workflows.
279
+ </div>
280
+
281
+ <div class="highlights">
282
+ <div class="highlight">
283
+ <strong>Per-tool `tab_index`</strong>
284
+ <span>No shared current-tab pointer. Each tool call targets the intended tab explicitly.</span>
285
+ </div>
286
+ <div class="highlight">
287
+ <strong>Shared auth state</strong>
288
+ <span>All tabs live in the same browser context, so cookies and login state are reused.</span>
289
+ </div>
290
+ <div class="highlight">
291
+ <strong>Multi-agent friendly</strong>
292
+ <span>Ideal for batch bug fixing, parallel QA, and side-by-side verification workflows.</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+
297
+ <div>
298
+ <div class="flow">
299
+ <div class="step">Main Agent Logs In</div>
300
+ <div class="arrow">→</div>
301
+ <div class="step">Create Tabs</div>
302
+ <div class="arrow">→</div>
303
+ <div class="step">Assign `tab_index`</div>
304
+ <div class="arrow">→</div>
305
+ <div class="step">Run In Parallel</div>
306
+ </div>
307
+
308
+ <div class="footer">
309
+ <span class="brand">songofhawk/playwright-mcp-tabbed</span>
310
+ <span>MIT Licensed</span>
311
+ </div>
312
+ </div>
313
+ </section>
314
+
315
+ <section class="panel right">
316
+ <div class="browser">
317
+ <div class="browser-top">
318
+ <span class="window-dot red"></span>
319
+ <span class="window-dot yellow"></span>
320
+ <span class="window-dot green"></span>
321
+ </div>
322
+
323
+ <div class="tabs">
324
+ <div class="tab">
325
+ <div class="tab-top">
326
+ <div class="tab-title">Tab 0 · Bug #A102</div>
327
+ <span class="pill blue">Agent A</span>
328
+ </div>
329
+ <div class="tab-lines">
330
+ <div class="line long"></div>
331
+ <div class="line mid"></div>
332
+ <div class="line short"></div>
333
+ </div>
334
+ </div>
335
+
336
+ <div class="tab">
337
+ <div class="tab-top">
338
+ <div class="tab-title">Tab 1 · Bug #A108</div>
339
+ <span class="pill green-pill">Agent B</span>
340
+ </div>
341
+ <div class="tab-lines">
342
+ <div class="line long"></div>
343
+ <div class="line long"></div>
344
+ <div class="line short"></div>
345
+ </div>
346
+ </div>
347
+
348
+ <div class="tab">
349
+ <div class="tab-top">
350
+ <div class="tab-title">Tab 2 · Compare Vue2 / Vue3</div>
351
+ <span class="pill amber">Agent C</span>
352
+ </div>
353
+ <div class="tab-lines">
354
+ <div class="line mid"></div>
355
+ <div class="line long"></div>
356
+ <div class="line short"></div>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="legend">
363
+ <div class="legend-card">
364
+ <h3>Typical use cases</h3>
365
+ <p>Batch-fix ClickUp bugs, verify multiple routes after a refactor, compare two environments, or split one long browser workflow across several agents.</p>
366
+ </div>
367
+ <div class="legend-card">
368
+ <h3>Why not just `browser_tabs.select`?</h3>
369
+ <p>Because a shared active tab becomes a race condition under concurrency. Explicit `tab_index` keeps intent stable on every tool call.</p>
370
+ </div>
371
+ </div>
372
+ </section>
373
+ </div>
374
+ </div>
375
+ </body>
376
+ </html>
Binary file
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const tabManager_1 = require("./tabManager");
8
+ const tools_1 = require("./tools");
9
+ async function main() {
10
+ const server = new index_js_1.Server({
11
+ name: 'playwright-mcp-tabbed',
12
+ version: '1.0.0',
13
+ }, {
14
+ capabilities: {
15
+ tools: {
16
+ listChanged: false,
17
+ },
18
+ },
19
+ });
20
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
21
+ return {
22
+ tools: tools_1.toolDefinitions,
23
+ };
24
+ });
25
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
26
+ const { name, arguments: args = {} } = request.params;
27
+ return (0, tools_1.handleTool)(name, args);
28
+ });
29
+ const transport = new stdio_js_1.StdioServerTransport();
30
+ await server.connect(transport);
31
+ const cleanup = async () => {
32
+ await tabManager_1.tabManager.close();
33
+ await server.close();
34
+ process.exit(0);
35
+ };
36
+ process.on('SIGINT', () => {
37
+ void cleanup();
38
+ });
39
+ process.on('SIGTERM', () => {
40
+ void cleanup();
41
+ });
42
+ }
43
+ main().catch(error => {
44
+ process.stderr.write(`playwright-mcp-tabbed error: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
45
+ process.exit(1);
46
+ });
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tabManager = exports.TabManager = void 0;
4
+ const playwright_1 = require("playwright");
5
+ class TabManager {
6
+ browser = null;
7
+ context = null;
8
+ tabs = new Map();
9
+ nextIndex = 0;
10
+ async ensureBrowser() {
11
+ if (!this.browser) {
12
+ this.browser = await playwright_1.chromium.launch({ headless: false });
13
+ this.context = await this.browser.newContext();
14
+ }
15
+ return this.context;
16
+ }
17
+ async getPage(tabIndex) {
18
+ // No tabs yet — create the first one
19
+ if (this.tabs.size === 0) {
20
+ return this.newTab();
21
+ }
22
+ // No index specified — return tab 0 (or the only tab)
23
+ if (tabIndex === undefined) {
24
+ const first = [...this.tabs.keys()].sort((a, b) => a - b)[0];
25
+ return this.tabs.get(first);
26
+ }
27
+ const page = this.tabs.get(tabIndex);
28
+ if (!page) {
29
+ throw new Error(`Tab index ${tabIndex} does not exist. Available tabs: [${[...this.tabs.keys()].join(', ')}]`);
30
+ }
31
+ return page;
32
+ }
33
+ async resolveTabIndex(tabIndex) {
34
+ if (this.tabs.size === 0) {
35
+ const { index } = await this.newTabAndGetIndex();
36
+ return index;
37
+ }
38
+ if (tabIndex === undefined) {
39
+ return [...this.tabs.keys()].sort((a, b) => a - b)[0];
40
+ }
41
+ if (!this.tabs.has(tabIndex)) {
42
+ throw new Error(`Tab index ${tabIndex} does not exist. Available tabs: [${[...this.tabs.keys()].join(', ')}]`);
43
+ }
44
+ return tabIndex;
45
+ }
46
+ async newTab() {
47
+ const context = await this.ensureBrowser();
48
+ const page = await context.newPage();
49
+ const index = this.nextIndex++;
50
+ this.tabs.set(index, page);
51
+ page.on('close', () => {
52
+ this.tabs.delete(index);
53
+ });
54
+ return page;
55
+ }
56
+ async newTabAndGetIndex() {
57
+ const context = await this.ensureBrowser();
58
+ const page = await context.newPage();
59
+ const index = this.nextIndex++;
60
+ this.tabs.set(index, page);
61
+ page.on('close', () => {
62
+ this.tabs.delete(index);
63
+ });
64
+ return { index, page };
65
+ }
66
+ async closeTab(tabIndex) {
67
+ const page = this.tabs.get(tabIndex);
68
+ if (!page) {
69
+ throw new Error(`Tab index ${tabIndex} does not exist`);
70
+ }
71
+ await page.close();
72
+ this.tabs.delete(tabIndex);
73
+ }
74
+ listTabs() {
75
+ return [...this.tabs.entries()].map(([index, page]) => ({
76
+ index,
77
+ url: page.url(),
78
+ title: '', // title needs async call; omit for sync listing
79
+ }));
80
+ }
81
+ async listTabsAsync() {
82
+ const results = [];
83
+ for (const [index, page] of this.tabs.entries()) {
84
+ results.push({
85
+ index,
86
+ url: page.url(),
87
+ title: await page.title(),
88
+ });
89
+ }
90
+ return results;
91
+ }
92
+ async close() {
93
+ if (this.browser) {
94
+ await this.browser.close();
95
+ this.browser = null;
96
+ this.context = null;
97
+ this.tabs.clear();
98
+ }
99
+ }
100
+ }
101
+ exports.TabManager = TabManager;
102
+ exports.tabManager = new TabManager();