framewatch-mcp-server 0.1.0 → 0.1.1

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 (40) hide show
  1. package/README.md +98 -7
  2. package/dist/constants.d.ts +10 -0
  3. package/dist/constants.js +11 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.d.ts +20 -4
  6. package/dist/engine/browser.js +22 -9
  7. package/dist/engine/browser.js.map +1 -1
  8. package/dist/engine/interaction.d.ts +6 -6
  9. package/dist/engine/interaction.js +89 -12
  10. package/dist/engine/interaction.js.map +1 -1
  11. package/dist/index.js +4 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/tools/accessibility.d.ts +4 -0
  14. package/dist/tools/accessibility.js +7 -1
  15. package/dist/tools/accessibility.js.map +1 -1
  16. package/dist/tools/capture.d.ts +21 -17
  17. package/dist/tools/capture.js +7 -1
  18. package/dist/tools/capture.js.map +1 -1
  19. package/dist/tools/compare.d.ts +4 -0
  20. package/dist/tools/compare.js +14 -4
  21. package/dist/tools/compare.js.map +1 -1
  22. package/dist/tools/index.d.ts +1 -0
  23. package/dist/tools/index.js +3 -0
  24. package/dist/tools/index.js.map +1 -1
  25. package/dist/tools/interact.d.ts +12 -6
  26. package/dist/tools/interact.js +21 -7
  27. package/dist/tools/interact.js.map +1 -1
  28. package/dist/tools/responsive.d.ts +4 -0
  29. package/dist/tools/responsive.js +20 -3
  30. package/dist/tools/responsive.js.map +1 -1
  31. package/dist/tools/save-auth.d.ts +263 -0
  32. package/dist/tools/save-auth.js +253 -0
  33. package/dist/tools/save-auth.js.map +1 -0
  34. package/dist/tools/screenshot.d.ts +4 -0
  35. package/dist/tools/screenshot.js +4 -1
  36. package/dist/tools/screenshot.js.map +1 -1
  37. package/dist/utils/storage-state.d.ts +35 -0
  38. package/dist/utils/storage-state.js +84 -0
  39. package/dist/utils/storage-state.js.map +1 -0
  40. package/package.json +1 -1
@@ -9,10 +9,31 @@ import { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DEL
9
9
  * a single actionable line naming the step that failed — a recording is a bad
10
10
  * place to surface a 30-line call log.
11
11
  */
12
- /** Actions `framewatch_capture` can replay during a recording. */
13
- export const CAPTURE_ACTIONS = ["click", "tap", "type", "scroll", "swipe", "wait", "navigate"];
14
- /** Actions `framewatch_interact` can perform as a one-off. */
15
- export const INTERACT_ACTIONS = ["click", "tap", "type", "scroll", "swipe", "navigate", "select", "hover"];
12
+ /** Actions a replayable script (`framewatch_capture`, `framewatch_save_auth`) can perform. */
13
+ export const CAPTURE_ACTIONS = [
14
+ "click",
15
+ "tap",
16
+ "type",
17
+ "key",
18
+ "scroll",
19
+ "swipe",
20
+ "hover",
21
+ "select",
22
+ "wait",
23
+ "navigate",
24
+ ];
25
+ /** Actions `framewatch_interact` can perform as a one-off — everything but `wait`, which needs a script to sit in. */
26
+ export const INTERACT_ACTIONS = [
27
+ "click",
28
+ "tap",
29
+ "type",
30
+ "key",
31
+ "scroll",
32
+ "swipe",
33
+ "navigate",
34
+ "select",
35
+ "hover",
36
+ ];
16
37
  /** Steps that need a touch-capable browser context (`hasTouch: true`). */
17
38
  const TOUCH_ACTIONS = new Set(["tap", "swipe"]);
18
39
  /** True when any step needs a touch-enabled browser context. */
@@ -41,7 +62,13 @@ export function validateInteraction(interaction) {
41
62
  ? `${action} needs both \`x\` and \`y\` (or a \`selector\`)`
42
63
  : `${action} needs a \`selector\`, or \`x\` and \`y\``;
43
64
  case "type":
44
- return typeof value === "string" ? null : "type needs a `value` (the text to type)";
65
+ return typeof value === "string"
66
+ ? null
67
+ : "type needs a `value` (the text to type; `\n` presses Enter and `\t` presses Tab)";
68
+ case "key":
69
+ return typeof value === "string" && value.length > 0
70
+ ? null
71
+ : "key needs a `value` (the key to press, e.g. `Enter`, `Escape`, `ArrowDown` or `Control+a`)";
45
72
  case "select":
46
73
  if (!selector)
47
74
  return "select needs a `selector` naming the <select> element";
@@ -83,6 +110,8 @@ export function describeInteraction(interaction) {
83
110
  return `type "${elide(value)}"${selector ? ` into "${selector}"` : ""}`;
84
111
  case "select":
85
112
  return `select "${elide(value)}"${selector ? ` in "${selector}"` : ""}`;
113
+ case "key":
114
+ return `press "${elide(value)}"${selector ? ` in "${selector}"` : ""}`;
86
115
  case "scroll":
87
116
  return `scroll${selector ? ` "${selector}"` : ""} by ${delta_x},${delta_y}`;
88
117
  case "swipe":
@@ -145,12 +174,14 @@ async function perform(page, interaction, timeout) {
145
174
  await page.touchscreen.tap(x, y);
146
175
  return;
147
176
  case "type":
148
- // `fill` clears the field first and dispatches a single input event, so
149
- // replaying a script twice does not append to what is already there.
177
+ await typeValue(page, selector, value, timeout);
178
+ return;
179
+ case "key":
180
+ // A key goes wherever focus is; `selector` is the way to say where that
181
+ // should be without spending a separate click step on it.
150
182
  if (selector)
151
- await page.locator(selector).first().fill(value, { timeout });
152
- else
153
- await page.keyboard.type(value);
183
+ await page.locator(selector).first().focus({ timeout });
184
+ await page.keyboard.press(value);
154
185
  return;
155
186
  case "select":
156
187
  await page.locator(selector).first().selectOption(value, { timeout });
@@ -175,6 +206,45 @@ async function perform(page, interaction, timeout) {
175
206
  return;
176
207
  }
177
208
  }
209
+ /** Characters a `type` value may carry inline to mean a key press. */
210
+ const TYPE_KEYS = { "\n": "Enter", "\t": "Tab" };
211
+ /**
212
+ * Type `value`, pressing Enter for each `\n` in it and Tab for each `\t`.
213
+ *
214
+ * Typing a newline is what ends most forms, and splitting "duo\n" into a type
215
+ * step and a key step is a step the caller should not have to think about.
216
+ * The first text run still goes in with `fill` — it clears the field, so
217
+ * replaying a script twice does not append to what is already there — and
218
+ * everything after a key press is typed on top of wherever focus ended up,
219
+ * which is what a person continuing to type would produce.
220
+ */
221
+ async function typeValue(page, selector, value, timeout) {
222
+ // Keep the separators: split(/([\n\t])/) yields text, key, text, key, …
223
+ const segments = value.split(/([\n\t])/).filter((segment) => segment !== "");
224
+ // An empty value still means "empty the field", which is `fill("")`.
225
+ if (segments.length === 0)
226
+ segments.push("");
227
+ let focused = false;
228
+ for (const segment of segments) {
229
+ const key = TYPE_KEYS[segment];
230
+ if (key === undefined) {
231
+ // The first run replaces the field's contents; later runs are typed at
232
+ // the caret, since a key press may well have moved focus elsewhere.
233
+ if (selector && !focused)
234
+ await page.locator(selector).first().fill(segment, { timeout });
235
+ else
236
+ await page.keyboard.type(segment);
237
+ focused = true;
238
+ continue;
239
+ }
240
+ // A value that opens with a key ("\n" alone, meaning "press Enter in this
241
+ // field") still has to land on the element the caller named.
242
+ if (selector && !focused)
243
+ await page.locator(selector).first().focus({ timeout });
244
+ focused = true;
245
+ await page.keyboard.press(key);
246
+ }
247
+ }
178
248
  /**
179
249
  * A real finger drag: touchStart, several touchMoves, touchEnd.
180
250
  *
@@ -233,8 +303,15 @@ function sleep(ms) {
233
303
  * adds `delay_ms` where `framewatch_interact` adds `wait_ms`).
234
304
  */
235
305
  export const interactionFieldShape = {
236
- selector: z.string().optional().describe("CSS selector for click/tap/type/select/hover targets, or the scroll container"),
237
- value: z.string().optional().describe("Text to type, option value to select, or URL to navigate to"),
306
+ selector: z
307
+ .string()
308
+ .optional()
309
+ .describe("CSS selector for click/tap/type/select/hover targets, the field to focus first for `key`, or the scroll container"),
310
+ value: z
311
+ .string()
312
+ .optional()
313
+ .describe("Text to type (`\n` in it presses Enter, `\t` presses Tab), key to press for `key` " +
314
+ "(e.g. `Enter`, `Escape`, `ArrowDown`, `Control+a`), option value to select, or URL to navigate to"),
238
315
  x: z.number().optional().describe("X coordinate for click/tap/swipe (used when no selector is given)"),
239
316
  y: z.number().optional().describe("Y coordinate for click/tap/swipe (used when no selector is given)"),
240
317
  delta_x: z.number().optional().describe("Horizontal distance for scroll/swipe"),
@@ -1 +1 @@
1
- {"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/engine/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DELAY_MS } from \"../constants.js\";\n\n/**\n * Interaction executor.\n *\n * Turns one declarative interaction step (see CLAUDE.md \"Interactions as JSON\n * script\") into real input on a Playwright page. Every step is validated\n * before anything touches the page, and every Playwright failure is reduced to\n * a single actionable line naming the step that failed — a recording is a bad\n * place to surface a 30-line call log.\n */\n\n/** Actions `framewatch_capture` can replay during a recording. */\nexport const CAPTURE_ACTIONS = [\"click\", \"tap\", \"type\", \"scroll\", \"swipe\", \"wait\", \"navigate\"] as const;\n\n/** Actions `framewatch_interact` can perform as a one-off. */\nexport const INTERACT_ACTIONS = [\"click\", \"tap\", \"type\", \"scroll\", \"swipe\", \"navigate\", \"select\", \"hover\"] as const;\n\nexport type InteractionAction = (typeof CAPTURE_ACTIONS)[number] | (typeof INTERACT_ACTIONS)[number];\n\nexport interface Interaction {\n action: InteractionAction;\n /** CSS selector for click/tap/type/select/hover, or the scroll container. */\n selector?: string;\n /** Text to type, option value to select, or URL to navigate to. */\n value?: string;\n x?: number;\n y?: number;\n delta_x?: number;\n delta_y?: number;\n /** Wait this long *before* performing the action. */\n delay_ms?: number;\n}\n\nexport interface ExecuteOptions {\n /** Timeout for selector-based actions. Default SELECTOR_TIMEOUT_MS. */\n timeout_ms?: number;\n}\n\n/** Steps that need a touch-capable browser context (`hasTouch: true`). */\nconst TOUCH_ACTIONS = new Set<InteractionAction>([\"tap\", \"swipe\"]);\n\n/** True when any step needs a touch-enabled browser context. */\nexport function needsTouch(interactions: readonly Interaction[]): boolean {\n return interactions.some((i) => TOUCH_ACTIONS.has(i.action));\n}\n\n/**\n * Check that a step carries the fields its action needs. Returns a message\n * naming what is missing, or null when the step is executable. Used both by\n * the tools (at input-validation time, before a browser is even launched) and\n * by `executeInteraction` itself.\n */\nexport function validateInteraction(interaction: Interaction): string | null {\n const { action, selector, value, x, y, delta_x, delta_y } = interaction;\n const hasPoint = typeof x === \"number\" && typeof y === \"number\";\n const hasPartialPoint = typeof x === \"number\" || typeof y === \"number\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n if (selector) return null;\n if (hasPoint) return null;\n return hasPartialPoint\n ? `${action} needs both \\`x\\` and \\`y\\` (or a \\`selector\\`)`\n : `${action} needs a \\`selector\\`, or \\`x\\` and \\`y\\``;\n\n case \"type\":\n return typeof value === \"string\" ? null : \"type needs a `value` (the text to type)\";\n\n case \"select\":\n if (!selector) return \"select needs a `selector` naming the <select> element\";\n return typeof value === \"string\" ? null : \"select needs a `value` (the option to choose)\";\n\n case \"scroll\":\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"scroll needs `delta_y` (and/or `delta_x`) — the distance to scroll\";\n\n case \"swipe\":\n if (!hasPoint) return \"swipe needs `x` and `y` — where the finger goes down\";\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"swipe needs `delta_x` and/or `delta_y` — how far the finger travels\";\n\n case \"navigate\":\n return typeof value === \"string\" && value.length > 0 ? null : \"navigate needs a `value` (the URL to go to)\";\n\n case \"wait\":\n return null;\n\n default:\n return `unknown action \"${String(action)}\"`;\n }\n}\n\n/** Longest `value` printed in a step description before it is elided. */\nconst DESCRIBE_VALUE_MAX = 60;\n\n/**\n * One-line, human-readable summary of a step, used in error messages and in\n * the capture summary. Long values (a pasted token, a password) are elided\n * rather than echoed in full.\n */\nexport function describeInteraction(interaction: Interaction): string {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0, delay_ms } = interaction;\n const target = selector ? ` \"${selector}\"` : typeof x === \"number\" && typeof y === \"number\" ? ` at ${x},${y}` : \"\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n return `${action}${target}`;\n case \"type\":\n return `type \"${elide(value)}\"${selector ? ` into \"${selector}\"` : \"\"}`;\n case \"select\":\n return `select \"${elide(value)}\"${selector ? ` in \"${selector}\"` : \"\"}`;\n case \"scroll\":\n return `scroll${selector ? ` \"${selector}\"` : \"\"} by ${delta_x},${delta_y}`;\n case \"swipe\":\n return `swipe${target} by ${delta_x},${delta_y}`;\n case \"navigate\":\n return `navigate to ${elide(value)}`;\n case \"wait\":\n return `wait ${delay_ms ?? 0}ms`;\n default:\n return String(action);\n }\n}\n\nfunction elide(value: string | undefined): string {\n if (value === undefined) return \"\";\n return value.length > DESCRIBE_VALUE_MAX ? `${value.slice(0, DESCRIBE_VALUE_MAX)}…` : value;\n}\n\n/**\n * Perform one interaction on `page`.\n *\n * Waits `delay_ms` first (that is what `delay_ms` means: \"wait before this\n * action\"), so a `wait` step is simply a step with no action of its own.\n * Throws a single-line Error naming the step on any failure; the page is never\n * touched when the step is invalid.\n */\nexport async function executeInteraction(page: Page, interaction: Interaction, options: ExecuteOptions = {}): Promise<void> {\n const problem = validateInteraction(interaction);\n if (problem) throw new Error(problem);\n\n const delay = interaction.delay_ms;\n if (typeof delay === \"number\" && delay > 0) await sleep(delay);\n\n const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;\n try {\n await perform(page, interaction, timeout);\n } catch (error) {\n throw new Error(describeFailure(interaction, error), { cause: error });\n }\n}\n\nasync function perform(page: Page, interaction: Interaction, timeout: number): Promise<void> {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0 } = interaction;\n\n switch (action) {\n case \"click\":\n if (selector) await page.locator(selector).first().click({ timeout });\n else await page.mouse.click(x!, y!);\n return;\n\n case \"hover\":\n if (selector) await page.locator(selector).first().hover({ timeout });\n else await page.mouse.move(x!, y!);\n return;\n\n case \"tap\":\n if (selector) await page.locator(selector).first().tap({ timeout });\n else await page.touchscreen.tap(x!, y!);\n return;\n\n case \"type\":\n // `fill` clears the field first and dispatches a single input event, so\n // replaying a script twice does not append to what is already there.\n if (selector) await page.locator(selector).first().fill(value!, { timeout });\n else await page.keyboard.type(value!);\n return;\n\n case \"select\":\n await page.locator(selector!).first().selectOption(value!, { timeout });\n return;\n\n case \"scroll\":\n // With a selector, put the pointer over that element first so the wheel\n // scrolls it rather than the page behind it.\n if (selector) await page.locator(selector).first().hover({ timeout });\n await page.mouse.wheel(delta_x, delta_y);\n return;\n\n case \"swipe\":\n await swipe(page, x!, y!, delta_x, delta_y);\n return;\n\n case \"navigate\":\n // Playwright only resolves relative URLs against a context `baseURL`;\n // resolve against the current page instead, which is what a script\n // author means by \"navigate to /login\".\n await page.goto(resolveUrl(value!, page.url()), { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n return;\n\n case \"wait\":\n return;\n }\n}\n\n/**\n * A real finger drag: touchStart, several touchMoves, touchEnd.\n *\n * Playwright's Touchscreen can only tap, so the drag goes through CDP (we are\n * Chromium-only). Intermediate moves matter — a carousel or pull-to-refresh\n * needs the travel and its velocity, not a teleport — and each one is spaced\n * about a frame apart so the page's own velocity maths sees a plausible gesture.\n */\nasync function swipe(page: Page, x: number, y: number, deltaX: number, deltaY: number): Promise<void> {\n const cdp = await page.context().newCDPSession(page);\n try {\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchStart\", touchPoints: [{ x, y }] });\n for (let step = 1; step <= SWIPE_STEPS; step++) {\n await sleep(SWIPE_STEP_DELAY_MS);\n await cdp.send(\"Input.dispatchTouchEvent\", {\n // Interpolate from the origin (not from the previous point) so the last\n // move lands exactly on x + deltaX, y + deltaY.\n type: \"touchMove\",\n touchPoints: [{ x: x + (deltaX * step) / SWIPE_STEPS, y: y + (deltaY * step) / SWIPE_STEPS }],\n });\n }\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchEnd\", touchPoints: [] });\n } finally {\n await cdp.detach().catch(() => {});\n }\n}\n\nfunction resolveUrl(value: string, base: string): string {\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Reduce a Playwright error to one actionable line naming the step. Touch\n * failures get their own wording: the fix is a context option the caller\n * controls, not anything about the page.\n */\nfunction describeFailure(interaction: Interaction, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const step = describeInteraction(interaction);\n\n if (/hasTouch|does not support tap/i.test(message)) {\n return `${step} failed: this page is not touch-enabled. Touch is turned on automatically for tap/swipe steps — if you see this, the page was opened without it.`;\n }\n return `${step} failed: ${firstLine}`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The shared zod fields every interaction step accepts. Both tools build their\n * own object around these (the action enums differ, and `framewatch_capture`\n * adds `delay_ms` where `framewatch_interact` adds `wait_ms`).\n */\nexport const interactionFieldShape = {\n selector: z.string().optional().describe(\"CSS selector for click/tap/type/select/hover targets, or the scroll container\"),\n value: z.string().optional().describe(\"Text to type, option value to select, or URL to navigate to\"),\n x: z.number().optional().describe(\"X coordinate for click/tap/swipe (used when no selector is given)\"),\n y: z.number().optional().describe(\"Y coordinate for click/tap/swipe (used when no selector is given)\"),\n delta_x: z.number().optional().describe(\"Horizontal distance for scroll/swipe\"),\n delta_y: z.number().optional().describe(\"Vertical distance for scroll/swipe\"),\n};\n\n/**\n * zod `superRefine` hook that rejects a step missing the fields its action\n * needs, so an unusable script is reported as invalid input instead of failing\n * half way through a recording.\n */\nexport function refineInteraction(value: Interaction, ctx: z.RefinementCtx): void {\n const problem = validateInteraction(value);\n if (problem !== null) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem, path: [\"action\"] });\n }\n}\n"]}
1
+ {"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/engine/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DELAY_MS } from \"../constants.js\";\n\n/**\n * Interaction executor.\n *\n * Turns one declarative interaction step (see CLAUDE.md \"Interactions as JSON\n * script\") into real input on a Playwright page. Every step is validated\n * before anything touches the page, and every Playwright failure is reduced to\n * a single actionable line naming the step that failed — a recording is a bad\n * place to surface a 30-line call log.\n */\n\n/** Actions a replayable script (`framewatch_capture`, `framewatch_save_auth`) can perform. */\nexport const CAPTURE_ACTIONS = [\n \"click\",\n \"tap\",\n \"type\",\n \"key\",\n \"scroll\",\n \"swipe\",\n \"hover\",\n \"select\",\n \"wait\",\n \"navigate\",\n] as const;\n\n/** Actions `framewatch_interact` can perform as a one-off — everything but `wait`, which needs a script to sit in. */\nexport const INTERACT_ACTIONS = [\n \"click\",\n \"tap\",\n \"type\",\n \"key\",\n \"scroll\",\n \"swipe\",\n \"navigate\",\n \"select\",\n \"hover\",\n] as const;\n\nexport type InteractionAction = (typeof CAPTURE_ACTIONS)[number] | (typeof INTERACT_ACTIONS)[number];\n\nexport interface Interaction {\n action: InteractionAction;\n /** CSS selector for click/tap/type/select/hover, the field to focus for `key`, or the scroll container. */\n selector?: string;\n /** Text to type, key to press, option value to select, or URL to navigate to. */\n value?: string;\n x?: number;\n y?: number;\n delta_x?: number;\n delta_y?: number;\n /** Wait this long *before* performing the action. */\n delay_ms?: number;\n}\n\nexport interface ExecuteOptions {\n /** Timeout for selector-based actions. Default SELECTOR_TIMEOUT_MS. */\n timeout_ms?: number;\n}\n\n/** Steps that need a touch-capable browser context (`hasTouch: true`). */\nconst TOUCH_ACTIONS = new Set<InteractionAction>([\"tap\", \"swipe\"]);\n\n/** True when any step needs a touch-enabled browser context. */\nexport function needsTouch(interactions: readonly Interaction[]): boolean {\n return interactions.some((i) => TOUCH_ACTIONS.has(i.action));\n}\n\n/**\n * Check that a step carries the fields its action needs. Returns a message\n * naming what is missing, or null when the step is executable. Used both by\n * the tools (at input-validation time, before a browser is even launched) and\n * by `executeInteraction` itself.\n */\nexport function validateInteraction(interaction: Interaction): string | null {\n const { action, selector, value, x, y, delta_x, delta_y } = interaction;\n const hasPoint = typeof x === \"number\" && typeof y === \"number\";\n const hasPartialPoint = typeof x === \"number\" || typeof y === \"number\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n if (selector) return null;\n if (hasPoint) return null;\n return hasPartialPoint\n ? `${action} needs both \\`x\\` and \\`y\\` (or a \\`selector\\`)`\n : `${action} needs a \\`selector\\`, or \\`x\\` and \\`y\\``;\n\n case \"type\":\n return typeof value === \"string\"\n ? null\n : \"type needs a `value` (the text to type; `\\n` presses Enter and `\\t` presses Tab)\";\n\n case \"key\":\n return typeof value === \"string\" && value.length > 0\n ? null\n : \"key needs a `value` (the key to press, e.g. `Enter`, `Escape`, `ArrowDown` or `Control+a`)\";\n\n case \"select\":\n if (!selector) return \"select needs a `selector` naming the <select> element\";\n return typeof value === \"string\" ? null : \"select needs a `value` (the option to choose)\";\n\n case \"scroll\":\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"scroll needs `delta_y` (and/or `delta_x`) — the distance to scroll\";\n\n case \"swipe\":\n if (!hasPoint) return \"swipe needs `x` and `y` — where the finger goes down\";\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"swipe needs `delta_x` and/or `delta_y` — how far the finger travels\";\n\n case \"navigate\":\n return typeof value === \"string\" && value.length > 0 ? null : \"navigate needs a `value` (the URL to go to)\";\n\n case \"wait\":\n return null;\n\n default:\n return `unknown action \"${String(action)}\"`;\n }\n}\n\n/** Longest `value` printed in a step description before it is elided. */\nconst DESCRIBE_VALUE_MAX = 60;\n\n/**\n * One-line, human-readable summary of a step, used in error messages and in\n * the capture summary. Long values (a pasted token, a password) are elided\n * rather than echoed in full.\n */\nexport function describeInteraction(interaction: Interaction): string {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0, delay_ms } = interaction;\n const target = selector ? ` \"${selector}\"` : typeof x === \"number\" && typeof y === \"number\" ? ` at ${x},${y}` : \"\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n return `${action}${target}`;\n case \"type\":\n return `type \"${elide(value)}\"${selector ? ` into \"${selector}\"` : \"\"}`;\n case \"select\":\n return `select \"${elide(value)}\"${selector ? ` in \"${selector}\"` : \"\"}`;\n case \"key\":\n return `press \"${elide(value)}\"${selector ? ` in \"${selector}\"` : \"\"}`;\n case \"scroll\":\n return `scroll${selector ? ` \"${selector}\"` : \"\"} by ${delta_x},${delta_y}`;\n case \"swipe\":\n return `swipe${target} by ${delta_x},${delta_y}`;\n case \"navigate\":\n return `navigate to ${elide(value)}`;\n case \"wait\":\n return `wait ${delay_ms ?? 0}ms`;\n default:\n return String(action);\n }\n}\n\nfunction elide(value: string | undefined): string {\n if (value === undefined) return \"\";\n return value.length > DESCRIBE_VALUE_MAX ? `${value.slice(0, DESCRIBE_VALUE_MAX)}…` : value;\n}\n\n/**\n * Perform one interaction on `page`.\n *\n * Waits `delay_ms` first (that is what `delay_ms` means: \"wait before this\n * action\"), so a `wait` step is simply a step with no action of its own.\n * Throws a single-line Error naming the step on any failure; the page is never\n * touched when the step is invalid.\n */\nexport async function executeInteraction(page: Page, interaction: Interaction, options: ExecuteOptions = {}): Promise<void> {\n const problem = validateInteraction(interaction);\n if (problem) throw new Error(problem);\n\n const delay = interaction.delay_ms;\n if (typeof delay === \"number\" && delay > 0) await sleep(delay);\n\n const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;\n try {\n await perform(page, interaction, timeout);\n } catch (error) {\n throw new Error(describeFailure(interaction, error), { cause: error });\n }\n}\n\nasync function perform(page: Page, interaction: Interaction, timeout: number): Promise<void> {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0 } = interaction;\n\n switch (action) {\n case \"click\":\n if (selector) await page.locator(selector).first().click({ timeout });\n else await page.mouse.click(x!, y!);\n return;\n\n case \"hover\":\n if (selector) await page.locator(selector).first().hover({ timeout });\n else await page.mouse.move(x!, y!);\n return;\n\n case \"tap\":\n if (selector) await page.locator(selector).first().tap({ timeout });\n else await page.touchscreen.tap(x!, y!);\n return;\n\n case \"type\":\n await typeValue(page, selector, value!, timeout);\n return;\n\n case \"key\":\n // A key goes wherever focus is; `selector` is the way to say where that\n // should be without spending a separate click step on it.\n if (selector) await page.locator(selector).first().focus({ timeout });\n await page.keyboard.press(value!);\n return;\n\n case \"select\":\n await page.locator(selector!).first().selectOption(value!, { timeout });\n return;\n\n case \"scroll\":\n // With a selector, put the pointer over that element first so the wheel\n // scrolls it rather than the page behind it.\n if (selector) await page.locator(selector).first().hover({ timeout });\n await page.mouse.wheel(delta_x, delta_y);\n return;\n\n case \"swipe\":\n await swipe(page, x!, y!, delta_x, delta_y);\n return;\n\n case \"navigate\":\n // Playwright only resolves relative URLs against a context `baseURL`;\n // resolve against the current page instead, which is what a script\n // author means by \"navigate to /login\".\n await page.goto(resolveUrl(value!, page.url()), { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n return;\n\n case \"wait\":\n return;\n }\n}\n\n/** Characters a `type` value may carry inline to mean a key press. */\nconst TYPE_KEYS: Record<string, string> = { \"\\n\": \"Enter\", \"\\t\": \"Tab\" };\n\n/**\n * Type `value`, pressing Enter for each `\\n` in it and Tab for each `\\t`.\n *\n * Typing a newline is what ends most forms, and splitting \"duo\\n\" into a type\n * step and a key step is a step the caller should not have to think about.\n * The first text run still goes in with `fill` — it clears the field, so\n * replaying a script twice does not append to what is already there — and\n * everything after a key press is typed on top of wherever focus ended up,\n * which is what a person continuing to type would produce.\n */\nasync function typeValue(page: Page, selector: string | undefined, value: string, timeout: number): Promise<void> {\n // Keep the separators: split(/([\\n\\t])/) yields text, key, text, key, …\n const segments = value.split(/([\\n\\t])/).filter((segment) => segment !== \"\");\n // An empty value still means \"empty the field\", which is `fill(\"\")`.\n if (segments.length === 0) segments.push(\"\");\n\n let focused = false;\n for (const segment of segments) {\n const key = TYPE_KEYS[segment];\n if (key === undefined) {\n // The first run replaces the field's contents; later runs are typed at\n // the caret, since a key press may well have moved focus elsewhere.\n if (selector && !focused) await page.locator(selector).first().fill(segment, { timeout });\n else await page.keyboard.type(segment);\n focused = true;\n continue;\n }\n // A value that opens with a key (\"\\n\" alone, meaning \"press Enter in this\n // field\") still has to land on the element the caller named.\n if (selector && !focused) await page.locator(selector).first().focus({ timeout });\n focused = true;\n await page.keyboard.press(key);\n }\n}\n\n/**\n * A real finger drag: touchStart, several touchMoves, touchEnd.\n *\n * Playwright's Touchscreen can only tap, so the drag goes through CDP (we are\n * Chromium-only). Intermediate moves matter — a carousel or pull-to-refresh\n * needs the travel and its velocity, not a teleport — and each one is spaced\n * about a frame apart so the page's own velocity maths sees a plausible gesture.\n */\nasync function swipe(page: Page, x: number, y: number, deltaX: number, deltaY: number): Promise<void> {\n const cdp = await page.context().newCDPSession(page);\n try {\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchStart\", touchPoints: [{ x, y }] });\n for (let step = 1; step <= SWIPE_STEPS; step++) {\n await sleep(SWIPE_STEP_DELAY_MS);\n await cdp.send(\"Input.dispatchTouchEvent\", {\n // Interpolate from the origin (not from the previous point) so the last\n // move lands exactly on x + deltaX, y + deltaY.\n type: \"touchMove\",\n touchPoints: [{ x: x + (deltaX * step) / SWIPE_STEPS, y: y + (deltaY * step) / SWIPE_STEPS }],\n });\n }\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchEnd\", touchPoints: [] });\n } finally {\n await cdp.detach().catch(() => {});\n }\n}\n\nfunction resolveUrl(value: string, base: string): string {\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Reduce a Playwright error to one actionable line naming the step. Touch\n * failures get their own wording: the fix is a context option the caller\n * controls, not anything about the page.\n */\nfunction describeFailure(interaction: Interaction, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const step = describeInteraction(interaction);\n\n if (/hasTouch|does not support tap/i.test(message)) {\n return `${step} failed: this page is not touch-enabled. Touch is turned on automatically for tap/swipe steps — if you see this, the page was opened without it.`;\n }\n return `${step} failed: ${firstLine}`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The shared zod fields every interaction step accepts. Both tools build their\n * own object around these (the action enums differ, and `framewatch_capture`\n * adds `delay_ms` where `framewatch_interact` adds `wait_ms`).\n */\nexport const interactionFieldShape = {\n selector: z\n .string()\n .optional()\n .describe(\n \"CSS selector for click/tap/type/select/hover targets, the field to focus first for `key`, or the scroll container\",\n ),\n value: z\n .string()\n .optional()\n .describe(\n \"Text to type (`\\n` in it presses Enter, `\\t` presses Tab), key to press for `key` \" +\n \"(e.g. `Enter`, `Escape`, `ArrowDown`, `Control+a`), option value to select, or URL to navigate to\",\n ),\n x: z.number().optional().describe(\"X coordinate for click/tap/swipe (used when no selector is given)\"),\n y: z.number().optional().describe(\"Y coordinate for click/tap/swipe (used when no selector is given)\"),\n delta_x: z.number().optional().describe(\"Horizontal distance for scroll/swipe\"),\n delta_y: z.number().optional().describe(\"Vertical distance for scroll/swipe\"),\n};\n\n/**\n * zod `superRefine` hook that rejects a step missing the fields its action\n * needs, so an unusable script is reported as invalid input instead of failing\n * half way through a recording.\n */\nexport function refineInteraction(value: Interaction, ctx: z.RefinementCtx): void {\n const problem = validateInteraction(value);\n if (problem !== null) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem, path: [\"action\"] });\n }\n}\n"]}
package/dist/index.js CHANGED
@@ -31,7 +31,10 @@ export function createServer() {
31
31
  "Use framewatch_responsive to see one page at mobile, tablet and desktop widths at once (it also " +
32
32
  "reports content that overflows its viewport), framewatch_accessibility to run an axe-core WCAG audit, " +
33
33
  "and framewatch_compare to diff two URLs — or the page interact has open against a URL — with an " +
34
- "overlay of every pixel that differs. If the app is not running yet, framewatch_start_server runs its " +
34
+ "overlay of every pixel that differs. For an app behind a login, run the flow once with " +
35
+ "framewatch_save_auth and pass the file it writes as `storage_state` to any of these tools — they then " +
36
+ "open the page already signed in instead of replaying the login every time. " +
37
+ "If the app is not running yet, framewatch_start_server runs its " +
35
38
  "dev server (e.g. `npm run dev`) and waits for the port; framewatch_stop_server stops it again.",
36
39
  });
37
40
  registerAllTools(server);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { closeBrowser } from \"./engine/browser.js\";\nimport { registerAllTools } from \"./tools/index.js\";\nimport { shutdownDevServer } from \"./utils/server-process.js\";\n\nexport const SERVER_NAME = \"framewatch\";\nexport const SERVER_VERSION: string = readPackageVersion();\n\n/**\n * Build a fully configured FrameWatch McpServer (not yet connected to a\n * transport). Exported so tests and embedders can wire their own transport;\n * importing this module has no side effects — the stdio server only starts\n * when this file is executed directly (`node dist/index.js` / the npm bin).\n */\nexport function createServer(): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: SERVER_VERSION },\n {\n instructions:\n \"FrameWatch gives you visual eyes on running web apps. Use framewatch_screenshot to see the \" +\n \"current state of a page. Pass a `selector` to zoom in on one element, or `wait_for` to wait \" +\n \"for an element before capturing. Use framewatch_capture to record a page for a few seconds \" +\n \"and get back only the frames where something meaningful changed (animations, splash screens, \" +\n \"transitions, anything that changes over time), each with a crop of the changed region and its \" +\n \"position. Lower `sensitivity` to keep more frames, raise `max_frames` for longer sequences. \" +\n \"Pass `interactions` to replay a click/type/scroll/tap/swipe script while it records — that is how you \" +\n \"test a flow such as a login. Use framewatch_interact for one action at a time: it keeps the page open \" +\n \"between calls, so you can click, look at the result, type, and look again without replaying the flow. \" +\n \"Both capture and interact can attach the context behind a frame — console output and uncaught errors \" +\n \"(on by default), plus network requests, DOM mutations and paint timing via `include_network`, \" +\n \"`include_dom` and `include_performance` — which is how you find out why a frame looks wrong. \" +\n \"Use framewatch_responsive to see one page at mobile, tablet and desktop widths at once (it also \" +\n \"reports content that overflows its viewport), framewatch_accessibility to run an axe-core WCAG audit, \" +\n \"and framewatch_compare to diff two URLs — or the page interact has open against a URL — with an \" +\n \"overlay of every pixel that differs. If the app is not running yet, framewatch_start_server runs its \" +\n \"dev server (e.g. `npm run dev`) and waits for the port; framewatch_stop_server stops it again.\",\n },\n );\n registerAllTools(server);\n return server;\n}\n\n/** Run the server on stdio until the client disconnects or the process is signalled. */\nexport async function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n\n // stderr may be closed by the host (e.g. parent died); never let a log write\n // turn into an uncaught 'error' event that re-enters the handlers below.\n process.stderr.on(\"error\", () => {});\n\n let shuttingDown = false;\n const shutdown = async (reason: string, exitCode = 0): Promise<void> => {\n if (shuttingDown) return;\n shuttingDown = true;\n log(`shutting down (${reason})`);\n try {\n // The dev server is a child process of this one; leaving it behind would\n // hold its port and outlive the client that asked for it.\n await shutdownDevServer();\n await closeBrowser();\n } finally {\n await server.close().catch(() => {});\n process.exit(exitCode);\n }\n };\n\n // Client went away. The SDK transport only fires onclose on protocol errors,\n // so watch stdin EOF directly — once Chromium is running it would otherwise\n // keep the event loop (and a headless browser) alive forever.\n process.stdin.on(\"end\", () => void shutdown(\"stdin closed\"));\n process.stdin.on(\"close\", () => void shutdown(\"stdin closed\"));\n transport.onclose = () => void shutdown(\"transport closed\");\n\n process.on(\"SIGINT\", () => void shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => void shutdown(\"SIGTERM\"));\n process.on(\"SIGHUP\", () => void shutdown(\"SIGHUP\"));\n process.on(\"uncaughtException\", (err) => {\n log(`uncaught exception: ${formatError(err)}`);\n void shutdown(\"uncaught exception\", 1);\n });\n process.on(\"unhandledRejection\", (err) => {\n log(`unhandled rejection: ${formatError(err)}`);\n });\n\n await server.connect(transport);\n log(`${SERVER_NAME} v${SERVER_VERSION} ready on stdio`);\n}\n\n/** stdout is the MCP channel — all diagnostics go to stderr, and never throw. */\nfunction log(message: string): void {\n try {\n process.stderr.write(`[framewatch] ${message}\\n`);\n } catch {\n // stderr is gone; nothing sensible to do.\n }\n}\n\nfunction formatError(err: unknown): string {\n return err instanceof Error ? (err.stack ?? err.message) : String(err);\n}\n\nfunction readPackageVersion(): string {\n try {\n const pkg = JSON.parse(readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n/**\n * True when this file is the process entry point. npm's bin shim is a symlink,\n * so compare real paths: import.meta.url is already resolved, argv[1] is not.\n */\nfunction isRunDirectly(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return import.meta.url === pathToFileURL(realpathSync(entry)).href;\n } catch {\n return false;\n }\n}\n\nif (isRunDirectly()) {\n main().catch((err) => {\n log(`fatal: ${formatError(err)}`);\n process.exit(1);\n });\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { closeBrowser } from \"./engine/browser.js\";\nimport { registerAllTools } from \"./tools/index.js\";\nimport { shutdownDevServer } from \"./utils/server-process.js\";\n\nexport const SERVER_NAME = \"framewatch\";\nexport const SERVER_VERSION: string = readPackageVersion();\n\n/**\n * Build a fully configured FrameWatch McpServer (not yet connected to a\n * transport). Exported so tests and embedders can wire their own transport;\n * importing this module has no side effects — the stdio server only starts\n * when this file is executed directly (`node dist/index.js` / the npm bin).\n */\nexport function createServer(): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: SERVER_VERSION },\n {\n instructions:\n \"FrameWatch gives you visual eyes on running web apps. Use framewatch_screenshot to see the \" +\n \"current state of a page. Pass a `selector` to zoom in on one element, or `wait_for` to wait \" +\n \"for an element before capturing. Use framewatch_capture to record a page for a few seconds \" +\n \"and get back only the frames where something meaningful changed (animations, splash screens, \" +\n \"transitions, anything that changes over time), each with a crop of the changed region and its \" +\n \"position. Lower `sensitivity` to keep more frames, raise `max_frames` for longer sequences. \" +\n \"Pass `interactions` to replay a click/type/scroll/tap/swipe script while it records — that is how you \" +\n \"test a flow such as a login. Use framewatch_interact for one action at a time: it keeps the page open \" +\n \"between calls, so you can click, look at the result, type, and look again without replaying the flow. \" +\n \"Both capture and interact can attach the context behind a frame — console output and uncaught errors \" +\n \"(on by default), plus network requests, DOM mutations and paint timing via `include_network`, \" +\n \"`include_dom` and `include_performance` — which is how you find out why a frame looks wrong. \" +\n \"Use framewatch_responsive to see one page at mobile, tablet and desktop widths at once (it also \" +\n \"reports content that overflows its viewport), framewatch_accessibility to run an axe-core WCAG audit, \" +\n \"and framewatch_compare to diff two URLs — or the page interact has open against a URL — with an \" +\n \"overlay of every pixel that differs. For an app behind a login, run the flow once with \" +\n \"framewatch_save_auth and pass the file it writes as `storage_state` to any of these tools — they then \" +\n \"open the page already signed in instead of replaying the login every time. \" +\n \"If the app is not running yet, framewatch_start_server runs its \" +\n \"dev server (e.g. `npm run dev`) and waits for the port; framewatch_stop_server stops it again.\",\n },\n );\n registerAllTools(server);\n return server;\n}\n\n/** Run the server on stdio until the client disconnects or the process is signalled. */\nexport async function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n\n // stderr may be closed by the host (e.g. parent died); never let a log write\n // turn into an uncaught 'error' event that re-enters the handlers below.\n process.stderr.on(\"error\", () => {});\n\n let shuttingDown = false;\n const shutdown = async (reason: string, exitCode = 0): Promise<void> => {\n if (shuttingDown) return;\n shuttingDown = true;\n log(`shutting down (${reason})`);\n try {\n // The dev server is a child process of this one; leaving it behind would\n // hold its port and outlive the client that asked for it.\n await shutdownDevServer();\n await closeBrowser();\n } finally {\n await server.close().catch(() => {});\n process.exit(exitCode);\n }\n };\n\n // Client went away. The SDK transport only fires onclose on protocol errors,\n // so watch stdin EOF directly — once Chromium is running it would otherwise\n // keep the event loop (and a headless browser) alive forever.\n process.stdin.on(\"end\", () => void shutdown(\"stdin closed\"));\n process.stdin.on(\"close\", () => void shutdown(\"stdin closed\"));\n transport.onclose = () => void shutdown(\"transport closed\");\n\n process.on(\"SIGINT\", () => void shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => void shutdown(\"SIGTERM\"));\n process.on(\"SIGHUP\", () => void shutdown(\"SIGHUP\"));\n process.on(\"uncaughtException\", (err) => {\n log(`uncaught exception: ${formatError(err)}`);\n void shutdown(\"uncaught exception\", 1);\n });\n process.on(\"unhandledRejection\", (err) => {\n log(`unhandled rejection: ${formatError(err)}`);\n });\n\n await server.connect(transport);\n log(`${SERVER_NAME} v${SERVER_VERSION} ready on stdio`);\n}\n\n/** stdout is the MCP channel — all diagnostics go to stderr, and never throw. */\nfunction log(message: string): void {\n try {\n process.stderr.write(`[framewatch] ${message}\\n`);\n } catch {\n // stderr is gone; nothing sensible to do.\n }\n}\n\nfunction formatError(err: unknown): string {\n return err instanceof Error ? (err.stack ?? err.message) : String(err);\n}\n\nfunction readPackageVersion(): string {\n try {\n const pkg = JSON.parse(readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\")) as { version?: string };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\n/**\n * True when this file is the process entry point. npm's bin shim is a symlink,\n * so compare real paths: import.meta.url is already resolved, argv[1] is not.\n */\nfunction isRunDirectly(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return import.meta.url === pathToFileURL(realpathSync(entry)).href;\n } catch {\n return false;\n }\n}\n\nif (isRunDirectly()) {\n main().catch((err) => {\n log(`fatal: ${formatError(err)}`);\n process.exit(1);\n });\n}\n"]}
@@ -27,6 +27,7 @@ export declare const accessibilityInputShape: {
27
27
  width?: number | undefined;
28
28
  height?: number | undefined;
29
29
  }>>;
30
+ storage_state: z.ZodOptional<z.ZodString>;
30
31
  };
31
32
  export declare const accessibilityInputSchema: z.ZodObject<{
32
33
  url: z.ZodString;
@@ -46,6 +47,7 @@ export declare const accessibilityInputSchema: z.ZodObject<{
46
47
  width?: number | undefined;
47
48
  height?: number | undefined;
48
49
  }>>;
50
+ storage_state: z.ZodOptional<z.ZodString>;
49
51
  }, "strip", z.ZodTypeAny, {
50
52
  url: string;
51
53
  standard: "wcag2a" | "wcag2aa" | "wcag21aa";
@@ -58,6 +60,7 @@ export declare const accessibilityInputSchema: z.ZodObject<{
58
60
  height: number;
59
61
  } | undefined;
60
62
  wait_for?: string | undefined;
63
+ storage_state?: string | undefined;
61
64
  }, {
62
65
  url: string;
63
66
  viewport?: {
@@ -70,6 +73,7 @@ export declare const accessibilityInputSchema: z.ZodObject<{
70
73
  wait_for_timeout_ms?: number | undefined;
71
74
  max_violations?: number | undefined;
72
75
  max_elements?: number | undefined;
76
+ storage_state?: string | undefined;
73
77
  }>;
74
78
  export type AccessibilityInput = z.input<typeof accessibilityInputSchema>;
75
79
  /** The trimmed shape the in-page script sends back — never raw axe results. */
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { z } from "zod";
4
4
  import { A11Y_FRAME_WAIT_MS, A11Y_RUN_TIMEOUT_MS, DEFAULT_A11Y_WAIT_MS, DEFAULT_VIEWPORT, MAX_A11Y_HTML_LENGTH, MAX_A11Y_NODES_PER_VIOLATION, MAX_A11Y_VIOLATIONS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, } from "../constants.js";
5
5
  import { withPage } from "../engine/browser.js";
6
+ import { readStorageState, storageStateField } from "../utils/storage-state.js";
6
7
  export const ACCESSIBILITY_TOOL_NAME = "framewatch_accessibility";
7
8
  /** WCAG conformance levels, and the axe-core tags each one runs. */
8
9
  const STANDARDS = {
@@ -52,6 +53,7 @@ export const accessibilityInputShape = {
52
53
  })
53
54
  .optional()
54
55
  .describe("Viewport size (defaults to 1280x720). Some rules — reflow, target size — depend on it."),
56
+ storage_state: storageStateField,
55
57
  };
56
58
  export const accessibilityInputSchema = z.object(accessibilityInputShape);
57
59
  /**
@@ -88,7 +90,11 @@ export async function auditAccessibility(rawInput) {
88
90
  // inline script and eval. The page's own CSP has no bearing on what axe
89
91
  // measures, and without this a strictly configured app could not be
90
92
  // audited at all.
91
- const report = await withPage({ viewport, contextOptions: { bypassCSP: true } }, async (page) => {
93
+ const contextOptions = {
94
+ bypassCSP: true,
95
+ ...(input.storage_state ? { storageState: await readStorageState(input.storage_state) } : {}),
96
+ };
97
+ const report = await withPage({ viewport, contextOptions }, async (page) => {
92
98
  await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
93
99
  if (input.wait_for) {
94
100
  await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
@@ -1 +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"]}
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\";\nimport { readStorageState, storageStateField } from \"../utils/storage-state.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 storage_state: storageStateField,\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 contextOptions = {\n bypassCSP: true,\n ...(input.storage_state ? { storageState: await readStorageState(input.storage_state) } : {}),\n };\n const report = await withPage({ viewport, contextOptions }, 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"]}