framewatch-mcp-server 0.1.0 → 0.2.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 (133) hide show
  1. package/README.md +895 -15
  2. package/dist/constants.d.ts +274 -0
  3. package/dist/constants.js +279 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.d.ts +20 -4
  6. package/dist/engine/browser.js +26 -9
  7. package/dist/engine/browser.js.map +1 -1
  8. package/dist/engine/clicks.d.ts +221 -0
  9. package/dist/engine/clicks.js +801 -0
  10. package/dist/engine/clicks.js.map +1 -0
  11. package/dist/engine/forms.d.ts +137 -0
  12. package/dist/engine/forms.js +474 -0
  13. package/dist/engine/forms.js.map +1 -0
  14. package/dist/engine/hmr.d.ts +41 -0
  15. package/dist/engine/hmr.js +91 -0
  16. package/dist/engine/hmr.js.map +1 -0
  17. package/dist/engine/inspect.d.ts +31 -0
  18. package/dist/engine/inspect.js +383 -0
  19. package/dist/engine/inspect.js.map +1 -0
  20. package/dist/engine/interaction.d.ts +12 -7
  21. package/dist/engine/interaction.js +110 -18
  22. package/dist/engine/interaction.js.map +1 -1
  23. package/dist/engine/links.d.ts +134 -0
  24. package/dist/engine/links.js +384 -0
  25. package/dist/engine/links.js.map +1 -0
  26. package/dist/engine/mocks.d.ts +53 -0
  27. package/dist/engine/mocks.js +148 -0
  28. package/dist/engine/mocks.js.map +1 -0
  29. package/dist/engine/rtl.d.ts +129 -0
  30. package/dist/engine/rtl.js +540 -0
  31. package/dist/engine/rtl.js.map +1 -0
  32. package/dist/engine/seo.d.ts +189 -0
  33. package/dist/engine/seo.js +398 -0
  34. package/dist/engine/seo.js.map +1 -0
  35. package/dist/engine/snapshot.d.ts +29 -0
  36. package/dist/engine/snapshot.js +10 -0
  37. package/dist/engine/snapshot.js.map +1 -0
  38. package/dist/engine/vue.d.ts +54 -0
  39. package/dist/engine/vue.js +419 -0
  40. package/dist/engine/vue.js.map +1 -0
  41. package/dist/index.js +45 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/tools/accessibility.d.ts +4 -0
  44. package/dist/tools/accessibility.js +9 -2
  45. package/dist/tools/accessibility.js.map +1 -1
  46. package/dist/tools/api-mock.d.ts +405 -0
  47. package/dist/tools/api-mock.js +186 -0
  48. package/dist/tools/api-mock.js.map +1 -0
  49. package/dist/tools/capture.d.ts +90 -26
  50. package/dist/tools/capture.js +109 -58
  51. package/dist/tools/capture.js.map +1 -1
  52. package/dist/tools/compare.d.ts +4 -0
  53. package/dist/tools/compare.js +16 -5
  54. package/dist/tools/compare.js.map +1 -1
  55. package/dist/tools/dead-clicks.d.ts +128 -0
  56. package/dist/tools/dead-clicks.js +570 -0
  57. package/dist/tools/dead-clicks.js.map +1 -0
  58. package/dist/tools/form-test.d.ts +112 -0
  59. package/dist/tools/form-test.js +477 -0
  60. package/dist/tools/form-test.js.map +1 -0
  61. package/dist/tools/index.d.ts +17 -1
  62. package/dist/tools/index.js +45 -1
  63. package/dist/tools/index.js.map +1 -1
  64. package/dist/tools/inspect.d.ts +78 -0
  65. package/dist/tools/inspect.js +136 -0
  66. package/dist/tools/inspect.js.map +1 -0
  67. package/dist/tools/interact.d.ts +37 -18
  68. package/dist/tools/interact.js +113 -13
  69. package/dist/tools/interact.js.map +1 -1
  70. package/dist/tools/links.d.ts +129 -0
  71. package/dist/tools/links.js +640 -0
  72. package/dist/tools/links.js.map +1 -0
  73. package/dist/tools/responsive.d.ts +10 -6
  74. package/dist/tools/responsive.js +21 -4
  75. package/dist/tools/responsive.js.map +1 -1
  76. package/dist/tools/rtl.d.ts +241 -0
  77. package/dist/tools/rtl.js +410 -0
  78. package/dist/tools/rtl.js.map +1 -0
  79. package/dist/tools/save-auth.d.ts +263 -0
  80. package/dist/tools/save-auth.js +253 -0
  81. package/dist/tools/save-auth.js.map +1 -0
  82. package/dist/tools/screenshot.d.ts +4 -0
  83. package/dist/tools/screenshot.js +15 -4
  84. package/dist/tools/screenshot.js.map +1 -1
  85. package/dist/tools/seo.d.ts +113 -0
  86. package/dist/tools/seo.js +281 -0
  87. package/dist/tools/seo.js.map +1 -0
  88. package/dist/tools/snapshot.d.ts +122 -0
  89. package/dist/tools/snapshot.js +183 -0
  90. package/dist/tools/snapshot.js.map +1 -0
  91. package/dist/tools/wait-for.d.ts +107 -0
  92. package/dist/tools/wait-for.js +167 -0
  93. package/dist/tools/wait-for.js.map +1 -0
  94. package/dist/utils/arabic-text.d.ts +14 -0
  95. package/dist/utils/arabic-text.js +193 -0
  96. package/dist/utils/arabic-text.js.map +1 -0
  97. package/dist/utils/budget.d.ts +41 -0
  98. package/dist/utils/budget.js +182 -0
  99. package/dist/utils/budget.js.map +1 -0
  100. package/dist/utils/format.d.ts +11 -1
  101. package/dist/utils/format.js +27 -4
  102. package/dist/utils/format.js.map +1 -1
  103. package/dist/utils/highlight.d.ts +69 -0
  104. package/dist/utils/highlight.js +181 -0
  105. package/dist/utils/highlight.js.map +1 -0
  106. package/dist/utils/link-rules.d.ts +100 -0
  107. package/dist/utils/link-rules.js +284 -0
  108. package/dist/utils/link-rules.js.map +1 -0
  109. package/dist/utils/mock-rules.d.ts +144 -0
  110. package/dist/utils/mock-rules.js +224 -0
  111. package/dist/utils/mock-rules.js.map +1 -0
  112. package/dist/utils/rtl-rules.d.ts +142 -0
  113. package/dist/utils/rtl-rules.js +296 -0
  114. package/dist/utils/rtl-rules.js.map +1 -0
  115. package/dist/utils/seo-rules.d.ts +129 -0
  116. package/dist/utils/seo-rules.js +726 -0
  117. package/dist/utils/seo-rules.js.map +1 -0
  118. package/dist/utils/snapshot-rules.d.ts +33 -0
  119. package/dist/utils/snapshot-rules.js +111 -0
  120. package/dist/utils/snapshot-rules.js.map +1 -0
  121. package/dist/utils/storage-state.d.ts +76 -0
  122. package/dist/utils/storage-state.js +195 -0
  123. package/dist/utils/storage-state.js.map +1 -0
  124. package/dist/utils/style-rules.d.ts +107 -0
  125. package/dist/utils/style-rules.js +223 -0
  126. package/dist/utils/style-rules.js.map +1 -0
  127. package/dist/utils/test-data.d.ts +75 -0
  128. package/dist/utils/test-data.js +294 -0
  129. package/dist/utils/test-data.js.map +1 -0
  130. package/dist/utils/vue-rules.d.ts +72 -0
  131. package/dist/utils/vue-rules.js +108 -0
  132. package/dist/utils/vue-rules.js.map +1 -0
  133. package/package.json +6 -4
@@ -20,6 +20,7 @@ export declare const compareInputShape: {
20
20
  width?: number | undefined;
21
21
  height?: number | undefined;
22
22
  }>>;
23
+ storage_state: z.ZodOptional<z.ZodString>;
23
24
  };
24
25
  export declare const compareInputSchema: z.ZodObject<{
25
26
  url_a: z.ZodEffects<z.ZodString, string, string>;
@@ -37,6 +38,7 @@ export declare const compareInputSchema: z.ZodObject<{
37
38
  width?: number | undefined;
38
39
  height?: number | undefined;
39
40
  }>>;
41
+ storage_state: z.ZodOptional<z.ZodString>;
40
42
  }, "strip", z.ZodTypeAny, {
41
43
  wait_ms: number;
42
44
  wait_for_timeout_ms: number;
@@ -47,6 +49,7 @@ export declare const compareInputSchema: z.ZodObject<{
47
49
  height: number;
48
50
  } | undefined;
49
51
  wait_for?: string | undefined;
52
+ storage_state?: string | undefined;
50
53
  }, {
51
54
  url_a: string;
52
55
  url_b: string;
@@ -57,6 +60,7 @@ export declare const compareInputSchema: z.ZodObject<{
57
60
  wait_ms?: number | undefined;
58
61
  wait_for?: string | undefined;
59
62
  wait_for_timeout_ms?: number | undefined;
63
+ storage_state?: string | undefined;
60
64
  }>;
61
65
  export type CompareInput = z.input<typeof compareInputSchema>;
62
66
  type ParsedCompareInput = z.output<typeof compareInputSchema>;
@@ -3,6 +3,7 @@ import { CROP_PADDING_PX, DEFAULT_COMPARE_WAIT_MS, DEFAULT_VIEWPORT, MAX_VIEWPOR
3
3
  import { getSessionPage, withPage, withSessionLock } from "../engine/browser.js";
4
4
  import { computePixelMask, padBoundingBox } from "../engine/differ.js";
5
5
  import { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from "../utils/image.js";
6
+ import { resolveStorageState, storageStateField, withAuthNote } from "../utils/storage-state.js";
6
7
  export const COMPARE_TOOL_NAME = "framewatch_compare";
7
8
  /** `url_a` accepts this instead of a URL, meaning "whatever framewatch_interact is looking at". */
8
9
  export const CURRENT_PAGE = "current";
@@ -38,6 +39,7 @@ export const compareInputShape = {
38
39
  })
39
40
  .optional()
40
41
  .describe("Viewport for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)"),
42
+ storage_state: storageStateField,
41
43
  };
42
44
  export const compareInputSchema = z.object(compareInputShape);
43
45
  /**
@@ -69,7 +71,12 @@ export async function comparePages(rawInput) {
69
71
  const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;
70
72
  const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];
71
73
  try {
72
- const first = await captureSide(input, firstSide);
74
+ // Both URL sides get the same saved auth: they are two views of one app,
75
+ // and a comparison where only one side is signed in compares the login
76
+ // screen with the app. ("current" is read as it is, never reloaded.)
77
+ const auth = await resolveStorageState(input.storage_state);
78
+ const storageState = auth?.state;
79
+ const first = await captureSide(input, firstSide, undefined, storageState);
73
80
  const notes = [];
74
81
  let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
75
82
  if (firstSide === CURRENT_PAGE) {
@@ -81,9 +88,9 @@ export async function comparePages(rawInput) {
81
88
  }
82
89
  viewport = open;
83
90
  }
84
- const second = await captureSide(input, secondSide, viewport);
91
+ const second = await captureSide(input, secondSide, viewport, storageState);
85
92
  const [a, b] = openIsB ? [second, first] : [first, second];
86
- return await render(input, a, b, notes);
93
+ return withAuthNote(await render(input, a, b, notes), auth);
87
94
  }
88
95
  catch (error) {
89
96
  return errorResult(describeCompareFailure(input, error));
@@ -99,7 +106,7 @@ export async function comparePages(rawInput) {
99
106
  * flow — against a plain URL, and reloading or resizing it would destroy the
100
107
  * very thing being compared.
101
108
  */
102
- async function captureSide(input, side, viewport) {
109
+ async function captureSide(input, side, viewport, storageState) {
103
110
  if (side === CURRENT_PAGE) {
104
111
  // Under the session lock: an interaction running in parallel would
105
112
  // otherwise be screenshotted half way through.
@@ -112,7 +119,11 @@ async function captureSide(input, side, viewport) {
112
119
  return { png: await page.screenshot({ type: "png" }), url: page.url(), title: await safeTitle(page) };
113
120
  });
114
121
  }
115
- return withPage({ viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT } }, async (page) => {
122
+ const options = {
123
+ viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT },
124
+ ...(storageState ? { contextOptions: { storageState } } : {}),
125
+ };
126
+ return withPage(options, async (page) => {
116
127
  await page.goto(side, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
117
128
  if (input.wait_for) {
118
129
  await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
@@ -1 +1 @@
1
- {"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/tools/compare.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CROP_PADDING_PX,\n DEFAULT_COMPARE_WAIT_MS,\n DEFAULT_VIEWPORT,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withPage, withSessionLock } from \"../engine/browser.js\";\nimport { computePixelMask, padBoundingBox } from \"../engine/differ.js\";\nimport type { BoundingBox, Viewport } from \"../types.js\";\nimport { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from \"../utils/image.js\";\n\nexport const COMPARE_TOOL_NAME = \"framewatch_compare\";\n\n/** `url_a` accepts this instead of a URL, meaning \"whatever framewatch_interact is looking at\". */\nexport const CURRENT_PAGE = \"current\";\n\n/**\n * Either a URL or the literal \"current\". `z.string().url()` cannot express\n * that, so the check is spelled out — and the message says both options,\n * because \"invalid url\" on the word `current` is a confusing thing to read.\n */\nconst sideSchema = z.string().refine((value) => value === CURRENT_PAGE || isUrl(value), {\n message: `must be a URL, or \"${CURRENT_PAGE}\" to use the page framewatch_interact currently has open`,\n});\n\nexport const compareInputShape = {\n url_a: sideSchema.describe(\n `First URL — the \"before\" side. Pass \"${CURRENT_PAGE}\" to compare against the page framewatch_interact has open, ` +\n \"in whatever state your interactions left it.\",\n ),\n url_b: sideSchema.describe('Second URL — the \"after\" side.'),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_COMPARE_WAIT_MS)\n .describe(\"Wait time (ms) after page load before each screenshot\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) on both sides before capturing\"),\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 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 for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)\"),\n};\n\nexport const compareInputSchema = z.object(compareInputShape);\nexport type CompareInput = z.input<typeof compareInputSchema>;\ntype ParsedCompareInput = z.output<typeof compareInputSchema>;\n\n/** One captured side of the comparison. */\ninterface Side {\n png: Buffer;\n /** Where the shot was actually taken (after redirects), or the open page's URL. */\n url: string;\n title?: string;\n}\n\n/**\n * Compare two pages — or the same page before and after a change — and show\n * what moved.\n *\n * Returns both screenshots plus an overlay: side B with every differing pixel\n * tinted, which is the part that turns \"3.4% changed\" into something a reader\n * can act on. The comparison is the same pixel comparison the capture engine\n * uses between frames, so a change region here means what it means there.\n *\n * The two sides are captured one after the other rather than at once, so both\n * get an unloaded machine and the same `wait_ms` means the same thing for\n * each; a difference in the result should come from the pages, not from which\n * of them was competing for the CPU. (Which of the two goes first is decided\n * by `\"current\"` — see below — not by their order in the input.)\n */\nexport async function comparePages(rawInput: CompareInput): Promise<CallToolResult> {\n const parsed = compareInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Compare failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n // The open page is the one side that cannot be resized — doing so would\n // change the state being compared — so it is captured first whichever side\n // it is, and its size is what the other side is then captured at. Frames of\n // two different sizes cannot be compared pixel for pixel at all.\n const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;\n const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];\n\n try {\n const first = await captureSide(input, firstSide);\n const notes: string[] = [];\n let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n if (firstSide === CURRENT_PAGE) {\n const open = await sizeOf(first.png);\n if (input.viewport && (input.viewport.width !== open.width || input.viewport.height !== open.height)) {\n notes.push(\n `Ignored \\`viewport\\` ${input.viewport.width}x${input.viewport.height}: the open page is ` +\n `${open.width}x${open.height} and resizing it would change the state being compared. ` +\n \"Both sides were captured at the open page's size.\",\n );\n }\n viewport = open;\n }\n\n const second = await captureSide(input, secondSide, viewport);\n const [a, b] = openIsB ? [second, first] : [first, second];\n return await render(input, a, b, notes);\n } catch (error) {\n return errorResult(describeCompareFailure(input, error));\n }\n}\n\n/**\n * Screenshot one side.\n *\n * A URL gets a fresh context, so nothing from the other side (or from an\n * earlier tool call) can leak into it. \"current\" instead reads the page\n * `framewatch_interact` has open, *without* touching it: the whole point of\n * that option is to compare a hand-built state — three clicks deep into a\n * flow — against a plain URL, and reloading or resizing it would destroy the\n * very thing being compared.\n */\nasync function captureSide(input: ParsedCompareInput, side: string, viewport?: Viewport): Promise<Side> {\n if (side === CURRENT_PAGE) {\n // Under the session lock: an interaction running in parallel would\n // otherwise be screenshotted half way through.\n return withSessionLock(async () => {\n const { page } = await getSessionPage();\n if (page.url() === \"about:blank\") {\n throw new Error(\n `no page is open, so there is no \"${CURRENT_PAGE}\" state to compare — ` +\n \"use framewatch_interact first, or pass a URL here.\",\n );\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n }\n\n return withPage({ viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT } }, async (page) => {\n await page.goto(side, { 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 return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n}\n\n/**\n * Build the result: a summary, both frames, and (when the two are comparable\n * and actually differ) the overlay.\n *\n * Sides of different sizes are reported as such rather than being stretched to\n * match. Rescaling one of them would resample every pixel in it, and the\n * \"differences\" that came back would be the resampling, not the pages.\n */\nasync function render(input: ParsedCompareInput, a: Side, b: Side, notes: string[]): Promise<CallToolResult> {\n const [grayA, grayB] = await Promise.all([toGrayscale(a.png), toGrayscale(b.png)]);\n const nameA = describeSide(input.url_a, a);\n const nameB = describeSide(input.url_b, b);\n const sameSize = grayA.width === grayB.width && grayA.height === grayB.height;\n\n const lines: string[] = [];\n let overlay: Buffer | undefined;\n let overlayNote = \"\";\n\n if (!sameSize) {\n lines.push(\n `Compared ${nameA} against ${nameB} — the two frames are different sizes ` +\n `(${grayA.width}x${grayA.height} vs ${grayB.width}x${grayB.height}), so they cannot be compared pixel ` +\n \"for pixel. Both are below.\",\n );\n } else {\n const diff = computePixelMask(grayA.data, grayB.data, grayA.width, grayA.height);\n const region: BoundingBox | null =\n diff.bbox === null ? null : padBoundingBox(diff.bbox, CROP_PADDING_PX, grayA.width, grayA.height);\n\n lines.push(\n `Compared ${nameA} against ${nameB} at ${grayA.width}x${grayA.height} — ` +\n `${diff.changePercent.toFixed(2)}% of pixels differ` +\n (region === null\n ? \". The two are pixel-identical.\"\n : `, all of it within ${region.x},${region.y} ${region.width}x${region.height}.`),\n );\n\n if (region !== null) {\n overlay = await resizeForOutput(await overlayMask(b.png, diff.mask, grayA.width, grayA.height));\n overlayNote =\n `Diff overlay — B with every differing pixel tinted. ${diff.changedPixels} of ${diff.totalPixels} ` +\n `pixels changed (${diff.changePercent.toFixed(2)}%), within ` +\n `${region.x},${region.y} ${region.width}x${region.height}.`;\n }\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: [...lines, ...notes].join(\"\\n\") }];\n await pushFrame(content, a.png, `A — ${nameA}`);\n await pushFrame(content, b.png, `B — ${nameB}`);\n if (overlay) {\n content.push({ type: \"image\", data: toBase64(overlay), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: overlayNote });\n }\n return { content };\n}\n\nasync function pushFrame(content: CallToolResult[\"content\"], png: Buffer, label: string): Promise<void> {\n const resized = await resizeForOutput(png);\n const { width, height } = await getDimensions(resized);\n content.push({ type: \"image\", data: toBase64(resized), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: `${label} — image ${width}x${height}` });\n}\n\n/** How a side is named in the output: where it ended up, and its title if it has one. */\nfunction describeSide(requested: string, side: Side): string {\n const where = requested === CURRENT_PAGE ? `the open page (${side.url})` : side.url;\n return side.title ? `${where} \"${side.title}\"` : where;\n}\n\n/** Cosmetic, and never allowed to fail or stall: a busy page still gets compared. */\nasync function safeTitle(page: Page): Promise<string | undefined> {\n return Promise.race([\n page.title().catch(() => undefined),\n new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS)),\n ]);\n}\n\nasync function sizeOf(png: Buffer): Promise<Viewport> {\n const { width, height } = await getDimensions(png);\n return { width, height };\n}\n\nfunction isUrl(value: string): boolean {\n try {\n new URL(value);\n return true;\n } catch {\n return false;\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 comparison. Mirrors `describeFailure` in\n * screenshot.ts, with one addition: a `page.goto` failure names which of the\n * two sides could not be opened, which is the first thing you want to know.\n */\nexport function describeCompareFailure(\n input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\" | \"wait_for\" | \"wait_for_timeout_ms\">,\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 = \"Compare 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 (/^page\\.goto:/.test(message)) {\n const side = failingSide(input, message);\n return `${prefix} could not open ${side} — ${firstLine}`;\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 return `${prefix} ${firstLine}`;\n}\n\n/** Playwright quotes the URL it tried in a goto failure; that says which side broke. */\nfunction failingSide(input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\">, message: string): string {\n if (input.url_b !== CURRENT_PAGE && message.includes(input.url_b)) return `url_b (${input.url_b})`;\n if (input.url_a !== CURRENT_PAGE && message.includes(input.url_a)) return `url_a (${input.url_a})`;\n return \"one of the two pages\";\n}\n\nexport function registerCompareTool(server: McpServer): void {\n server.registerTool(\n COMPARE_TOOL_NAME,\n {\n title: \"Compare\",\n description:\n \"Compare two pages — two URLs, or the same URL before and after a code change — and show what moved. \" +\n \"Returns both screenshots, the percentage of pixels that differ, the region they differ in, and an \" +\n `overlay of the second page with every changed pixel tinted. Pass \"${CURRENT_PAGE}\" as \\`url_a\\` to ` +\n \"compare against the page framewatch_interact has open, in whatever state your interactions left it.\",\n inputSchema: compareInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => comparePages(args),\n );\n}\n"]}
1
+ {"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/tools/compare.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CROP_PADDING_PX,\n DEFAULT_COMPARE_WAIT_MS,\n DEFAULT_VIEWPORT,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withPage, withSessionLock } from \"../engine/browser.js\";\nimport { computePixelMask, padBoundingBox } from \"../engine/differ.js\";\nimport type { BoundingBox, Viewport } from \"../types.js\";\nimport { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from \"../utils/image.js\";\nimport { resolveStorageState, storageStateField, withAuthNote, type StorageState } from \"../utils/storage-state.js\";\n\nexport const COMPARE_TOOL_NAME = \"framewatch_compare\";\n\n/** `url_a` accepts this instead of a URL, meaning \"whatever framewatch_interact is looking at\". */\nexport const CURRENT_PAGE = \"current\";\n\n/**\n * Either a URL or the literal \"current\". `z.string().url()` cannot express\n * that, so the check is spelled out — and the message says both options,\n * because \"invalid url\" on the word `current` is a confusing thing to read.\n */\nconst sideSchema = z.string().refine((value) => value === CURRENT_PAGE || isUrl(value), {\n message: `must be a URL, or \"${CURRENT_PAGE}\" to use the page framewatch_interact currently has open`,\n});\n\nexport const compareInputShape = {\n url_a: sideSchema.describe(\n `First URL — the \"before\" side. Pass \"${CURRENT_PAGE}\" to compare against the page framewatch_interact has open, ` +\n \"in whatever state your interactions left it.\",\n ),\n url_b: sideSchema.describe('Second URL — the \"after\" side.'),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_COMPARE_WAIT_MS)\n .describe(\"Wait time (ms) after page load before each screenshot\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) on both sides before capturing\"),\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 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 for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)\"),\n storage_state: storageStateField,\n};\n\nexport const compareInputSchema = z.object(compareInputShape);\nexport type CompareInput = z.input<typeof compareInputSchema>;\ntype ParsedCompareInput = z.output<typeof compareInputSchema>;\n\n/** One captured side of the comparison. */\ninterface Side {\n png: Buffer;\n /** Where the shot was actually taken (after redirects), or the open page's URL. */\n url: string;\n title?: string;\n}\n\n/**\n * Compare two pages — or the same page before and after a change — and show\n * what moved.\n *\n * Returns both screenshots plus an overlay: side B with every differing pixel\n * tinted, which is the part that turns \"3.4% changed\" into something a reader\n * can act on. The comparison is the same pixel comparison the capture engine\n * uses between frames, so a change region here means what it means there.\n *\n * The two sides are captured one after the other rather than at once, so both\n * get an unloaded machine and the same `wait_ms` means the same thing for\n * each; a difference in the result should come from the pages, not from which\n * of them was competing for the CPU. (Which of the two goes first is decided\n * by `\"current\"` — see below — not by their order in the input.)\n */\nexport async function comparePages(rawInput: CompareInput): Promise<CallToolResult> {\n const parsed = compareInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Compare failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n // The open page is the one side that cannot be resized — doing so would\n // change the state being compared — so it is captured first whichever side\n // it is, and its size is what the other side is then captured at. Frames of\n // two different sizes cannot be compared pixel for pixel at all.\n const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;\n const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];\n\n try {\n // Both URL sides get the same saved auth: they are two views of one app,\n // and a comparison where only one side is signed in compares the login\n // screen with the app. (\"current\" is read as it is, never reloaded.)\n const auth = await resolveStorageState(input.storage_state);\n const storageState = auth?.state;\n\n const first = await captureSide(input, firstSide, undefined, storageState);\n const notes: string[] = [];\n let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n if (firstSide === CURRENT_PAGE) {\n const open = await sizeOf(first.png);\n if (input.viewport && (input.viewport.width !== open.width || input.viewport.height !== open.height)) {\n notes.push(\n `Ignored \\`viewport\\` ${input.viewport.width}x${input.viewport.height}: the open page is ` +\n `${open.width}x${open.height} and resizing it would change the state being compared. ` +\n \"Both sides were captured at the open page's size.\",\n );\n }\n viewport = open;\n }\n\n const second = await captureSide(input, secondSide, viewport, storageState);\n const [a, b] = openIsB ? [second, first] : [first, second];\n return withAuthNote(await render(input, a, b, notes), auth);\n } catch (error) {\n return errorResult(describeCompareFailure(input, error));\n }\n}\n\n/**\n * Screenshot one side.\n *\n * A URL gets a fresh context, so nothing from the other side (or from an\n * earlier tool call) can leak into it. \"current\" instead reads the page\n * `framewatch_interact` has open, *without* touching it: the whole point of\n * that option is to compare a hand-built state — three clicks deep into a\n * flow — against a plain URL, and reloading or resizing it would destroy the\n * very thing being compared.\n */\nasync function captureSide(\n input: ParsedCompareInput,\n side: string,\n viewport?: Viewport,\n storageState?: StorageState,\n): Promise<Side> {\n if (side === CURRENT_PAGE) {\n // Under the session lock: an interaction running in parallel would\n // otherwise be screenshotted half way through.\n return withSessionLock(async () => {\n const { page } = await getSessionPage();\n if (page.url() === \"about:blank\") {\n throw new Error(\n `no page is open, so there is no \"${CURRENT_PAGE}\" state to compare — ` +\n \"use framewatch_interact first, or pass a URL here.\",\n );\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n }\n\n const options = {\n viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT },\n ...(storageState ? { contextOptions: { storageState } } : {}),\n };\n return withPage(options, async (page) => {\n await page.goto(side, { 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 return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n}\n\n/**\n * Build the result: a summary, both frames, and (when the two are comparable\n * and actually differ) the overlay.\n *\n * Sides of different sizes are reported as such rather than being stretched to\n * match. Rescaling one of them would resample every pixel in it, and the\n * \"differences\" that came back would be the resampling, not the pages.\n */\nasync function render(input: ParsedCompareInput, a: Side, b: Side, notes: string[]): Promise<CallToolResult> {\n const [grayA, grayB] = await Promise.all([toGrayscale(a.png), toGrayscale(b.png)]);\n const nameA = describeSide(input.url_a, a);\n const nameB = describeSide(input.url_b, b);\n const sameSize = grayA.width === grayB.width && grayA.height === grayB.height;\n\n const lines: string[] = [];\n let overlay: Buffer | undefined;\n let overlayNote = \"\";\n\n if (!sameSize) {\n lines.push(\n `Compared ${nameA} against ${nameB} — the two frames are different sizes ` +\n `(${grayA.width}x${grayA.height} vs ${grayB.width}x${grayB.height}), so they cannot be compared pixel ` +\n \"for pixel. Both are below.\",\n );\n } else {\n const diff = computePixelMask(grayA.data, grayB.data, grayA.width, grayA.height);\n const region: BoundingBox | null =\n diff.bbox === null ? null : padBoundingBox(diff.bbox, CROP_PADDING_PX, grayA.width, grayA.height);\n\n lines.push(\n `Compared ${nameA} against ${nameB} at ${grayA.width}x${grayA.height} — ` +\n `${diff.changePercent.toFixed(2)}% of pixels differ` +\n (region === null\n ? \". The two are pixel-identical.\"\n : `, all of it within ${region.x},${region.y} ${region.width}x${region.height}.`),\n );\n\n if (region !== null) {\n overlay = await resizeForOutput(await overlayMask(b.png, diff.mask, grayA.width, grayA.height));\n overlayNote =\n `Diff overlay — B with every differing pixel tinted. ${diff.changedPixels} of ${diff.totalPixels} ` +\n `pixels changed (${diff.changePercent.toFixed(2)}%), within ` +\n `${region.x},${region.y} ${region.width}x${region.height}.`;\n }\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: [...lines, ...notes].join(\"\\n\") }];\n await pushFrame(content, a.png, `A — ${nameA}`);\n await pushFrame(content, b.png, `B — ${nameB}`);\n if (overlay) {\n content.push({ type: \"image\", data: toBase64(overlay), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: overlayNote });\n }\n return { content };\n}\n\nasync function pushFrame(content: CallToolResult[\"content\"], png: Buffer, label: string): Promise<void> {\n const resized = await resizeForOutput(png);\n const { width, height } = await getDimensions(resized);\n content.push({ type: \"image\", data: toBase64(resized), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: `${label} — image ${width}x${height}` });\n}\n\n/** How a side is named in the output: where it ended up, and its title if it has one. */\nfunction describeSide(requested: string, side: Side): string {\n const where = requested === CURRENT_PAGE ? `the open page (${side.url})` : side.url;\n return side.title ? `${where} \"${side.title}\"` : where;\n}\n\n/** Cosmetic, and never allowed to fail or stall: a busy page still gets compared. */\nasync function safeTitle(page: Page): Promise<string | undefined> {\n return Promise.race([\n page.title().catch(() => undefined),\n new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS)),\n ]);\n}\n\nasync function sizeOf(png: Buffer): Promise<Viewport> {\n const { width, height } = await getDimensions(png);\n return { width, height };\n}\n\nfunction isUrl(value: string): boolean {\n try {\n new URL(value);\n return true;\n } catch {\n return false;\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 comparison. Mirrors `describeFailure` in\n * screenshot.ts, with one addition: a `page.goto` failure names which of the\n * two sides could not be opened, which is the first thing you want to know.\n */\nexport function describeCompareFailure(\n input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\" | \"wait_for\" | \"wait_for_timeout_ms\">,\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 = \"Compare 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 (/^page\\.goto:/.test(message)) {\n const side = failingSide(input, message);\n return `${prefix} could not open ${side} — ${firstLine}`;\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 return `${prefix} ${firstLine}`;\n}\n\n/** Playwright quotes the URL it tried in a goto failure; that says which side broke. */\nfunction failingSide(input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\">, message: string): string {\n if (input.url_b !== CURRENT_PAGE && message.includes(input.url_b)) return `url_b (${input.url_b})`;\n if (input.url_a !== CURRENT_PAGE && message.includes(input.url_a)) return `url_a (${input.url_a})`;\n return \"one of the two pages\";\n}\n\nexport function registerCompareTool(server: McpServer): void {\n server.registerTool(\n COMPARE_TOOL_NAME,\n {\n title: \"Compare\",\n description:\n \"Compare two pages — two URLs, or the same URL before and after a code change — and show what moved. \" +\n \"Returns both screenshots, the percentage of pixels that differ, the region they differ in, and an \" +\n `overlay of the second page with every changed pixel tinted. Pass \"${CURRENT_PAGE}\" as \\`url_a\\` to ` +\n \"compare against the page framewatch_interact has open, in whatever state your interactions left it.\",\n inputSchema: compareInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => comparePages(args),\n );\n}\n"]}
@@ -0,0 +1,128 @@
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 { type ClickEffect, type Clickable, type HoverProbe } from "../engine/clicks.js";
5
+ export declare const DEAD_CLICKS_TOOL_NAME = "framewatch_dead_clicks";
6
+ export declare const deadClicksInputShape: {
7
+ url: z.ZodString;
8
+ wait_ms: z.ZodDefault<z.ZodNumber>;
9
+ wait_for: z.ZodOptional<z.ZodString>;
10
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
11
+ settle_ms: z.ZodDefault<z.ZodNumber>;
12
+ selector: z.ZodOptional<z.ZodString>;
13
+ exclude: z.ZodOptional<z.ZodString>;
14
+ include_pointer: z.ZodDefault<z.ZodBoolean>;
15
+ include_hover: z.ZodDefault<z.ZodBoolean>;
16
+ max_elements: z.ZodDefault<z.ZodNumber>;
17
+ full_page: z.ZodDefault<z.ZodBoolean>;
18
+ viewport: z.ZodOptional<z.ZodObject<{
19
+ width: z.ZodDefault<z.ZodNumber>;
20
+ height: z.ZodDefault<z.ZodNumber>;
21
+ }, "strip", z.ZodTypeAny, {
22
+ width: number;
23
+ height: number;
24
+ }, {
25
+ width?: number | undefined;
26
+ height?: number | undefined;
27
+ }>>;
28
+ storage_state: z.ZodOptional<z.ZodString>;
29
+ };
30
+ export declare const deadClicksInputSchema: z.ZodObject<{
31
+ url: z.ZodString;
32
+ wait_ms: z.ZodDefault<z.ZodNumber>;
33
+ wait_for: z.ZodOptional<z.ZodString>;
34
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
35
+ settle_ms: z.ZodDefault<z.ZodNumber>;
36
+ selector: z.ZodOptional<z.ZodString>;
37
+ exclude: z.ZodOptional<z.ZodString>;
38
+ include_pointer: z.ZodDefault<z.ZodBoolean>;
39
+ include_hover: z.ZodDefault<z.ZodBoolean>;
40
+ max_elements: z.ZodDefault<z.ZodNumber>;
41
+ full_page: z.ZodDefault<z.ZodBoolean>;
42
+ viewport: z.ZodOptional<z.ZodObject<{
43
+ width: z.ZodDefault<z.ZodNumber>;
44
+ height: z.ZodDefault<z.ZodNumber>;
45
+ }, "strip", z.ZodTypeAny, {
46
+ width: number;
47
+ height: number;
48
+ }, {
49
+ width?: number | undefined;
50
+ height?: number | undefined;
51
+ }>>;
52
+ storage_state: z.ZodOptional<z.ZodString>;
53
+ }, "strip", z.ZodTypeAny, {
54
+ url: string;
55
+ wait_ms: number;
56
+ wait_for_timeout_ms: number;
57
+ max_elements: number;
58
+ include_pointer: boolean;
59
+ settle_ms: number;
60
+ include_hover: boolean;
61
+ full_page: boolean;
62
+ viewport?: {
63
+ width: number;
64
+ height: number;
65
+ } | undefined;
66
+ wait_for?: string | undefined;
67
+ storage_state?: string | undefined;
68
+ selector?: string | undefined;
69
+ exclude?: string | undefined;
70
+ }, {
71
+ url: string;
72
+ viewport?: {
73
+ width?: number | undefined;
74
+ height?: number | undefined;
75
+ } | undefined;
76
+ wait_ms?: number | undefined;
77
+ wait_for?: string | undefined;
78
+ wait_for_timeout_ms?: number | undefined;
79
+ max_elements?: number | undefined;
80
+ storage_state?: string | undefined;
81
+ selector?: string | undefined;
82
+ exclude?: string | undefined;
83
+ include_pointer?: boolean | undefined;
84
+ settle_ms?: number | undefined;
85
+ include_hover?: boolean | undefined;
86
+ full_page?: boolean | undefined;
87
+ }>;
88
+ export type DeadClicksInput = z.input<typeof deadClicksInputSchema>;
89
+ type ParsedDeadClicksInput = z.output<typeof deadClicksInputSchema>;
90
+ /** What one element did — or did not do — when it was clicked. */
91
+ export interface Verdict {
92
+ clickable: Clickable;
93
+ /** Everything the click caused. Empty means the click did nothing at all. */
94
+ effects: ClickEffect[];
95
+ /** Set when the element was never clicked, and why. */
96
+ skipped?: string;
97
+ /** How the page reacts to the pointer, asked only of dead elements. */
98
+ hover?: HoverProbe;
99
+ }
100
+ /**
101
+ * Find the elements on a page that look clickable and do nothing.
102
+ *
103
+ * Every candidate is clicked for real, and the page is then interrogated from
104
+ * every angle at once — URL, DOM mutations, form state, storage, scroll,
105
+ * focus, console, network, dialogs, popups, downloads. An element is only
106
+ * called dead when all of them stayed silent (see engine/clicks.ts).
107
+ *
108
+ * Two things make the answer trustworthy rather than merely plausible. The
109
+ * page is measured once with nobody clicking, so a ticking clock or a polling
110
+ * fetch cannot make every element look alive. And the page is reloaded after
111
+ * any click that changed it, so element five is judged on the page as it
112
+ * shipped rather than on whatever elements one to four left behind — a click
113
+ * that changed nothing needs no reload, which is exactly the case this tool is
114
+ * looking for, so a page full of dead controls is also the fastest to sweep.
115
+ *
116
+ * This is the one read-only-looking tool that is not read-only: it presses
117
+ * every button on the page, including the one that deletes the account. Use
118
+ * `exclude` (or `selector`) on anything that matters.
119
+ */
120
+ export declare function findDeadClicks(rawInput: DeadClicksInput): Promise<CallToolResult>;
121
+ /**
122
+ * One actionable line for a sweep that could not run. Mirrors `describeFailure`
123
+ * in screenshot.ts: match on the failing Playwright call, never on substrings
124
+ * of a user-supplied selector.
125
+ */
126
+ export declare function describeDeadClicksFailure(input: Pick<ParsedDeadClicksInput, "url" | "wait_for" | "wait_for_timeout_ms">, error: unknown): string;
127
+ export declare function registerDeadClicksTool(server: McpServer): void;
128
+ export {};