safari-mcp 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/index.js ADDED
@@ -0,0 +1,1061 @@
1
+ #!/usr/bin/env node
2
+ // Safari MCP Server - שליטה מלאה ב-Safari דרך AppleScript
3
+ // קל על המחשב, שומר לוגינים, רץ ברקע
4
+
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import * as safari from "./safari.js";
9
+
10
+ const server = new McpServer({
11
+ name: "safari-mcp",
12
+ version: "1.0.0",
13
+ description: "Safari browser automation - lightweight, keeps logins",
14
+ });
15
+
16
+ // ========== NAVIGATION ==========
17
+
18
+ server.tool(
19
+ "safari_navigate",
20
+ "Navigate to a URL in Safari. Waits for page to fully load.",
21
+ { url: z.string().describe("URL to navigate to") },
22
+ async ({ url }) => {
23
+ const result = await safari.navigate(url);
24
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
25
+ }
26
+ );
27
+
28
+ server.tool(
29
+ "safari_go_back",
30
+ "Go back in browser history",
31
+ {},
32
+ async () => {
33
+ const result = await safari.goBack();
34
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
35
+ }
36
+ );
37
+
38
+ server.tool(
39
+ "safari_go_forward",
40
+ "Go forward in browser history",
41
+ {},
42
+ async () => {
43
+ const result = await safari.goForward();
44
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
45
+ }
46
+ );
47
+
48
+ server.tool(
49
+ "safari_reload",
50
+ "Reload the current page",
51
+ { hard: z.boolean().optional().describe("Hard reload (bypass cache)") },
52
+ async ({ hard }) => {
53
+ const result = await safari.reload(hard);
54
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
55
+ }
56
+ );
57
+
58
+ // ========== PAGE INFO ==========
59
+
60
+ server.tool(
61
+ "safari_read_page",
62
+ "Read page content (title, URL, text). Use selector to read specific element. Use maxLength to limit output.",
63
+ {
64
+ selector: z.string().optional().describe("CSS selector to read specific element"),
65
+ maxLength: z.number().optional().describe("Max chars to return (default: 50000)"),
66
+ },
67
+ async ({ selector, maxLength }) => {
68
+ const result = await safari.readPage({ selector, maxLength });
69
+ return { content: [{ type: "text", text: result }] };
70
+ }
71
+ );
72
+
73
+ server.tool(
74
+ "safari_get_source",
75
+ "Get HTML source of current page",
76
+ { maxLength: z.number().optional().describe("Max chars (default: 200000)") },
77
+ async ({ maxLength }) => {
78
+ const result = await safari.getPageSource({ maxLength });
79
+ return { content: [{ type: "text", text: result }] };
80
+ }
81
+ );
82
+
83
+ // ========== SNAPSHOT (ref-based interaction like Chrome DevTools MCP) ==========
84
+
85
+ server.tool(
86
+ "safari_snapshot",
87
+ "Get page accessibility tree with ref IDs for every interactive element. Use refs with click/fill/type instead of CSS selectors. PREFERRED workflow: snapshot → see refs → click({ref:'0_5'})",
88
+ { selector: z.string().optional().describe("CSS selector for subtree (default: full page)") },
89
+ async (args) => {
90
+ const result = await safari.takeSnapshot(args);
91
+ return { content: [{ type: "text", text: result }] };
92
+ }
93
+ );
94
+
95
+ // ========== CLICK ==========
96
+
97
+ server.tool(
98
+ "safari_click",
99
+ "Click element. Use ref (from snapshot), selector, text, or x/y",
100
+ {
101
+ ref: z.string().optional().describe("Ref ID from safari_snapshot (e.g. '0_5')"),
102
+ selector: z.string().optional().describe("CSS selector"),
103
+ text: z.string().optional().describe("Visible text to find and click"),
104
+ x: z.number().optional().describe("X coordinate"),
105
+ y: z.number().optional().describe("Y coordinate"),
106
+ },
107
+ async (args) => {
108
+ const result = await safari.click(args);
109
+ return { content: [{ type: "text", text: result }] };
110
+ }
111
+ );
112
+
113
+ server.tool(
114
+ "safari_double_click",
115
+ "Double-click an element by CSS selector or x/y coordinates (e.g. to select a word in text)",
116
+ {
117
+ selector: z.string().optional().describe("CSS selector"),
118
+ x: z.number().optional().describe("X coordinate"),
119
+ y: z.number().optional().describe("Y coordinate"),
120
+ },
121
+ async (args) => {
122
+ const result = await safari.doubleClick(args);
123
+ return { content: [{ type: "text", text: result }] };
124
+ }
125
+ );
126
+
127
+ server.tool(
128
+ "safari_right_click",
129
+ "Right-click (context menu) an element by CSS selector or x/y coordinates",
130
+ {
131
+ selector: z.string().optional().describe("CSS selector"),
132
+ x: z.number().optional().describe("X coordinate"),
133
+ y: z.number().optional().describe("Y coordinate"),
134
+ },
135
+ async (args) => {
136
+ const result = await safari.rightClick(args);
137
+ return { content: [{ type: "text", text: result }] };
138
+ }
139
+ );
140
+
141
+ // ========== FORM INPUT ==========
142
+
143
+ server.tool(
144
+ "safari_fill",
145
+ "Fill input field. Use ref (from snapshot) or CSS selector",
146
+ {
147
+ ref: z.string().optional().describe("Ref ID from safari_snapshot"),
148
+ selector: z.string().optional().describe("CSS selector"),
149
+ value: z.string().describe("Value to fill"),
150
+ },
151
+ async (args) => {
152
+ const result = await safari.fill(args);
153
+ return { content: [{ type: "text", text: result }] };
154
+ }
155
+ );
156
+
157
+ server.tool(
158
+ "safari_clear_field",
159
+ "Clear an input field",
160
+ { selector: z.string().describe("CSS selector of the input") },
161
+ async (args) => {
162
+ const result = await safari.clearField(args);
163
+ return { content: [{ type: "text", text: result }] };
164
+ }
165
+ );
166
+
167
+ server.tool(
168
+ "safari_select_option",
169
+ "Select an option in a dropdown/select element",
170
+ {
171
+ selector: z.string().describe("CSS selector of the select"),
172
+ value: z.string().describe("Option value to select"),
173
+ },
174
+ async (args) => {
175
+ const result = await safari.selectOption(args);
176
+ return { content: [{ type: "text", text: result }] };
177
+ }
178
+ );
179
+
180
+ server.tool(
181
+ "safari_fill_form",
182
+ "Fill multiple form fields at once",
183
+ {
184
+ fields: z.array(z.object({
185
+ selector: z.string().describe("CSS selector"),
186
+ value: z.string().describe("Value to fill"),
187
+ })).describe("Array of {selector, value} pairs"),
188
+ },
189
+ async (args) => {
190
+ const result = await safari.fillForm(args);
191
+ return { content: [{ type: "text", text: result }] };
192
+ }
193
+ );
194
+
195
+ // ========== KEYBOARD ==========
196
+
197
+ server.tool(
198
+ "safari_press_key",
199
+ "Press a keyboard key (enter, tab, escape, arrows, etc). Supports modifiers (cmd, shift, alt, ctrl).",
200
+ {
201
+ key: z.string().describe("Key name: enter, tab, escape, space, delete, up, down, left, right, or a single character"),
202
+ modifiers: z.array(z.string()).optional().describe("Modifier keys: cmd, shift, alt, ctrl"),
203
+ },
204
+ async (args) => {
205
+ const result = await safari.pressKey(args);
206
+ return { content: [{ type: "text", text: result }] };
207
+ }
208
+ );
209
+
210
+ server.tool(
211
+ "safari_type_text",
212
+ "Type text character by character. Use ref or selector to focus first.",
213
+ {
214
+ text: z.string().describe("Text to type"),
215
+ ref: z.string().optional().describe("Ref ID from safari_snapshot"),
216
+ selector: z.string().optional().describe("CSS selector to focus"),
217
+ },
218
+ async (args) => {
219
+ const result = await safari.typeText(args);
220
+ return { content: [{ type: "text", text: result }] };
221
+ }
222
+ );
223
+
224
+ // ========== SCREENSHOT ==========
225
+
226
+ server.tool(
227
+ "safari_screenshot",
228
+ "Take a screenshot of the current Safari window. Returns base64 PNG image.",
229
+ {
230
+ fullPage: z.boolean().optional().describe("Capture full page (not just viewport)"),
231
+ },
232
+ async ({ fullPage }) => {
233
+ const base64 = await safari.screenshot({ fullPage });
234
+ return {
235
+ content: [{ type: "image", data: base64, mimeType: "image/png" }],
236
+ };
237
+ }
238
+ );
239
+
240
+ server.tool(
241
+ "safari_screenshot_element",
242
+ "Take a screenshot of a specific element (by CSS selector). Returns base64 PNG image.",
243
+ { selector: z.string().describe("CSS selector of the element to capture") },
244
+ async ({ selector }) => {
245
+ const base64 = await safari.screenshotElement({ selector });
246
+ return {
247
+ content: [{ type: "image", data: base64, mimeType: "image/png" }],
248
+ };
249
+ }
250
+ );
251
+
252
+ // ========== SCROLL ==========
253
+
254
+ server.tool(
255
+ "safari_scroll",
256
+ "Scroll the page up or down by a specified amount",
257
+ {
258
+ direction: z.enum(["up", "down"]).optional().describe("Scroll direction (default: down)"),
259
+ amount: z.number().optional().describe("Pixels to scroll (default: 500)"),
260
+ },
261
+ async (args) => {
262
+ const result = await safari.scroll(args);
263
+ return { content: [{ type: "text", text: result }] };
264
+ }
265
+ );
266
+
267
+ server.tool(
268
+ "safari_scroll_to",
269
+ "Scroll to a specific position on the page",
270
+ {
271
+ x: z.number().optional().describe("X position (default: 0)"),
272
+ y: z.number().optional().describe("Y position (default: 0)"),
273
+ },
274
+ async (args) => {
275
+ const result = await safari.scrollTo(args);
276
+ return { content: [{ type: "text", text: result }] };
277
+ }
278
+ );
279
+
280
+ // ========== TAB MANAGEMENT ==========
281
+
282
+ server.tool(
283
+ "safari_list_tabs",
284
+ "List all open tabs in Safari with their titles and URLs",
285
+ {},
286
+ async () => {
287
+ const result = await safari.listTabs();
288
+ return { content: [{ type: "text", text: result }] };
289
+ }
290
+ );
291
+
292
+ server.tool(
293
+ "safari_new_tab",
294
+ "Open a new tab, optionally with a URL",
295
+ { url: z.string().optional().describe("URL to open (empty for blank tab)") },
296
+ async ({ url }) => {
297
+ const result = await safari.newTab(url);
298
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
299
+ }
300
+ );
301
+
302
+ server.tool(
303
+ "safari_close_tab",
304
+ "Close the current tab",
305
+ {},
306
+ async () => {
307
+ const result = await safari.closeTab();
308
+ return { content: [{ type: "text", text: result }] };
309
+ }
310
+ );
311
+
312
+ server.tool(
313
+ "safari_switch_tab",
314
+ "Switch to a specific tab by index (use safari_list_tabs to see indices)",
315
+ { index: z.number().describe("Tab index (starting from 1)") },
316
+ async ({ index }) => {
317
+ const result = await safari.switchTab(index);
318
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
319
+ }
320
+ );
321
+
322
+ // ========== WAIT ==========
323
+
324
+ server.tool(
325
+ "safari_wait_for",
326
+ "Wait for an element or text to appear on the page",
327
+ {
328
+ selector: z.string().optional().describe("CSS selector to wait for"),
329
+ text: z.string().optional().describe("Text to wait for"),
330
+ timeout: z.number().optional().describe("Timeout in ms (default: 10000)"),
331
+ },
332
+ async (args) => {
333
+ const result = await safari.waitFor(args);
334
+ return { content: [{ type: "text", text: result }] };
335
+ }
336
+ );
337
+
338
+ // ========== EVALUATE JAVASCRIPT ==========
339
+
340
+ server.tool(
341
+ "safari_evaluate",
342
+ "Execute arbitrary JavaScript in the current page and return the result",
343
+ { script: z.string().describe("JavaScript code to execute") },
344
+ async (args) => {
345
+ const result = await safari.evaluate(args);
346
+ return { content: [{ type: "text", text: result || "(no return value)" }] };
347
+ }
348
+ );
349
+
350
+ // ========== ELEMENT INFO ==========
351
+
352
+ server.tool(
353
+ "safari_get_element",
354
+ "Get detailed info about an element (tag, text, rect, attributes, visibility)",
355
+ { selector: z.string().describe("CSS selector") },
356
+ async (args) => {
357
+ const result = await safari.getElementInfo(args);
358
+ return { content: [{ type: "text", text: result }] };
359
+ }
360
+ );
361
+
362
+ server.tool(
363
+ "safari_query_all",
364
+ "Find all elements matching a CSS selector (returns tag, text, href, value)",
365
+ {
366
+ selector: z.string().describe("CSS selector"),
367
+ limit: z.number().optional().describe("Max results (default: 20)"),
368
+ },
369
+ async (args) => {
370
+ const result = await safari.querySelectorAll(args);
371
+ return { content: [{ type: "text", text: result }] };
372
+ }
373
+ );
374
+
375
+ // ========== HOVER ==========
376
+
377
+ server.tool(
378
+ "safari_hover",
379
+ "Hover over element. Use ref, selector, or x/y",
380
+ {
381
+ ref: z.string().optional().describe("Ref ID from safari_snapshot"),
382
+ selector: z.string().optional().describe("CSS selector"),
383
+ x: z.number().optional().describe("X coordinate"),
384
+ y: z.number().optional().describe("Y coordinate"),
385
+ },
386
+ async (args) => {
387
+ const result = await safari.hover(args);
388
+ return { content: [{ type: "text", text: result }] };
389
+ }
390
+ );
391
+
392
+ // ========== DIALOG HANDLING ==========
393
+
394
+ server.tool(
395
+ "safari_handle_dialog",
396
+ "Set up handler for the next alert/confirm/prompt dialog",
397
+ {
398
+ action: z.enum(["accept", "dismiss"]).optional().describe("Accept or dismiss (default: accept)"),
399
+ text: z.string().optional().describe("Text to enter for prompt dialogs"),
400
+ },
401
+ async (args) => {
402
+ const result = await safari.handleDialog(args);
403
+ return { content: [{ type: "text", text: result }] };
404
+ }
405
+ );
406
+
407
+ // ========== WINDOW ==========
408
+
409
+ server.tool(
410
+ "safari_resize",
411
+ "Resize the Safari window",
412
+ {
413
+ width: z.number().describe("Window width"),
414
+ height: z.number().describe("Window height"),
415
+ },
416
+ async (args) => {
417
+ const result = await safari.resizeWindow(args);
418
+ return { content: [{ type: "text", text: result }] };
419
+ }
420
+ );
421
+
422
+ // ========== DRAG ==========
423
+
424
+ server.tool(
425
+ "safari_drag",
426
+ "Drag an element to another element or position. Use CSS selectors or x/y coordinates.",
427
+ {
428
+ sourceSelector: z.string().optional().describe("CSS selector of element to drag"),
429
+ targetSelector: z.string().optional().describe("CSS selector of drop target"),
430
+ sourceX: z.number().optional().describe("Source X coordinate"),
431
+ sourceY: z.number().optional().describe("Source Y coordinate"),
432
+ targetX: z.number().optional().describe("Target X coordinate"),
433
+ targetY: z.number().optional().describe("Target Y coordinate"),
434
+ },
435
+ async (args) => {
436
+ const result = await safari.drag(args);
437
+ return { content: [{ type: "text", text: result }] };
438
+ }
439
+ );
440
+
441
+ // ========== UPLOAD FILE ==========
442
+
443
+ server.tool(
444
+ "safari_upload_file",
445
+ "Upload a file to a <input type='file'> element via JavaScript DataTransfer — NO file dialog, NO UI interaction. IMPORTANT: Do NOT click the file input before calling this tool — just provide the selector and file path. If a file dialog is already open, this tool will close it first.",
446
+ {
447
+ selector: z.string().describe("CSS selector of the file input"),
448
+ filePath: z.string().describe("Absolute path to the file to upload"),
449
+ },
450
+ async (args) => {
451
+ const result = await safari.uploadFile(args);
452
+ return { content: [{ type: "text", text: result }] };
453
+ }
454
+ );
455
+
456
+ // ========== PASTE IMAGE ==========
457
+
458
+ server.tool(
459
+ "safari_paste_image",
460
+ "Paste an image from a local file into the focused element. Copies to clipboard then Cmd+V. Works on Medium, dev.to, HackerNoon, etc.",
461
+ {
462
+ filePath: z.string().describe("Absolute path to the image file (PNG, JPG, WebP)"),
463
+ },
464
+ async ({ filePath }) => {
465
+ const result = await safari.pasteImageFromFile({ filePath });
466
+ return { content: [{ type: "text", text: result }] };
467
+ }
468
+ );
469
+
470
+ // ========== EMULATE (DEVICE SIMULATION) ==========
471
+
472
+ server.tool(
473
+ "safari_emulate",
474
+ "Emulate a mobile device by resizing window and setting user agent. Devices: iphone-14, iphone-14-pro-max, ipad, ipad-pro, pixel-7, galaxy-s24. Or use custom width/height.",
475
+ {
476
+ device: z.string().optional().describe("Device name: iphone-14, ipad, pixel-7, galaxy-s24, etc."),
477
+ width: z.number().optional().describe("Custom viewport width"),
478
+ height: z.number().optional().describe("Custom viewport height"),
479
+ userAgent: z.string().optional().describe("Custom user agent string"),
480
+ scale: z.number().optional().describe("Initial scale (default: 1)"),
481
+ },
482
+ async (args) => {
483
+ const result = await safari.emulate(args);
484
+ return { content: [{ type: "text", text: result }] };
485
+ }
486
+ );
487
+
488
+ server.tool(
489
+ "safari_reset_emulation",
490
+ "Reset device emulation back to desktop mode",
491
+ {},
492
+ async () => {
493
+ const result = await safari.resetEmulation();
494
+ return { content: [{ type: "text", text: result }] };
495
+ }
496
+ );
497
+
498
+ // ========== COOKIES & STORAGE ==========
499
+
500
+ server.tool(
501
+ "safari_get_cookies",
502
+ "Get cookies for the current page",
503
+ {},
504
+ async () => {
505
+ const result = await safari.getCookies();
506
+ return { content: [{ type: "text", text: result || "(no cookies)" }] };
507
+ }
508
+ );
509
+
510
+ server.tool(
511
+ "safari_local_storage",
512
+ "Get localStorage data for the current page",
513
+ { key: z.string().optional().describe("Specific key to get (omit for all)") },
514
+ async ({ key }) => {
515
+ const result = await safari.getLocalStorage({ key });
516
+ return { content: [{ type: "text", text: result || "(empty)" }] };
517
+ }
518
+ );
519
+
520
+ // ========== NETWORK ==========
521
+
522
+ server.tool(
523
+ "safari_network",
524
+ "Get network requests made by the current page (via Performance API)",
525
+ { limit: z.number().optional().describe("Max requests to return (default: 50)") },
526
+ async ({ limit }) => {
527
+ const result = await safari.getNetworkRequests({ limit });
528
+ return { content: [{ type: "text", text: result }] };
529
+ }
530
+ );
531
+
532
+ // ========== RUN SCRIPT (multi-step) ==========
533
+
534
+ server.tool(
535
+ "safari_run_script",
536
+ "Batch multiple Safari actions in ONE call. Steps: [{action, args}]. Actions match other safari_* tool names without prefix (e.g. 'navigate', 'click', 'fill', 'evaluate', 'readPage').",
537
+ {
538
+ steps: z.array(z.object({
539
+ action: z.string().describe("Action name (e.g. 'navigate', 'click', 'fill')"),
540
+ args: z.record(z.string(), z.unknown()).optional().describe("Arguments for the action"),
541
+ })).describe("Array of steps to execute sequentially"),
542
+ },
543
+ async ({ steps }) => {
544
+ const result = await safari.runScript({ steps });
545
+ return { content: [{ type: "text", text: result }] };
546
+ }
547
+ );
548
+
549
+ // ========== CONSOLE ==========
550
+
551
+ server.tool(
552
+ "safari_start_console",
553
+ "Start capturing console messages (log, warn, error, info). Call once per page.",
554
+ {},
555
+ async () => {
556
+ const result = await safari.startConsoleCapture();
557
+ return { content: [{ type: "text", text: result }] };
558
+ }
559
+ );
560
+
561
+ server.tool(
562
+ "safari_get_console",
563
+ "Get captured console messages (must call safari_start_console first)",
564
+ {},
565
+ async () => {
566
+ const result = await safari.getConsoleMessages();
567
+ return { content: [{ type: "text", text: result }] };
568
+ }
569
+ );
570
+
571
+ server.tool(
572
+ "safari_clear_console",
573
+ "Clear all captured console messages",
574
+ {},
575
+ async () => {
576
+ const result = await safari.clearConsoleCapture();
577
+ return { content: [{ type: "text", text: result }] };
578
+ }
579
+ );
580
+
581
+ // ========== PDF SAVE ==========
582
+
583
+ server.tool(
584
+ "safari_save_pdf",
585
+ "Save the current page as a PDF file. Uses Safari's native Export as PDF.",
586
+ { path: z.string().describe("Absolute file path to save the PDF (e.g. /Users/am/Downloads/page.pdf)") },
587
+ async (args) => {
588
+ const result = await safari.savePDF(args);
589
+ return { content: [{ type: "text", text: result }] };
590
+ }
591
+ );
592
+
593
+ // ========== ACCESSIBILITY SNAPSHOT ==========
594
+
595
+ server.tool(
596
+ "safari_accessibility_snapshot",
597
+ "Get the accessibility tree of the page (roles, ARIA labels, focusable elements, form states). Essential for a11y auditing.",
598
+ {
599
+ selector: z.string().optional().describe("CSS selector for subtree (default: full page)"),
600
+ maxDepth: z.number().optional().describe("Max tree depth (default: 5)"),
601
+ },
602
+ async (args) => {
603
+ const result = await safari.getAccessibilityTree(args);
604
+ return { content: [{ type: "text", text: result }] };
605
+ }
606
+ );
607
+
608
+ // ========== COOKIE CRUD ==========
609
+
610
+ server.tool(
611
+ "safari_set_cookie",
612
+ "Set a cookie on the current page",
613
+ {
614
+ name: z.string().describe("Cookie name"),
615
+ value: z.string().describe("Cookie value"),
616
+ domain: z.string().optional().describe("Cookie domain"),
617
+ path: z.string().optional().describe("Cookie path (default: /)"),
618
+ expires: z.string().optional().describe("Expiry date (e.g. 'Thu, 01 Jan 2030 00:00:00 GMT')"),
619
+ secure: z.boolean().optional().describe("Secure flag"),
620
+ sameSite: z.enum(["Strict", "Lax", "None"]).optional().describe("SameSite attribute"),
621
+ },
622
+ async (args) => {
623
+ const result = await safari.setCookie(args);
624
+ return { content: [{ type: "text", text: result }] };
625
+ }
626
+ );
627
+
628
+ server.tool(
629
+ "safari_delete_cookies",
630
+ "Delete a specific cookie or all cookies for the current page",
631
+ {
632
+ name: z.string().optional().describe("Cookie name to delete"),
633
+ all: z.boolean().optional().describe("Delete all cookies"),
634
+ },
635
+ async (args) => {
636
+ const result = await safari.deleteCookies(args);
637
+ return { content: [{ type: "text", text: result }] };
638
+ }
639
+ );
640
+
641
+ // ========== SESSION STORAGE ==========
642
+
643
+ server.tool(
644
+ "safari_session_storage",
645
+ "Get sessionStorage data for the current page",
646
+ { key: z.string().optional().describe("Specific key (omit for all)") },
647
+ async ({ key }) => {
648
+ const result = await safari.getSessionStorage({ key });
649
+ return { content: [{ type: "text", text: result || "(empty)" }] };
650
+ }
651
+ );
652
+
653
+ server.tool(
654
+ "safari_set_session_storage",
655
+ "Set a value in sessionStorage",
656
+ {
657
+ key: z.string().describe("Storage key"),
658
+ value: z.string().describe("Value to store"),
659
+ },
660
+ async (args) => {
661
+ const result = await safari.setSessionStorage(args);
662
+ return { content: [{ type: "text", text: result }] };
663
+ }
664
+ );
665
+
666
+ server.tool(
667
+ "safari_set_local_storage",
668
+ "Set a value in localStorage",
669
+ {
670
+ key: z.string().describe("Storage key"),
671
+ value: z.string().describe("Value to store"),
672
+ },
673
+ async (args) => {
674
+ const result = await safari.setLocalStorage(args);
675
+ return { content: [{ type: "text", text: result }] };
676
+ }
677
+ );
678
+
679
+ // ========== STORAGE DELETE / CLEAR ==========
680
+
681
+ server.tool(
682
+ "safari_delete_local_storage",
683
+ "Delete a localStorage key, or clear all localStorage (omit key to clear all)",
684
+ { key: z.string().optional().describe("Key to delete (omit to clear ALL)") },
685
+ async ({ key }) => {
686
+ const result = await safari.deleteLocalStorage({ key });
687
+ return { content: [{ type: "text", text: result }] };
688
+ }
689
+ );
690
+
691
+ server.tool(
692
+ "safari_delete_session_storage",
693
+ "Delete a sessionStorage key, or clear all sessionStorage (omit key to clear all)",
694
+ { key: z.string().optional().describe("Key to delete (omit to clear ALL)") },
695
+ async ({ key }) => {
696
+ const result = await safari.deleteSessionStorage({ key });
697
+ return { content: [{ type: "text", text: result }] };
698
+ }
699
+ );
700
+
701
+ // ========== STORAGE STATE EXPORT/IMPORT ==========
702
+
703
+ server.tool(
704
+ "safari_export_storage",
705
+ "Export all storage state (cookies + localStorage + sessionStorage) as JSON — useful for saving and restoring login sessions",
706
+ {},
707
+ async () => {
708
+ const result = await safari.exportStorageState();
709
+ return { content: [{ type: "text", text: result }] };
710
+ }
711
+ );
712
+
713
+ server.tool(
714
+ "safari_import_storage",
715
+ "Import storage state from JSON (as exported by safari_export_storage) — restores cookies, localStorage, sessionStorage",
716
+ { state: z.string().describe("JSON string from safari_export_storage") },
717
+ async ({ state }) => {
718
+ const result = await safari.importStorageState({ state });
719
+ return { content: [{ type: "text", text: result }] };
720
+ }
721
+ );
722
+
723
+ // ========== CLIPBOARD ==========
724
+
725
+ server.tool(
726
+ "safari_clipboard_read",
727
+ "Read the current clipboard content (text)",
728
+ {},
729
+ async () => {
730
+ const result = await safari.clipboardRead();
731
+ return { content: [{ type: "text", text: result }] };
732
+ }
733
+ );
734
+
735
+ server.tool(
736
+ "safari_clipboard_write",
737
+ "Write text to the system clipboard",
738
+ { text: z.string().describe("Text to copy to clipboard") },
739
+ async (args) => {
740
+ const result = await safari.clipboardWrite(args);
741
+ return { content: [{ type: "text", text: result }] };
742
+ }
743
+ );
744
+
745
+ // ========== NETWORK MOCKING ==========
746
+
747
+ server.tool(
748
+ "safari_mock_route",
749
+ "Intercept network requests matching a URL pattern and return a mock response. Works with both fetch and XHR. Useful for testing API error states, offline behavior, or replacing API responses.",
750
+ {
751
+ urlPattern: z.string().describe("URL substring or regex pattern to match (e.g. '/api/users' or 'example\\.com')"),
752
+ response: z.object({
753
+ status: z.number().optional().describe("HTTP status code (default: 200)"),
754
+ body: z.string().optional().describe("Response body string (JSON, HTML, text)"),
755
+ contentType: z.string().optional().describe("Content-Type header (default: application/json)"),
756
+ }).describe("Mock response to return"),
757
+ },
758
+ async ({ urlPattern, response }) => {
759
+ const result = await safari.mockNetworkRoute({ urlPattern, response });
760
+ return { content: [{ type: "text", text: result }] };
761
+ }
762
+ );
763
+
764
+ server.tool(
765
+ "safari_clear_mocks",
766
+ "Remove all network route mocks (restore real network behavior)",
767
+ {},
768
+ async () => {
769
+ const result = await safari.clearNetworkMocks();
770
+ return { content: [{ type: "text", text: result }] };
771
+ }
772
+ );
773
+
774
+ // ========== WAIT FOR TIME ==========
775
+
776
+ server.tool(
777
+ "safari_wait",
778
+ "Wait for a specified number of milliseconds. Use between actions that need time to settle.",
779
+ { ms: z.number().describe("Milliseconds to wait") },
780
+ async ({ ms }) => {
781
+ const result = await safari.waitForTime({ ms });
782
+ return { content: [{ type: "text", text: result }] };
783
+ }
784
+ );
785
+
786
+ // ========== NETWORK CAPTURE (Detailed) ==========
787
+
788
+ server.tool(
789
+ "safari_start_network_capture",
790
+ "Start capturing detailed network requests (fetch + XHR) with headers, status, timing. Call once per page.",
791
+ {},
792
+ async () => {
793
+ const result = await safari.startNetworkCapture();
794
+ return { content: [{ type: "text", text: result }] };
795
+ }
796
+ );
797
+
798
+ server.tool(
799
+ "safari_network_details",
800
+ "Get captured network requests with full details (must call safari_start_network_capture first)",
801
+ {
802
+ limit: z.number().optional().describe("Max requests (default: 50)"),
803
+ filter: z.string().optional().describe("Filter by URL substring"),
804
+ },
805
+ async (args) => {
806
+ const result = await safari.getNetworkDetails(args);
807
+ return { content: [{ type: "text", text: result }] };
808
+ }
809
+ );
810
+
811
+ server.tool(
812
+ "safari_clear_network",
813
+ "Clear all captured network requests",
814
+ {},
815
+ async () => {
816
+ const result = await safari.clearNetworkCapture();
817
+ return { content: [{ type: "text", text: result }] };
818
+ }
819
+ );
820
+
821
+ // ========== PERFORMANCE METRICS ==========
822
+
823
+ server.tool(
824
+ "safari_performance_metrics",
825
+ "Get detailed performance metrics: navigation timing, Web Vitals (FCP, LCP, CLS), resource breakdown, memory usage",
826
+ {},
827
+ async () => {
828
+ const result = await safari.getPerformanceMetrics();
829
+ return { content: [{ type: "text", text: result }] };
830
+ }
831
+ );
832
+
833
+ // ========== NETWORK THROTTLING ==========
834
+
835
+ server.tool(
836
+ "safari_throttle_network",
837
+ "Simulate slow network conditions. Profiles: slow-3g, fast-3g, 4g, offline. Or custom latency/speed. Call with no args to reset.",
838
+ {
839
+ profile: z.string().optional().describe("Preset: slow-3g, fast-3g, 4g, offline"),
840
+ latency: z.number().optional().describe("Custom latency in ms"),
841
+ downloadKbps: z.number().optional().describe("Custom download speed in Kbps"),
842
+ },
843
+ async (args) => {
844
+ const result = await safari.throttleNetwork(args);
845
+ return { content: [{ type: "text", text: result }] };
846
+ }
847
+ );
848
+
849
+ // ========== CONSOLE FILTER ==========
850
+
851
+ server.tool(
852
+ "safari_console_filter",
853
+ "Get console messages filtered by level (must call safari_start_console first)",
854
+ { level: z.enum(["log", "warn", "error", "info"]).describe("Console level to filter") },
855
+ async (args) => {
856
+ const result = await safari.getConsoleByLevel(args);
857
+ return { content: [{ type: "text", text: result }] };
858
+ }
859
+ );
860
+
861
+ // ========== DATA EXTRACTION ==========
862
+
863
+ server.tool(
864
+ "safari_extract_tables",
865
+ "Extract HTML tables as structured JSON (headers + rows). Perfect for scraping data tables.",
866
+ {
867
+ selector: z.string().optional().describe("CSS selector (default: 'table')"),
868
+ limit: z.number().optional().describe("Max tables (default: 10)"),
869
+ },
870
+ async (args) => {
871
+ const result = await safari.extractTables(args);
872
+ return { content: [{ type: "text", text: result }] };
873
+ }
874
+ );
875
+
876
+ server.tool(
877
+ "safari_extract_meta",
878
+ "Extract all meta tags: title, description, canonical, OG tags, Twitter cards, JSON-LD, alternate languages, RSS feeds",
879
+ {},
880
+ async () => {
881
+ const result = await safari.extractMeta();
882
+ return { content: [{ type: "text", text: result }] };
883
+ }
884
+ );
885
+
886
+ server.tool(
887
+ "safari_extract_images",
888
+ "Extract all images with src, alt, dimensions, loading strategy, viewport visibility",
889
+ { limit: z.number().optional().describe("Max images (default: 50)") },
890
+ async (args) => {
891
+ const result = await safari.extractImages(args);
892
+ return { content: [{ type: "text", text: result }] };
893
+ }
894
+ );
895
+
896
+ server.tool(
897
+ "safari_extract_links",
898
+ "Extract all links with href, text, rel, target, external/nofollow detection",
899
+ {
900
+ limit: z.number().optional().describe("Max links (default: 100)"),
901
+ filter: z.string().optional().describe("Filter by URL or text substring"),
902
+ },
903
+ async (args) => {
904
+ const result = await safari.extractLinks(args);
905
+ return { content: [{ type: "text", text: result }] };
906
+ }
907
+ );
908
+
909
+ // ========== GEOLOCATION OVERRIDE ==========
910
+
911
+ server.tool(
912
+ "safari_override_geolocation",
913
+ "Override the browser's geolocation API to return custom coordinates",
914
+ {
915
+ latitude: z.number().describe("Latitude (-90 to 90)"),
916
+ longitude: z.number().describe("Longitude (-180 to 180)"),
917
+ accuracy: z.number().optional().describe("Accuracy in meters (default: 100)"),
918
+ },
919
+ async (args) => {
920
+ const result = await safari.overrideGeolocation(args);
921
+ return { content: [{ type: "text", text: result }] };
922
+ }
923
+ );
924
+
925
+ // ========== COMPUTED STYLES ==========
926
+
927
+ server.tool(
928
+ "safari_get_computed_style",
929
+ "Get computed CSS styles for an element. Optionally filter specific properties.",
930
+ {
931
+ selector: z.string().describe("CSS selector"),
932
+ properties: z.array(z.string()).optional().describe("Specific CSS properties to get (e.g. ['color', 'font-size'])"),
933
+ },
934
+ async (args) => {
935
+ const result = await safari.getComputedStyles(args);
936
+ return { content: [{ type: "text", text: result }] };
937
+ }
938
+ );
939
+
940
+ // ========== INDEXEDDB ==========
941
+
942
+ server.tool(
943
+ "safari_list_indexed_dbs",
944
+ "List all IndexedDB databases on the current page",
945
+ {},
946
+ async () => {
947
+ const result = await safari.listIndexedDBs();
948
+ return { content: [{ type: "text", text: result }] };
949
+ }
950
+ );
951
+
952
+ server.tool(
953
+ "safari_get_indexed_db",
954
+ "Read records from an IndexedDB database store",
955
+ {
956
+ dbName: z.string().describe("Database name"),
957
+ storeName: z.string().describe("Object store name"),
958
+ limit: z.number().optional().describe("Max records (default: 20)"),
959
+ },
960
+ async (args) => {
961
+ const result = await safari.getIndexedDB(args);
962
+ return { content: [{ type: "text", text: result }] };
963
+ }
964
+ );
965
+
966
+ // ========== CSS COVERAGE ==========
967
+
968
+ server.tool(
969
+ "safari_css_coverage",
970
+ "Analyze CSS coverage: find unused CSS rules across all stylesheets. Shows coverage percentage per stylesheet.",
971
+ {},
972
+ async () => {
973
+ const result = await safari.getCSSCoverage();
974
+ return { content: [{ type: "text", text: result }] };
975
+ }
976
+ );
977
+
978
+ // ========== FORM AUTO-DETECT ==========
979
+
980
+ server.tool(
981
+ "safari_detect_forms",
982
+ "Auto-detect all forms on the page with their fields, types, selectors, and submit buttons. Great for automated form filling.",
983
+ {},
984
+ async () => {
985
+ const result = await safari.detectForms();
986
+ return { content: [{ type: "text", text: result }] };
987
+ }
988
+ );
989
+
990
+ // ========== SCROLL TO ELEMENT ==========
991
+
992
+ server.tool(
993
+ "safari_scroll_to_element",
994
+ "Scroll smoothly to a specific element by CSS selector",
995
+ {
996
+ selector: z.string().describe("CSS selector of target element"),
997
+ block: z.enum(["start", "center", "end", "nearest"]).optional().describe("Scroll alignment (default: center)"),
998
+ },
999
+ async (args) => {
1000
+ const result = await safari.scrollToElement(args);
1001
+ return { content: [{ type: "text", text: result }] };
1002
+ }
1003
+ );
1004
+
1005
+ // ========== COMBO TOOLS (fast multi-step operations) ==========
1006
+
1007
+ server.tool(
1008
+ "safari_navigate_and_read",
1009
+ "Navigate to URL AND read the page content in one fast operation. Returns title, URL, and text. Use this instead of navigate + read_page separately.",
1010
+ { url: z.string().describe("URL to navigate to") },
1011
+ async ({ url }) => {
1012
+ const result = await safari.navigateAndRead(url);
1013
+ return { content: [{ type: "text", text: result }] };
1014
+ }
1015
+ );
1016
+
1017
+ server.tool(
1018
+ "safari_click_and_wait",
1019
+ "Click an element AND wait for the result (page load or element). Use instead of click + wait_for separately.",
1020
+ {
1021
+ selector: z.string().optional().describe("CSS selector to click"),
1022
+ text: z.string().optional().describe("Visible text to click"),
1023
+ waitFor: z.string().optional().describe("CSS selector to wait for after click"),
1024
+ timeout: z.number().optional().describe("Wait timeout in ms (default: 10000)"),
1025
+ },
1026
+ async (args) => {
1027
+ const result = await safari.clickAndWait(args);
1028
+ return { content: [{ type: "text", text: result }] };
1029
+ }
1030
+ );
1031
+
1032
+ server.tool(
1033
+ "safari_fill_and_submit",
1034
+ "Fill a form AND submit it in one operation. Finds submit button automatically if not specified.",
1035
+ {
1036
+ fields: z.array(z.object({
1037
+ selector: z.string().describe("CSS selector"),
1038
+ value: z.string().describe("Value to fill"),
1039
+ })).describe("Fields to fill"),
1040
+ submitSelector: z.string().optional().describe("Submit button selector (auto-detected if omitted)"),
1041
+ },
1042
+ async (args) => {
1043
+ const result = await safari.fillAndSubmit(args);
1044
+ return { content: [{ type: "text", text: result }] };
1045
+ }
1046
+ );
1047
+
1048
+ server.tool(
1049
+ "safari_analyze_page",
1050
+ "Full page analysis in ONE call: title, URL, meta tags, OG, headings, link stats, image stats, forms, and text preview. Perfect for SEO/audit.",
1051
+ {},
1052
+ async () => {
1053
+ const result = await safari.analyzePage();
1054
+ return { content: [{ type: "text", text: result }] };
1055
+ }
1056
+ );
1057
+
1058
+ // ========== START SERVER ==========
1059
+
1060
+ const transport = new StdioServerTransport();
1061
+ await server.connect(transport);