framewatch-mcp-server 0.1.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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +537 -0
  3. package/dist/constants.d.ts +172 -0
  4. package/dist/constants.js +168 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/engine/browser.d.ts +56 -0
  7. package/dist/engine/browser.js +142 -0
  8. package/dist/engine/browser.js.map +1 -0
  9. package/dist/engine/differ.d.ts +88 -0
  10. package/dist/engine/differ.js +373 -0
  11. package/dist/engine/differ.js.map +1 -0
  12. package/dist/engine/interaction.d.ts +76 -0
  13. package/dist/engine/interaction.js +254 -0
  14. package/dist/engine/interaction.js.map +1 -0
  15. package/dist/engine/layers/console.d.ts +63 -0
  16. package/dist/engine/layers/console.js +118 -0
  17. package/dist/engine/layers/console.js.map +1 -0
  18. package/dist/engine/layers/dom.d.ts +53 -0
  19. package/dist/engine/layers/dom.js +282 -0
  20. package/dist/engine/layers/dom.js.map +1 -0
  21. package/dist/engine/layers/index.d.ts +95 -0
  22. package/dist/engine/layers/index.js +184 -0
  23. package/dist/engine/layers/index.js.map +1 -0
  24. package/dist/engine/layers/network.d.ts +62 -0
  25. package/dist/engine/layers/network.js +169 -0
  26. package/dist/engine/layers/network.js.map +1 -0
  27. package/dist/engine/layers/performance.d.ts +55 -0
  28. package/dist/engine/layers/performance.js +215 -0
  29. package/dist/engine/layers/performance.js.map +1 -0
  30. package/dist/engine/layers/probe.d.ts +50 -0
  31. package/dist/engine/layers/probe.js +39 -0
  32. package/dist/engine/layers/probe.js.map +1 -0
  33. package/dist/engine/layers/session.d.ts +46 -0
  34. package/dist/engine/layers/session.js +131 -0
  35. package/dist/engine/layers/session.js.map +1 -0
  36. package/dist/engine/recorder.d.ts +61 -0
  37. package/dist/engine/recorder.js +256 -0
  38. package/dist/engine/recorder.js.map +1 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +125 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/tools/accessibility.d.ts +140 -0
  43. package/dist/tools/accessibility.js +357 -0
  44. package/dist/tools/accessibility.js.map +1 -0
  45. package/dist/tools/capture.d.ts +279 -0
  46. package/dist/tools/capture.js +275 -0
  47. package/dist/tools/capture.js.map +1 -0
  48. package/dist/tools/compare.d.ts +86 -0
  49. package/dist/tools/compare.js +247 -0
  50. package/dist/tools/compare.js.map +1 -0
  51. package/dist/tools/index.d.ts +10 -0
  52. package/dist/tools/index.js +25 -0
  53. package/dist/tools/index.js.map +1 -0
  54. package/dist/tools/interact.d.ts +160 -0
  55. package/dist/tools/interact.js +203 -0
  56. package/dist/tools/interact.js.map +1 -0
  57. package/dist/tools/responsive.d.ts +89 -0
  58. package/dist/tools/responsive.js +197 -0
  59. package/dist/tools/responsive.js.map +1 -0
  60. package/dist/tools/screenshot.d.ts +76 -0
  61. package/dist/tools/screenshot.js +117 -0
  62. package/dist/tools/screenshot.js.map +1 -0
  63. package/dist/tools/server.d.ts +89 -0
  64. package/dist/tools/server.js +201 -0
  65. package/dist/tools/server.js.map +1 -0
  66. package/dist/types.d.ts +123 -0
  67. package/dist/types.js +9 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/utils/bounded-log.d.ts +41 -0
  70. package/dist/utils/bounded-log.js +78 -0
  71. package/dist/utils/bounded-log.js.map +1 -0
  72. package/dist/utils/format.d.ts +56 -0
  73. package/dist/utils/format.js +130 -0
  74. package/dist/utils/format.js.map +1 -0
  75. package/dist/utils/image.d.ts +44 -0
  76. package/dist/utils/image.js +81 -0
  77. package/dist/utils/image.js.map +1 -0
  78. package/dist/utils/server-process.d.ts +84 -0
  79. package/dist/utils/server-process.js +251 -0
  80. package/dist/utils/server-process.js.map +1 -0
  81. package/package.json +74 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../../src/tools/accessibility.ts"],"names":[],"mappings":"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","sourcesContent":["import { createRequire } from \"node:module\";\nimport { readFileSync } from \"node:fs\";\nimport { z } from \"zod\";\nimport type { Frame, Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n A11Y_FRAME_WAIT_MS,\n A11Y_RUN_TIMEOUT_MS,\n DEFAULT_A11Y_WAIT_MS,\n DEFAULT_VIEWPORT,\n MAX_A11Y_HTML_LENGTH,\n MAX_A11Y_NODES_PER_VIOLATION,\n MAX_A11Y_VIOLATIONS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\n\nexport const ACCESSIBILITY_TOOL_NAME = \"framewatch_accessibility\";\n\n/** WCAG conformance levels, and the axe-core tags each one runs. */\nconst STANDARDS = {\n wcag2a: [\"wcag2a\"],\n wcag2aa: [\"wcag2a\", \"wcag2aa\"],\n wcag21aa: [\"wcag2a\", \"wcag2aa\", \"wcag21a\", \"wcag21aa\"],\n} as const;\n\nexport type A11yStandard = keyof typeof STANDARDS;\n\n/** Worst first — this is the order violations are reported in. */\nconst IMPACT_ORDER = [\"critical\", \"serious\", \"moderate\", \"minor\"] as const;\n\nexport const accessibilityInputShape = {\n url: z.string().url().describe(\"URL to audit, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n standard: z\n .enum([\"wcag2a\", \"wcag2aa\", \"wcag21aa\"])\n .default(\"wcag2aa\")\n .describe(\"Conformance level to test against. wcag2aa is the usual legal baseline; wcag21aa adds WCAG 2.1.\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_A11Y_WAIT_MS)\n .describe(\"Wait time (ms) after page load before auditing, so the app can finish rendering\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before auditing\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n max_violations: z\n .number()\n .int()\n .min(1)\n .max(MAX_A11Y_VIOLATIONS)\n .default(MAX_A11Y_VIOLATIONS)\n .describe(\"Maximum violation types to report (they are reported worst-impact first)\"),\n max_elements: z\n .number()\n .int()\n .min(1)\n .max(20)\n .default(MAX_A11Y_NODES_PER_VIOLATION)\n .describe(\"Maximum offending elements listed under each violation\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720). Some rules — reflow, target size — depend on it.\"),\n};\n\nexport const accessibilityInputSchema = z.object(accessibilityInputShape);\nexport type AccessibilityInput = z.input<typeof accessibilityInputSchema>;\n\n/** The trimmed shape the in-page script sends back — never raw axe results. */\ninterface AxeNode {\n target: string;\n html: string;\n summary: string;\n}\n\ninterface AxeViolation {\n id: string;\n impact: string;\n help: string;\n description: string;\n help_url: string;\n tags: string[];\n node_count: number;\n nodes: AxeNode[];\n}\n\ninterface AxeReport {\n violations: AxeViolation[];\n /** Rules that passed, that found nothing to test, and that need a human. */\n passes: number;\n inapplicable: number;\n incomplete: number;\n /** Total violation types found, before `max_violations` trimmed the list. */\n total_violations: number;\n /** Offending elements across every violation, including those not listed. */\n total_nodes: number;\n url: string;\n axe_version: string;\n}\n\n/**\n * Run an axe-core accessibility audit and report what it found.\n *\n * axe is injected into the page rather than reimplemented: it is the engine\n * behind most commercial accessibility tooling, so a violation reported here\n * is one an auditor would also raise. It is injected as a script through\n * `evaluate`, into a context created with CSP bypassed, so a page with a\n * strict Content-Security-Policy can still be audited.\n *\n * Only violations come back. Passing rules are counted, not listed — a list of\n * two hundred rules that did not fire is noise, but the count is what tells\n * you the audit actually ran.\n */\nexport async function auditAccessibility(rawInput: AccessibilityInput): Promise<CallToolResult> {\n const parsed = accessibilityInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Accessibility audit failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n let source: string;\n try {\n source = axeSource();\n } catch (error) {\n return errorResult(\n \"Accessibility audit failed: the axe-core library could not be loaded from this installation \" +\n `(${error instanceof Error ? error.message : String(error)}). Reinstall dependencies and try again.`,\n );\n }\n\n try {\n // `bypassCSP` so the audit can be injected into an app that forbids\n // inline script and eval. The page's own CSP has no bearing on what axe\n // measures, and without this a strictly configured app could not be\n // audited at all.\n const report = await withPage({ viewport, contextOptions: { bypassCSP: true } }, async (page) => {\n await page.goto(input.url, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n await injectAxe(page, source);\n // axe walks the whole DOM; on a big enough app that takes a while, and\n // `page.evaluate` has no timeout of its own — a page that wedges\n // mid-audit would otherwise hang the tool call forever.\n return withTimeout(\n runAxe(page, {\n tags: [...STANDARDS[input.standard]],\n max_violations: input.max_violations,\n max_elements: input.max_elements,\n max_html: MAX_A11Y_HTML_LENGTH,\n frame_wait_ms: A11Y_FRAME_WAIT_MS,\n }),\n A11Y_RUN_TIMEOUT_MS,\n `axe-core did not finish within ${A11Y_RUN_TIMEOUT_MS}ms`,\n );\n });\n\n return { content: [{ type: \"text\", text: formatReport(report, input.standard) }] };\n } catch (error) {\n return errorResult(describeAuditFailure(input, error));\n }\n}\n\n/** Options handed to the in-page runner. Must stay JSON-serialisable. */\ninterface RunOptions {\n tags: string[];\n max_violations: number;\n max_elements: number;\n max_html: number;\n frame_wait_ms: number;\n}\n\n/**\n * Put axe in the main frame and in every child frame.\n *\n * axe audits iframes by talking to a copy of itself inside each one, so a page\n * that embeds anything (a preview pane, an embedded checkout) needs it\n * everywhere. Child frames are best-effort: one that refuses injection — it\n * may be mid-navigation, or gone by the time we reach it — must not stop the\n * audit of the page around it.\n */\nasync function injectAxe(page: Page, source: string): Promise<void> {\n await evaluateSource(page.mainFrame(), source);\n for (const frame of page.frames()) {\n if (frame === page.mainFrame()) continue;\n await evaluateSource(frame, source).catch(() => {});\n }\n}\n\n/**\n * Evaluate the axe bundle as a script in `frame`.\n *\n * The source goes in as a *string*, which Playwright evaluates as a script in\n * the frame — no script tag to be blocked, and the audit context is created\n * with CSP bypassed so injection works on strictly configured apps too.\n *\n * `allowedOrigins` is widened because axe defaults to same-origin frame\n * messaging only, and a cross-origin iframe would otherwise never answer.\n */\nasync function evaluateSource(frame: Frame, source: string): Promise<void> {\n await frame.evaluate(source as unknown as () => void);\n await frame.evaluate(() => {\n const axe = (globalThis as any).axe;\n if (axe?.configure) axe.configure({ allowedOrigins: [\"<unsafe_all_origins>\"] });\n });\n}\n\n/**\n * Run axe in the page and bring back only what gets printed.\n *\n * The trimming happens in the page on purpose: a full axe result carries every\n * passing rule with every element it checked, which on a real app is megabytes\n * of JSON to serialise across the protocol and then throw away.\n */\nasync function runAxe(page: Page, options: RunOptions): Promise<AxeReport> {\n return page.evaluate(async (opts: RunOptions): Promise<AxeReport> => {\n const g = globalThis as any;\n const axe = g.axe;\n if (!axe?.run) throw new Error(\"axe-core did not load in the page\");\n\n const results = await axe.run(g.document, {\n runOnly: { type: \"tag\", values: opts.tags },\n // Only violations are reported, so this is the only result type whose\n // element details need collecting.\n resultTypes: [\"violations\"],\n frameWaitTime: opts.frame_wait_ms,\n });\n\n const order = [\"critical\", \"serious\", \"moderate\", \"minor\"];\n const rank = (impact: string): number => {\n const at = order.indexOf(impact);\n return at === -1 ? order.length : at;\n };\n\n const all = (results.violations ?? []).slice().sort((a: any, b: any) => {\n const byImpact = rank(String(a.impact)) - rank(String(b.impact));\n if (byImpact !== 0) return byImpact;\n // Then by blast radius: the rule breaking twenty elements first.\n return (b.nodes?.length ?? 0) - (a.nodes?.length ?? 0);\n });\n\n const elide = (text: string): string => {\n const flat = String(text ?? \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n return flat.length > opts.max_html ? `${flat.slice(0, opts.max_html)}…` : flat;\n };\n\n return {\n violations: all.slice(0, opts.max_violations).map((violation: any) => ({\n id: String(violation.id ?? \"unknown\"),\n impact: String(violation.impact ?? \"unknown\"),\n help: String(violation.help ?? \"\"),\n description: String(violation.description ?? \"\"),\n help_url: String(violation.helpUrl ?? \"\"),\n tags: (violation.tags ?? []).map((tag: unknown) => String(tag)),\n node_count: violation.nodes?.length ?? 0,\n nodes: (violation.nodes ?? []).slice(0, opts.max_elements).map((node: any) => ({\n target: (node.target ?? []).map((part: unknown) => String(part)).join(\" \"),\n html: elide(node.html),\n // The first line is the rule that failed for this element; the rest\n // repeats the violation's own help text.\n summary: elide(String(node.failureSummary ?? \"\").split(\"\\n\").slice(1).join(\" \")),\n })),\n })),\n passes: results.passes?.length ?? 0,\n inapplicable: results.inapplicable?.length ?? 0,\n incomplete: results.incomplete?.length ?? 0,\n total_violations: all.length,\n total_nodes: all.reduce((sum: number, violation: any) => sum + (violation.nodes?.length ?? 0), 0),\n url: String(results.url ?? \"\"),\n axe_version: String(results.testEngine?.version ?? \"\"),\n };\n }, options);\n}\n\n/**\n * Render the report as text.\n *\n * Each violation gets its impact, the rule that failed, what to do about it,\n * the elements at fault and the URL of Deque's write-up — everything needed to\n * fix it without a second lookup.\n */\nexport function formatReport(report: AxeReport, standard: A11yStandard): string {\n const scope = `${standard.toUpperCase()} (axe-core ${report.axe_version || \"?\"})`;\n const lines: string[] = [];\n\n if (report.total_violations === 0) {\n lines.push(`No ${scope} violations on ${report.url}.`);\n lines.push(auditFooter(report));\n return lines.join(\"\\n\");\n }\n\n lines.push(\n `${report.total_violations} ${scope} violation ${report.total_violations === 1 ? \"type\" : \"types\"} on ${report.url}, ` +\n `affecting ${report.total_nodes} ${report.total_nodes === 1 ? \"element\" : \"elements\"} — ${countByImpact(report)}.`,\n );\n lines.push(auditFooter(report));\n lines.push(\"\");\n\n report.violations.forEach((violation, index) => {\n lines.push(\n `${index + 1}. [${violation.impact}] ${violation.help} (${violation.id}) — ` +\n `${violation.node_count} ${violation.node_count === 1 ? \"element\" : \"elements\"}`,\n );\n lines.push(` ${violation.description}`);\n for (const node of violation.nodes) {\n lines.push(` • ${node.target || \"?\"}`);\n if (node.html) lines.push(` ${node.html}`);\n if (node.summary) lines.push(` ${node.summary}`);\n }\n if (violation.node_count > violation.nodes.length) {\n lines.push(` … and ${violation.node_count - violation.nodes.length} more elements`);\n }\n if (violation.help_url) lines.push(` ${violation.help_url}`);\n });\n\n if (report.total_violations > report.violations.length) {\n lines.push(\"\");\n lines.push(\n `… and ${report.total_violations - report.violations.length} more violation types not shown ` +\n \"(raise `max_violations` to see them).\",\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The counts that say how much of the page was actually judged. `incomplete`\n * is the important one: those are the checks axe could not decide on its own\n * (typically colour contrast over an image) and they are the rules a human\n * still has to look at.\n */\nfunction auditFooter(report: AxeReport): string {\n return `${report.passes} rules passed, ${report.incomplete} need a human to check, ${report.inapplicable} did not apply.`;\n}\n\nfunction countByImpact(report: AxeReport): string {\n const counts = new Map<string, number>();\n for (const violation of report.violations) {\n counts.set(violation.impact, (counts.get(violation.impact) ?? 0) + 1);\n }\n const known = IMPACT_ORDER.filter((impact) => counts.has(impact)).map((impact) => `${counts.get(impact)} ${impact}`);\n const other = [...counts.keys()]\n .filter((impact) => !IMPACT_ORDER.includes(impact as (typeof IMPACT_ORDER)[number]))\n .map((impact) => `${counts.get(impact)} ${impact}`);\n return [...known, ...other].join(\", \");\n}\n\n/**\n * The axe-core bundle as source text, read once and cached.\n *\n * The minified build is preferred purely for injection size; the readable one\n * is an equivalent fallback for installations that ship only it.\n */\nlet cachedSource: string | null = null;\n\nexport function axeSource(): string {\n if (cachedSource !== null) return cachedSource;\n const require = createRequire(import.meta.url);\n const candidates = [\"axe-core/axe.min.js\", \"axe-core/axe.js\", \"axe-core\"];\n const problems: string[] = [];\n for (const candidate of candidates) {\n try {\n cachedSource = readFileSync(require.resolve(candidate), \"utf8\");\n return cachedSource;\n } catch (error) {\n problems.push(`${candidate}: ${error instanceof Error ? error.message.split(\"\\n\")[0] : String(error)}`);\n }\n }\n throw new Error(problems.join(\"; \"));\n}\n\n/** Reject with `message` if `work` has not settled within `ms`. */\nasync function withTimeout<T>(work: Promise<T>, ms: number, message: string): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const expire = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(message)), ms);\n });\n try {\n return await Promise.race([work, expire]);\n } finally {\n clearTimeout(timer);\n // The losing side has to be settled too: the audit outlives a timeout and\n // would otherwise reject unobserved once the context is torn down.\n void work.catch(() => {});\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a failed audit. Mirrors `describeFailure` in\n * screenshot.ts: match on the failing Playwright call, never on substrings of\n * a user-supplied selector.\n */\nexport function describeAuditFailure(\n input: { url: string; wait_for?: string; wait_for_timeout_ms: number },\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Accessibility audit of ${input.url} failed:`;\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n if (/^page\\.goto:/.test(message)) {\n return `${prefix} the page could not be opened — ${firstLine}`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerAccessibilityTool(server: McpServer): void {\n server.registerTool(\n ACCESSIBILITY_TOOL_NAME,\n {\n title: \"Accessibility\",\n description:\n \"Run an axe-core accessibility audit on a page and report the violations: impact level, the rule that \" +\n \"failed, the elements at fault and a link to how to fix each one. Choose the conformance level with \" +\n \"`standard` (wcag2a, wcag2aa, wcag21aa). Rules that passed are counted rather than listed, and the \" +\n \"'needs review' count tells you what axe could not decide on its own.\",\n inputSchema: accessibilityInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => auditAccessibility(args),\n );\n}\n\n/** Exported for tests: the axe tag list each standard maps to. */\nexport const A11Y_STANDARD_TAGS: Readonly<Record<A11yStandard, readonly string[]>> = STANDARDS;\n"]}
@@ -0,0 +1,279 @@
1
+ import { z } from "zod";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4
+ import { summariseContext } from "../engine/layers/index.js";
5
+ export declare const CAPTURE_TOOL_NAME = "framewatch_capture";
6
+ /**
7
+ * Re-exported where it is used: the capture summary's context lines. It lives
8
+ * with the layers themselves now that `framewatch_interact` reports them too.
9
+ */
10
+ export { summariseContext };
11
+ /**
12
+ * One step of a replayable interaction script. `delay_ms` is a wait *before*
13
+ * the action, so a `wait` step is just a delay with nothing after it, and the
14
+ * delays of successive steps accumulate over the recording.
15
+ */
16
+ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
17
+ delay_ms: z.ZodDefault<z.ZodNumber>;
18
+ selector: z.ZodOptional<z.ZodString>;
19
+ value: z.ZodOptional<z.ZodString>;
20
+ x: z.ZodOptional<z.ZodNumber>;
21
+ y: z.ZodOptional<z.ZodNumber>;
22
+ delta_x: z.ZodOptional<z.ZodNumber>;
23
+ delta_y: z.ZodOptional<z.ZodNumber>;
24
+ action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
27
+ delay_ms: number;
28
+ value?: string | undefined;
29
+ x?: number | undefined;
30
+ y?: number | undefined;
31
+ selector?: string | undefined;
32
+ delta_x?: number | undefined;
33
+ delta_y?: number | undefined;
34
+ }, {
35
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
36
+ value?: string | undefined;
37
+ x?: number | undefined;
38
+ y?: number | undefined;
39
+ selector?: string | undefined;
40
+ delta_x?: number | undefined;
41
+ delta_y?: number | undefined;
42
+ delay_ms?: number | undefined;
43
+ }>, {
44
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
45
+ delay_ms: number;
46
+ value?: string | undefined;
47
+ x?: number | undefined;
48
+ y?: number | undefined;
49
+ selector?: string | undefined;
50
+ delta_x?: number | undefined;
51
+ delta_y?: number | undefined;
52
+ }, {
53
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
54
+ value?: string | undefined;
55
+ x?: number | undefined;
56
+ y?: number | undefined;
57
+ selector?: string | undefined;
58
+ delta_x?: number | undefined;
59
+ delta_y?: number | undefined;
60
+ delay_ms?: number | undefined;
61
+ }>;
62
+ export declare const captureInputShape: {
63
+ url: z.ZodString;
64
+ duration_ms: z.ZodDefault<z.ZodNumber>;
65
+ sensitivity: z.ZodDefault<z.ZodNumber>;
66
+ max_frames: z.ZodDefault<z.ZodNumber>;
67
+ interval_ms: z.ZodDefault<z.ZodNumber>;
68
+ viewport: z.ZodOptional<z.ZodObject<{
69
+ width: z.ZodDefault<z.ZodNumber>;
70
+ height: z.ZodDefault<z.ZodNumber>;
71
+ }, "strip", z.ZodTypeAny, {
72
+ width: number;
73
+ height: number;
74
+ }, {
75
+ width?: number | undefined;
76
+ height?: number | undefined;
77
+ }>>;
78
+ wait_for: z.ZodOptional<z.ZodString>;
79
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
80
+ interactions: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
81
+ delay_ms: z.ZodDefault<z.ZodNumber>;
82
+ selector: z.ZodOptional<z.ZodString>;
83
+ value: z.ZodOptional<z.ZodString>;
84
+ x: z.ZodOptional<z.ZodNumber>;
85
+ y: z.ZodOptional<z.ZodNumber>;
86
+ delta_x: z.ZodOptional<z.ZodNumber>;
87
+ delta_y: z.ZodOptional<z.ZodNumber>;
88
+ action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
89
+ }, "strip", z.ZodTypeAny, {
90
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
91
+ delay_ms: number;
92
+ value?: string | undefined;
93
+ x?: number | undefined;
94
+ y?: number | undefined;
95
+ selector?: string | undefined;
96
+ delta_x?: number | undefined;
97
+ delta_y?: number | undefined;
98
+ }, {
99
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
100
+ value?: string | undefined;
101
+ x?: number | undefined;
102
+ y?: number | undefined;
103
+ selector?: string | undefined;
104
+ delta_x?: number | undefined;
105
+ delta_y?: number | undefined;
106
+ delay_ms?: number | undefined;
107
+ }>, {
108
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
109
+ delay_ms: number;
110
+ value?: string | undefined;
111
+ x?: number | undefined;
112
+ y?: number | undefined;
113
+ selector?: string | undefined;
114
+ delta_x?: number | undefined;
115
+ delta_y?: number | undefined;
116
+ }, {
117
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
118
+ value?: string | undefined;
119
+ x?: number | undefined;
120
+ y?: number | undefined;
121
+ selector?: string | undefined;
122
+ delta_x?: number | undefined;
123
+ delta_y?: number | undefined;
124
+ delay_ms?: number | undefined;
125
+ }>, "many">>;
126
+ interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
127
+ include_console: z.ZodDefault<z.ZodBoolean>;
128
+ include_network: z.ZodDefault<z.ZodBoolean>;
129
+ include_dom: z.ZodDefault<z.ZodBoolean>;
130
+ include_performance: z.ZodDefault<z.ZodBoolean>;
131
+ };
132
+ export declare const captureInputSchema: z.ZodObject<{
133
+ url: z.ZodString;
134
+ duration_ms: z.ZodDefault<z.ZodNumber>;
135
+ sensitivity: z.ZodDefault<z.ZodNumber>;
136
+ max_frames: z.ZodDefault<z.ZodNumber>;
137
+ interval_ms: z.ZodDefault<z.ZodNumber>;
138
+ viewport: z.ZodOptional<z.ZodObject<{
139
+ width: z.ZodDefault<z.ZodNumber>;
140
+ height: z.ZodDefault<z.ZodNumber>;
141
+ }, "strip", z.ZodTypeAny, {
142
+ width: number;
143
+ height: number;
144
+ }, {
145
+ width?: number | undefined;
146
+ height?: number | undefined;
147
+ }>>;
148
+ wait_for: z.ZodOptional<z.ZodString>;
149
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
150
+ interactions: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
151
+ delay_ms: z.ZodDefault<z.ZodNumber>;
152
+ selector: z.ZodOptional<z.ZodString>;
153
+ value: z.ZodOptional<z.ZodString>;
154
+ x: z.ZodOptional<z.ZodNumber>;
155
+ y: z.ZodOptional<z.ZodNumber>;
156
+ delta_x: z.ZodOptional<z.ZodNumber>;
157
+ delta_y: z.ZodOptional<z.ZodNumber>;
158
+ action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
159
+ }, "strip", z.ZodTypeAny, {
160
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
161
+ delay_ms: number;
162
+ value?: string | undefined;
163
+ x?: number | undefined;
164
+ y?: number | undefined;
165
+ selector?: string | undefined;
166
+ delta_x?: number | undefined;
167
+ delta_y?: number | undefined;
168
+ }, {
169
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
170
+ value?: string | undefined;
171
+ x?: number | undefined;
172
+ y?: number | undefined;
173
+ selector?: string | undefined;
174
+ delta_x?: number | undefined;
175
+ delta_y?: number | undefined;
176
+ delay_ms?: number | undefined;
177
+ }>, {
178
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
179
+ delay_ms: number;
180
+ value?: string | undefined;
181
+ x?: number | undefined;
182
+ y?: number | undefined;
183
+ selector?: string | undefined;
184
+ delta_x?: number | undefined;
185
+ delta_y?: number | undefined;
186
+ }, {
187
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
188
+ value?: string | undefined;
189
+ x?: number | undefined;
190
+ y?: number | undefined;
191
+ selector?: string | undefined;
192
+ delta_x?: number | undefined;
193
+ delta_y?: number | undefined;
194
+ delay_ms?: number | undefined;
195
+ }>, "many">>;
196
+ interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
197
+ include_console: z.ZodDefault<z.ZodBoolean>;
198
+ include_network: z.ZodDefault<z.ZodBoolean>;
199
+ include_dom: z.ZodDefault<z.ZodBoolean>;
200
+ include_performance: z.ZodDefault<z.ZodBoolean>;
201
+ }, "strip", z.ZodTypeAny, {
202
+ url: string;
203
+ wait_for_timeout_ms: number;
204
+ duration_ms: number;
205
+ sensitivity: number;
206
+ max_frames: number;
207
+ interval_ms: number;
208
+ interaction_timeout_ms: number;
209
+ include_console: boolean;
210
+ include_network: boolean;
211
+ include_dom: boolean;
212
+ include_performance: boolean;
213
+ viewport?: {
214
+ width: number;
215
+ height: number;
216
+ } | undefined;
217
+ wait_for?: string | undefined;
218
+ interactions?: {
219
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
220
+ delay_ms: number;
221
+ value?: string | undefined;
222
+ x?: number | undefined;
223
+ y?: number | undefined;
224
+ selector?: string | undefined;
225
+ delta_x?: number | undefined;
226
+ delta_y?: number | undefined;
227
+ }[] | undefined;
228
+ }, {
229
+ url: string;
230
+ viewport?: {
231
+ width?: number | undefined;
232
+ height?: number | undefined;
233
+ } | undefined;
234
+ wait_for?: string | undefined;
235
+ wait_for_timeout_ms?: number | undefined;
236
+ duration_ms?: number | undefined;
237
+ sensitivity?: number | undefined;
238
+ max_frames?: number | undefined;
239
+ interval_ms?: number | undefined;
240
+ interactions?: {
241
+ action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
242
+ value?: string | undefined;
243
+ x?: number | undefined;
244
+ y?: number | undefined;
245
+ selector?: string | undefined;
246
+ delta_x?: number | undefined;
247
+ delta_y?: number | undefined;
248
+ delay_ms?: number | undefined;
249
+ }[] | undefined;
250
+ interaction_timeout_ms?: number | undefined;
251
+ include_console?: boolean | undefined;
252
+ include_network?: boolean | undefined;
253
+ include_dom?: boolean | undefined;
254
+ include_performance?: boolean | undefined;
255
+ }>;
256
+ export type CaptureInput = z.input<typeof captureInputSchema>;
257
+ type ParsedCaptureInput = z.output<typeof captureInputSchema>;
258
+ /**
259
+ * The main FrameWatch tool: record a page for `duration_ms` and return only
260
+ * the meaningful visual changes as diff cards (see CLAUDE.md "Diff Cards" and
261
+ * "MCP Response Format").
262
+ *
263
+ * Flow: navigate (waiting only for the navigation to commit, so loading and
264
+ * splash animations are recorded from their first frame) → optionally wait
265
+ * for `wait_for` → record raw frames every `interval_ms`, replaying
266
+ * `interactions` as the recording runs → smart-diff them into at most
267
+ * `max_frames` cards → format. All failures — including invalid input — are
268
+ * reported as `isError` results rather than thrown so the MCP client sees a
269
+ * useful message.
270
+ */
271
+ export declare function capturePage(rawInput: CaptureInput): Promise<CallToolResult>;
272
+ /**
273
+ * Turn a Playwright/Node error into a one-line, actionable message. Mirrors
274
+ * `describeFailure` in screenshot.ts: matches on the failing Playwright call
275
+ * (the message prefix) rather than on substrings of user-supplied selectors,
276
+ * so a navigation failure is never blamed on an element.
277
+ */
278
+ export declare function describeCaptureFailure(input: Pick<ParsedCaptureInput, "url" | "wait_for" | "wait_for_timeout_ms">, error: unknown): string;
279
+ export declare function registerCaptureTool(server: McpServer): void;
@@ -0,0 +1,275 @@
1
+ import { z } from "zod";
2
+ import { CAPTURE_INTERVAL_MS, DEFAULT_CAPTURE_DURATION_MS, DEFAULT_MAX_FRAMES, DEFAULT_SENSITIVITY, DEFAULT_VIEWPORT, MAX_CAPTURE_DURATION_MS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MAX_FRAMES_CAP, MAX_INTERACTIONS, MIN_CAPTURE_DURATION_MS, NAVIGATION_TIMEOUT_MS, PAGE_INFO_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, } from "../constants.js";
3
+ import { withPage } from "../engine/browser.js";
4
+ import { buildDiffCards } from "../engine/differ.js";
5
+ import { applyContext, attachLayers, summariseContext } from "../engine/layers/index.js";
6
+ import { CAPTURE_ACTIONS, describeInteraction, executeInteraction, interactionFieldShape, needsTouch, refineInteraction, } from "../engine/interaction.js";
7
+ import { recordFrames } from "../engine/recorder.js";
8
+ import { formatDiffCards } from "../utils/format.js";
9
+ export const CAPTURE_TOOL_NAME = "framewatch_capture";
10
+ /**
11
+ * Re-exported where it is used: the capture summary's context lines. It lives
12
+ * with the layers themselves now that `framewatch_interact` reports them too.
13
+ */
14
+ export { summariseContext };
15
+ /**
16
+ * One step of a replayable interaction script. `delay_ms` is a wait *before*
17
+ * the action, so a `wait` step is just a delay with nothing after it, and the
18
+ * delays of successive steps accumulate over the recording.
19
+ */
20
+ export const captureInteractionSchema = z
21
+ .object({
22
+ action: z.enum(CAPTURE_ACTIONS).describe("What to do: click, tap, type, scroll, swipe, wait or navigate"),
23
+ ...interactionFieldShape,
24
+ delay_ms: z.number().int().min(0).default(0).describe("Wait this long (ms) before performing this action"),
25
+ })
26
+ .superRefine(refineInteraction);
27
+ export const captureInputShape = {
28
+ url: z.string().url().describe("URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)"),
29
+ duration_ms: z
30
+ .number()
31
+ .int()
32
+ .min(MIN_CAPTURE_DURATION_MS)
33
+ .max(MAX_CAPTURE_DURATION_MS)
34
+ .default(DEFAULT_CAPTURE_DURATION_MS)
35
+ .describe("How long to record (ms)"),
36
+ sensitivity: z
37
+ .number()
38
+ .min(0)
39
+ .max(1)
40
+ .default(DEFAULT_SENSITIVITY)
41
+ .describe("Frame change threshold (0 = keep all, 1 = keep none). 0.06 means ~6% of grid cells must change."),
42
+ max_frames: z
43
+ .number()
44
+ .int()
45
+ .min(1)
46
+ .max(MAX_FRAMES_CAP)
47
+ .default(DEFAULT_MAX_FRAMES)
48
+ .describe("Maximum diff cards to return"),
49
+ interval_ms: z
50
+ .number()
51
+ .int()
52
+ .min(16)
53
+ .max(2000)
54
+ .default(CAPTURE_INTERVAL_MS)
55
+ .describe("Raw frame capture interval (ms) — 100 = 10fps"),
56
+ viewport: z
57
+ .object({
58
+ width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),
59
+ height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),
60
+ })
61
+ .optional()
62
+ .describe("Viewport size (defaults to 1280x720)"),
63
+ wait_for: z.string().optional().describe("CSS selector to wait for (visible) before recording starts"),
64
+ wait_for_timeout_ms: z
65
+ .number()
66
+ .int()
67
+ .min(1)
68
+ .default(SELECTOR_TIMEOUT_MS)
69
+ .describe("Max time (ms) to wait for `wait_for` to appear (must be > 0)"),
70
+ interactions: z
71
+ .array(captureInteractionSchema)
72
+ .max(MAX_INTERACTIONS)
73
+ .optional()
74
+ .describe("Sequence of user interactions to replay during the recording"),
75
+ interaction_timeout_ms: z
76
+ .number()
77
+ .int()
78
+ .min(1)
79
+ .default(SELECTOR_TIMEOUT_MS)
80
+ .describe("Max time (ms) an interaction step may spend waiting for its target element"),
81
+ include_console: z
82
+ .boolean()
83
+ .default(true)
84
+ .describe("Attach console logs, uncaught errors and unhandled rejections to the frames they happened between"),
85
+ include_network: z
86
+ .boolean()
87
+ .default(false)
88
+ .describe("Attach network requests (method, url, status, duration) to the frames they settled between"),
89
+ include_dom: z
90
+ .boolean()
91
+ .default(false)
92
+ .describe("Attach a summary of the DOM mutations between frames — which elements were added, removed or restyled"),
93
+ include_performance: z
94
+ .boolean()
95
+ .default(false)
96
+ .describe("Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at"),
97
+ };
98
+ export const captureInputSchema = z.object(captureInputShape);
99
+ /**
100
+ * The main FrameWatch tool: record a page for `duration_ms` and return only
101
+ * the meaningful visual changes as diff cards (see CLAUDE.md "Diff Cards" and
102
+ * "MCP Response Format").
103
+ *
104
+ * Flow: navigate (waiting only for the navigation to commit, so loading and
105
+ * splash animations are recorded from their first frame) → optionally wait
106
+ * for `wait_for` → record raw frames every `interval_ms`, replaying
107
+ * `interactions` as the recording runs → smart-diff them into at most
108
+ * `max_frames` cards → format. All failures — including invalid input — are
109
+ * reported as `isError` results rather than thrown so the MCP client sees a
110
+ * useful message.
111
+ */
112
+ export async function capturePage(rawInput) {
113
+ const parsed = captureInputSchema.safeParse(rawInput);
114
+ if (!parsed.success) {
115
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
116
+ return errorResult(`Capture failed: invalid input — ${issues}`);
117
+ }
118
+ const input = parsed.data;
119
+ const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
120
+ const interactions = input.interactions ?? [];
121
+ // Touch has to be decided when the context is created, so read it off the
122
+ // script rather than turning it on for every capture: `hasTouch` puts
123
+ // `ontouchstart` on window, which flips feature detection in most UI
124
+ // libraries and would silently change what a plain capture records.
125
+ const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};
126
+ const report = interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;
127
+ try {
128
+ const recording = await withPage({ viewport, contextOptions }, async (page) => {
129
+ // Layers go on before the navigation: a script that throws while the page
130
+ // loads, the request that never comes back and first paint all happen
131
+ // before frame 0 exists, and they are the most useful things here.
132
+ const layers = await attachLayers(page, {
133
+ console: input.include_console,
134
+ network: input.include_network,
135
+ dom: input.include_dom,
136
+ performance: input.include_performance,
137
+ });
138
+ try {
139
+ // "commit" rather than "load": splash/loading animations must be recorded from the very start.
140
+ await page.goto(input.url, { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
141
+ if (input.wait_for) {
142
+ await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
143
+ }
144
+ const result = await recordFrames(page, { duration_ms: input.duration_ms, interval_ms: input.interval_ms }, report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)));
145
+ // Drain the layers before anything else can close the page. `collect`
146
+ // reads only what Node already holds, so a page that froze, navigated
147
+ // away or crashed still yields everything it managed to report.
148
+ const context = layers.collect(result.started_at);
149
+ // The page may have died mid-recording (crash, closed browser). Frames
150
+ // already captured are still worth returning, so never let reading the
151
+ // title or url turn a partial recording into an error.
152
+ return { ...result, context, ...(await describePage(page)) };
153
+ }
154
+ finally {
155
+ layers.detach();
156
+ }
157
+ });
158
+ const { cards, total_frames } = await buildDiffCards(recording.frames, {
159
+ sensitivity: input.sensitivity,
160
+ max_frames: input.max_frames,
161
+ });
162
+ applyContext(cards, recording.context);
163
+ return formatDiffCards({
164
+ cards,
165
+ total_frames,
166
+ duration_ms: recording.duration_ms,
167
+ url: input.url,
168
+ final_url: recording.finalUrl,
169
+ title: recording.title,
170
+ dropped: recording.dropped,
171
+ interactions: report,
172
+ notes: summariseContext(recording.context, cards.length),
173
+ });
174
+ }
175
+ catch (error) {
176
+ return errorResult(describeCaptureFailure(input, error));
177
+ }
178
+ }
179
+ /**
180
+ * Replay the interaction script while the recorder runs, forcing a frame after
181
+ * every step so the differ always keeps the result of each action.
182
+ *
183
+ * A failing step ends the script but never the capture: the frames recorded so
184
+ * far are the most useful thing FrameWatch can hand back (they show the state
185
+ * the page was actually in when the selector did not match), so the failure is
186
+ * recorded in `report` and a forced "error" frame is captured instead of
187
+ * throwing. The recording then plays out the rest of its duration.
188
+ */
189
+ async function replayScript(page, recorder, interactions, timeoutMs, report) {
190
+ for (let i = 0; i < interactions.length; i++) {
191
+ const step = interactions[i];
192
+ try {
193
+ await executeInteraction(page, step, { timeout_ms: timeoutMs });
194
+ }
195
+ catch (error) {
196
+ report.error = error instanceof Error ? error.message : String(error);
197
+ report.failed_index = i + 1;
198
+ await recorder.captureNow("error");
199
+ return;
200
+ }
201
+ report.completed++;
202
+ report.steps.push(describeInteraction(step));
203
+ await recorder.captureNow("interaction");
204
+ }
205
+ }
206
+ /**
207
+ * Title and final url of a page. Both are cosmetic, so neither is allowed to
208
+ * fail or delay the result: the page may have died during the recording, and
209
+ * `page.title()` blocks for Playwright's full default timeout while the main
210
+ * thread is busy — exactly the case a developer points this tool at.
211
+ */
212
+ async function describePage(page) {
213
+ if (page.isClosed())
214
+ return {};
215
+ const finalUrl = safely(() => page.url());
216
+ // Settle the losing side too: a page.title() that outlives the race would
217
+ // otherwise reject unobserved when the context is closed underneath it.
218
+ const title = page.title().catch(() => undefined);
219
+ let timer;
220
+ const giveUp = new Promise((resolve) => {
221
+ timer = setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS);
222
+ });
223
+ try {
224
+ return { title: await Promise.race([title, giveUp]), finalUrl };
225
+ }
226
+ finally {
227
+ clearTimeout(timer);
228
+ }
229
+ }
230
+ function safely(read) {
231
+ try {
232
+ return read();
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ }
238
+ function errorResult(text) {
239
+ return { isError: true, content: [{ type: "text", text }] };
240
+ }
241
+ /**
242
+ * Turn a Playwright/Node error into a one-line, actionable message. Mirrors
243
+ * `describeFailure` in screenshot.ts: matches on the failing Playwright call
244
+ * (the message prefix) rather than on substrings of user-supplied selectors,
245
+ * so a navigation failure is never blamed on an element.
246
+ */
247
+ export function describeCaptureFailure(input, error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ const firstLine = message.split("\n")[0];
250
+ const prefix = `Capture of ${input.url} failed:`;
251
+ if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
252
+ return (`${prefix} Playwright's Chromium browser is not installed. ` +
253
+ `Run \`npx playwright install chromium\` and try again. (${firstLine})`);
254
+ }
255
+ if (input.wait_for && /^page\.waitForSelector:/.test(message)) {
256
+ return `${prefix} selector "${input.wait_for}" did not become visible within ${input.wait_for_timeout_ms}ms.`;
257
+ }
258
+ return `${prefix} ${firstLine}`;
259
+ }
260
+ export function registerCaptureTool(server) {
261
+ server.registerTool(CAPTURE_TOOL_NAME, {
262
+ title: "Capture",
263
+ description: "Record a web page for a few seconds and return only the meaningful visual changes as diff cards: " +
264
+ "a full-frame PNG plus a crop of the changed region and its position for each kept frame. " +
265
+ "Use it to check animations, splash/loading screens, transitions and anything that changes over time. " +
266
+ "Lower `sensitivity` to keep more frames; `max_frames` caps the output. " +
267
+ "Each frame can also carry the context for the interval that produced it: console output and uncaught " +
268
+ "errors (on by default), plus network requests, DOM mutations and paint/layout-shift timing via " +
269
+ "`include_network`, `include_dom` and `include_performance` — that is how you find out *why* a frame " +
270
+ "looks wrong, not just that it does.",
271
+ inputSchema: captureInputShape,
272
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
273
+ }, async (args) => capturePage(args));
274
+ }
275
+ //# sourceMappingURL=capture.js.map