@kirosnn/mosaic 0.73.0 → 0.75.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.
@@ -1,854 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
4
- import { z } from 'zod';
5
-
6
- type BrowserTypeName = 'chromium' | 'firefox' | 'webkit';
7
-
8
- const browserTypes = { chromium, firefox, webkit };
9
-
10
- type LaunchConfig = {
11
- browserType: BrowserTypeName;
12
- headless: boolean;
13
- channel?: string;
14
- executablePath?: string;
15
- args: string[];
16
- };
17
-
18
- type ContextConfig = {
19
- userAgent?: string;
20
- viewport?: { width: number; height: number } | null;
21
- extraHTTPHeaders?: Record<string, string>;
22
- locale?: string;
23
- timezoneId?: string;
24
- };
25
-
26
- let browser: Browser | null = null;
27
- let context: BrowserContext | null = null;
28
-
29
- let launchConfig: LaunchConfig = {
30
- browserType: 'chromium',
31
- headless: true,
32
- args: [],
33
- };
34
-
35
- let contextConfig: ContextConfig = {};
36
-
37
- const tabs = new Map<string, Page>();
38
- const tabOrder: string[] = [];
39
- const pageToId = new WeakMap<Page, string>();
40
- let currentTabId: string | null = null;
41
- let tabSeq = 0;
42
-
43
- function toLocator(selector: string, selectorType?: 'css' | 'xpath'): string {
44
- if (selectorType === 'xpath') {
45
- return `xpath=${selector}`;
46
- }
47
- return selector;
48
- }
49
-
50
- function normalizeBrowserType(value?: string): BrowserTypeName {
51
- if (!value) return 'chromium';
52
- if (value === 'firefox' || value === 'webkit' || value === 'chromium') return value;
53
- return 'chromium';
54
- }
55
-
56
- function registerPage(page: Page): string {
57
- const existing = pageToId.get(page);
58
- if (existing) return existing;
59
- const id = `tab_${++tabSeq}`;
60
- pageToId.set(page, id);
61
- tabs.set(id, page);
62
- tabOrder.push(id);
63
- currentTabId = id;
64
- page.on('close', () => {
65
- tabs.delete(id);
66
- const index = tabOrder.indexOf(id);
67
- if (index >= 0) tabOrder.splice(index, 1);
68
- if (currentTabId === id) {
69
- currentTabId = tabOrder.length > 0 ? tabOrder[tabOrder.length - 1]! : null;
70
- }
71
- });
72
- return id;
73
- }
74
-
75
- async function ensureBrowser(): Promise<Browser> {
76
- if (browser) return browser;
77
- const launcher = browserTypes[launchConfig.browserType];
78
- browser = await launcher.launch({
79
- headless: launchConfig.headless,
80
- channel: launchConfig.channel,
81
- executablePath: launchConfig.executablePath,
82
- args: launchConfig.args.length > 0 ? launchConfig.args : undefined,
83
- });
84
- return browser;
85
- }
86
-
87
- async function resetContext(): Promise<void> {
88
- if (context) {
89
- try { await context.close(); } catch {}
90
- }
91
- context = null;
92
- tabs.clear();
93
- tabOrder.splice(0, tabOrder.length);
94
- currentTabId = null;
95
- }
96
-
97
- async function ensureContext(): Promise<BrowserContext> {
98
- if (context) return context;
99
- const b = await ensureBrowser();
100
- context = await b.newContext({
101
- userAgent: contextConfig.userAgent,
102
- viewport: contextConfig.viewport === null ? null : contextConfig.viewport,
103
- extraHTTPHeaders: contextConfig.extraHTTPHeaders,
104
- locale: contextConfig.locale,
105
- timezoneId: contextConfig.timezoneId,
106
- });
107
- context.on('page', page => {
108
- registerPage(page);
109
- });
110
- return context;
111
- }
112
-
113
- async function ensurePage(tabId?: string): Promise<{ page: Page; tabId: string }> {
114
- const ctx = await ensureContext();
115
- if (tabId) {
116
- const existing = tabs.get(tabId);
117
- if (!existing) {
118
- throw new Error(`Tab not found: ${tabId}`);
119
- }
120
- currentTabId = tabId;
121
- return { page: existing, tabId };
122
- }
123
- if (currentTabId) {
124
- const current = tabs.get(currentTabId);
125
- if (current) return { page: current, tabId: currentTabId };
126
- }
127
- const page = await ctx.newPage();
128
- const id = registerPage(page);
129
- return { page, tabId: id };
130
- }
131
-
132
- async function setLaunchConfig(next: Partial<LaunchConfig>): Promise<void> {
133
- const updated: LaunchConfig = {
134
- ...launchConfig,
135
- ...next,
136
- browserType: normalizeBrowserType(next.browserType ?? launchConfig.browserType),
137
- args: next.args ?? launchConfig.args,
138
- };
139
- const changed = JSON.stringify(updated) !== JSON.stringify(launchConfig);
140
- launchConfig = updated;
141
- if (changed && browser) {
142
- await closeBrowser();
143
- }
144
- }
145
-
146
- async function setContextConfig(next: Partial<ContextConfig>): Promise<void> {
147
- const updated: ContextConfig = { ...contextConfig, ...next };
148
- const changed = JSON.stringify(updated) !== JSON.stringify(contextConfig);
149
- contextConfig = updated;
150
- if (changed && context) {
151
- await resetContext();
152
- }
153
- }
154
-
155
- async function closeBrowser(): Promise<void> {
156
- if (context) {
157
- try { await context.close(); } catch { }
158
- }
159
- context = null;
160
- if (browser) {
161
- try { await browser.close(); } catch { }
162
- }
163
- browser = null;
164
- tabs.clear();
165
- tabOrder.splice(0, tabOrder.length);
166
- currentTabId = null;
167
- }
168
-
169
- const server = new McpServer({ name: 'navigation', version: '1.0.0' });
170
-
171
- server.registerTool('navigation_launch', {
172
- description: 'Launch or reconfigure the browser instance',
173
- inputSchema: {
174
- browserType: z.string().optional(),
175
- headless: z.boolean().optional(),
176
- channel: z.string().optional(),
177
- executablePath: z.string().optional(),
178
- args: z.array(z.string()).optional(),
179
- },
180
- }, async (args) => {
181
- await setLaunchConfig({
182
- browserType: args.browserType as BrowserTypeName | undefined,
183
- headless: args.headless ?? launchConfig.headless,
184
- channel: args.channel,
185
- executablePath: args.executablePath,
186
- args: args.args ?? launchConfig.args,
187
- });
188
- await ensureBrowser();
189
- return {
190
- content: [{ type: 'text', text: JSON.stringify({ status: 'ready', launchConfig }, null, 2) }],
191
- };
192
- });
193
-
194
- server.registerTool('navigation_close', {
195
- description: 'Close the browser instance and all tabs',
196
- inputSchema: {},
197
- }, async () => {
198
- await closeBrowser();
199
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'closed' }, null, 2) }] };
200
- });
201
-
202
- server.registerTool('navigation_context', {
203
- description: 'Configure browser context settings (user-agent, viewport, headers, cookies)',
204
- inputSchema: {
205
- userAgent: z.string().optional(),
206
- viewport: z.object({ width: z.number(), height: z.number() }).nullable().optional(),
207
- extraHTTPHeaders: z.record(z.string()).optional(),
208
- locale: z.string().optional(),
209
- timezoneId: z.string().optional(),
210
- cookies: z.array(z.object({
211
- name: z.string(),
212
- value: z.string(),
213
- url: z.string().optional(),
214
- domain: z.string().optional(),
215
- path: z.string().optional(),
216
- expires: z.number().optional(),
217
- httpOnly: z.boolean().optional(),
218
- secure: z.boolean().optional(),
219
- sameSite: z.string().optional(),
220
- })).optional(),
221
- },
222
- }, async (args) => {
223
- await setContextConfig({
224
- userAgent: args.userAgent,
225
- viewport: args.viewport === undefined ? contextConfig.viewport : args.viewport,
226
- extraHTTPHeaders: args.extraHTTPHeaders,
227
- locale: args.locale,
228
- timezoneId: args.timezoneId,
229
- });
230
- const ctx = await ensureContext();
231
- if (args.extraHTTPHeaders) {
232
- await ctx.setExtraHTTPHeaders(args.extraHTTPHeaders);
233
- }
234
- if (args.cookies && args.cookies.length > 0) {
235
- await ctx.addCookies(args.cookies);
236
- }
237
- return {
238
- content: [{ type: 'text', text: JSON.stringify({ status: 'ready', contextConfig }, null, 2) }],
239
- };
240
- });
241
-
242
- server.registerTool('navigation_new_tab', {
243
- description: 'Open a new tab, optionally navigating to a URL',
244
- inputSchema: {
245
- url: z.string().optional(),
246
- waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
247
- },
248
- }, async (args) => {
249
- const ctx = await ensureContext();
250
- const page = await ctx.newPage();
251
- const tabId = registerPage(page);
252
- if (args.url) {
253
- await page.goto(args.url, { waitUntil: args.waitUntil ?? 'load' });
254
- }
255
- const title = await page.title();
256
- const url = page.url();
257
- return {
258
- content: [{ type: 'text', text: JSON.stringify({ tabId, title, url }, null, 2) }],
259
- };
260
- });
261
-
262
- server.registerTool('navigation_open', {
263
- description: 'Open a URL in the current tab or a new tab',
264
- inputSchema: {
265
- url: z.string(),
266
- newTab: z.boolean().optional(),
267
- waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
268
- },
269
- }, async (args) => {
270
- const target = args.newTab ? await ensureContext().then(async ctx => {
271
- const page = await ctx.newPage();
272
- const tabId = registerPage(page);
273
- return { page, tabId };
274
- }) : await ensurePage();
275
- await target.page.goto(args.url, { waitUntil: args.waitUntil ?? 'load' });
276
- const title = await target.page.title();
277
- const url = target.page.url();
278
- return {
279
- content: [{ type: 'text', text: JSON.stringify({ tabId: target.tabId, title, url }, null, 2) }],
280
- };
281
- });
282
-
283
- server.registerTool('navigation_search', {
284
- description: 'Search the web using a search engine and return top results',
285
- inputSchema: {
286
- query: z.string(),
287
- engine: z.string().optional(),
288
- limit: z.number().optional(),
289
- newTab: z.boolean().optional(),
290
- },
291
- }, async (args) => {
292
- const engine = args.engine ?? 'https://duckduckgo.com/?q=';
293
- const url = `${engine}${encodeURIComponent(args.query)}`;
294
- const target = args.newTab ? await ensureContext().then(async ctx => {
295
- const page = await ctx.newPage();
296
- const tabId = registerPage(page);
297
- return { page, tabId };
298
- }) : await ensurePage();
299
- await target.page.goto(url, { waitUntil: 'domcontentloaded' });
300
- const limit = args.limit ?? 8;
301
- const results = await target.page.evaluate((max) => {
302
- const items = Array.from(document.querySelectorAll('a'))
303
- .map(a => {
304
- const text = (a.textContent || '').trim();
305
- const href = a.getAttribute('href') || '';
306
- return { text, href };
307
- })
308
- .filter(r => r.text && r.href && !r.href.startsWith('#'));
309
- const unique: { text: string; href: string }[] = [];
310
- const seen = new Set<string>();
311
- for (const item of items) {
312
- if (seen.has(item.href)) continue;
313
- seen.add(item.href);
314
- unique.push(item);
315
- if (unique.length >= max) break;
316
- }
317
- return unique;
318
- }, limit);
319
- return {
320
- content: [{ type: 'text', text: JSON.stringify({ tabId: target.tabId, url, results }, null, 2) }],
321
- };
322
- });
323
-
324
- server.registerTool('navigation_back', {
325
- description: 'Navigate back in the current tab history',
326
- inputSchema: { tabId: z.string().optional() },
327
- }, async (args) => {
328
- const { page, tabId } = await ensurePage(args.tabId);
329
- await page.goBack();
330
- return {
331
- content: [{ type: 'text', text: JSON.stringify({ tabId, url: page.url() }, null, 2) }],
332
- };
333
- });
334
-
335
- server.registerTool('navigation_forward', {
336
- description: 'Navigate forward in the current tab history',
337
- inputSchema: { tabId: z.string().optional() },
338
- }, async (args) => {
339
- const { page, tabId } = await ensurePage(args.tabId);
340
- await page.goForward();
341
- return {
342
- content: [{ type: 'text', text: JSON.stringify({ tabId, url: page.url() }, null, 2) }],
343
- };
344
- });
345
-
346
- server.registerTool('navigation_reload', {
347
- description: 'Reload the current tab',
348
- inputSchema: {
349
- tabId: z.string().optional(),
350
- waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
351
- },
352
- }, async (args) => {
353
- const { page, tabId } = await ensurePage(args.tabId);
354
- await page.reload({ waitUntil: args.waitUntil ?? 'load' });
355
- return {
356
- content: [{ type: 'text', text: JSON.stringify({ tabId, url: page.url() }, null, 2) }],
357
- };
358
- });
359
-
360
- server.registerTool('navigation_tabs', {
361
- description: 'List open tabs and current tab',
362
- inputSchema: {},
363
- }, async () => {
364
- const list = await Promise.all(tabOrder.map(async id => {
365
- const page = tabs.get(id);
366
- return { tabId: id, url: page?.url() ?? '', title: page ? await page.title() : '' };
367
- }));
368
- return {
369
- content: [{ type: 'text', text: JSON.stringify({ currentTabId, tabs: list }, null, 2) }],
370
- };
371
- });
372
-
373
- server.registerTool('navigation_tab_switch', {
374
- description: 'Switch active tab',
375
- inputSchema: { tabId: z.string() },
376
- }, async (args) => {
377
- const page = tabs.get(args.tabId);
378
- if (!page) throw new Error(`Tab not found: ${args.tabId}`);
379
- currentTabId = args.tabId;
380
- return {
381
- content: [{ type: 'text', text: JSON.stringify({ currentTabId }, null, 2) }],
382
- };
383
- });
384
-
385
- server.registerTool('navigation_tab_close', {
386
- description: 'Close a tab',
387
- inputSchema: { tabId: z.string().optional() },
388
- }, async (args) => {
389
- const id = args.tabId ?? currentTabId;
390
- if (!id) throw new Error('No active tab');
391
- const page = tabs.get(id);
392
- if (!page) throw new Error(`Tab not found: ${id}`);
393
- await page.close();
394
- return {
395
- content: [{ type: 'text', text: JSON.stringify({ closed: id, currentTabId }, null, 2) }],
396
- };
397
- });
398
-
399
- server.registerTool('navigation_wait', {
400
- description: 'Wait for network idle, selector, URL, or a timeout',
401
- inputSchema: {
402
- tabId: z.string().optional(),
403
- selector: z.string().optional(),
404
- selectorType: z.enum(['css', 'xpath']).optional(),
405
- state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
406
- url: z.string().optional(),
407
- timeoutMs: z.number().optional(),
408
- waitForNetworkIdle: z.boolean().optional(),
409
- },
410
- }, async (args) => {
411
- const { page, tabId } = await ensurePage(args.tabId);
412
- if (args.selector) {
413
- await page.waitForSelector(toLocator(args.selector, args.selectorType), {
414
- state: args.state ?? 'visible',
415
- timeout: args.timeoutMs,
416
- });
417
- }
418
- if (args.url) {
419
- await page.waitForURL(args.url, { timeout: args.timeoutMs });
420
- }
421
- if (args.waitForNetworkIdle) {
422
- await page.waitForLoadState('networkidle', { timeout: args.timeoutMs });
423
- }
424
- if (args.timeoutMs && !args.selector && !args.url && !args.waitForNetworkIdle) {
425
- await page.waitForTimeout(args.timeoutMs);
426
- }
427
- return {
428
- content: [{ type: 'text', text: JSON.stringify({ tabId, url: page.url() }, null, 2) }],
429
- };
430
- });
431
-
432
- server.registerTool('navigation_click', {
433
- description: 'Click an element',
434
- inputSchema: {
435
- tabId: z.string().optional(),
436
- selector: z.string(),
437
- selectorType: z.enum(['css', 'xpath']).optional(),
438
- button: z.enum(['left', 'right', 'middle']).optional(),
439
- clickCount: z.number().optional(),
440
- modifiers: z.array(z.enum(['Alt', 'Control', 'Meta', 'Shift'])).optional(),
441
- },
442
- }, async (args) => {
443
- const { page, tabId } = await ensurePage(args.tabId);
444
- await page.click(toLocator(args.selector, args.selectorType), {
445
- button: args.button,
446
- clickCount: args.clickCount,
447
- modifiers: args.modifiers,
448
- });
449
- return {
450
- content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }],
451
- };
452
- });
453
-
454
- server.registerTool('navigation_hover', {
455
- description: 'Hover an element',
456
- inputSchema: {
457
- tabId: z.string().optional(),
458
- selector: z.string(),
459
- selectorType: z.enum(['css', 'xpath']).optional(),
460
- },
461
- }, async (args) => {
462
- const { page, tabId } = await ensurePage(args.tabId);
463
- await page.hover(toLocator(args.selector, args.selectorType));
464
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
465
- });
466
-
467
- server.registerTool('navigation_fill', {
468
- description: 'Fill an input, textarea, or contenteditable element',
469
- inputSchema: {
470
- tabId: z.string().optional(),
471
- selector: z.string(),
472
- selectorType: z.enum(['css', 'xpath']).optional(),
473
- value: z.string(),
474
- },
475
- }, async (args) => {
476
- const { page, tabId } = await ensurePage(args.tabId);
477
- await page.fill(toLocator(args.selector, args.selectorType), args.value);
478
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
479
- });
480
-
481
- server.registerTool('navigation_type', {
482
- description: 'Type into an element',
483
- inputSchema: {
484
- tabId: z.string().optional(),
485
- selector: z.string(),
486
- selectorType: z.enum(['css', 'xpath']).optional(),
487
- text: z.string(),
488
- delayMs: z.number().optional(),
489
- },
490
- }, async (args) => {
491
- const { page, tabId } = await ensurePage(args.tabId);
492
- await page.type(toLocator(args.selector, args.selectorType), args.text, { delay: args.delayMs });
493
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
494
- });
495
-
496
- server.registerTool('navigation_select', {
497
- description: 'Select option(s) in a select element',
498
- inputSchema: {
499
- tabId: z.string().optional(),
500
- selector: z.string(),
501
- selectorType: z.enum(['css', 'xpath']).optional(),
502
- values: z.array(z.string()),
503
- },
504
- }, async (args) => {
505
- const { page, tabId } = await ensurePage(args.tabId);
506
- await page.selectOption(toLocator(args.selector, args.selectorType), args.values);
507
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
508
- });
509
-
510
- server.registerTool('navigation_submit', {
511
- description: 'Submit a form',
512
- inputSchema: {
513
- tabId: z.string().optional(),
514
- selector: z.string().optional(),
515
- selectorType: z.enum(['css', 'xpath']).optional(),
516
- },
517
- }, async (args) => {
518
- const { page, tabId } = await ensurePage(args.tabId);
519
- const selector = toLocator(args.selector ?? 'form', args.selectorType);
520
- await page.locator(selector).first().evaluate(form => {
521
- const el = form as HTMLFormElement;
522
- if (el.requestSubmit) {
523
- el.requestSubmit();
524
- } else {
525
- el.submit();
526
- }
527
- });
528
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
529
- });
530
-
531
- server.registerTool('navigation_press', {
532
- description: 'Press a keyboard shortcut',
533
- inputSchema: {
534
- tabId: z.string().optional(),
535
- key: z.string(),
536
- },
537
- }, async (args) => {
538
- const { page, tabId } = await ensurePage(args.tabId);
539
- await page.keyboard.press(args.key);
540
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
541
- });
542
-
543
- server.registerTool('navigation_scroll', {
544
- description: 'Scroll the page or an element',
545
- inputSchema: {
546
- tabId: z.string().optional(),
547
- x: z.number().optional(),
548
- y: z.number().optional(),
549
- selector: z.string().optional(),
550
- selectorType: z.enum(['css', 'xpath']).optional(),
551
- },
552
- }, async (args) => {
553
- const { page, tabId } = await ensurePage(args.tabId);
554
- const x = args.x ?? 0;
555
- const y = args.y ?? 0;
556
- if (args.selector) {
557
- const loc = toLocator(args.selector, args.selectorType);
558
- const locator = page.locator(loc).first();
559
- await locator.evaluate((el, delta) => {
560
- (el as HTMLElement).scrollBy(delta.x, delta.y);
561
- }, { x, y });
562
- } else {
563
- await page.mouse.wheel(x, y);
564
- }
565
- return { content: [{ type: 'text', text: JSON.stringify({ tabId }, null, 2) }] };
566
- });
567
-
568
- server.registerTool('navigation_text', {
569
- description: 'Read visible text from the page or a selector',
570
- inputSchema: {
571
- tabId: z.string().optional(),
572
- selector: z.string().optional(),
573
- selectorType: z.enum(['css', 'xpath']).optional(),
574
- },
575
- }, async (args) => {
576
- const { page, tabId } = await ensurePage(args.tabId);
577
- let text = '';
578
- if (args.selector) {
579
- text = await page.innerText(toLocator(args.selector, args.selectorType));
580
- } else {
581
- text = await page.evaluate(() => {
582
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
583
- const parts: string[] = [];
584
- let node = walker.nextNode();
585
- while (node) {
586
- const value = node.nodeValue?.trim();
587
- if (value) parts.push(value);
588
- node = walker.nextNode();
589
- }
590
- return parts.join('\n');
591
- });
592
- }
593
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, text }, null, 2) }] };
594
- });
595
-
596
- server.registerTool('navigation_query', {
597
- description: 'Extract elements via CSS or XPath selectors',
598
- inputSchema: {
599
- tabId: z.string().optional(),
600
- selector: z.string(),
601
- selectorType: z.enum(['css', 'xpath']).optional(),
602
- attributes: z.array(z.string()).optional(),
603
- limit: z.number().optional(),
604
- },
605
- }, async (args) => {
606
- const { page, tabId } = await ensurePage(args.tabId);
607
- const selector = toLocator(args.selector, args.selectorType);
608
- const limit = args.limit ?? 50;
609
- const attributes = args.attributes ?? [];
610
- const items = await page.$$eval(selector, (els, attrs, max) => {
611
- return Array.from(els).slice(0, max).map(el => {
612
- const record: Record<string, string> = {};
613
- for (const attr of attrs as string[]) {
614
- const val = (el as Element).getAttribute(attr);
615
- if (val !== null) record[attr] = val;
616
- }
617
- return {
618
- text: (el.textContent || '').trim(),
619
- attributes: record,
620
- };
621
- });
622
- }, attributes, limit);
623
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, items }, null, 2) }] };
624
- });
625
-
626
- server.registerTool('navigation_extract', {
627
- description: 'Extract structured data from lists, tables, or cards',
628
- inputSchema: {
629
- tabId: z.string().optional(),
630
- itemSelector: z.string(),
631
- selectorType: z.enum(['css', 'xpath']).optional(),
632
- fields: z.record(z.union([
633
- z.string(),
634
- z.object({ selector: z.string(), attr: z.string().optional(), text: z.boolean().optional() }),
635
- ])),
636
- limit: z.number().optional(),
637
- },
638
- }, async (args) => {
639
- const { page, tabId } = await ensurePage(args.tabId);
640
- const selector = toLocator(args.itemSelector, args.selectorType);
641
- const limit = args.limit ?? 50;
642
- const fields = args.fields;
643
- const items = await page.$$eval(selector, (els, fieldsDef, max) => {
644
- const list = Array.from(els).slice(0, max);
645
- return list.map(el => {
646
- const record: Record<string, string> = {};
647
- for (const key of Object.keys(fieldsDef as Record<string, any>)) {
648
- const def = (fieldsDef as any)[key];
649
- if (typeof def === 'string') {
650
- const target = (el as Element).querySelector(def);
651
- record[key] = (target?.textContent || '').trim();
652
- } else if (def && typeof def === 'object') {
653
- const target = (el as Element).querySelector(def.selector);
654
- if (!target) {
655
- record[key] = '';
656
- } else if (def.attr) {
657
- record[key] = target.getAttribute(def.attr) || '';
658
- } else if (def.text === false) {
659
- record[key] = target.innerHTML || '';
660
- } else {
661
- record[key] = (target.textContent || '').trim();
662
- }
663
- }
664
- }
665
- return record;
666
- });
667
- }, fields, limit);
668
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, items }, null, 2) }] };
669
- });
670
-
671
- server.registerTool('navigation_attributes', {
672
- description: 'Get attributes for an element or list of elements',
673
- inputSchema: {
674
- tabId: z.string().optional(),
675
- selector: z.string(),
676
- selectorType: z.enum(['css', 'xpath']).optional(),
677
- attributes: z.array(z.string()),
678
- limit: z.number().optional(),
679
- },
680
- }, async (args) => {
681
- const { page, tabId } = await ensurePage(args.tabId);
682
- const selector = toLocator(args.selector, args.selectorType);
683
- const limit = args.limit ?? 50;
684
- const items = await page.$$eval(selector, (els, attrs, max) => {
685
- return Array.from(els).slice(0, max).map(el => {
686
- const record: Record<string, string> = {};
687
- for (const attr of attrs as string[]) {
688
- const val = (el as Element).getAttribute(attr);
689
- if (val !== null) record[attr] = val;
690
- }
691
- return record;
692
- });
693
- }, args.attributes, limit);
694
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, items }, null, 2) }] };
695
- });
696
-
697
- server.registerTool('navigation_dom', {
698
- description: 'Read raw or cleaned DOM HTML',
699
- inputSchema: {
700
- tabId: z.string().optional(),
701
- mode: z.enum(['raw', 'clean']).optional(),
702
- },
703
- }, async (args) => {
704
- const { page, tabId } = await ensurePage(args.tabId);
705
- let html = '';
706
- if (args.mode === 'clean') {
707
- html = await page.evaluate(() => {
708
- const clone = document.documentElement.cloneNode(true) as HTMLElement;
709
- clone.querySelectorAll('script, style, noscript').forEach(el => el.remove());
710
- return clone.outerHTML;
711
- });
712
- } else {
713
- html = await page.content();
714
- }
715
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, html }, null, 2) }] };
716
- });
717
-
718
- server.registerTool('navigation_ui_state', {
719
- description: 'Detect UI states, errors, warnings, or success messages',
720
- inputSchema: {
721
- tabId: z.string().optional(),
722
- selectors: z.record(z.string()).optional(),
723
- patterns: z.array(z.string()).optional(),
724
- },
725
- }, async (args) => {
726
- const { page, tabId } = await ensurePage(args.tabId);
727
- const selectors = args.selectors ?? {};
728
- const stateEntries: Record<string, string[]> = {};
729
- for (const [key, selector] of Object.entries(selectors)) {
730
- const items = await page.$$eval(selector, els => els.map(el => (el.textContent || '').trim()).filter(Boolean));
731
- stateEntries[key] = items;
732
- }
733
- const text = await page.evaluate(() => document.body?.innerText || '');
734
- const patterns = args.patterns ?? [];
735
- const matches = patterns.map(p => {
736
- const re = new RegExp(p, 'i');
737
- return { pattern: p, matched: re.test(text) };
738
- });
739
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, selectors: stateEntries, matches }, null, 2) }] };
740
- });
741
-
742
- server.registerTool('navigation_cookies', {
743
- description: 'Get, set, or clear cookies',
744
- inputSchema: {
745
- action: z.enum(['get', 'set', 'clear']),
746
- cookies: z.array(z.object({
747
- name: z.string(),
748
- value: z.string(),
749
- url: z.string().optional(),
750
- domain: z.string().optional(),
751
- path: z.string().optional(),
752
- expires: z.number().optional(),
753
- httpOnly: z.boolean().optional(),
754
- secure: z.boolean().optional(),
755
- sameSite: z.string().optional(),
756
- })).optional(),
757
- },
758
- }, async (args) => {
759
- const ctx = await ensureContext();
760
- if (args.action === 'set') {
761
- if (args.cookies && args.cookies.length > 0) {
762
- await ctx.addCookies(args.cookies);
763
- }
764
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok' }, null, 2) }] };
765
- }
766
- if (args.action === 'clear') {
767
- await ctx.clearCookies();
768
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'cleared' }, null, 2) }] };
769
- }
770
- const cookies = await ctx.cookies();
771
- return { content: [{ type: 'text', text: JSON.stringify({ cookies }, null, 2) }] };
772
- });
773
-
774
- server.registerTool('navigation_headers', {
775
- description: 'Set extra HTTP headers for the browser context',
776
- inputSchema: {
777
- headers: z.record(z.string()),
778
- },
779
- }, async (args) => {
780
- await setContextConfig({ extraHTTPHeaders: args.headers });
781
- const ctx = await ensureContext();
782
- await ctx.setExtraHTTPHeaders(args.headers);
783
- return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok' }, null, 2) }] };
784
- });
785
-
786
- server.registerTool('navigation_dialog', {
787
- description: 'Handle JavaScript dialogs (alert, confirm, prompt)',
788
- inputSchema: {
789
- tabId: z.string().optional(),
790
- action: z.enum(['accept', 'dismiss']),
791
- promptText: z.string().optional(),
792
- timeoutMs: z.number().optional(),
793
- },
794
- }, async (args) => {
795
- const { page, tabId } = await ensurePage(args.tabId);
796
- const dialog = await page.waitForEvent('dialog', { timeout: args.timeoutMs ?? 5000 });
797
- const message = dialog.message();
798
- if (args.action === 'accept') {
799
- await dialog.accept(args.promptText);
800
- } else {
801
- await dialog.dismiss();
802
- }
803
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, message }, null, 2) }] };
804
- });
805
-
806
- server.registerTool('navigation_wait_popup', {
807
- description: 'Wait for a popup window and register it as a new tab',
808
- inputSchema: {
809
- tabId: z.string().optional(),
810
- timeoutMs: z.number().optional(),
811
- },
812
- }, async (args) => {
813
- const { page } = await ensurePage(args.tabId);
814
- const popup = await page.waitForEvent('popup', { timeout: args.timeoutMs ?? 10000 });
815
- const tabId = registerPage(popup);
816
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, url: popup.url() }, null, 2) }] };
817
- });
818
-
819
- server.registerTool('navigation_cookie_consent', {
820
- description: 'Attempt to accept or reject common cookie consent banners',
821
- inputSchema: {
822
- tabId: z.string().optional(),
823
- action: z.enum(['accept', 'reject']).optional(),
824
- },
825
- }, async (args) => {
826
- const { page, tabId } = await ensurePage(args.tabId);
827
- const action = args.action ?? 'accept';
828
- const labels = action === 'accept'
829
- ? ['accept', 'agree', 'allow', 'ok', 'continue', 'tout accepter', 'accepter', 'autoriser']
830
- : ['reject', 'refuse', 'decline', 'deny', 'tout refuser', 'refuser'];
831
- const result = await page.evaluate((labels) => {
832
- const buttons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="submit"], a'));
833
- for (const btn of buttons) {
834
- const text = (btn.textContent || (btn as HTMLInputElement).value || '').trim().toLowerCase();
835
- if (!text) continue;
836
- if (labels.some(label => text.includes(label))) {
837
- (btn as HTMLElement).click();
838
- return { clicked: true, text };
839
- }
840
- }
841
- return { clicked: false, text: '' };
842
- }, labels);
843
- return { content: [{ type: 'text', text: JSON.stringify({ tabId, result }, null, 2) }] };
844
- });
845
-
846
- async function main(): Promise<void> {
847
- const transport = new StdioServerTransport();
848
- await server.connect(transport);
849
- }
850
-
851
- main().catch(error => {
852
- console.error(error);
853
- process.exit(1);
854
- });