mcp-camoufox 0.2.4 → 0.3.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/dist/index.js +353 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ function getPage() {
|
|
|
29
29
|
return pages[activePage];
|
|
30
30
|
}
|
|
31
31
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
32
|
-
import { mkdirSync } from "fs";
|
|
32
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
33
33
|
import { join } from "path";
|
|
34
34
|
function ensureDirs() {
|
|
35
35
|
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
@@ -565,6 +565,358 @@ server.tool("save_pdf", "Save page as PDF.", {
|
|
|
565
565
|
await page.pdf({ path: target });
|
|
566
566
|
return { content: [{ type: "text", text: `PDF saved: ${target}` }] };
|
|
567
567
|
});
|
|
568
|
+
// ── Tools: Batch Operations ────────────────────────────────────────────────
|
|
569
|
+
server.tool("batch_actions", "Execute multiple actions in one call. Each action: {type, ref?, value?, text?, key?, url?}.", {
|
|
570
|
+
actions: z.array(z.object({
|
|
571
|
+
type: z.enum(["click", "fill", "type", "press", "select", "check", "uncheck", "wait"]),
|
|
572
|
+
ref: z.string().optional(),
|
|
573
|
+
value: z.string().optional(),
|
|
574
|
+
text: z.string().optional(),
|
|
575
|
+
key: z.string().optional(),
|
|
576
|
+
timeout: z.number().optional(),
|
|
577
|
+
})).describe("List of actions to execute sequentially"),
|
|
578
|
+
}, async ({ actions }) => {
|
|
579
|
+
const page = getPage();
|
|
580
|
+
const results = [];
|
|
581
|
+
for (const action of actions) {
|
|
582
|
+
try {
|
|
583
|
+
if (action.type === "click" && action.ref) {
|
|
584
|
+
const loc = page.locator(`[data-mcp-ref="${action.ref}"]`).first();
|
|
585
|
+
try {
|
|
586
|
+
await loc.click({ timeout: 5000 });
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
await loc.evaluate((el) => el.click());
|
|
590
|
+
}
|
|
591
|
+
results.push(`click ${action.ref}: OK`);
|
|
592
|
+
}
|
|
593
|
+
else if (action.type === "fill" && action.ref && action.value !== undefined) {
|
|
594
|
+
await page.locator(`[data-mcp-ref="${action.ref}"]`).first().fill(action.value, { timeout: 5000 });
|
|
595
|
+
results.push(`fill ${action.ref}: OK`);
|
|
596
|
+
}
|
|
597
|
+
else if (action.type === "type" && action.text) {
|
|
598
|
+
await page.keyboard.type(action.text, { delay: 50 });
|
|
599
|
+
results.push(`type: OK`);
|
|
600
|
+
}
|
|
601
|
+
else if (action.type === "press" && action.key) {
|
|
602
|
+
await page.keyboard.press(action.key);
|
|
603
|
+
results.push(`press ${action.key}: OK`);
|
|
604
|
+
}
|
|
605
|
+
else if (action.type === "select" && action.ref && action.value) {
|
|
606
|
+
await page.locator(`[data-mcp-ref="${action.ref}"]`).first().selectOption(action.value, { timeout: 5000 });
|
|
607
|
+
results.push(`select ${action.ref}: OK`);
|
|
608
|
+
}
|
|
609
|
+
else if (action.type === "check" && action.ref) {
|
|
610
|
+
await page.locator(`[data-mcp-ref="${action.ref}"]`).first().check({ timeout: 5000 });
|
|
611
|
+
results.push(`check ${action.ref}: OK`);
|
|
612
|
+
}
|
|
613
|
+
else if (action.type === "uncheck" && action.ref) {
|
|
614
|
+
await page.locator(`[data-mcp-ref="${action.ref}"]`).first().uncheck({ timeout: 5000 });
|
|
615
|
+
results.push(`uncheck ${action.ref}: OK`);
|
|
616
|
+
}
|
|
617
|
+
else if (action.type === "wait") {
|
|
618
|
+
await page.waitForTimeout(action.timeout || 1000);
|
|
619
|
+
results.push(`wait ${action.timeout || 1000}ms: OK`);
|
|
620
|
+
}
|
|
621
|
+
await page.waitForTimeout(300);
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
results.push(`${action.type} ${action.ref || ""}: FAIL — ${e.message?.slice(0, 60)}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { content: [{ type: "text", text: `Batch (${actions.length} actions):\n${results.map(r => " " + r).join("\n")}` }] };
|
|
628
|
+
});
|
|
629
|
+
server.tool("fill_form", "Fill multiple form fields and optionally submit.", {
|
|
630
|
+
fields: z.array(z.object({
|
|
631
|
+
ref: z.string().describe("Element ref from snapshot"),
|
|
632
|
+
value: z.string().describe("Value to fill"),
|
|
633
|
+
})),
|
|
634
|
+
submit_ref: z.string().optional().describe("Ref of submit button to click after filling"),
|
|
635
|
+
}, async ({ fields, submit_ref }) => {
|
|
636
|
+
const page = getPage();
|
|
637
|
+
for (const f of fields) {
|
|
638
|
+
await page.locator(`[data-mcp-ref="${f.ref}"]`).first().fill(f.value, { timeout: 5000 });
|
|
639
|
+
}
|
|
640
|
+
if (submit_ref) {
|
|
641
|
+
const btn = page.locator(`[data-mcp-ref="${submit_ref}"]`).first();
|
|
642
|
+
try {
|
|
643
|
+
await btn.click({ timeout: 5000 });
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
await btn.evaluate((el) => el.click());
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
await page.waitForTimeout(1000);
|
|
650
|
+
return { content: [{ type: "text", text: `Filled ${fields.length} fields${submit_ref ? " + submitted" : ""}. URL: ${page.url()}` }] };
|
|
651
|
+
});
|
|
652
|
+
server.tool("navigate_and_snapshot", "Navigate to URL then return snapshot — combined in one call.", {
|
|
653
|
+
url: z.string(),
|
|
654
|
+
wait_until: z.enum(["domcontentloaded", "load", "networkidle"]).default("domcontentloaded"),
|
|
655
|
+
}, async ({ url, wait_until }) => {
|
|
656
|
+
const page = getPage();
|
|
657
|
+
await page.goto(url, { waitUntil: wait_until, timeout: 30000 });
|
|
658
|
+
await page.waitForTimeout(1500);
|
|
659
|
+
const elements = await page.evaluate(SNAPSHOT_JS) || [];
|
|
660
|
+
const text = formatSnapshot(elements, page.url(), await page.title());
|
|
661
|
+
return { content: [{ type: "text", text }] };
|
|
662
|
+
});
|
|
663
|
+
// ── Tools: Element Inspection ──────────────────────────────────────────────
|
|
664
|
+
server.tool("inspect_element", "Get detailed info about an element (tag, attributes, bounding box, styles).", {
|
|
665
|
+
ref: z.string(),
|
|
666
|
+
}, async ({ ref }) => {
|
|
667
|
+
const page = getPage();
|
|
668
|
+
const info = await page.locator(`[data-mcp-ref="${ref}"]`).first().evaluate((el) => {
|
|
669
|
+
const r = el.getBoundingClientRect();
|
|
670
|
+
const cs = getComputedStyle(el);
|
|
671
|
+
const attrs = {};
|
|
672
|
+
for (const a of el.attributes)
|
|
673
|
+
attrs[a.name] = a.value;
|
|
674
|
+
return {
|
|
675
|
+
tag: el.tagName.toLowerCase(), id: el.id, className: el.className,
|
|
676
|
+
text: (el.innerText || "").slice(0, 200), value: el.value || "",
|
|
677
|
+
attrs, rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
678
|
+
visible: cs.display !== "none" && cs.visibility !== "hidden",
|
|
679
|
+
fontSize: cs.fontSize, color: cs.color, bg: cs.backgroundColor,
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
683
|
+
});
|
|
684
|
+
server.tool("get_attribute", "Get a specific attribute value from an element.", {
|
|
685
|
+
ref: z.string(), attribute: z.string(),
|
|
686
|
+
}, async ({ ref, attribute }) => {
|
|
687
|
+
const page = getPage();
|
|
688
|
+
const val = await page.locator(`[data-mcp-ref="${ref}"]`).first().getAttribute(attribute);
|
|
689
|
+
return { content: [{ type: "text", text: `${attribute}=${val}` }] };
|
|
690
|
+
});
|
|
691
|
+
server.tool("query_selector_all", "Query elements by CSS selector, return text/attributes of all matches.", {
|
|
692
|
+
selector: z.string(),
|
|
693
|
+
attribute: z.string().default("").describe("Attribute to extract (empty = innerText)"),
|
|
694
|
+
limit: z.number().default(20),
|
|
695
|
+
}, async ({ selector, attribute, limit }) => {
|
|
696
|
+
const page = getPage();
|
|
697
|
+
const results = await page.evaluate(`(() => {
|
|
698
|
+
var els = document.querySelectorAll("${selector.replace(/"/g, '\\"')}");
|
|
699
|
+
var out = [];
|
|
700
|
+
for (var i = 0; i < Math.min(els.length, ${limit}); i++) {
|
|
701
|
+
out.push({
|
|
702
|
+
i: i,
|
|
703
|
+
text: (els[i].innerText || '').trim().slice(0, 100),
|
|
704
|
+
attr: "${attribute}" ? els[i].getAttribute("${attribute}") || '' : '',
|
|
705
|
+
tag: els[i].tagName.toLowerCase()
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return { total: els.length, items: out };
|
|
709
|
+
})()`);
|
|
710
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
711
|
+
});
|
|
712
|
+
server.tool("get_links", "Get all links on the page with URL and text.", {
|
|
713
|
+
filter: z.string().default("").describe("Filter links by URL pattern (empty = all)"),
|
|
714
|
+
}, async ({ filter }) => {
|
|
715
|
+
const page = getPage();
|
|
716
|
+
const links = await page.evaluate(`(() => {
|
|
717
|
+
var links = document.querySelectorAll('a[href]');
|
|
718
|
+
var out = [];
|
|
719
|
+
for (var i = 0; i < links.length; i++) {
|
|
720
|
+
var href = links[i].href || '';
|
|
721
|
+
var text = (links[i].innerText || '').trim().slice(0, 80);
|
|
722
|
+
if (!text && !href) continue;
|
|
723
|
+
if ("${filter}" && href.indexOf("${filter}") === -1) continue;
|
|
724
|
+
out.push({ text: text, href: href.slice(0, 150) });
|
|
725
|
+
}
|
|
726
|
+
return out;
|
|
727
|
+
})()`);
|
|
728
|
+
const arr = links;
|
|
729
|
+
const lines = arr.slice(0, 50).map((l) => ` ${l.text || "(no text)"} → ${l.href}`);
|
|
730
|
+
return { content: [{ type: "text", text: `Links (${arr.length}):\n${lines.join("\n")}` }] };
|
|
731
|
+
});
|
|
732
|
+
// ── Tools: Storage ─────────────────────────────────────────────────────────
|
|
733
|
+
server.tool("localstorage_get", "Get all localStorage data or a specific key.", {
|
|
734
|
+
key: z.string().default("").describe("Key to get (empty = all)"),
|
|
735
|
+
}, async ({ key }) => {
|
|
736
|
+
const page = getPage();
|
|
737
|
+
if (key) {
|
|
738
|
+
const val = await page.evaluate(`localStorage.getItem("${key.replace(/"/g, '\\"')}")`);
|
|
739
|
+
return { content: [{ type: "text", text: `${key}=${val}` }] };
|
|
740
|
+
}
|
|
741
|
+
const all = await page.evaluate(`(() => { var o = {}; for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); o[k] = localStorage.getItem(k); } return o; })()`);
|
|
742
|
+
return { content: [{ type: "text", text: JSON.stringify(all, null, 2) }] };
|
|
743
|
+
});
|
|
744
|
+
server.tool("localstorage_set", "Set a localStorage item.", {
|
|
745
|
+
key: z.string(), value: z.string(),
|
|
746
|
+
}, async ({ key, value }) => {
|
|
747
|
+
const page = getPage();
|
|
748
|
+
await page.evaluate(`localStorage.setItem("${key.replace(/"/g, '\\"')}", "${value.replace(/"/g, '\\"')}")`);
|
|
749
|
+
return { content: [{ type: "text", text: `localStorage set: ${key}` }] };
|
|
750
|
+
});
|
|
751
|
+
server.tool("localstorage_clear", "Clear all localStorage.", {}, async () => {
|
|
752
|
+
const page = getPage();
|
|
753
|
+
await page.evaluate(`localStorage.clear()`);
|
|
754
|
+
return { content: [{ type: "text", text: "localStorage cleared." }] };
|
|
755
|
+
});
|
|
756
|
+
server.tool("sessionstorage_get", "Get all sessionStorage data or a specific key.", {
|
|
757
|
+
key: z.string().default(""),
|
|
758
|
+
}, async ({ key }) => {
|
|
759
|
+
const page = getPage();
|
|
760
|
+
if (key) {
|
|
761
|
+
const val = await page.evaluate(`sessionStorage.getItem("${key.replace(/"/g, '\\"')}")`);
|
|
762
|
+
return { content: [{ type: "text", text: `${key}=${val}` }] };
|
|
763
|
+
}
|
|
764
|
+
const all = await page.evaluate(`(() => { var o = {}; for (var i = 0; i < sessionStorage.length; i++) { var k = sessionStorage.key(i); o[k] = sessionStorage.getItem(k); } return o; })()`);
|
|
765
|
+
return { content: [{ type: "text", text: JSON.stringify(all, null, 2) }] };
|
|
766
|
+
});
|
|
767
|
+
server.tool("sessionstorage_set", "Set a sessionStorage item.", {
|
|
768
|
+
key: z.string(), value: z.string(),
|
|
769
|
+
}, async ({ key, value }) => {
|
|
770
|
+
const page = getPage();
|
|
771
|
+
await page.evaluate(`sessionStorage.setItem("${key.replace(/"/g, '\\"')}", "${value.replace(/"/g, '\\"')}")`);
|
|
772
|
+
return { content: [{ type: "text", text: `sessionStorage set: ${key}` }] };
|
|
773
|
+
});
|
|
774
|
+
// ── Tools: Mouse XY ────────────────────────────────────────────────────────
|
|
775
|
+
server.tool("mouse_click_xy", "Click at exact x,y coordinates on the page.", {
|
|
776
|
+
x: z.number(), y: z.number(),
|
|
777
|
+
button: z.enum(["left", "right", "middle"]).default("left"),
|
|
778
|
+
}, async ({ x, y, button }) => {
|
|
779
|
+
const page = getPage();
|
|
780
|
+
await page.mouse.click(x, y, { button });
|
|
781
|
+
await page.waitForTimeout(500);
|
|
782
|
+
return { content: [{ type: "text", text: `Clicked at (${x}, ${y}) button=${button}` }] };
|
|
783
|
+
});
|
|
784
|
+
server.tool("mouse_move", "Move mouse to exact x,y coordinates.", {
|
|
785
|
+
x: z.number(), y: z.number(),
|
|
786
|
+
}, async ({ x, y }) => {
|
|
787
|
+
const page = getPage();
|
|
788
|
+
await page.mouse.move(x, y);
|
|
789
|
+
return { content: [{ type: "text", text: `Mouse moved to (${x}, ${y})` }] };
|
|
790
|
+
});
|
|
791
|
+
server.tool("drag_and_drop", "Drag from one element to another.", {
|
|
792
|
+
source_ref: z.string().describe("Ref of element to drag"),
|
|
793
|
+
target_ref: z.string().describe("Ref of drop target"),
|
|
794
|
+
}, async ({ source_ref, target_ref }) => {
|
|
795
|
+
const page = getPage();
|
|
796
|
+
const src = page.locator(`[data-mcp-ref="${source_ref}"]`).first();
|
|
797
|
+
const tgt = page.locator(`[data-mcp-ref="${target_ref}"]`).first();
|
|
798
|
+
await src.dragTo(tgt, { timeout: 5000 });
|
|
799
|
+
return { content: [{ type: "text", text: `Dragged ${source_ref} → ${target_ref}` }] };
|
|
800
|
+
});
|
|
801
|
+
// ── Tools: Frames/Iframes ──────────────────────────────────────────────────
|
|
802
|
+
server.tool("list_frames", "List all frames/iframes in the page.", {}, async () => {
|
|
803
|
+
const page = getPage();
|
|
804
|
+
const frames = page.frames();
|
|
805
|
+
const lines = frames.map((f, i) => ` [${i}] ${f.name() || "(unnamed)"} — ${f.url().slice(0, 100)}`);
|
|
806
|
+
return { content: [{ type: "text", text: `Frames (${frames.length}):\n${lines.join("\n")}` }] };
|
|
807
|
+
});
|
|
808
|
+
server.tool("frame_evaluate", "Execute JavaScript inside a specific frame/iframe.", {
|
|
809
|
+
frame_name: z.string().default("").describe("Frame name (empty = by index)"),
|
|
810
|
+
frame_index: z.number().default(0).describe("Frame index from list_frames"),
|
|
811
|
+
expression: z.string(),
|
|
812
|
+
}, async ({ frame_name, frame_index, expression }) => {
|
|
813
|
+
const page = getPage();
|
|
814
|
+
const frame = frame_name
|
|
815
|
+
? page.frames().find(f => f.name() === frame_name)
|
|
816
|
+
: page.frames()[frame_index];
|
|
817
|
+
if (!frame)
|
|
818
|
+
return { content: [{ type: "text", text: "Frame not found." }] };
|
|
819
|
+
const result = await frame.evaluate(expression);
|
|
820
|
+
return { content: [{ type: "text", text: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result) }] };
|
|
821
|
+
});
|
|
822
|
+
// ── Tools: Wait (extended) ─────────────────────────────────────────────────
|
|
823
|
+
server.tool("wait_for_url", "Wait for URL to match a pattern.", {
|
|
824
|
+
pattern: z.string().describe("URL substring or regex pattern"),
|
|
825
|
+
timeout: z.number().default(15000),
|
|
826
|
+
}, async ({ pattern, timeout }) => {
|
|
827
|
+
const page = getPage();
|
|
828
|
+
await page.waitForURL(pattern.startsWith("/") ? new RegExp(pattern.slice(1, -1)) : `**/*${pattern}*`, { timeout });
|
|
829
|
+
return { content: [{ type: "text", text: `URL matched pattern '${pattern}'. Current: ${page.url()}` }] };
|
|
830
|
+
});
|
|
831
|
+
server.tool("wait_for_response", "Wait for a network response matching a URL pattern.", {
|
|
832
|
+
url_pattern: z.string().describe("URL substring to match"),
|
|
833
|
+
timeout: z.number().default(15000),
|
|
834
|
+
}, async ({ url_pattern, timeout }) => {
|
|
835
|
+
const page = getPage();
|
|
836
|
+
const resp = await page.waitForResponse(r => r.url().includes(url_pattern), { timeout });
|
|
837
|
+
return { content: [{ type: "text", text: `Response: ${resp.status()} ${resp.url().slice(0, 120)}` }] };
|
|
838
|
+
});
|
|
839
|
+
// ── Tools: Viewport ────────────────────────────────────────────────────────
|
|
840
|
+
server.tool("get_viewport_size", "Get current viewport dimensions.", {}, async () => {
|
|
841
|
+
const page = getPage();
|
|
842
|
+
const size = page.viewportSize();
|
|
843
|
+
return { content: [{ type: "text", text: `Viewport: ${size?.width || "?"}x${size?.height || "?"}` }] };
|
|
844
|
+
});
|
|
845
|
+
server.tool("set_viewport_size", "Set viewport width and height.", {
|
|
846
|
+
width: z.number(), height: z.number(),
|
|
847
|
+
}, async ({ width, height }) => {
|
|
848
|
+
const page = getPage();
|
|
849
|
+
await page.setViewportSize({ width, height });
|
|
850
|
+
return { content: [{ type: "text", text: `Viewport set to ${width}x${height}` }] };
|
|
851
|
+
});
|
|
852
|
+
// ── Tools: Accessibility ───────────────────────────────────────────────────
|
|
853
|
+
server.tool("accessibility_snapshot", "Get accessibility tree snapshot — compact view of page structure for LLM understanding.", {}, async () => {
|
|
854
|
+
const page = getPage();
|
|
855
|
+
const snap = await page.evaluate(`(() => {
|
|
856
|
+
function walk(el, depth) {
|
|
857
|
+
if (depth > 4) return null;
|
|
858
|
+
var role = el.getAttribute ? (el.getAttribute('role') || el.tagName.toLowerCase()) : '';
|
|
859
|
+
var name = el.getAttribute ? (el.getAttribute('aria-label') || el.innerText || '').trim().slice(0, 60) : '';
|
|
860
|
+
var node = { role: role, name: name };
|
|
861
|
+
if (el.children && el.children.length > 0 && depth < 3) {
|
|
862
|
+
node.children = [];
|
|
863
|
+
for (var i = 0; i < Math.min(el.children.length, 20); i++) {
|
|
864
|
+
var child = walk(el.children[i], depth + 1);
|
|
865
|
+
if (child && child.name) node.children.push(child);
|
|
866
|
+
}
|
|
867
|
+
if (node.children.length === 0) delete node.children;
|
|
868
|
+
}
|
|
869
|
+
return node;
|
|
870
|
+
}
|
|
871
|
+
return walk(document.body, 0);
|
|
872
|
+
})()`);
|
|
873
|
+
const text = JSON.stringify(snap, null, 2);
|
|
874
|
+
if (text.length > 8000)
|
|
875
|
+
return { content: [{ type: "text", text: text.slice(0, 8000) + "\n... (truncated)" }] };
|
|
876
|
+
return { content: [{ type: "text", text }] };
|
|
877
|
+
});
|
|
878
|
+
// ── Tools: Debug & Health ──────────────────────────────────────────────────
|
|
879
|
+
server.tool("server_status", "Health check — verify server, browser status, active tabs.", {}, async () => {
|
|
880
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
881
|
+
browser_up: browserUp,
|
|
882
|
+
active_tabs: pages.length,
|
|
883
|
+
active_page: activePage,
|
|
884
|
+
current_url: browserUp && pages.length > 0 ? pages[activePage]?.url() : null,
|
|
885
|
+
profile_dir: PROFILE_DIR,
|
|
886
|
+
screenshot_dir: SCREENSHOT_DIR,
|
|
887
|
+
}, null, 2) }] };
|
|
888
|
+
});
|
|
889
|
+
server.tool("get_page_errors", "Get JavaScript errors from the page.", {}, async () => {
|
|
890
|
+
const page = getPage();
|
|
891
|
+
const errors = await page.evaluate(`(() => {
|
|
892
|
+
var errs = window.__mcp_errors || [];
|
|
893
|
+
return errs.slice(-20);
|
|
894
|
+
})()`);
|
|
895
|
+
return { content: [{ type: "text", text: JSON.stringify(errors, null, 2) }] };
|
|
896
|
+
});
|
|
897
|
+
server.tool("inject_init_script", "Inject a script that runs before every page load.", {
|
|
898
|
+
script: z.string().describe("JavaScript code to inject"),
|
|
899
|
+
}, async ({ script }) => {
|
|
900
|
+
if (!browserContext)
|
|
901
|
+
throw new Error("Browser not running.");
|
|
902
|
+
await browserContext.addInitScript(script);
|
|
903
|
+
return { content: [{ type: "text", text: "Init script injected. Will run on every new page/navigation." }] };
|
|
904
|
+
});
|
|
905
|
+
// ── Tools: Export ──────────────────────────────────────────────────────────
|
|
906
|
+
server.tool("export_har", "Export network traffic as HAR file.", {
|
|
907
|
+
path: z.string().default(""),
|
|
908
|
+
}, async ({ path: harPath }) => {
|
|
909
|
+
const page = getPage();
|
|
910
|
+
const target = harPath || join(SCREENSHOT_DIR, "network.har");
|
|
911
|
+
// Collect network entries
|
|
912
|
+
const entries = networkRequests.slice(-100).map(r => ({
|
|
913
|
+
request: { method: r.method, url: r.url },
|
|
914
|
+
response: { status: r.status },
|
|
915
|
+
}));
|
|
916
|
+
const har = { log: { version: "1.2", entries } };
|
|
917
|
+
writeFileSync(target, JSON.stringify(har, null, 2));
|
|
918
|
+
return { content: [{ type: "text", text: `HAR exported: ${target} (${entries.length} entries)` }] };
|
|
919
|
+
});
|
|
568
920
|
// ── Start Server ───────────────────────────────────────────────────────────
|
|
569
921
|
async function main() {
|
|
570
922
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED