import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
initStderrTee("browser");
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
import { z } from "zod";
import { writeFile } from "node:fs/promises";
import { renderPage } from "./lib/cdp-render.js";
import { BrowserSession, DEFAULT_COMMAND_TIMEOUT_MS, DEFAULT_LOAD_TIMEOUT_MS, } from "./lib/cdp-session.js";
import * as actions from "./lib/cdp-actions.js";
// Cast to `any` to avoid TS2589 the same way the other plugin servers do.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const server = new McpServer({ name: "maxy-browser", version: "0.1.0" });
// Module-init must never throw — plugin-system spawn enumerates tools/list
// with empty env (Task 202). The CDP port is read per-call, not at boot.
const cdpPortRaw = process.env.CDP_PORT ?? "";
const cdpPort = Number.parseInt(cdpPortRaw, 10);
process.stderr.write(`[browser] boot cdpPort=${cdpPortRaw || "-"}\n`);
const log = (line) => process.stderr.write(`${line}\n`);
const WebSocketImpl = typeof WebSocket !== "undefined"
    ? WebSocket
    : undefined;
// Cap what we hand back to the model so a large SPA cannot blow the agent's
// context window. domBytes in the header always reports the true size.
const MAX_TEXT_CHARS = 200_000;
const MAX_HTML_CHARS = 800_000;
function cap(s, max) {
    if (s.length <= max)
        return { body: s, truncated: false };
    return { body: s.slice(0, max), truncated: true };
}
eagerTool(server, "browser-render", "Render a JavaScript-heavy web page in the device's real Chromium and return its fully rendered DOM. Use this for pages that WebFetch and url-get return empty or as a shell because they require JS execution (client-rendered SPAs, content gated behind in-page scripts). Returns the rendered HTML and the page's visible text. For static, server-rendered pages prefer url-get (verbatim) or WebFetch (summary) — this tool is the heavier JS-rendering path.", {
    url: z.string().url().describe("Absolute http(s) URL to render"),
    loadTimeoutMs: z.number().int().positive().optional().describe("Max wait for the page load event (default 30000)"),
}, async (params) => {
    const result = await renderPage(params.url, { loadTimeoutMs: params.loadTimeoutMs }, { fetchImpl: fetch, WebSocketImpl, cdpHost: "127.0.0.1", cdpPort, log });
    if (!result.rendered) {
        return {
            content: [{
                    type: "text",
                    text: `browser-render failed: ${result.outcome}${result.detail ? ` — ${result.detail}` : ""} (url: ${result.url})`,
                }],
            isError: true,
        };
    }
    const text = cap(result.text, MAX_TEXT_CHARS);
    const html = cap(result.html, MAX_HTML_CHARS);
    const note = text.truncated || html.truncated
        ? ` Output truncated for length (full DOM was ${result.domBytes} bytes); re-fetch a specific section if you need more.`
        : "";
    return {
        content: [{
                type: "text",
                text: `Rendered ${result.url} (${result.domBytes} bytes of DOM).${note}\n\n--- VISIBLE TEXT${text.truncated ? " (truncated)" : ""} ---\n${text.body}\n\n--- RENDERED HTML${html.truncated ? " (truncated)" : ""} ---\n${html.body}`,
            }],
    };
});
// ── Interactive automation + output tools ────────────────────────────────────
//
// All of these drive ONE persistent page held in `browserSession`, against the
// same device Chromium over CDP. The page is created by browser-navigate and
// reused across calls; browser-tabs opens/switches/closes pages. A dead page
// (operator closed the tab, crash, browser restart) yields a `session-lost`
// outcome telling the agent to navigate again — only navigate re-attaches.
const cdpDeps = { fetchImpl: fetch, WebSocketImpl, cdpHost: "127.0.0.1", cdpPort, log };
const browserSession = new BrowserSession(cdpDeps);
const actionCtx = {
    deps: cdpDeps,
    commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
    loadTimeoutMs: DEFAULT_LOAD_TIMEOUT_MS,
    writeFile: (path, data) => writeFile(path, data),
};
function reply(label, r, successText) {
    if (!r.ok) {
        return {
            content: [{ type: "text", text: `${label} failed: ${r.outcome}${r.detail ? ` — ${r.detail}` : ""}` }],
            isError: true,
        };
    }
    return { content: [{ type: "text", text: successText ?? `${label}: ok` }] };
}
eagerTool(server, "browser-navigate", "Open a URL in the device's Chromium and keep the page alive for follow-up automation (click, fill, snapshot, pdf-save, screenshot). This is the entry point for interactive browser work — call it before any other browser-* automation tool. Reuses the one live automation page; use browser-tabs to open a separate page. For one-shot read-only retrieval of a JS page use browser-render instead.", {
    url: z.string().url().describe("Absolute http(s) URL to open"),
    loadTimeoutMs: z.number().int().positive().optional().describe("Max wait for the page load event (default 30000)"),
}, async (params) => reply("browser-navigate", await actions.navigate(browserSession, params, actionCtx), `Navigated to ${params.url}`));
eagerTool(server, "browser-snapshot", "Return an accessibility snapshot of the current page: each interactive or landmark element as role, accessible name, and a unique CSS selector. Use the returned selector verbatim with browser-click / browser-fill / browser-hover — it targets exactly the element you saw. Call this after browser-navigate (or after an action changes the page) to know what is on the page.", {}, async () => {
    const r = await actions.snapshot(browserSession, {}, actionCtx);
    return reply("browser-snapshot", r, r.data?.text);
});
eagerTool(server, "browser-click", "Click the element matching a CSS selector on the current page. Use a selector from browser-snapshot. Returns selector-not-found if nothing matches.", { selector: z.string().describe("CSS selector of the element to click") }, async (params) => reply("browser-click", await actions.click(browserSession, params, actionCtx), `Clicked ${params.selector}`));
eagerTool(server, "browser-fill", "Set the value of one input, textarea, or contenteditable matching a CSS selector, firing input/change events. For multiple fields in one call use browser-fill-form.", {
    selector: z.string().describe("CSS selector of the field"),
    value: z.string().describe("Value to set"),
}, async (params) => reply("browser-fill", await actions.fill(browserSession, params, actionCtx), `Filled ${params.selector}`));
eagerTool(server, "browser-fill-form", "Fill several fields in one call. Each field is a CSS selector and the value to set. Reports every selector that matched nothing.", {
    fields: z.array(z.object({
        selector: z.string().describe("CSS selector of the field"),
        value: z.string().describe("Value to set"),
    })).describe("Fields to fill"),
}, async (params) => reply("browser-fill-form", await actions.fillForm(browserSession, params, actionCtx), `Filled ${params.fields.length} field(s)`));
eagerTool(server, "browser-type", "Type text into the focused element via real keyboard input. Pass a selector to focus that element first. Use this when setting value directly (browser-fill) does not trigger the page's keystroke handlers (autocompletes, rich editors).", {
    text: z.string().describe("Text to type"),
    selector: z.string().optional().describe("Optional CSS selector to focus before typing"),
}, async (params) => reply("browser-type", await actions.type(browserSession, params, actionCtx), "Typed text"));
eagerTool(server, "browser-press-key", "Press a single key on the focused element (Enter, Tab, Escape, Backspace, Delete, Arrow keys, Home, End, PageUp, PageDown, Space, or a single character). Use for submitting forms (Enter) or keyboard navigation.", { key: z.string().describe("Key name or single character") }, async (params) => reply("browser-press-key", await actions.pressKey(browserSession, params, actionCtx), `Pressed ${params.key}`));
eagerTool(server, "browser-hover", "Hover the pointer over the element matching a CSS selector, firing pointer/mouse-over events. Use to reveal hover menus or tooltips before reading or clicking.", { selector: z.string().describe("CSS selector of the element to hover") }, async (params) => reply("browser-hover", await actions.hover(browserSession, params, actionCtx), `Hovered ${params.selector}`));
eagerTool(server, "browser-select-option", "Choose an option in a <select> dropdown by option value or visible label, firing a change event.", {
    selector: z.string().describe("CSS selector of the <select>"),
    value: z.string().optional().describe("Option value to select"),
    label: z.string().optional().describe("Option visible text/label to select"),
}, async (params) => reply("browser-select-option", await actions.selectOption(browserSession, params, actionCtx), `Selected option in ${params.selector}`));
eagerTool(server, "browser-wait-for", "Wait until an element (CSS selector) appears or some text is present on the page, up to a timeout. Use after an action that loads content asynchronously, before reading or interacting.", {
    selector: z.string().optional().describe("CSS selector to wait for"),
    text: z.string().optional().describe("Visible text to wait for"),
    timeoutMs: z.number().int().positive().optional().describe("Max wait (default 30000)"),
}, async (params) => reply("browser-wait-for", await actions.waitFor(browserSession, params, actionCtx), "Condition met"));
eagerTool(server, "browser-handle-dialog", "Arm how the next JavaScript dialog(s) — alert, confirm, prompt — on the current page are answered. Set this BEFORE the action that triggers the dialog, because dialogs block the page and cannot be answered after the fact. The policy stands until changed. Unarmed dialogs are auto-dismissed and reported by browser-console-messages.", {
    accept: z.boolean().describe("true to accept/OK, false to dismiss/Cancel"),
    promptText: z.string().optional().describe("Text to enter into a prompt() dialog when accepting"),
}, async (params) => reply("browser-handle-dialog", actions.handleDialog(browserSession, params), `Dialog policy armed: accept=${params.accept}`));
eagerTool(server, "browser-evaluate", "Run a JavaScript expression in the current page and return its value (awaits a returned promise). Use for reading page state or driving behaviour that no other tool covers. The expression runs in the page, not in Node.", { expression: z.string().describe("JavaScript expression to evaluate in the page") }, async (params) => {
    const r = await actions.evaluate(browserSession, params, actionCtx);
    return reply("browser-evaluate", r, `Result: ${JSON.stringify(r.data?.value ?? null)}`);
});
eagerTool(server, "browser-console-messages", "Return and clear the buffered console output and uncaught errors from the current page (captured since navigation). Use to debug why a page or interaction did not behave as expected.", {}, async () => {
    const r = actions.consoleMessages(browserSession);
    return reply("browser-console-messages", r, r.data?.text);
});
eagerTool(server, "browser-tabs", "Manage browser tabs/pages: list open pages, open a new page (optionally at a URL and switch to it), select an existing page by id, or close a page by id. Use 'new' or 'select' to get a fresh page so stale state from a prior task does not leak in.", {
    action: z.enum(["list", "new", "select", "close"]).describe("Tab action"),
    id: z.string().optional().describe("Target id (required for select/close)"),
    url: z.string().optional().describe("URL to open (optional for new)"),
}, async (params) => {
    const r = await actions.tabs(browserSession, params, actionCtx);
    const text = params.action === "list"
        ? "Tabs:\n" + (r.data?.tabs ?? [])
            .map((t) => `${t.active ? "* " : "  "}${t.id} — ${t.title || "(untitled)"} — ${t.url}`).join("\n")
        : `tabs ${params.action}: ok`;
    return reply("browser-tabs", r, text);
});
eagerTool(server, "browser-pdf-save", "Render the current page to a PDF file on the device using Chromium's print pipeline (honours @page/@media print CSS, prints backgrounds). Pass an absolute output path. This is the on-device replacement for the old Playwright browser_pdf_save: navigate to the HTML (served over http, or a file path the browser can open), then call this.", {
    path: z.string().describe("Absolute output path for the .pdf file"),
    landscape: z.boolean().optional().describe("Landscape orientation (default false)"),
    scale: z.number().optional().describe("Scale of the page rendering (default 1)"),
    printBackground: z.boolean().optional().describe("Print background graphics (default true)"),
}, async (params) => {
    const r = await actions.pdfSave(browserSession, params, actionCtx);
    return reply("browser-pdf-save", r, `Saved PDF to ${params.path} (${r.data?.bytes ?? 0} bytes)`);
});
eagerTool(server, "browser-screenshot", "Capture a PNG of the current page (or one element, via a CSS selector clip) to a file on the device. Pass an absolute output path. Use selector clips to capture glassmorphism/blur sections as print fallback images for A4 PDFs.", {
    path: z.string().describe("Absolute output path for the .png file"),
    selector: z.string().optional().describe("CSS selector to clip to a single element"),
    fullPage: z.boolean().optional().describe("Capture beyond the viewport (default false)"),
}, async (params) => {
    const r = await actions.screenshot(browserSession, params, actionCtx);
    return reply("browser-screenshot", r, `Saved screenshot to ${params.path} (${r.data?.bytes ?? 0} bytes)`);
});
eagerTool(server, "browser-resize", "Set the current page's viewport size in CSS pixels. Use before a screenshot to control the rendered dimensions (e.g. match an element's measured size to avoid scrollbars).", {
    width: z.number().int().positive().describe("Viewport width in CSS pixels"),
    height: z.number().int().positive().describe("Viewport height in CSS pixels"),
}, async (params) => reply("browser-resize", await actions.resize(browserSession, params, actionCtx), `Resized to ${params.width}x${params.height}`));
process.on("SIGINT", () => process.exit(0));
const transport = new StdioServerTransport();
await server.connect(transport);
//# sourceMappingURL=index.js.map