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.
- package/README.md +98 -7
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +22 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/interaction.d.ts +6 -6
- package/dist/engine/interaction.js +89 -12
- package/dist/engine/interaction.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +7 -1
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/capture.d.ts +21 -17
- package/dist/tools/capture.js +7 -1
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +14 -4
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interact.d.ts +12 -6
- package/dist/tools/interact.js +21 -7
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/responsive.d.ts +4 -0
- package/dist/tools/responsive.js +20 -3
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +4 -1
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/utils/storage-state.d.ts +35 -0
- package/dist/utils/storage-state.js +84 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/package.json +1 -1
package/dist/tools/interact.d.ts
CHANGED
|
@@ -20,13 +20,14 @@ export declare const interactInputShape: {
|
|
|
20
20
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
21
21
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
22
22
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
23
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
23
24
|
selector: z.ZodOptional<z.ZodString>;
|
|
24
25
|
value: z.ZodOptional<z.ZodString>;
|
|
25
26
|
x: z.ZodOptional<z.ZodNumber>;
|
|
26
27
|
y: z.ZodOptional<z.ZodNumber>;
|
|
27
28
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
28
29
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
29
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "navigate", "select", "hover"]>;
|
|
30
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "navigate", "select", "hover"]>;
|
|
30
31
|
};
|
|
31
32
|
export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
32
33
|
url: z.ZodOptional<z.ZodString>;
|
|
@@ -46,16 +47,17 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
46
47
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
47
48
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
48
49
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
50
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
49
51
|
selector: z.ZodOptional<z.ZodString>;
|
|
50
52
|
value: z.ZodOptional<z.ZodString>;
|
|
51
53
|
x: z.ZodOptional<z.ZodNumber>;
|
|
52
54
|
y: z.ZodOptional<z.ZodNumber>;
|
|
53
55
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
54
56
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
55
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "navigate", "select", "hover"]>;
|
|
57
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "navigate", "select", "hover"]>;
|
|
56
58
|
}, "strip", z.ZodTypeAny, {
|
|
57
59
|
wait_ms: number;
|
|
58
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "
|
|
60
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "navigate";
|
|
59
61
|
include_console: boolean;
|
|
60
62
|
include_network: boolean;
|
|
61
63
|
include_dom: boolean;
|
|
@@ -67,13 +69,14 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
67
69
|
} | undefined;
|
|
68
70
|
value?: string | undefined;
|
|
69
71
|
url?: string | undefined;
|
|
72
|
+
storage_state?: string | undefined;
|
|
70
73
|
x?: number | undefined;
|
|
71
74
|
y?: number | undefined;
|
|
72
75
|
selector?: string | undefined;
|
|
73
76
|
delta_x?: number | undefined;
|
|
74
77
|
delta_y?: number | undefined;
|
|
75
78
|
}, {
|
|
76
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "
|
|
79
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "navigate";
|
|
77
80
|
viewport?: {
|
|
78
81
|
width: number;
|
|
79
82
|
height: number;
|
|
@@ -81,6 +84,7 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
81
84
|
value?: string | undefined;
|
|
82
85
|
url?: string | undefined;
|
|
83
86
|
wait_ms?: number | undefined;
|
|
87
|
+
storage_state?: string | undefined;
|
|
84
88
|
x?: number | undefined;
|
|
85
89
|
y?: number | undefined;
|
|
86
90
|
selector?: string | undefined;
|
|
@@ -93,7 +97,7 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
93
97
|
timeout_ms?: number | undefined;
|
|
94
98
|
}>, {
|
|
95
99
|
wait_ms: number;
|
|
96
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "
|
|
100
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "navigate";
|
|
97
101
|
include_console: boolean;
|
|
98
102
|
include_network: boolean;
|
|
99
103
|
include_dom: boolean;
|
|
@@ -105,13 +109,14 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
105
109
|
} | undefined;
|
|
106
110
|
value?: string | undefined;
|
|
107
111
|
url?: string | undefined;
|
|
112
|
+
storage_state?: string | undefined;
|
|
108
113
|
x?: number | undefined;
|
|
109
114
|
y?: number | undefined;
|
|
110
115
|
selector?: string | undefined;
|
|
111
116
|
delta_x?: number | undefined;
|
|
112
117
|
delta_y?: number | undefined;
|
|
113
118
|
}, {
|
|
114
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "
|
|
119
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "navigate";
|
|
115
120
|
viewport?: {
|
|
116
121
|
width: number;
|
|
117
122
|
height: number;
|
|
@@ -119,6 +124,7 @@ export declare const interactInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
119
124
|
value?: string | undefined;
|
|
120
125
|
url?: string | undefined;
|
|
121
126
|
wait_ms?: number | undefined;
|
|
127
|
+
storage_state?: string | undefined;
|
|
122
128
|
x?: number | undefined;
|
|
123
129
|
y?: number | undefined;
|
|
124
130
|
selector?: string | undefined;
|
package/dist/tools/interact.js
CHANGED
|
@@ -5,11 +5,12 @@ import { buildDiffCards } from "../engine/differ.js";
|
|
|
5
5
|
import { applyContext, layersFor, summariseContext } from "../engine/layers/index.js";
|
|
6
6
|
import { INTERACT_ACTIONS, describeInteraction, executeInteraction, interactionFieldShape, needsTouch, refineInteraction, } from "../engine/interaction.js";
|
|
7
7
|
import { formatCardMeta } from "../utils/format.js";
|
|
8
|
+
import { readStorageState, storageStateField } from "../utils/storage-state.js";
|
|
8
9
|
export const INTERACT_TOOL_NAME = "framewatch_interact";
|
|
9
10
|
export const interactInputShape = {
|
|
10
11
|
action: z
|
|
11
12
|
.enum(INTERACT_ACTIONS)
|
|
12
|
-
.describe("What to do: click, tap, type, scroll, swipe, navigate, select or hover"),
|
|
13
|
+
.describe("What to do: click, tap, type, key, scroll, swipe, navigate, select or hover"),
|
|
13
14
|
...interactionFieldShape,
|
|
14
15
|
url: z
|
|
15
16
|
.string()
|
|
@@ -51,6 +52,7 @@ export const interactInputShape = {
|
|
|
51
52
|
.boolean()
|
|
52
53
|
.default(false)
|
|
53
54
|
.describe("Report paint timing and layout shifts measured around the action"),
|
|
55
|
+
storage_state: storageStateField,
|
|
54
56
|
};
|
|
55
57
|
export const interactInputSchema = z.object(interactInputShape).superRefine(refineInteraction);
|
|
56
58
|
/**
|
|
@@ -95,9 +97,16 @@ async function runInteraction(rawInput) {
|
|
|
95
97
|
performance: input.include_performance,
|
|
96
98
|
};
|
|
97
99
|
try {
|
|
98
|
-
|
|
100
|
+
// Cookies and storage are fixed when a context is created, so a state named
|
|
101
|
+
// here applies to the page this call opens; a session already running on
|
|
102
|
+
// the same state simply carries on (see `getSessionPage`).
|
|
103
|
+
const storageState = input.storage_state
|
|
104
|
+
? { path: input.storage_state, state: await readStorageState(input.storage_state) }
|
|
105
|
+
: undefined;
|
|
106
|
+
const { page, previousUrl, reopenedFor } = await getSessionPage({
|
|
99
107
|
...(input.viewport ? { viewport: input.viewport } : {}),
|
|
100
108
|
hasTouch: needsTouch([step]),
|
|
109
|
+
...(storageState ? { storageState } : {}),
|
|
101
110
|
});
|
|
102
111
|
// The layers belong to the page, not to this call (see SessionLayers), so
|
|
103
112
|
// they are installed once and emptied here — this call reports what this
|
|
@@ -106,8 +115,9 @@ async function runInteraction(rawInput) {
|
|
|
106
115
|
const layers = layersFor(page);
|
|
107
116
|
await layers.ensure(flags);
|
|
108
117
|
layers.clear();
|
|
109
|
-
// `previousUrl` means the session had to be reopened
|
|
110
|
-
// back to where the user was unless they asked
|
|
118
|
+
// `previousUrl` means the session had to be reopened (for touch, or to take
|
|
119
|
+
// a different auth state); go back to where the user was unless they asked
|
|
120
|
+
// for somewhere else.
|
|
111
121
|
const target = input.url ?? previousUrl;
|
|
112
122
|
if (target !== undefined) {
|
|
113
123
|
await page.goto(target, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
|
|
@@ -140,7 +150,7 @@ async function runInteraction(rawInput) {
|
|
|
140
150
|
applyContext(cards, context);
|
|
141
151
|
const [beforeCard, afterCard] = cards;
|
|
142
152
|
const headline = [
|
|
143
|
-
summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0,
|
|
153
|
+
summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor),
|
|
144
154
|
...(summariseContext(context, cards.length) ?? []),
|
|
145
155
|
].join("\n");
|
|
146
156
|
const content = [
|
|
@@ -161,8 +171,12 @@ async function runInteraction(rawInput) {
|
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
173
|
/** One line: what was done, where, and how much of the frame it moved. */
|
|
164
|
-
function summarise(step, url, changePercent,
|
|
165
|
-
const note =
|
|
174
|
+
function summarise(step, url, changePercent, reopenedFor) {
|
|
175
|
+
const note = reopenedFor === "touch"
|
|
176
|
+
? " (page reopened to enable touch — page state was reset)"
|
|
177
|
+
: reopenedFor === "auth"
|
|
178
|
+
? " (page reopened with the saved auth state — page state was reset)"
|
|
179
|
+
: "";
|
|
166
180
|
const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : "no visual change";
|
|
167
181
|
return `${describeInteraction(step)} on ${url}${note} — ${changed}`;
|
|
168
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interact.js","sourceRoot":"","sources":["../../src/tools/interact.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_INTERACT_WAIT_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withSessionLock } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, layersFor, summariseContext, type LayerFlags } from \"../engine/layers/index.js\";\nimport {\n INTERACT_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport type { RawFrame } from \"../types.js\";\nimport { formatCardMeta } from \"../utils/format.js\";\n\nexport const INTERACT_TOOL_NAME = \"framewatch_interact\";\n\nexport const interactInputShape = {\n action: z\n .enum(INTERACT_ACTIONS)\n .describe(\"What to do: click, tap, type, scroll, swipe, navigate, select or hover\"),\n ...interactionFieldShape,\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to act on the page left open by the previous call.\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_INTERACT_WAIT_MS)\n .describe(\"Wait time (ms) after the action before the 'after' screenshot, so animations can settle\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for the target element\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n })\n .optional()\n .describe(\"Resize the page to this before acting (defaults to leaving it as it is)\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Report console logs, uncaught errors and unhandled rejections the action caused\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Report network requests (method, url, status, duration) the action caused\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Report the DOM mutations the action caused — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Report paint timing and layout shifts measured around the action\"),\n};\n\nexport const interactInputSchema = z.object(interactInputShape).superRefine(refineInteraction);\nexport type InteractInput = z.input<typeof interactInputSchema>;\n\n/**\n * Perform one interaction on the current page and show what it did.\n *\n * Unlike every other FrameWatch tool this one is stateful on purpose: the page\n * stays open between calls (see `getSessionPage`), which is what makes\n * click → look → type → look iteration possible. Pass `url` to open or move\n * the page; omit it to keep working on what is already there.\n *\n * The result is a two-frame diff card sequence — before, after, and the\n * change region between them — reusing the same diff engine as\n * `framewatch_capture` so the change bbox and crop mean the same thing in\n * both tools.\n *\n * Calls are serialised through `withSessionLock`, which every tool that\n * touches the session page shares: one page, one hand.\n */\nexport function performInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n return withSessionLock(() => runInteraction(rawInput));\n}\n\nasync function runInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n const parsed = interactInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Interaction failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const step: Interaction = {\n action: input.action,\n ...(input.selector !== undefined ? { selector: input.selector } : {}),\n ...(input.value !== undefined ? { value: input.value } : {}),\n ...(input.x !== undefined ? { x: input.x } : {}),\n ...(input.y !== undefined ? { y: input.y } : {}),\n ...(input.delta_x !== undefined ? { delta_x: input.delta_x } : {}),\n ...(input.delta_y !== undefined ? { delta_y: input.delta_y } : {}),\n };\n\n const flags: LayerFlags = {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n };\n\n try {\n const { page, previousUrl } = await getSessionPage({\n ...(input.viewport ? { viewport: input.viewport } : {}),\n hasTouch: needsTouch([step]),\n });\n\n // The layers belong to the page, not to this call (see SessionLayers), so\n // they are installed once and emptied here — this call reports what this\n // action caused, not what the last twenty did. Both happen before the\n // navigation, so a page opened by `url` has its load watched too.\n const layers = layersFor(page);\n await layers.ensure(flags);\n layers.clear();\n\n // `previousUrl` means the session had to be reopened to enable touch; go\n // back to where the user was unless they asked for somewhere else.\n const target = input.url ?? previousUrl;\n if (target !== undefined) {\n await page.goto(target, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n }\n if (page.url() === \"about:blank\") {\n return errorResult(\n \"Interaction failed: no page is open yet — pass `url` to open one (e.g. http://localhost:3000).\",\n );\n }\n\n const before = await page.screenshot({ type: \"png\" });\n const startedAt = Date.now();\n await executeInteraction(page, step, { timeout_ms: input.timeout_ms });\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n const after = await page.screenshot({ type: \"png\" });\n\n const frames: RawFrame[] = [\n { buffer: before, timestamp_ms: 0, is_interaction: false },\n { buffer: after, timestamp_ms: Date.now() - startedAt, is_interaction: true },\n ];\n // sensitivity 0 keeps both frames however small the change is — the point\n // of this tool is to show what one action did, including \"nothing\".\n const { cards } = await buildDiffCards(frames, { sensitivity: 0, max_frames: 2 });\n if (cards.length < 2) {\n return errorResult(`Interaction failed: ${describeInteraction(step)} ran, but the screenshots could not be compared.`);\n }\n\n // `startedAt` is the instant of the \"before\" frame, which puts the split\n // exactly where it belongs: everything from opening the page lands on the\n // before card (negative, i.e. \"how we got here\") and everything the action\n // caused lands on the after card.\n const context = layers.collect(startedAt, flags);\n applyContext(cards, context);\n\n const [beforeCard, afterCard] = cards;\n const headline = [\n summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, previousUrl !== undefined),\n ...(summariseContext(context, cards.length) ?? []),\n ].join(\"\\n\");\n\n const content: CallToolResult[\"content\"] = [\n { type: \"text\", text: headline },\n { type: \"image\", data: beforeCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `Before — ${formatCardMeta(beforeCard)}` },\n { type: \"image\", data: afterCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `After — ${formatCardMeta(afterCard)}` },\n ];\n const crop = afterCard.change_region?.crop;\n if (crop) {\n content.push({ type: \"image\", data: crop, mimeType: \"image/png\" });\n }\n return { content };\n } catch (error) {\n return errorResult(describeInteractFailure(input, error));\n }\n}\n\n/** One line: what was done, where, and how much of the frame it moved. */\nfunction summarise(step: Interaction, url: string, changePercent: number, reopened: boolean): string {\n const note = reopened ? \" (page reopened to enable touch — page state was reset)\" : \"\";\n const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : \"no visual change\";\n return `${describeInteraction(step)} on ${url}${note} — ${changed}`;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a failure into one actionable line. Interaction errors already name the\n * step and the reason (see `executeInteraction`), so they are passed through\n * with the page appended for context; everything else is a navigation or\n * browser problem, worded as in the other tools.\n */\nexport function describeInteractFailure(input: { url?: string }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n \"Interaction failed: 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 return `Interaction failed: could not open ${input.url ?? \"the page\"} — ${firstLine}`;\n }\n return input.url ? `${firstLine} (page: ${input.url})` : firstLine;\n}\n\nexport function registerInteractTool(server: McpServer): void {\n server.registerTool(\n INTERACT_TOOL_NAME,\n {\n title: \"Interact\",\n description:\n \"Perform one interaction (click, tap, type, scroll, swipe, hover, select, navigate) on a page and return \" +\n \"before/after screenshots plus a crop of what changed. The page stays open between calls, so you can \" +\n \"click, look, type and look again without replaying the whole flow — pass `url` only to open or move it. \" +\n \"Each call also reports the context its own action produced: console output and uncaught errors (on by \" +\n \"default), plus network requests, DOM mutations and paint/layout-shift timing via `include_network`, \" +\n \"`include_dom` and `include_performance` — that is how you find out why a click did nothing.\",\n inputSchema: interactInputShape,\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => performInteraction(args),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"interact.js","sourceRoot":"","sources":["../../src/tools/interact.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_INTERACT_WAIT_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withSessionLock } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, layersFor, summariseContext, type LayerFlags } from \"../engine/layers/index.js\";\nimport {\n INTERACT_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport type { RawFrame } from \"../types.js\";\nimport { formatCardMeta } from \"../utils/format.js\";\nimport { readStorageState, storageStateField } from \"../utils/storage-state.js\";\n\nexport const INTERACT_TOOL_NAME = \"framewatch_interact\";\n\nexport const interactInputShape = {\n action: z\n .enum(INTERACT_ACTIONS)\n .describe(\"What to do: click, tap, type, key, scroll, swipe, navigate, select or hover\"),\n ...interactionFieldShape,\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to act on the page left open by the previous call.\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_INTERACT_WAIT_MS)\n .describe(\"Wait time (ms) after the action before the 'after' screenshot, so animations can settle\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for the target element\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n })\n .optional()\n .describe(\"Resize the page to this before acting (defaults to leaving it as it is)\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Report console logs, uncaught errors and unhandled rejections the action caused\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Report network requests (method, url, status, duration) the action caused\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Report the DOM mutations the action caused — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Report paint timing and layout shifts measured around the action\"),\n storage_state: storageStateField,\n};\n\nexport const interactInputSchema = z.object(interactInputShape).superRefine(refineInteraction);\nexport type InteractInput = z.input<typeof interactInputSchema>;\n\n/**\n * Perform one interaction on the current page and show what it did.\n *\n * Unlike every other FrameWatch tool this one is stateful on purpose: the page\n * stays open between calls (see `getSessionPage`), which is what makes\n * click → look → type → look iteration possible. Pass `url` to open or move\n * the page; omit it to keep working on what is already there.\n *\n * The result is a two-frame diff card sequence — before, after, and the\n * change region between them — reusing the same diff engine as\n * `framewatch_capture` so the change bbox and crop mean the same thing in\n * both tools.\n *\n * Calls are serialised through `withSessionLock`, which every tool that\n * touches the session page shares: one page, one hand.\n */\nexport function performInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n return withSessionLock(() => runInteraction(rawInput));\n}\n\nasync function runInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n const parsed = interactInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Interaction failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const step: Interaction = {\n action: input.action,\n ...(input.selector !== undefined ? { selector: input.selector } : {}),\n ...(input.value !== undefined ? { value: input.value } : {}),\n ...(input.x !== undefined ? { x: input.x } : {}),\n ...(input.y !== undefined ? { y: input.y } : {}),\n ...(input.delta_x !== undefined ? { delta_x: input.delta_x } : {}),\n ...(input.delta_y !== undefined ? { delta_y: input.delta_y } : {}),\n };\n\n const flags: LayerFlags = {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n };\n\n try {\n // Cookies and storage are fixed when a context is created, so a state named\n // here applies to the page this call opens; a session already running on\n // the same state simply carries on (see `getSessionPage`).\n const storageState = input.storage_state\n ? { path: input.storage_state, state: await readStorageState(input.storage_state) }\n : undefined;\n\n const { page, previousUrl, reopenedFor } = await getSessionPage({\n ...(input.viewport ? { viewport: input.viewport } : {}),\n hasTouch: needsTouch([step]),\n ...(storageState ? { storageState } : {}),\n });\n\n // The layers belong to the page, not to this call (see SessionLayers), so\n // they are installed once and emptied here — this call reports what this\n // action caused, not what the last twenty did. Both happen before the\n // navigation, so a page opened by `url` has its load watched too.\n const layers = layersFor(page);\n await layers.ensure(flags);\n layers.clear();\n\n // `previousUrl` means the session had to be reopened (for touch, or to take\n // a different auth state); go back to where the user was unless they asked\n // for somewhere else.\n const target = input.url ?? previousUrl;\n if (target !== undefined) {\n await page.goto(target, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n }\n if (page.url() === \"about:blank\") {\n return errorResult(\n \"Interaction failed: no page is open yet — pass `url` to open one (e.g. http://localhost:3000).\",\n );\n }\n\n const before = await page.screenshot({ type: \"png\" });\n const startedAt = Date.now();\n await executeInteraction(page, step, { timeout_ms: input.timeout_ms });\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n const after = await page.screenshot({ type: \"png\" });\n\n const frames: RawFrame[] = [\n { buffer: before, timestamp_ms: 0, is_interaction: false },\n { buffer: after, timestamp_ms: Date.now() - startedAt, is_interaction: true },\n ];\n // sensitivity 0 keeps both frames however small the change is — the point\n // of this tool is to show what one action did, including \"nothing\".\n const { cards } = await buildDiffCards(frames, { sensitivity: 0, max_frames: 2 });\n if (cards.length < 2) {\n return errorResult(`Interaction failed: ${describeInteraction(step)} ran, but the screenshots could not be compared.`);\n }\n\n // `startedAt` is the instant of the \"before\" frame, which puts the split\n // exactly where it belongs: everything from opening the page lands on the\n // before card (negative, i.e. \"how we got here\") and everything the action\n // caused lands on the after card.\n const context = layers.collect(startedAt, flags);\n applyContext(cards, context);\n\n const [beforeCard, afterCard] = cards;\n const headline = [\n summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor),\n ...(summariseContext(context, cards.length) ?? []),\n ].join(\"\\n\");\n\n const content: CallToolResult[\"content\"] = [\n { type: \"text\", text: headline },\n { type: \"image\", data: beforeCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `Before — ${formatCardMeta(beforeCard)}` },\n { type: \"image\", data: afterCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `After — ${formatCardMeta(afterCard)}` },\n ];\n const crop = afterCard.change_region?.crop;\n if (crop) {\n content.push({ type: \"image\", data: crop, mimeType: \"image/png\" });\n }\n return { content };\n } catch (error) {\n return errorResult(describeInteractFailure(input, error));\n }\n}\n\n/** One line: what was done, where, and how much of the frame it moved. */\nfunction summarise(step: Interaction, url: string, changePercent: number, reopenedFor?: \"touch\" | \"auth\"): string {\n const note =\n reopenedFor === \"touch\"\n ? \" (page reopened to enable touch — page state was reset)\"\n : reopenedFor === \"auth\"\n ? \" (page reopened with the saved auth state — page state was reset)\"\n : \"\";\n const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : \"no visual change\";\n return `${describeInteraction(step)} on ${url}${note} — ${changed}`;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a failure into one actionable line. Interaction errors already name the\n * step and the reason (see `executeInteraction`), so they are passed through\n * with the page appended for context; everything else is a navigation or\n * browser problem, worded as in the other tools.\n */\nexport function describeInteractFailure(input: { url?: string }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n \"Interaction failed: 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 return `Interaction failed: could not open ${input.url ?? \"the page\"} — ${firstLine}`;\n }\n return input.url ? `${firstLine} (page: ${input.url})` : firstLine;\n}\n\nexport function registerInteractTool(server: McpServer): void {\n server.registerTool(\n INTERACT_TOOL_NAME,\n {\n title: \"Interact\",\n description:\n \"Perform one interaction (click, tap, type, scroll, swipe, hover, select, navigate) on a page and return \" +\n \"before/after screenshots plus a crop of what changed. The page stays open between calls, so you can \" +\n \"click, look, type and look again without replaying the whole flow — pass `url` only to open or move it. \" +\n \"Each call also reports the context its own action produced: console output and uncaught errors (on by \" +\n \"default), plus network requests, DOM mutations and paint/layout-shift timing via `include_network`, \" +\n \"`include_dom` and `include_performance` — that is how you find out why a click did nothing.\",\n inputSchema: interactInputShape,\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => performInteraction(args),\n );\n}\n"]}
|
|
@@ -20,6 +20,7 @@ export declare const responsiveInputShape: {
|
|
|
20
20
|
wait_ms: z.ZodDefault<z.ZodNumber>;
|
|
21
21
|
wait_for: z.ZodOptional<z.ZodString>;
|
|
22
22
|
wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
23
24
|
};
|
|
24
25
|
export declare const responsiveInputSchema: z.ZodObject<{
|
|
25
26
|
url: z.ZodString;
|
|
@@ -39,6 +40,7 @@ export declare const responsiveInputSchema: z.ZodObject<{
|
|
|
39
40
|
wait_ms: z.ZodDefault<z.ZodNumber>;
|
|
40
41
|
wait_for: z.ZodOptional<z.ZodString>;
|
|
41
42
|
wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
43
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
42
44
|
}, "strip", z.ZodTypeAny, {
|
|
43
45
|
url: string;
|
|
44
46
|
wait_ms: number;
|
|
@@ -49,11 +51,13 @@ export declare const responsiveInputSchema: z.ZodObject<{
|
|
|
49
51
|
name: string;
|
|
50
52
|
}[];
|
|
51
53
|
wait_for?: string | undefined;
|
|
54
|
+
storage_state?: string | undefined;
|
|
52
55
|
}, {
|
|
53
56
|
url: string;
|
|
54
57
|
wait_ms?: number | undefined;
|
|
55
58
|
wait_for?: string | undefined;
|
|
56
59
|
wait_for_timeout_ms?: number | undefined;
|
|
60
|
+
storage_state?: string | undefined;
|
|
57
61
|
viewports?: {
|
|
58
62
|
width: number;
|
|
59
63
|
height: number;
|
package/dist/tools/responsive.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { DEFAULT_RESPONSIVE_VIEWPORTS, DEFAULT_RESPONSIVE_WAIT_MS, MAX_RESPONSIVE_VIEWPORTS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, NAVIGATION_TIMEOUT_MS, OVERFLOW_TOLERANCE_PX, SELECTOR_TIMEOUT_MS, } from "../constants.js";
|
|
3
3
|
import { withPage } from "../engine/browser.js";
|
|
4
4
|
import { getDimensions, resizeForOutput, toBase64 } from "../utils/image.js";
|
|
5
|
+
import { readStorageState, storageStateField } from "../utils/storage-state.js";
|
|
5
6
|
export const RESPONSIVE_TOOL_NAME = "framewatch_responsive";
|
|
6
7
|
const viewportSchema = z.object({
|
|
7
8
|
name: z.string().min(1).describe("Label for this size, e.g. 'mobile', 'tablet', 'desktop'"),
|
|
@@ -29,6 +30,7 @@ export const responsiveInputShape = {
|
|
|
29
30
|
.min(1)
|
|
30
31
|
.default(SELECTOR_TIMEOUT_MS)
|
|
31
32
|
.describe("Max time (ms) to wait for `wait_for` to appear (must be > 0)"),
|
|
33
|
+
storage_state: storageStateField,
|
|
32
34
|
};
|
|
33
35
|
export const responsiveInputSchema = z.object(responsiveInputShape);
|
|
34
36
|
/**
|
|
@@ -53,7 +55,18 @@ export async function captureResponsive(rawInput) {
|
|
|
53
55
|
return errorResult(`Responsive capture failed: invalid input — ${issues}`);
|
|
54
56
|
}
|
|
55
57
|
const input = parsed.data;
|
|
56
|
-
|
|
58
|
+
// Read the auth state once, before any context exists: a missing or stale
|
|
59
|
+
// file is one problem with the call, not one problem per viewport.
|
|
60
|
+
let storageState;
|
|
61
|
+
if (input.storage_state) {
|
|
62
|
+
try {
|
|
63
|
+
storageState = await readStorageState(input.storage_state);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return errorResult(`Responsive capture of ${input.url} failed: ${describeViewportFailure(input, error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const shots = await Promise.all(input.viewports.map((viewport) => captureOne(input, viewport, storageState)));
|
|
57
70
|
const captured = shots.filter((shot) => shot.png !== undefined);
|
|
58
71
|
if (captured.length === 0) {
|
|
59
72
|
const reasons = shots.map((shot) => `${shot.viewport.name}: ${shot.error ?? "no screenshot"}`).join("; ");
|
|
@@ -78,9 +91,13 @@ export async function captureResponsive(rawInput) {
|
|
|
78
91
|
return { content };
|
|
79
92
|
}
|
|
80
93
|
/** Screenshot `url` at one viewport. Never throws — a failure is part of the result. */
|
|
81
|
-
async function captureOne(input, viewport) {
|
|
94
|
+
async function captureOne(input, viewport, storageState) {
|
|
82
95
|
try {
|
|
83
|
-
const
|
|
96
|
+
const options = {
|
|
97
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
98
|
+
...(storageState ? { contextOptions: { storageState } } : {}),
|
|
99
|
+
};
|
|
100
|
+
const result = await withPage(options, async (page) => {
|
|
84
101
|
await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
|
|
85
102
|
if (input.wait_for) {
|
|
86
103
|
await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responsive.js","sourceRoot":"","sources":["../../src/tools/responsive.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 DEFAULT_RESPONSIVE_VIEWPORTS,\n DEFAULT_RESPONSIVE_WAIT_MS,\n MAX_RESPONSIVE_VIEWPORTS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n OVERFLOW_TOLERANCE_PX,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { getDimensions, resizeForOutput, toBase64 } from \"../utils/image.js\";\n\nexport const RESPONSIVE_TOOL_NAME = \"framewatch_responsive\";\n\nconst viewportSchema = z.object({\n name: z.string().min(1).describe(\"Label for this size, e.g. 'mobile', 'tablet', 'desktop'\"),\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n});\n\nexport const responsiveInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n viewports: z\n .array(viewportSchema)\n .min(1)\n .max(MAX_RESPONSIVE_VIEWPORTS)\n .default([...DEFAULT_RESPONSIVE_VIEWPORTS])\n .describe(\"Viewport sizes to capture (defaults to mobile 375x812, tablet 768x1024, desktop 1440x900)\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_RESPONSIVE_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) before each screenshot\"),\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};\n\nexport const responsiveInputSchema = z.object(responsiveInputShape);\nexport type ResponsiveInput = z.input<typeof responsiveInputSchema>;\ntype ParsedViewport = z.output<typeof viewportSchema>;\n\n/** How one viewport turned out. Either it produced a shot, or it produced a reason. */\ninterface ViewportShot {\n viewport: ParsedViewport;\n png?: Buffer;\n /** Document width vs viewport width, for the overflow check. */\n layout?: LayoutInfo;\n error?: string;\n}\n\ninterface LayoutInfo {\n scroll_width: number;\n client_width: number;\n scroll_height: number;\n client_height: number;\n}\n\n/**\n * Capture one page at several viewport sizes and return one screenshot per\n * size, labelled, so a whole responsive range can be reviewed in a single\n * look.\n *\n * Each viewport gets its own browser context rather than one page being\n * resized, for two reasons: a page that has already laid itself out at 1440px\n * often keeps state a genuine mobile visitor never had (a menu built by a\n * matchMedia listener that ran once), and independent contexts can load\n * concurrently — three 2s waits cost 2s, not 6s.\n *\n * A viewport that fails is reported next to the ones that worked instead of\n * failing the call: \"desktop is fine, mobile times out\" is itself the finding.\n * Only a run where every viewport failed comes back as an error.\n */\nexport async function captureResponsive(rawInput: ResponsiveInput): Promise<CallToolResult> {\n const parsed = responsiveInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Responsive capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n const shots = await Promise.all(input.viewports.map((viewport) => captureOne(input, viewport)));\n const captured = shots.filter((shot) => shot.png !== undefined);\n if (captured.length === 0) {\n const reasons = shots.map((shot) => `${shot.viewport.name}: ${shot.error ?? \"no screenshot\"}`).join(\"; \");\n return errorResult(`Responsive capture of ${input.url} failed at every viewport — ${reasons}`);\n }\n\n const content: CallToolResult[\"content\"] = [\n {\n type: \"text\",\n text: summarise(input.url, shots),\n },\n ];\n\n for (const shot of shots) {\n if (!shot.png) {\n content.push({ type: \"text\", text: `${label(shot.viewport)} — failed: ${shot.error ?? \"no screenshot\"}` });\n continue;\n }\n const resized = await resizeForOutput(shot.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: describeShot(shot, width, height) });\n }\n\n return { content };\n}\n\n/** Screenshot `url` at one viewport. Never throws — a failure is part of the result. */\nasync function captureOne(\n input: z.output<typeof responsiveInputSchema>,\n viewport: ParsedViewport,\n): Promise<ViewportShot> {\n try {\n const result = await withPage({ viewport: { width: viewport.width, height: viewport.height } }, 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 const png = await page.screenshot({ type: \"png\" });\n return { png, layout: await readLayout(page) };\n });\n return { viewport, ...result };\n } catch (error) {\n return { viewport, error: describeViewportFailure(input, error) };\n }\n}\n\n/**\n * The document's laid-out size against the viewport it was given. Horizontal\n * overflow is the classic responsive bug and it is invisible in a screenshot —\n * the content that sticks out is simply cropped away — so it is measured\n * rather than left to the eye.\n *\n * Cosmetic: a page that dies before this is read still returns its screenshot.\n */\nasync function readLayout(page: Page): Promise<LayoutInfo | undefined> {\n try {\n // `globalThis as any` rather than the DOM globals: this package is\n // compiled with the Node lib only, and the page it lands in may be\n // mid-teardown, so nothing here can be assumed to exist.\n return await page.evaluate(() => {\n const el = (globalThis as any).document?.documentElement;\n if (!el) return undefined;\n return {\n scroll_width: Math.round(el.scrollWidth),\n client_width: Math.round(el.clientWidth),\n scroll_height: Math.round(el.scrollHeight),\n client_height: Math.round(el.clientHeight),\n };\n });\n } catch {\n return undefined;\n }\n}\n\nfunction label(viewport: ParsedViewport): string {\n return `${viewport.name} ${viewport.width}x${viewport.height}`;\n}\n\n/** One line per viewport: what it is, how big the image is, and whether the content fits. */\nfunction describeShot(shot: ViewportShot, imageWidth: number, imageHeight: number): string {\n const parts = [label(shot.viewport), `image ${imageWidth}x${imageHeight}`];\n\n const layout = shot.layout;\n if (layout) {\n if (layout.scroll_width > layout.client_width + OVERFLOW_TOLERANCE_PX) {\n parts.push(\n `horizontal overflow: content is ${layout.scroll_width}px wide in a ${layout.client_width}px viewport ` +\n `(+${layout.scroll_width - layout.client_width}px)`,\n );\n }\n if (layout.scroll_height > layout.client_height) {\n parts.push(`page scrolls to ${layout.scroll_height}px (${scrolls(layout)} screens)`);\n }\n }\n\n return parts.join(\" — \");\n}\n\nfunction scrolls(layout: LayoutInfo): string {\n return (layout.scroll_height / Math.max(1, layout.client_height)).toFixed(1);\n}\n\n/** The opening line: what was captured, and an up-front warning about anything that overflowed. */\nfunction summarise(url: string, shots: ViewportShot[]): string {\n const captured = shots.filter((shot) => shot.png !== undefined);\n const failed = shots.filter((shot) => shot.png === undefined);\n\n const names = captured.map((shot) => label(shot.viewport)).join(\", \");\n const lines = [`Captured ${url} at ${captured.length} of ${shots.length} viewports: ${names}`];\n\n if (failed.length > 0) {\n lines.push(`Failed: ${failed.map((shot) => shot.viewport.name).join(\", \")}`);\n }\n\n const overflowing = captured.filter(\n (shot) => shot.layout !== undefined && shot.layout.scroll_width > shot.layout.client_width + OVERFLOW_TOLERANCE_PX,\n );\n if (overflowing.length > 0) {\n lines.push(\n `Horizontal overflow at ${overflowing.map((shot) => shot.viewport.name).join(\", \")} — ` +\n \"content is wider than the viewport, so something is sticking out past the right edge.\",\n );\n }\n\n return lines.join(\"\\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 viewport that failed. Mirrors `describeFailure` in\n * screenshot.ts: match on the failing Playwright call, never on substrings of\n * a user-supplied selector.\n */\nexport function describeViewportFailure(input: { wait_for?: string; wait_for_timeout_ms: number }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return \"Playwright's Chromium browser is not installed. Run `npx playwright install chromium` and try again.\";\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms`;\n }\n return firstLine;\n}\n\nexport function registerResponsiveTool(server: McpServer): void {\n server.registerTool(\n RESPONSIVE_TOOL_NAME,\n {\n title: \"Responsive\",\n description:\n \"Screenshot the same page at several viewport sizes in one call (mobile, tablet and desktop by default) \" +\n \"and return one labelled image per size. Each size loads in its own fresh browser context, so a mobile \" +\n \"shot is what a phone would really get rather than a resized desktop layout. Content that is wider than \" +\n \"its viewport is reported as horizontal overflow — the commonest responsive bug, and one a screenshot \" +\n \"alone hides because the overflowing part is simply cropped off.\",\n inputSchema: responsiveInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => captureResponsive(args),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"responsive.js","sourceRoot":"","sources":["../../src/tools/responsive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAqB,MAAM,2BAA2B,CAAC;AAEnG,MAAM,CAAC,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE5D,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,yDAAyD,CAAC;IAC3F,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACtD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC;CACzD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,qFAAqF,CAAC;IACrH,SAAS,EAAE,CAAC;SACT,KAAK,CAAC,cAAc,CAAC;SACrB,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,wBAAwB,CAAC;SAC7B,OAAO,CAAC,CAAC,GAAG,4BAA4B,CAAC,CAAC;SAC1C,QAAQ,CAAC,2FAA2F,CAAC;IACxG,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,0BAA0B,CAAC;SACnC,QAAQ,CAAC,uDAAuD,CAAC;IACpE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2DAA2D,CAAC;IACrG,mBAAmB,EAAE,CAAC;SACnB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,mBAAmB,CAAC;SAC5B,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,aAAa,EAAE,iBAAiB;CACjC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAoBpE;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAyB;IAC/D,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzG,OAAO,WAAW,CAAC,8CAA8C,MAAM,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAE1B,0EAA0E;IAC1E,mEAAmE;IACnE,IAAI,YAAsC,CAAC;IAC3C,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,YAAY,GAAG,MAAM,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,yBAAyB,KAAK,CAAC,GAAG,YAAY,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5G,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9G,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IAChE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1G,OAAO,WAAW,CAAC,yBAAyB,KAAK,CAAC,GAAG,+BAA+B,OAAO,EAAE,CAAC,CAAC;IACjG,CAAC;IAED,MAAM,OAAO,GAA8B;QACzC;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC;SAClC;KACF,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,IAAI,CAAC,KAAK,IAAI,eAAe,EAAE,EAAE,CAAC,CAAC;YAC3G,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,UAAU,CACvB,KAA6C,EAC7C,QAAwB,EACxB,YAA2B;IAE3B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG;YACd,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE;YAC5D,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACpD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;YAClF,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACnB,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,mBAAmB,EAAE,CAAC,CAAC;YACvG,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;gBACtB,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACnD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;IACpE,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,UAAU,CAAC,IAAU;IAClC,IAAI,CAAC;QACH,mEAAmE;QACnE,mEAAmE;QACnE,yDAAyD;QACzD,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC9B,MAAM,EAAE,GAAI,UAAkB,CAAC,QAAQ,EAAE,eAAe,CAAC;YACzD,IAAI,CAAC,EAAE;gBAAE,OAAO,SAAS,CAAC;YAC1B,OAAO;gBACL,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC;gBACxC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC;gBACxC,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC;gBAC1C,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC;aAC3C,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,QAAwB;IACrC,OAAO,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;AACjE,CAAC;AAED,6FAA6F;AAC7F,SAAS,YAAY,CAAC,IAAkB,EAAE,UAAkB,EAAE,WAAmB;IAC/E,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,SAAS,UAAU,IAAI,WAAW,EAAE,CAAC,CAAC;IAE3E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,MAAM,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,GAAG,qBAAqB,EAAE,CAAC;YACtE,KAAK,CAAC,IAAI,CACR,mCAAmC,MAAM,CAAC,YAAY,gBAAgB,MAAM,CAAC,YAAY,cAAc;gBACrG,KAAK,MAAM,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,KAAK,CACtD,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;YAChD,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,aAAa,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,OAAO,CAAC,MAAkB;IACjC,OAAO,CAAC,MAAM,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED,mGAAmG;AACnG,SAAS,SAAS,CAAC,GAAW,EAAE,KAAqB;IACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;IAE9D,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,CAAC,YAAY,GAAG,OAAO,QAAQ,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,eAAe,KAAK,EAAE,CAAC,CAAC;IAE/F,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CACjC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,qBAAqB,CACnH,CAAC;IACF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CACR,0BAA0B,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YACrF,uFAAuF,CAC1F,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAyD,EAAE,KAAc;IAC/G,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzC,IAAI,+CAA+C,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAClE,OAAO,sGAAsG,CAAC;IAChH,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,IAAI,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9D,OAAO,aAAa,KAAK,CAAC,QAAQ,mCAAmC,KAAK,CAAC,mBAAmB,IAAI,CAAC;IACrG,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAiB;IACtD,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,KAAK,EAAE,YAAY;QACnB,WAAW,EACT,yGAAyG;YACzG,wGAAwG;YACxG,yGAAyG;YACzG,uGAAuG;YACvG,iEAAiE;QACnE,WAAW,EAAE,oBAAoB;QACjC,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE;KACvG,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CACxC,CAAC;AACJ,CAAC","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 DEFAULT_RESPONSIVE_VIEWPORTS,\n DEFAULT_RESPONSIVE_WAIT_MS,\n MAX_RESPONSIVE_VIEWPORTS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n OVERFLOW_TOLERANCE_PX,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { getDimensions, resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { readStorageState, storageStateField, type StorageState } from \"../utils/storage-state.js\";\n\nexport const RESPONSIVE_TOOL_NAME = \"framewatch_responsive\";\n\nconst viewportSchema = z.object({\n name: z.string().min(1).describe(\"Label for this size, e.g. 'mobile', 'tablet', 'desktop'\"),\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n});\n\nexport const responsiveInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n viewports: z\n .array(viewportSchema)\n .min(1)\n .max(MAX_RESPONSIVE_VIEWPORTS)\n .default([...DEFAULT_RESPONSIVE_VIEWPORTS])\n .describe(\"Viewport sizes to capture (defaults to mobile 375x812, tablet 768x1024, desktop 1440x900)\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_RESPONSIVE_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) before each screenshot\"),\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 storage_state: storageStateField,\n};\n\nexport const responsiveInputSchema = z.object(responsiveInputShape);\nexport type ResponsiveInput = z.input<typeof responsiveInputSchema>;\ntype ParsedViewport = z.output<typeof viewportSchema>;\n\n/** How one viewport turned out. Either it produced a shot, or it produced a reason. */\ninterface ViewportShot {\n viewport: ParsedViewport;\n png?: Buffer;\n /** Document width vs viewport width, for the overflow check. */\n layout?: LayoutInfo;\n error?: string;\n}\n\ninterface LayoutInfo {\n scroll_width: number;\n client_width: number;\n scroll_height: number;\n client_height: number;\n}\n\n/**\n * Capture one page at several viewport sizes and return one screenshot per\n * size, labelled, so a whole responsive range can be reviewed in a single\n * look.\n *\n * Each viewport gets its own browser context rather than one page being\n * resized, for two reasons: a page that has already laid itself out at 1440px\n * often keeps state a genuine mobile visitor never had (a menu built by a\n * matchMedia listener that ran once), and independent contexts can load\n * concurrently — three 2s waits cost 2s, not 6s.\n *\n * A viewport that fails is reported next to the ones that worked instead of\n * failing the call: \"desktop is fine, mobile times out\" is itself the finding.\n * Only a run where every viewport failed comes back as an error.\n */\nexport async function captureResponsive(rawInput: ResponsiveInput): Promise<CallToolResult> {\n const parsed = responsiveInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Responsive capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n // Read the auth state once, before any context exists: a missing or stale\n // file is one problem with the call, not one problem per viewport.\n let storageState: StorageState | undefined;\n if (input.storage_state) {\n try {\n storageState = await readStorageState(input.storage_state);\n } catch (error) {\n return errorResult(`Responsive capture of ${input.url} failed: ${describeViewportFailure(input, error)}`);\n }\n }\n\n const shots = await Promise.all(input.viewports.map((viewport) => captureOne(input, viewport, storageState)));\n const captured = shots.filter((shot) => shot.png !== undefined);\n if (captured.length === 0) {\n const reasons = shots.map((shot) => `${shot.viewport.name}: ${shot.error ?? \"no screenshot\"}`).join(\"; \");\n return errorResult(`Responsive capture of ${input.url} failed at every viewport — ${reasons}`);\n }\n\n const content: CallToolResult[\"content\"] = [\n {\n type: \"text\",\n text: summarise(input.url, shots),\n },\n ];\n\n for (const shot of shots) {\n if (!shot.png) {\n content.push({ type: \"text\", text: `${label(shot.viewport)} — failed: ${shot.error ?? \"no screenshot\"}` });\n continue;\n }\n const resized = await resizeForOutput(shot.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: describeShot(shot, width, height) });\n }\n\n return { content };\n}\n\n/** Screenshot `url` at one viewport. Never throws — a failure is part of the result. */\nasync function captureOne(\n input: z.output<typeof responsiveInputSchema>,\n viewport: ParsedViewport,\n storageState?: StorageState,\n): Promise<ViewportShot> {\n try {\n const options = {\n viewport: { width: viewport.width, height: viewport.height },\n ...(storageState ? { contextOptions: { storageState } } : {}),\n };\n const result = await withPage(options, 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 const png = await page.screenshot({ type: \"png\" });\n return { png, layout: await readLayout(page) };\n });\n return { viewport, ...result };\n } catch (error) {\n return { viewport, error: describeViewportFailure(input, error) };\n }\n}\n\n/**\n * The document's laid-out size against the viewport it was given. Horizontal\n * overflow is the classic responsive bug and it is invisible in a screenshot —\n * the content that sticks out is simply cropped away — so it is measured\n * rather than left to the eye.\n *\n * Cosmetic: a page that dies before this is read still returns its screenshot.\n */\nasync function readLayout(page: Page): Promise<LayoutInfo | undefined> {\n try {\n // `globalThis as any` rather than the DOM globals: this package is\n // compiled with the Node lib only, and the page it lands in may be\n // mid-teardown, so nothing here can be assumed to exist.\n return await page.evaluate(() => {\n const el = (globalThis as any).document?.documentElement;\n if (!el) return undefined;\n return {\n scroll_width: Math.round(el.scrollWidth),\n client_width: Math.round(el.clientWidth),\n scroll_height: Math.round(el.scrollHeight),\n client_height: Math.round(el.clientHeight),\n };\n });\n } catch {\n return undefined;\n }\n}\n\nfunction label(viewport: ParsedViewport): string {\n return `${viewport.name} ${viewport.width}x${viewport.height}`;\n}\n\n/** One line per viewport: what it is, how big the image is, and whether the content fits. */\nfunction describeShot(shot: ViewportShot, imageWidth: number, imageHeight: number): string {\n const parts = [label(shot.viewport), `image ${imageWidth}x${imageHeight}`];\n\n const layout = shot.layout;\n if (layout) {\n if (layout.scroll_width > layout.client_width + OVERFLOW_TOLERANCE_PX) {\n parts.push(\n `horizontal overflow: content is ${layout.scroll_width}px wide in a ${layout.client_width}px viewport ` +\n `(+${layout.scroll_width - layout.client_width}px)`,\n );\n }\n if (layout.scroll_height > layout.client_height) {\n parts.push(`page scrolls to ${layout.scroll_height}px (${scrolls(layout)} screens)`);\n }\n }\n\n return parts.join(\" — \");\n}\n\nfunction scrolls(layout: LayoutInfo): string {\n return (layout.scroll_height / Math.max(1, layout.client_height)).toFixed(1);\n}\n\n/** The opening line: what was captured, and an up-front warning about anything that overflowed. */\nfunction summarise(url: string, shots: ViewportShot[]): string {\n const captured = shots.filter((shot) => shot.png !== undefined);\n const failed = shots.filter((shot) => shot.png === undefined);\n\n const names = captured.map((shot) => label(shot.viewport)).join(\", \");\n const lines = [`Captured ${url} at ${captured.length} of ${shots.length} viewports: ${names}`];\n\n if (failed.length > 0) {\n lines.push(`Failed: ${failed.map((shot) => shot.viewport.name).join(\", \")}`);\n }\n\n const overflowing = captured.filter(\n (shot) => shot.layout !== undefined && shot.layout.scroll_width > shot.layout.client_width + OVERFLOW_TOLERANCE_PX,\n );\n if (overflowing.length > 0) {\n lines.push(\n `Horizontal overflow at ${overflowing.map((shot) => shot.viewport.name).join(\", \")} — ` +\n \"content is wider than the viewport, so something is sticking out past the right edge.\",\n );\n }\n\n return lines.join(\"\\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 viewport that failed. Mirrors `describeFailure` in\n * screenshot.ts: match on the failing Playwright call, never on substrings of\n * a user-supplied selector.\n */\nexport function describeViewportFailure(input: { wait_for?: string; wait_for_timeout_ms: number }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return \"Playwright's Chromium browser is not installed. Run `npx playwright install chromium` and try again.\";\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms`;\n }\n return firstLine;\n}\n\nexport function registerResponsiveTool(server: McpServer): void {\n server.registerTool(\n RESPONSIVE_TOOL_NAME,\n {\n title: \"Responsive\",\n description:\n \"Screenshot the same page at several viewport sizes in one call (mobile, tablet and desktop by default) \" +\n \"and return one labelled image per size. Each size loads in its own fresh browser context, so a mobile \" +\n \"shot is what a phone would really get rather than a resized desktop layout. Content that is wider than \" +\n \"its viewport is reported as horizontal overflow — the commonest responsive bug, and one a screenshot \" +\n \"alone hides because the overflowing part is simply cropped off.\",\n inputSchema: responsiveInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => captureResponsive(args),\n );\n}\n"]}
|