Skip to main content

browser_control/dom/
scripts.rs

1//! JavaScript payloads injected into the browser by MCP tools.
2
3/// Serializes the DOM with shadow roots included.
4pub const GET_DOM_JS: &str = r#"
5(function(selector) {
6    const root = selector ? document.querySelector(selector) : document.documentElement;
7    if (!root) return null;
8    if (typeof root.getHTML === 'function') {
9        try { return root.getHTML({ serializableShadowRoots: true }); } catch (e) {}
10    }
11    return root.outerHTML;
12})
13"#;
14
15/// Resolves a selector to a clip rectangle in *document* coordinates,
16/// scrolling the element into view first. Returns `null` when the selector
17/// matches nothing or the element has zero area (hidden / detached).
18pub const GET_CLIP_RECT_JS: &str = r#"
19(function(selector) {
20    const el = document.querySelector(selector);
21    if (!el) return null;
22    el.scrollIntoView({ block: 'center', inline: 'center' });
23    const r = el.getBoundingClientRect();
24    if (r.width === 0 || r.height === 0) return null;
25    return {
26        x: r.left + window.scrollX,
27        y: r.top + window.scrollY,
28        width: r.width,
29        height: r.height,
30    };
31})
32"#;
33
34/// Interactive element picker. Resolves with the selector string for the picked element.
35pub const SELECT_ELEMENT_JS: &str = r#"
36(function() {
37    function cssPath(el) {
38        if (!(el instanceof Element)) return '';
39        const path = [];
40        while (el && el.nodeType === 1) {
41            let selector = el.nodeName.toLowerCase();
42            if (el.id) { selector += '#' + el.id; path.unshift(selector); break; }
43            else {
44                let sib = el, nth = 1;
45                while ((sib = sib.previousElementSibling)) { if (sib.nodeName === el.nodeName) nth++; }
46                selector += ':nth-of-type(' + nth + ')';
47            }
48            path.unshift(selector);
49            el = el.parentNode;
50        }
51        return path.join(' > ');
52    }
53    return new Promise((resolve) => {
54        const overlay = document.createElement('div');
55        overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483647;cursor:crosshair;background:rgba(0,150,255,0.05);';
56        document.body.appendChild(overlay);
57        overlay.addEventListener('click', (e) => {
58            e.preventDefault(); e.stopPropagation();
59            overlay.remove();
60            const x = e.clientX, y = e.clientY;
61            const el = document.elementFromPoint(x, y);
62            resolve(cssPath(el));
63        }, { capture: true, once: true });
64    });
65})()
66"#;
67
68/// Performs a fetch from the page context. Receives JSON-string args.
69pub const FETCH_JS: &str = r#"
70(async function(argsJson) {
71    const args = JSON.parse(argsJson);
72    const timeoutMs = Number(args.timeoutMs || 0);
73    const controller = timeoutMs > 0 && typeof AbortController !== 'undefined'
74        ? new AbortController()
75        : null;
76    const timer = controller
77        ? setTimeout(() => controller.abort(), timeoutMs)
78        : null;
79    try {
80        const r = await fetch(args.url, {
81            method: args.method || 'GET',
82            headers: args.headers || {},
83            body: args.body,
84            credentials: 'include',
85            signal: controller ? controller.signal : undefined,
86        });
87        const text = await r.text();
88        const headers = {};
89        r.headers.forEach((v,k) => { headers[k] = v; });
90        return JSON.stringify({ ok: true, status: r.status, statusText: r.statusText, headers, body: text });
91    } catch (e) {
92        const aborted = controller && controller.signal.aborted;
93        return JSON.stringify({
94            ok: false,
95            error: aborted ? `fetch timed out after ${timeoutMs}ms` : String(e),
96            errorName: e && e.name ? e.name : null,
97        });
98    } finally {
99        if (timer) clearTimeout(timer);
100    }
101})
102"#;