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/capture.d.ts
CHANGED
|
@@ -21,9 +21,9 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
21
21
|
y: z.ZodOptional<z.ZodNumber>;
|
|
22
22
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
23
23
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
24
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
24
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
25
25
|
}, "strip", z.ZodTypeAny, {
|
|
26
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
26
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
27
27
|
delay_ms: number;
|
|
28
28
|
value?: string | undefined;
|
|
29
29
|
x?: number | undefined;
|
|
@@ -32,7 +32,7 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
32
32
|
delta_x?: number | undefined;
|
|
33
33
|
delta_y?: number | undefined;
|
|
34
34
|
}, {
|
|
35
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
35
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
36
36
|
value?: string | undefined;
|
|
37
37
|
x?: number | undefined;
|
|
38
38
|
y?: number | undefined;
|
|
@@ -41,7 +41,7 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
41
41
|
delta_y?: number | undefined;
|
|
42
42
|
delay_ms?: number | undefined;
|
|
43
43
|
}>, {
|
|
44
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
44
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
45
45
|
delay_ms: number;
|
|
46
46
|
value?: string | undefined;
|
|
47
47
|
x?: number | undefined;
|
|
@@ -50,7 +50,7 @@ export declare const captureInteractionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
50
50
|
delta_x?: number | undefined;
|
|
51
51
|
delta_y?: number | undefined;
|
|
52
52
|
}, {
|
|
53
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
53
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
54
54
|
value?: string | undefined;
|
|
55
55
|
x?: number | undefined;
|
|
56
56
|
y?: number | undefined;
|
|
@@ -85,9 +85,9 @@ export declare const captureInputShape: {
|
|
|
85
85
|
y: z.ZodOptional<z.ZodNumber>;
|
|
86
86
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
87
87
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
88
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
88
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
89
89
|
}, "strip", z.ZodTypeAny, {
|
|
90
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
90
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
91
91
|
delay_ms: number;
|
|
92
92
|
value?: string | undefined;
|
|
93
93
|
x?: number | undefined;
|
|
@@ -96,7 +96,7 @@ export declare const captureInputShape: {
|
|
|
96
96
|
delta_x?: number | undefined;
|
|
97
97
|
delta_y?: number | undefined;
|
|
98
98
|
}, {
|
|
99
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
99
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
100
100
|
value?: string | undefined;
|
|
101
101
|
x?: number | undefined;
|
|
102
102
|
y?: number | undefined;
|
|
@@ -105,7 +105,7 @@ export declare const captureInputShape: {
|
|
|
105
105
|
delta_y?: number | undefined;
|
|
106
106
|
delay_ms?: number | undefined;
|
|
107
107
|
}>, {
|
|
108
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
108
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
109
109
|
delay_ms: number;
|
|
110
110
|
value?: string | undefined;
|
|
111
111
|
x?: number | undefined;
|
|
@@ -114,7 +114,7 @@ export declare const captureInputShape: {
|
|
|
114
114
|
delta_x?: number | undefined;
|
|
115
115
|
delta_y?: number | undefined;
|
|
116
116
|
}, {
|
|
117
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
117
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
118
118
|
value?: string | undefined;
|
|
119
119
|
x?: number | undefined;
|
|
120
120
|
y?: number | undefined;
|
|
@@ -128,6 +128,7 @@ export declare const captureInputShape: {
|
|
|
128
128
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
129
129
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
130
130
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
131
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
131
132
|
};
|
|
132
133
|
export declare const captureInputSchema: z.ZodObject<{
|
|
133
134
|
url: z.ZodString;
|
|
@@ -155,9 +156,9 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
155
156
|
y: z.ZodOptional<z.ZodNumber>;
|
|
156
157
|
delta_x: z.ZodOptional<z.ZodNumber>;
|
|
157
158
|
delta_y: z.ZodOptional<z.ZodNumber>;
|
|
158
|
-
action: z.ZodEnum<["click", "tap", "type", "scroll", "swipe", "wait", "navigate"]>;
|
|
159
|
+
action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
|
|
159
160
|
}, "strip", z.ZodTypeAny, {
|
|
160
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
161
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
161
162
|
delay_ms: number;
|
|
162
163
|
value?: string | undefined;
|
|
163
164
|
x?: number | undefined;
|
|
@@ -166,7 +167,7 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
166
167
|
delta_x?: number | undefined;
|
|
167
168
|
delta_y?: number | undefined;
|
|
168
169
|
}, {
|
|
169
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
170
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
170
171
|
value?: string | undefined;
|
|
171
172
|
x?: number | undefined;
|
|
172
173
|
y?: number | undefined;
|
|
@@ -175,7 +176,7 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
175
176
|
delta_y?: number | undefined;
|
|
176
177
|
delay_ms?: number | undefined;
|
|
177
178
|
}>, {
|
|
178
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
179
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
179
180
|
delay_ms: number;
|
|
180
181
|
value?: string | undefined;
|
|
181
182
|
x?: number | undefined;
|
|
@@ -184,7 +185,7 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
184
185
|
delta_x?: number | undefined;
|
|
185
186
|
delta_y?: number | undefined;
|
|
186
187
|
}, {
|
|
187
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
188
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
188
189
|
value?: string | undefined;
|
|
189
190
|
x?: number | undefined;
|
|
190
191
|
y?: number | undefined;
|
|
@@ -198,6 +199,7 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
198
199
|
include_network: z.ZodDefault<z.ZodBoolean>;
|
|
199
200
|
include_dom: z.ZodDefault<z.ZodBoolean>;
|
|
200
201
|
include_performance: z.ZodDefault<z.ZodBoolean>;
|
|
202
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
201
203
|
}, "strip", z.ZodTypeAny, {
|
|
202
204
|
url: string;
|
|
203
205
|
wait_for_timeout_ms: number;
|
|
@@ -215,8 +217,9 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
215
217
|
height: number;
|
|
216
218
|
} | undefined;
|
|
217
219
|
wait_for?: string | undefined;
|
|
220
|
+
storage_state?: string | undefined;
|
|
218
221
|
interactions?: {
|
|
219
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
222
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
220
223
|
delay_ms: number;
|
|
221
224
|
value?: string | undefined;
|
|
222
225
|
x?: number | undefined;
|
|
@@ -233,12 +236,13 @@ export declare const captureInputSchema: z.ZodObject<{
|
|
|
233
236
|
} | undefined;
|
|
234
237
|
wait_for?: string | undefined;
|
|
235
238
|
wait_for_timeout_ms?: number | undefined;
|
|
239
|
+
storage_state?: string | undefined;
|
|
236
240
|
duration_ms?: number | undefined;
|
|
237
241
|
sensitivity?: number | undefined;
|
|
238
242
|
max_frames?: number | undefined;
|
|
239
243
|
interval_ms?: number | undefined;
|
|
240
244
|
interactions?: {
|
|
241
|
-
action: "type" | "click" | "tap" | "scroll" | "swipe" | "wait" | "navigate";
|
|
245
|
+
action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
|
|
242
246
|
value?: string | undefined;
|
|
243
247
|
x?: number | undefined;
|
|
244
248
|
y?: number | undefined;
|
package/dist/tools/capture.js
CHANGED
|
@@ -6,6 +6,7 @@ import { applyContext, attachLayers, summariseContext } from "../engine/layers/i
|
|
|
6
6
|
import { CAPTURE_ACTIONS, describeInteraction, executeInteraction, interactionFieldShape, needsTouch, refineInteraction, } from "../engine/interaction.js";
|
|
7
7
|
import { recordFrames } from "../engine/recorder.js";
|
|
8
8
|
import { formatDiffCards } from "../utils/format.js";
|
|
9
|
+
import { readStorageState, storageStateField } from "../utils/storage-state.js";
|
|
9
10
|
export const CAPTURE_TOOL_NAME = "framewatch_capture";
|
|
10
11
|
/**
|
|
11
12
|
* Re-exported where it is used: the capture summary's context lines. It lives
|
|
@@ -19,7 +20,9 @@ export { summariseContext };
|
|
|
19
20
|
*/
|
|
20
21
|
export const captureInteractionSchema = z
|
|
21
22
|
.object({
|
|
22
|
-
action: z
|
|
23
|
+
action: z
|
|
24
|
+
.enum(CAPTURE_ACTIONS)
|
|
25
|
+
.describe("What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate"),
|
|
23
26
|
...interactionFieldShape,
|
|
24
27
|
delay_ms: z.number().int().min(0).default(0).describe("Wait this long (ms) before performing this action"),
|
|
25
28
|
})
|
|
@@ -94,6 +97,7 @@ export const captureInputShape = {
|
|
|
94
97
|
.boolean()
|
|
95
98
|
.default(false)
|
|
96
99
|
.describe("Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at"),
|
|
100
|
+
storage_state: storageStateField,
|
|
97
101
|
};
|
|
98
102
|
export const captureInputSchema = z.object(captureInputShape);
|
|
99
103
|
/**
|
|
@@ -125,6 +129,8 @@ export async function capturePage(rawInput) {
|
|
|
125
129
|
const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};
|
|
126
130
|
const report = interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;
|
|
127
131
|
try {
|
|
132
|
+
if (input.storage_state)
|
|
133
|
+
contextOptions.storageState = await readStorageState(input.storage_state);
|
|
128
134
|
const recording = await withPage({ viewport, contextOptions }, async (page) => {
|
|
129
135
|
// Layers go on before the navigation: a script that throws while the page
|
|
130
136
|
// loads, the request that never comes back and first paint all happen
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/tools/capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACzF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,EACV,iBAAiB,GAElB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,YAAY,EAAsB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAAE,eAAe,EAA0B,MAAM,oBAAoB,CAAC;AAE7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAEtD;;;GAGG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC;KACtC,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,+DAA+D,CAAC;IACzG,GAAG,qBAAqB;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,mDAAmD,CAAC;CAC3G,CAAC;KACD,WAAW,CAAC,iBAAiB,CAAC,CAAC;AAElC,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,qFAAqF,CAAC;IACrH,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,uBAAuB,CAAC;SAC5B,GAAG,CAAC,uBAAuB,CAAC;SAC5B,OAAO,CAAC,2BAA2B,CAAC;SACpC,QAAQ,CAAC,yBAAyB,CAAC;IACtC,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,mBAAmB,CAAC;SAC5B,QAAQ,CAAC,iGAAiG,CAAC;IAC9G,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,cAAc,CAAC;SACnB,OAAO,CAAC,kBAAkB,CAAC;SAC3B,QAAQ,CAAC,8BAA8B,CAAC;IAC3C,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,EAAE,CAAC;SACP,GAAG,CAAC,IAAI,CAAC;SACT,OAAO,CAAC,mBAAmB,CAAC;SAC5B,QAAQ,CAAC,+CAA+C,CAAC;IAC5D,QAAQ,EAAE,CAAC;SACR,MAAM,CAAC;QACN,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC;QACtF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,CAAC;KAC1F,CAAC;SACD,QAAQ,EAAE;SACV,QAAQ,CAAC,sCAAsC,CAAC;IACnD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4DAA4D,CAAC;IACtG,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,YAAY,EAAE,CAAC;SACZ,KAAK,CAAC,wBAAwB,CAAC;SAC/B,GAAG,CAAC,gBAAgB,CAAC;SACrB,QAAQ,EAAE;SACV,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,sBAAsB,EAAE,CAAC;SACtB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,mBAAmB,CAAC;SAC5B,QAAQ,CAAC,4EAA4E,CAAC;IACzF,eAAe,EAAE,CAAC;SACf,OAAO,EAAE;SACT,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CAAC,mGAAmG,CAAC;IAChH,eAAe,EAAE,CAAC;SACf,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,4FAA4F,CAAC;IACzG,WAAW,EAAE,CAAC;SACX,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,uGAAuG,CAAC;IACpH,mBAAmB,EAAE,CAAC;SACnB,OAAO,EAAE;SACT,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,qGAAqG,CAAC;CACnH,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAI9D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAsB;IACtD,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtD,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,mCAAmC,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAC3D,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC;IAC9C,0EAA0E;IAC1E,sEAAsE;IACtE,qEAAqE;IACrE,oEAAoE;IACpE,MAAM,cAAc,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAE1E,MAAM,MAAM,GACV,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhG,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC5E,0EAA0E;YAC1E,sEAAsE;YACtE,mEAAmE;YACnE,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE;gBACtC,OAAO,EAAE,KAAK,CAAC,eAAe;gBAC9B,OAAO,EAAE,KAAK,CAAC,eAAe;gBAC9B,GAAG,EAAE,KAAK,CAAC,WAAW;gBACtB,WAAW,EAAE,KAAK,CAAC,mBAAmB;aACvC,CAAC,CAAC;YACH,IAAI,CAAC;gBACH,+FAA+F;gBAC/F,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;gBACpF,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;oBACnB,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,mBAAmB,EAAE,CAAC,CAAC;gBACvG,CAAC;gBACD,MAAM,MAAM,GAAG,MAAM,YAAY,CAC/B,IAAI,EACJ,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,EAClE,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC,CAC3G,CAAC;gBACF,sEAAsE;gBACtE,sEAAsE;gBACtE,gEAAgE;gBAChE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;gBAClD,uEAAuE;gBACvE,uEAAuE;gBACvE,uDAAuD;gBACvD,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC/D,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,MAAM,cAAc,CAAC,SAAS,CAAC,MAAM,EAAE;YACrE,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAC,CAAC;QACH,YAAY,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;QAEvC,OAAO,eAAe,CAAC;YACrB,KAAK;YACL,YAAY;YACZ,WAAW,EAAE,SAAS,CAAC,WAAW;YAClC,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,SAAS,EAAE,SAAS,CAAC,QAAQ;YAC7B,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,YAAY,EAAE,MAAM;YACpB,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC;SACzD,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,WAAW,CAAC,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,YAAY,CACzB,IAAU,EACV,QAAuB,EACvB,YAA2B,EAC3B,SAAiB,EACjB,MAAyB;IAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,CAAC,YAAY,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QACD,MAAM,CAAC,SAAS,EAAE,CAAC;QACnB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,MAAM,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,YAAY,CAAC,IAAU;IACpC,IAAI,IAAI,CAAC,QAAQ,EAAE;QAAE,OAAO,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAClD,IAAI,KAAiC,CAAC;IACtC,MAAM,MAAM,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QAChD,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IACH,IAAI,CAAC;QACH,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;IAClE,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAI,IAAa;IAC9B,IAAI,CAAC;QACH,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,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;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAA2E,EAC3E,KAAc;IAEd,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;IACzC,MAAM,MAAM,GAAG,cAAc,KAAK,CAAC,GAAG,UAAU,CAAC;IAEjD,IAAI,+CAA+C,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAClE,OAAO,CACL,GAAG,MAAM,mDAAmD;YAC5D,2DAA2D,SAAS,GAAG,CACxE,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,IAAI,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9D,OAAO,GAAG,MAAM,cAAc,KAAK,CAAC,QAAQ,mCAAmC,KAAK,CAAC,mBAAmB,KAAK,CAAC;IAChH,CAAC;IACD,OAAO,GAAG,MAAM,IAAI,SAAS,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,KAAK,EAAE,SAAS;QAChB,WAAW,EACT,mGAAmG;YACnG,2FAA2F;YAC3F,uGAAuG;YACvG,yEAAyE;YACzE,uGAAuG;YACvG,iGAAiG;YACjG,sGAAsG;YACtG,qCAAqC;QACvC,WAAW,EAAE,iBAAiB;QAC9B,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;KACxG,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAClC,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 CAPTURE_INTERVAL_MS,\n DEFAULT_CAPTURE_DURATION_MS,\n DEFAULT_MAX_FRAMES,\n DEFAULT_SENSITIVITY,\n DEFAULT_VIEWPORT,\n MAX_CAPTURE_DURATION_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_FRAMES_CAP,\n MAX_INTERACTIONS,\n MIN_CAPTURE_DURATION_MS,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, attachLayers, summariseContext } from \"../engine/layers/index.js\";\nimport {\n CAPTURE_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport { recordFrames, type FrameRecorder } from \"../engine/recorder.js\";\nimport { formatDiffCards, type InteractionReport } from \"../utils/format.js\";\n\nexport const CAPTURE_TOOL_NAME = \"framewatch_capture\";\n\n/**\n * Re-exported where it is used: the capture summary's context lines. It lives\n * with the layers themselves now that `framewatch_interact` reports them too.\n */\nexport { summariseContext };\n\n/**\n * One step of a replayable interaction script. `delay_ms` is a wait *before*\n * the action, so a `wait` step is just a delay with nothing after it, and the\n * delays of successive steps accumulate over the recording.\n */\nexport const captureInteractionSchema = z\n .object({\n action: z.enum(CAPTURE_ACTIONS).describe(\"What to do: click, tap, type, scroll, swipe, wait or navigate\"),\n ...interactionFieldShape,\n delay_ms: z.number().int().min(0).default(0).describe(\"Wait this long (ms) before performing this action\"),\n })\n .superRefine(refineInteraction);\n\nexport const captureInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n duration_ms: z\n .number()\n .int()\n .min(MIN_CAPTURE_DURATION_MS)\n .max(MAX_CAPTURE_DURATION_MS)\n .default(DEFAULT_CAPTURE_DURATION_MS)\n .describe(\"How long to record (ms)\"),\n sensitivity: z\n .number()\n .min(0)\n .max(1)\n .default(DEFAULT_SENSITIVITY)\n .describe(\"Frame change threshold (0 = keep all, 1 = keep none). 0.06 means ~6% of grid cells must change.\"),\n max_frames: z\n .number()\n .int()\n .min(1)\n .max(MAX_FRAMES_CAP)\n .default(DEFAULT_MAX_FRAMES)\n .describe(\"Maximum diff cards to return\"),\n interval_ms: z\n .number()\n .int()\n .min(16)\n .max(2000)\n .default(CAPTURE_INTERVAL_MS)\n .describe(\"Raw frame capture interval (ms) — 100 = 10fps\"),\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)\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before recording starts\"),\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 interactions: z\n .array(captureInteractionSchema)\n .max(MAX_INTERACTIONS)\n .optional()\n .describe(\"Sequence of user interactions to replay during the recording\"),\n interaction_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) an interaction step may spend waiting for its target element\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Attach console logs, uncaught errors and unhandled rejections to the frames they happened between\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Attach network requests (method, url, status, duration) to the frames they settled between\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Attach a summary of the DOM mutations between frames — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at\"),\n};\n\nexport const captureInputSchema = z.object(captureInputShape);\nexport type CaptureInput = z.input<typeof captureInputSchema>;\ntype ParsedCaptureInput = z.output<typeof captureInputSchema>;\n\n/**\n * The main FrameWatch tool: record a page for `duration_ms` and return only\n * the meaningful visual changes as diff cards (see CLAUDE.md \"Diff Cards\" and\n * \"MCP Response Format\").\n *\n * Flow: navigate (waiting only for the navigation to commit, so loading and\n * splash animations are recorded from their first frame) → optionally wait\n * for `wait_for` → record raw frames every `interval_ms`, replaying\n * `interactions` as the recording runs → smart-diff them into at most\n * `max_frames` cards → format. All failures — including invalid input — are\n * reported as `isError` results rather than thrown so the MCP client sees a\n * useful message.\n */\nexport async function capturePage(rawInput: CaptureInput): Promise<CallToolResult> {\n const parsed = captureInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n const interactions = input.interactions ?? [];\n // Touch has to be decided when the context is created, so read it off the\n // script rather than turning it on for every capture: `hasTouch` puts\n // `ontouchstart` on window, which flips feature detection in most UI\n // libraries and would silently change what a plain capture records.\n const contextOptions = needsTouch(interactions) ? { hasTouch: true } : {};\n\n const report: InteractionReport | undefined =\n interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;\n\n try {\n const recording = await withPage({ viewport, contextOptions }, async (page) => {\n // Layers go on before the navigation: a script that throws while the page\n // loads, the request that never comes back and first paint all happen\n // before frame 0 exists, and they are the most useful things here.\n const layers = await attachLayers(page, {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n });\n try {\n // \"commit\" rather than \"load\": splash/loading animations must be recorded from the very start.\n await page.goto(input.url, { waitUntil: \"commit\", 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 const result = await recordFrames(\n page,\n { duration_ms: input.duration_ms, interval_ms: input.interval_ms },\n report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)),\n );\n // Drain the layers before anything else can close the page. `collect`\n // reads only what Node already holds, so a page that froze, navigated\n // away or crashed still yields everything it managed to report.\n const context = layers.collect(result.started_at);\n // The page may have died mid-recording (crash, closed browser). Frames\n // already captured are still worth returning, so never let reading the\n // title or url turn a partial recording into an error.\n return { ...result, context, ...(await describePage(page)) };\n } finally {\n layers.detach();\n }\n });\n\n const { cards, total_frames } = await buildDiffCards(recording.frames, {\n sensitivity: input.sensitivity,\n max_frames: input.max_frames,\n });\n applyContext(cards, recording.context);\n\n return formatDiffCards({\n cards,\n total_frames,\n duration_ms: recording.duration_ms,\n url: input.url,\n final_url: recording.finalUrl,\n title: recording.title,\n dropped: recording.dropped,\n interactions: report,\n notes: summariseContext(recording.context, cards.length),\n });\n } catch (error) {\n return errorResult(describeCaptureFailure(input, error));\n }\n}\n\n/**\n * Replay the interaction script while the recorder runs, forcing a frame after\n * every step so the differ always keeps the result of each action.\n *\n * A failing step ends the script but never the capture: the frames recorded so\n * far are the most useful thing FrameWatch can hand back (they show the state\n * the page was actually in when the selector did not match), so the failure is\n * recorded in `report` and a forced \"error\" frame is captured instead of\n * throwing. The recording then plays out the rest of its duration.\n */\nasync function replayScript(\n page: Page,\n recorder: FrameRecorder,\n interactions: Interaction[],\n timeoutMs: number,\n report: InteractionReport,\n): Promise<void> {\n for (let i = 0; i < interactions.length; i++) {\n const step = interactions[i];\n try {\n await executeInteraction(page, step, { timeout_ms: timeoutMs });\n } catch (error) {\n report.error = error instanceof Error ? error.message : String(error);\n report.failed_index = i + 1;\n await recorder.captureNow(\"error\");\n return;\n }\n report.completed++;\n report.steps.push(describeInteraction(step));\n await recorder.captureNow(\"interaction\");\n }\n}\n\n/**\n * Title and final url of a page. Both are cosmetic, so neither is allowed to\n * fail or delay the result: the page may have died during the recording, and\n * `page.title()` blocks for Playwright's full default timeout while the main\n * thread is busy — exactly the case a developer points this tool at.\n */\nasync function describePage(page: Page): Promise<{ title?: string; finalUrl?: string }> {\n if (page.isClosed()) return {};\n const finalUrl = safely(() => page.url());\n // Settle the losing side too: a page.title() that outlives the race would\n // otherwise reject unobserved when the context is closed underneath it.\n const title = page.title().catch(() => undefined);\n let timer: NodeJS.Timeout | undefined;\n const giveUp = new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS);\n });\n try {\n return { title: await Promise.race([title, giveUp]), finalUrl };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Mirrors\n * `describeFailure` in screenshot.ts: matches on the failing Playwright call\n * (the message prefix) rather than on substrings of user-supplied selectors,\n * so a navigation failure is never blamed on an element.\n */\nexport function describeCaptureFailure(\n input: Pick<ParsedCaptureInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Capture 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 return `${prefix} ${firstLine}`;\n}\n\nexport function registerCaptureTool(server: McpServer): void {\n server.registerTool(\n CAPTURE_TOOL_NAME,\n {\n title: \"Capture\",\n description:\n \"Record a web page for a few seconds and return only the meaningful visual changes as diff cards: \" +\n \"a full-frame PNG plus a crop of the changed region and its position for each kept frame. \" +\n \"Use it to check animations, splash/loading screens, transitions and anything that changes over time. \" +\n \"Lower `sensitivity` to keep more frames; `max_frames` caps the output. \" +\n \"Each frame can also carry the context for the interval that produced it: console output and uncaught \" +\n \"errors (on by default), plus network requests, DOM mutations and paint/layout-shift timing via \" +\n \"`include_network`, `include_dom` and `include_performance` — that is how you find out *why* a frame \" +\n \"looks wrong, not just that it does.\",\n inputSchema: captureInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => capturePage(args),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/tools/capture.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { BrowserContextOptions, Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CAPTURE_INTERVAL_MS,\n DEFAULT_CAPTURE_DURATION_MS,\n DEFAULT_MAX_FRAMES,\n DEFAULT_SENSITIVITY,\n DEFAULT_VIEWPORT,\n MAX_CAPTURE_DURATION_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_FRAMES_CAP,\n MAX_INTERACTIONS,\n MIN_CAPTURE_DURATION_MS,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { buildDiffCards } from \"../engine/differ.js\";\nimport { applyContext, attachLayers, summariseContext } from \"../engine/layers/index.js\";\nimport {\n CAPTURE_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n type Interaction,\n} from \"../engine/interaction.js\";\nimport { recordFrames, type FrameRecorder } from \"../engine/recorder.js\";\nimport { formatDiffCards, type InteractionReport } from \"../utils/format.js\";\nimport { readStorageState, storageStateField } from \"../utils/storage-state.js\";\n\nexport const CAPTURE_TOOL_NAME = \"framewatch_capture\";\n\n/**\n * Re-exported where it is used: the capture summary's context lines. It lives\n * with the layers themselves now that `framewatch_interact` reports them too.\n */\nexport { summariseContext };\n\n/**\n * One step of a replayable interaction script. `delay_ms` is a wait *before*\n * the action, so a `wait` step is just a delay with nothing after it, and the\n * delays of successive steps accumulate over the recording.\n */\nexport const captureInteractionSchema = z\n .object({\n action: z\n .enum(CAPTURE_ACTIONS)\n .describe(\"What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate\"),\n ...interactionFieldShape,\n delay_ms: z.number().int().min(0).default(0).describe(\"Wait this long (ms) before performing this action\"),\n })\n .superRefine(refineInteraction);\n\nexport const captureInputShape = {\n url: z.string().url().describe(\"URL to capture, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n duration_ms: z\n .number()\n .int()\n .min(MIN_CAPTURE_DURATION_MS)\n .max(MAX_CAPTURE_DURATION_MS)\n .default(DEFAULT_CAPTURE_DURATION_MS)\n .describe(\"How long to record (ms)\"),\n sensitivity: z\n .number()\n .min(0)\n .max(1)\n .default(DEFAULT_SENSITIVITY)\n .describe(\"Frame change threshold (0 = keep all, 1 = keep none). 0.06 means ~6% of grid cells must change.\"),\n max_frames: z\n .number()\n .int()\n .min(1)\n .max(MAX_FRAMES_CAP)\n .default(DEFAULT_MAX_FRAMES)\n .describe(\"Maximum diff cards to return\"),\n interval_ms: z\n .number()\n .int()\n .min(16)\n .max(2000)\n .default(CAPTURE_INTERVAL_MS)\n .describe(\"Raw frame capture interval (ms) — 100 = 10fps\"),\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)\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before recording starts\"),\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 interactions: z\n .array(captureInteractionSchema)\n .max(MAX_INTERACTIONS)\n .optional()\n .describe(\"Sequence of user interactions to replay during the recording\"),\n interaction_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) an interaction step may spend waiting for its target element\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Attach console logs, uncaught errors and unhandled rejections to the frames they happened between\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Attach network requests (method, url, status, duration) to the frames they settled between\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Attach a summary of the DOM mutations between frames — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Attach paint timing, largest contentful paint and layout shifts to the frames they were measured at\"),\n storage_state: storageStateField,\n};\n\nexport const captureInputSchema = z.object(captureInputShape);\nexport type CaptureInput = z.input<typeof captureInputSchema>;\ntype ParsedCaptureInput = z.output<typeof captureInputSchema>;\n\n/**\n * The main FrameWatch tool: record a page for `duration_ms` and return only\n * the meaningful visual changes as diff cards (see CLAUDE.md \"Diff Cards\" and\n * \"MCP Response Format\").\n *\n * Flow: navigate (waiting only for the navigation to commit, so loading and\n * splash animations are recorded from their first frame) → optionally wait\n * for `wait_for` → record raw frames every `interval_ms`, replaying\n * `interactions` as the recording runs → smart-diff them into at most\n * `max_frames` cards → format. All failures — including invalid input — are\n * reported as `isError` results rather than thrown so the MCP client sees a\n * useful message.\n */\nexport async function capturePage(rawInput: CaptureInput): Promise<CallToolResult> {\n const parsed = captureInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Capture failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n const interactions = input.interactions ?? [];\n // Touch has to be decided when the context is created, so read it off the\n // script rather than turning it on for every capture: `hasTouch` puts\n // `ontouchstart` on window, which flips feature detection in most UI\n // libraries and would silently change what a plain capture records.\n const contextOptions: BrowserContextOptions = needsTouch(interactions) ? { hasTouch: true } : {};\n\n const report: InteractionReport | undefined =\n interactions.length > 0 ? { total: interactions.length, completed: 0, steps: [] } : undefined;\n\n try {\n if (input.storage_state) contextOptions.storageState = await readStorageState(input.storage_state);\n\n const recording = await withPage({ viewport, contextOptions }, async (page) => {\n // Layers go on before the navigation: a script that throws while the page\n // loads, the request that never comes back and first paint all happen\n // before frame 0 exists, and they are the most useful things here.\n const layers = await attachLayers(page, {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n });\n try {\n // \"commit\" rather than \"load\": splash/loading animations must be recorded from the very start.\n await page.goto(input.url, { waitUntil: \"commit\", 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 const result = await recordFrames(\n page,\n { duration_ms: input.duration_ms, interval_ms: input.interval_ms },\n report && ((recorder) => replayScript(page, recorder, interactions, input.interaction_timeout_ms, report)),\n );\n // Drain the layers before anything else can close the page. `collect`\n // reads only what Node already holds, so a page that froze, navigated\n // away or crashed still yields everything it managed to report.\n const context = layers.collect(result.started_at);\n // The page may have died mid-recording (crash, closed browser). Frames\n // already captured are still worth returning, so never let reading the\n // title or url turn a partial recording into an error.\n return { ...result, context, ...(await describePage(page)) };\n } finally {\n layers.detach();\n }\n });\n\n const { cards, total_frames } = await buildDiffCards(recording.frames, {\n sensitivity: input.sensitivity,\n max_frames: input.max_frames,\n });\n applyContext(cards, recording.context);\n\n return formatDiffCards({\n cards,\n total_frames,\n duration_ms: recording.duration_ms,\n url: input.url,\n final_url: recording.finalUrl,\n title: recording.title,\n dropped: recording.dropped,\n interactions: report,\n notes: summariseContext(recording.context, cards.length),\n });\n } catch (error) {\n return errorResult(describeCaptureFailure(input, error));\n }\n}\n\n/**\n * Replay the interaction script while the recorder runs, forcing a frame after\n * every step so the differ always keeps the result of each action.\n *\n * A failing step ends the script but never the capture: the frames recorded so\n * far are the most useful thing FrameWatch can hand back (they show the state\n * the page was actually in when the selector did not match), so the failure is\n * recorded in `report` and a forced \"error\" frame is captured instead of\n * throwing. The recording then plays out the rest of its duration.\n */\nasync function replayScript(\n page: Page,\n recorder: FrameRecorder,\n interactions: Interaction[],\n timeoutMs: number,\n report: InteractionReport,\n): Promise<void> {\n for (let i = 0; i < interactions.length; i++) {\n const step = interactions[i];\n try {\n await executeInteraction(page, step, { timeout_ms: timeoutMs });\n } catch (error) {\n report.error = error instanceof Error ? error.message : String(error);\n report.failed_index = i + 1;\n await recorder.captureNow(\"error\");\n return;\n }\n report.completed++;\n report.steps.push(describeInteraction(step));\n await recorder.captureNow(\"interaction\");\n }\n}\n\n/**\n * Title and final url of a page. Both are cosmetic, so neither is allowed to\n * fail or delay the result: the page may have died during the recording, and\n * `page.title()` blocks for Playwright's full default timeout while the main\n * thread is busy — exactly the case a developer points this tool at.\n */\nasync function describePage(page: Page): Promise<{ title?: string; finalUrl?: string }> {\n if (page.isClosed()) return {};\n const finalUrl = safely(() => page.url());\n // Settle the losing side too: a page.title() that outlives the race would\n // otherwise reject unobserved when the context is closed underneath it.\n const title = page.title().catch(() => undefined);\n let timer: NodeJS.Timeout | undefined;\n const giveUp = new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS);\n });\n try {\n return { title: await Promise.race([title, giveUp]), finalUrl };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Mirrors\n * `describeFailure` in screenshot.ts: matches on the failing Playwright call\n * (the message prefix) rather than on substrings of user-supplied selectors,\n * so a navigation failure is never blamed on an element.\n */\nexport function describeCaptureFailure(\n input: Pick<ParsedCaptureInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Capture 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 return `${prefix} ${firstLine}`;\n}\n\nexport function registerCaptureTool(server: McpServer): void {\n server.registerTool(\n CAPTURE_TOOL_NAME,\n {\n title: \"Capture\",\n description:\n \"Record a web page for a few seconds and return only the meaningful visual changes as diff cards: \" +\n \"a full-frame PNG plus a crop of the changed region and its position for each kept frame. \" +\n \"Use it to check animations, splash/loading screens, transitions and anything that changes over time. \" +\n \"Lower `sensitivity` to keep more frames; `max_frames` caps the output. \" +\n \"Each frame can also carry the context for the interval that produced it: console output and uncaught \" +\n \"errors (on by default), plus network requests, DOM mutations and paint/layout-shift timing via \" +\n \"`include_network`, `include_dom` and `include_performance` — that is how you find out *why* a frame \" +\n \"looks wrong, not just that it does.\",\n inputSchema: captureInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => capturePage(args),\n );\n}\n"]}
|
package/dist/tools/compare.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const compareInputShape: {
|
|
|
20
20
|
width?: number | undefined;
|
|
21
21
|
height?: number | undefined;
|
|
22
22
|
}>>;
|
|
23
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
23
24
|
};
|
|
24
25
|
export declare const compareInputSchema: z.ZodObject<{
|
|
25
26
|
url_a: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -37,6 +38,7 @@ export declare const compareInputSchema: z.ZodObject<{
|
|
|
37
38
|
width?: number | undefined;
|
|
38
39
|
height?: number | undefined;
|
|
39
40
|
}>>;
|
|
41
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
40
42
|
}, "strip", z.ZodTypeAny, {
|
|
41
43
|
wait_ms: number;
|
|
42
44
|
wait_for_timeout_ms: number;
|
|
@@ -47,6 +49,7 @@ export declare const compareInputSchema: z.ZodObject<{
|
|
|
47
49
|
height: number;
|
|
48
50
|
} | undefined;
|
|
49
51
|
wait_for?: string | undefined;
|
|
52
|
+
storage_state?: string | undefined;
|
|
50
53
|
}, {
|
|
51
54
|
url_a: string;
|
|
52
55
|
url_b: string;
|
|
@@ -57,6 +60,7 @@ export declare const compareInputSchema: z.ZodObject<{
|
|
|
57
60
|
wait_ms?: number | undefined;
|
|
58
61
|
wait_for?: string | undefined;
|
|
59
62
|
wait_for_timeout_ms?: number | undefined;
|
|
63
|
+
storage_state?: string | undefined;
|
|
60
64
|
}>;
|
|
61
65
|
export type CompareInput = z.input<typeof compareInputSchema>;
|
|
62
66
|
type ParsedCompareInput = z.output<typeof compareInputSchema>;
|
package/dist/tools/compare.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CROP_PADDING_PX, DEFAULT_COMPARE_WAIT_MS, DEFAULT_VIEWPORT, MAX_VIEWPOR
|
|
|
3
3
|
import { getSessionPage, withPage, withSessionLock } from "../engine/browser.js";
|
|
4
4
|
import { computePixelMask, padBoundingBox } from "../engine/differ.js";
|
|
5
5
|
import { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from "../utils/image.js";
|
|
6
|
+
import { readStorageState, storageStateField } from "../utils/storage-state.js";
|
|
6
7
|
export const COMPARE_TOOL_NAME = "framewatch_compare";
|
|
7
8
|
/** `url_a` accepts this instead of a URL, meaning "whatever framewatch_interact is looking at". */
|
|
8
9
|
export const CURRENT_PAGE = "current";
|
|
@@ -38,6 +39,7 @@ export const compareInputShape = {
|
|
|
38
39
|
})
|
|
39
40
|
.optional()
|
|
40
41
|
.describe("Viewport for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)"),
|
|
42
|
+
storage_state: storageStateField,
|
|
41
43
|
};
|
|
42
44
|
export const compareInputSchema = z.object(compareInputShape);
|
|
43
45
|
/**
|
|
@@ -69,7 +71,11 @@ export async function comparePages(rawInput) {
|
|
|
69
71
|
const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;
|
|
70
72
|
const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];
|
|
71
73
|
try {
|
|
72
|
-
|
|
74
|
+
// Both URL sides get the same saved auth: they are two views of one app,
|
|
75
|
+
// and a comparison where only one side is signed in compares the login
|
|
76
|
+
// screen with the app. ("current" is read as it is, never reloaded.)
|
|
77
|
+
const storageState = input.storage_state ? await readStorageState(input.storage_state) : undefined;
|
|
78
|
+
const first = await captureSide(input, firstSide, undefined, storageState);
|
|
73
79
|
const notes = [];
|
|
74
80
|
let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
75
81
|
if (firstSide === CURRENT_PAGE) {
|
|
@@ -81,7 +87,7 @@ export async function comparePages(rawInput) {
|
|
|
81
87
|
}
|
|
82
88
|
viewport = open;
|
|
83
89
|
}
|
|
84
|
-
const second = await captureSide(input, secondSide, viewport);
|
|
90
|
+
const second = await captureSide(input, secondSide, viewport, storageState);
|
|
85
91
|
const [a, b] = openIsB ? [second, first] : [first, second];
|
|
86
92
|
return await render(input, a, b, notes);
|
|
87
93
|
}
|
|
@@ -99,7 +105,7 @@ export async function comparePages(rawInput) {
|
|
|
99
105
|
* flow — against a plain URL, and reloading or resizing it would destroy the
|
|
100
106
|
* very thing being compared.
|
|
101
107
|
*/
|
|
102
|
-
async function captureSide(input, side, viewport) {
|
|
108
|
+
async function captureSide(input, side, viewport, storageState) {
|
|
103
109
|
if (side === CURRENT_PAGE) {
|
|
104
110
|
// Under the session lock: an interaction running in parallel would
|
|
105
111
|
// otherwise be screenshotted half way through.
|
|
@@ -112,7 +118,11 @@ async function captureSide(input, side, viewport) {
|
|
|
112
118
|
return { png: await page.screenshot({ type: "png" }), url: page.url(), title: await safeTitle(page) };
|
|
113
119
|
});
|
|
114
120
|
}
|
|
115
|
-
|
|
121
|
+
const options = {
|
|
122
|
+
viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT },
|
|
123
|
+
...(storageState ? { contextOptions: { storageState } } : {}),
|
|
124
|
+
};
|
|
125
|
+
return withPage(options, async (page) => {
|
|
116
126
|
await page.goto(side, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
|
|
117
127
|
if (input.wait_for) {
|
|
118
128
|
await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/tools/compare.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CROP_PADDING_PX,\n DEFAULT_COMPARE_WAIT_MS,\n DEFAULT_VIEWPORT,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withPage, withSessionLock } from \"../engine/browser.js\";\nimport { computePixelMask, padBoundingBox } from \"../engine/differ.js\";\nimport type { BoundingBox, Viewport } from \"../types.js\";\nimport { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from \"../utils/image.js\";\n\nexport const COMPARE_TOOL_NAME = \"framewatch_compare\";\n\n/** `url_a` accepts this instead of a URL, meaning \"whatever framewatch_interact is looking at\". */\nexport const CURRENT_PAGE = \"current\";\n\n/**\n * Either a URL or the literal \"current\". `z.string().url()` cannot express\n * that, so the check is spelled out — and the message says both options,\n * because \"invalid url\" on the word `current` is a confusing thing to read.\n */\nconst sideSchema = z.string().refine((value) => value === CURRENT_PAGE || isUrl(value), {\n message: `must be a URL, or \"${CURRENT_PAGE}\" to use the page framewatch_interact currently has open`,\n});\n\nexport const compareInputShape = {\n url_a: sideSchema.describe(\n `First URL — the \"before\" side. Pass \"${CURRENT_PAGE}\" to compare against the page framewatch_interact has open, ` +\n \"in whatever state your interactions left it.\",\n ),\n url_b: sideSchema.describe('Second URL — the \"after\" side.'),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_COMPARE_WAIT_MS)\n .describe(\"Wait time (ms) after page load before each screenshot\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) on both sides before capturing\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)\"),\n};\n\nexport const compareInputSchema = z.object(compareInputShape);\nexport type CompareInput = z.input<typeof compareInputSchema>;\ntype ParsedCompareInput = z.output<typeof compareInputSchema>;\n\n/** One captured side of the comparison. */\ninterface Side {\n png: Buffer;\n /** Where the shot was actually taken (after redirects), or the open page's URL. */\n url: string;\n title?: string;\n}\n\n/**\n * Compare two pages — or the same page before and after a change — and show\n * what moved.\n *\n * Returns both screenshots plus an overlay: side B with every differing pixel\n * tinted, which is the part that turns \"3.4% changed\" into something a reader\n * can act on. The comparison is the same pixel comparison the capture engine\n * uses between frames, so a change region here means what it means there.\n *\n * The two sides are captured one after the other rather than at once, so both\n * get an unloaded machine and the same `wait_ms` means the same thing for\n * each; a difference in the result should come from the pages, not from which\n * of them was competing for the CPU. (Which of the two goes first is decided\n * by `\"current\"` — see below — not by their order in the input.)\n */\nexport async function comparePages(rawInput: CompareInput): Promise<CallToolResult> {\n const parsed = compareInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Compare failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n // The open page is the one side that cannot be resized — doing so would\n // change the state being compared — so it is captured first whichever side\n // it is, and its size is what the other side is then captured at. Frames of\n // two different sizes cannot be compared pixel for pixel at all.\n const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;\n const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];\n\n try {\n const first = await captureSide(input, firstSide);\n const notes: string[] = [];\n let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n if (firstSide === CURRENT_PAGE) {\n const open = await sizeOf(first.png);\n if (input.viewport && (input.viewport.width !== open.width || input.viewport.height !== open.height)) {\n notes.push(\n `Ignored \\`viewport\\` ${input.viewport.width}x${input.viewport.height}: the open page is ` +\n `${open.width}x${open.height} and resizing it would change the state being compared. ` +\n \"Both sides were captured at the open page's size.\",\n );\n }\n viewport = open;\n }\n\n const second = await captureSide(input, secondSide, viewport);\n const [a, b] = openIsB ? [second, first] : [first, second];\n return await render(input, a, b, notes);\n } catch (error) {\n return errorResult(describeCompareFailure(input, error));\n }\n}\n\n/**\n * Screenshot one side.\n *\n * A URL gets a fresh context, so nothing from the other side (or from an\n * earlier tool call) can leak into it. \"current\" instead reads the page\n * `framewatch_interact` has open, *without* touching it: the whole point of\n * that option is to compare a hand-built state — three clicks deep into a\n * flow — against a plain URL, and reloading or resizing it would destroy the\n * very thing being compared.\n */\nasync function captureSide(input: ParsedCompareInput, side: string, viewport?: Viewport): Promise<Side> {\n if (side === CURRENT_PAGE) {\n // Under the session lock: an interaction running in parallel would\n // otherwise be screenshotted half way through.\n return withSessionLock(async () => {\n const { page } = await getSessionPage();\n if (page.url() === \"about:blank\") {\n throw new Error(\n `no page is open, so there is no \"${CURRENT_PAGE}\" state to compare — ` +\n \"use framewatch_interact first, or pass a URL here.\",\n );\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n }\n\n return withPage({ viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT } }, async (page) => {\n await page.goto(side, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n}\n\n/**\n * Build the result: a summary, both frames, and (when the two are comparable\n * and actually differ) the overlay.\n *\n * Sides of different sizes are reported as such rather than being stretched to\n * match. Rescaling one of them would resample every pixel in it, and the\n * \"differences\" that came back would be the resampling, not the pages.\n */\nasync function render(input: ParsedCompareInput, a: Side, b: Side, notes: string[]): Promise<CallToolResult> {\n const [grayA, grayB] = await Promise.all([toGrayscale(a.png), toGrayscale(b.png)]);\n const nameA = describeSide(input.url_a, a);\n const nameB = describeSide(input.url_b, b);\n const sameSize = grayA.width === grayB.width && grayA.height === grayB.height;\n\n const lines: string[] = [];\n let overlay: Buffer | undefined;\n let overlayNote = \"\";\n\n if (!sameSize) {\n lines.push(\n `Compared ${nameA} against ${nameB} — the two frames are different sizes ` +\n `(${grayA.width}x${grayA.height} vs ${grayB.width}x${grayB.height}), so they cannot be compared pixel ` +\n \"for pixel. Both are below.\",\n );\n } else {\n const diff = computePixelMask(grayA.data, grayB.data, grayA.width, grayA.height);\n const region: BoundingBox | null =\n diff.bbox === null ? null : padBoundingBox(diff.bbox, CROP_PADDING_PX, grayA.width, grayA.height);\n\n lines.push(\n `Compared ${nameA} against ${nameB} at ${grayA.width}x${grayA.height} — ` +\n `${diff.changePercent.toFixed(2)}% of pixels differ` +\n (region === null\n ? \". The two are pixel-identical.\"\n : `, all of it within ${region.x},${region.y} ${region.width}x${region.height}.`),\n );\n\n if (region !== null) {\n overlay = await resizeForOutput(await overlayMask(b.png, diff.mask, grayA.width, grayA.height));\n overlayNote =\n `Diff overlay — B with every differing pixel tinted. ${diff.changedPixels} of ${diff.totalPixels} ` +\n `pixels changed (${diff.changePercent.toFixed(2)}%), within ` +\n `${region.x},${region.y} ${region.width}x${region.height}.`;\n }\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: [...lines, ...notes].join(\"\\n\") }];\n await pushFrame(content, a.png, `A — ${nameA}`);\n await pushFrame(content, b.png, `B — ${nameB}`);\n if (overlay) {\n content.push({ type: \"image\", data: toBase64(overlay), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: overlayNote });\n }\n return { content };\n}\n\nasync function pushFrame(content: CallToolResult[\"content\"], png: Buffer, label: string): Promise<void> {\n const resized = await resizeForOutput(png);\n const { width, height } = await getDimensions(resized);\n content.push({ type: \"image\", data: toBase64(resized), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: `${label} — image ${width}x${height}` });\n}\n\n/** How a side is named in the output: where it ended up, and its title if it has one. */\nfunction describeSide(requested: string, side: Side): string {\n const where = requested === CURRENT_PAGE ? `the open page (${side.url})` : side.url;\n return side.title ? `${where} \"${side.title}\"` : where;\n}\n\n/** Cosmetic, and never allowed to fail or stall: a busy page still gets compared. */\nasync function safeTitle(page: Page): Promise<string | undefined> {\n return Promise.race([\n page.title().catch(() => undefined),\n new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS)),\n ]);\n}\n\nasync function sizeOf(png: Buffer): Promise<Viewport> {\n const { width, height } = await getDimensions(png);\n return { width, height };\n}\n\nfunction isUrl(value: string): boolean {\n try {\n new URL(value);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a failed comparison. Mirrors `describeFailure` in\n * screenshot.ts, with one addition: a `page.goto` failure names which of the\n * two sides could not be opened, which is the first thing you want to know.\n */\nexport function describeCompareFailure(\n input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = \"Compare failed:\";\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (/^page\\.goto:/.test(message)) {\n const side = failingSide(input, message);\n return `${prefix} could not open ${side} — ${firstLine}`;\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n return `${prefix} ${firstLine}`;\n}\n\n/** Playwright quotes the URL it tried in a goto failure; that says which side broke. */\nfunction failingSide(input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\">, message: string): string {\n if (input.url_b !== CURRENT_PAGE && message.includes(input.url_b)) return `url_b (${input.url_b})`;\n if (input.url_a !== CURRENT_PAGE && message.includes(input.url_a)) return `url_a (${input.url_a})`;\n return \"one of the two pages\";\n}\n\nexport function registerCompareTool(server: McpServer): void {\n server.registerTool(\n COMPARE_TOOL_NAME,\n {\n title: \"Compare\",\n description:\n \"Compare two pages — two URLs, or the same URL before and after a code change — and show what moved. \" +\n \"Returns both screenshots, the percentage of pixels that differ, the region they differ in, and an \" +\n `overlay of the second page with every changed pixel tinted. Pass \"${CURRENT_PAGE}\" as \\`url_a\\` to ` +\n \"compare against the page framewatch_interact has open, in whatever state your interactions left it.\",\n inputSchema: compareInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => comparePages(args),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../src/tools/compare.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n CROP_PADDING_PX,\n DEFAULT_COMPARE_WAIT_MS,\n DEFAULT_VIEWPORT,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n PAGE_INFO_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withPage, withSessionLock } from \"../engine/browser.js\";\nimport { computePixelMask, padBoundingBox } from \"../engine/differ.js\";\nimport type { BoundingBox, Viewport } from \"../types.js\";\nimport { getDimensions, overlayMask, resizeForOutput, toBase64, toGrayscale } from \"../utils/image.js\";\nimport { readStorageState, storageStateField, type StorageState } from \"../utils/storage-state.js\";\n\nexport const COMPARE_TOOL_NAME = \"framewatch_compare\";\n\n/** `url_a` accepts this instead of a URL, meaning \"whatever framewatch_interact is looking at\". */\nexport const CURRENT_PAGE = \"current\";\n\n/**\n * Either a URL or the literal \"current\". `z.string().url()` cannot express\n * that, so the check is spelled out — and the message says both options,\n * because \"invalid url\" on the word `current` is a confusing thing to read.\n */\nconst sideSchema = z.string().refine((value) => value === CURRENT_PAGE || isUrl(value), {\n message: `must be a URL, or \"${CURRENT_PAGE}\" to use the page framewatch_interact currently has open`,\n});\n\nexport const compareInputShape = {\n url_a: sideSchema.describe(\n `First URL — the \"before\" side. Pass \"${CURRENT_PAGE}\" to compare against the page framewatch_interact has open, ` +\n \"in whatever state your interactions left it.\",\n ),\n url_b: sideSchema.describe('Second URL — the \"after\" side.'),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_COMPARE_WAIT_MS)\n .describe(\"Wait time (ms) after page load before each screenshot\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) on both sides before capturing\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport for both sides (defaults to 1280x720, or the open page's size when comparing against `current`)\"),\n storage_state: storageStateField,\n};\n\nexport const compareInputSchema = z.object(compareInputShape);\nexport type CompareInput = z.input<typeof compareInputSchema>;\ntype ParsedCompareInput = z.output<typeof compareInputSchema>;\n\n/** One captured side of the comparison. */\ninterface Side {\n png: Buffer;\n /** Where the shot was actually taken (after redirects), or the open page's URL. */\n url: string;\n title?: string;\n}\n\n/**\n * Compare two pages — or the same page before and after a change — and show\n * what moved.\n *\n * Returns both screenshots plus an overlay: side B with every differing pixel\n * tinted, which is the part that turns \"3.4% changed\" into something a reader\n * can act on. The comparison is the same pixel comparison the capture engine\n * uses between frames, so a change region here means what it means there.\n *\n * The two sides are captured one after the other rather than at once, so both\n * get an unloaded machine and the same `wait_ms` means the same thing for\n * each; a difference in the result should come from the pages, not from which\n * of them was competing for the CPU. (Which of the two goes first is decided\n * by `\"current\"` — see below — not by their order in the input.)\n */\nexport async function comparePages(rawInput: CompareInput): Promise<CallToolResult> {\n const parsed = compareInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Compare failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n // The open page is the one side that cannot be resized — doing so would\n // change the state being compared — so it is captured first whichever side\n // it is, and its size is what the other side is then captured at. Frames of\n // two different sizes cannot be compared pixel for pixel at all.\n const openIsB = input.url_b === CURRENT_PAGE && input.url_a !== CURRENT_PAGE;\n const [firstSide, secondSide] = openIsB ? [input.url_b, input.url_a] : [input.url_a, input.url_b];\n\n try {\n // Both URL sides get the same saved auth: they are two views of one app,\n // and a comparison where only one side is signed in compares the login\n // screen with the app. (\"current\" is read as it is, never reloaded.)\n const storageState = input.storage_state ? await readStorageState(input.storage_state) : undefined;\n\n const first = await captureSide(input, firstSide, undefined, storageState);\n const notes: string[] = [];\n let viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n if (firstSide === CURRENT_PAGE) {\n const open = await sizeOf(first.png);\n if (input.viewport && (input.viewport.width !== open.width || input.viewport.height !== open.height)) {\n notes.push(\n `Ignored \\`viewport\\` ${input.viewport.width}x${input.viewport.height}: the open page is ` +\n `${open.width}x${open.height} and resizing it would change the state being compared. ` +\n \"Both sides were captured at the open page's size.\",\n );\n }\n viewport = open;\n }\n\n const second = await captureSide(input, secondSide, viewport, storageState);\n const [a, b] = openIsB ? [second, first] : [first, second];\n return await render(input, a, b, notes);\n } catch (error) {\n return errorResult(describeCompareFailure(input, error));\n }\n}\n\n/**\n * Screenshot one side.\n *\n * A URL gets a fresh context, so nothing from the other side (or from an\n * earlier tool call) can leak into it. \"current\" instead reads the page\n * `framewatch_interact` has open, *without* touching it: the whole point of\n * that option is to compare a hand-built state — three clicks deep into a\n * flow — against a plain URL, and reloading or resizing it would destroy the\n * very thing being compared.\n */\nasync function captureSide(\n input: ParsedCompareInput,\n side: string,\n viewport?: Viewport,\n storageState?: StorageState,\n): Promise<Side> {\n if (side === CURRENT_PAGE) {\n // Under the session lock: an interaction running in parallel would\n // otherwise be screenshotted half way through.\n return withSessionLock(async () => {\n const { page } = await getSessionPage();\n if (page.url() === \"about:blank\") {\n throw new Error(\n `no page is open, so there is no \"${CURRENT_PAGE}\" state to compare — ` +\n \"use framewatch_interact first, or pass a URL here.\",\n );\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n }\n\n const options = {\n viewport: viewport ?? input.viewport ?? { ...DEFAULT_VIEWPORT },\n ...(storageState ? { contextOptions: { storageState } } : {}),\n };\n return withPage(options, async (page) => {\n await page.goto(side, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n return { png: await page.screenshot({ type: \"png\" }), url: page.url(), title: await safeTitle(page) };\n });\n}\n\n/**\n * Build the result: a summary, both frames, and (when the two are comparable\n * and actually differ) the overlay.\n *\n * Sides of different sizes are reported as such rather than being stretched to\n * match. Rescaling one of them would resample every pixel in it, and the\n * \"differences\" that came back would be the resampling, not the pages.\n */\nasync function render(input: ParsedCompareInput, a: Side, b: Side, notes: string[]): Promise<CallToolResult> {\n const [grayA, grayB] = await Promise.all([toGrayscale(a.png), toGrayscale(b.png)]);\n const nameA = describeSide(input.url_a, a);\n const nameB = describeSide(input.url_b, b);\n const sameSize = grayA.width === grayB.width && grayA.height === grayB.height;\n\n const lines: string[] = [];\n let overlay: Buffer | undefined;\n let overlayNote = \"\";\n\n if (!sameSize) {\n lines.push(\n `Compared ${nameA} against ${nameB} — the two frames are different sizes ` +\n `(${grayA.width}x${grayA.height} vs ${grayB.width}x${grayB.height}), so they cannot be compared pixel ` +\n \"for pixel. Both are below.\",\n );\n } else {\n const diff = computePixelMask(grayA.data, grayB.data, grayA.width, grayA.height);\n const region: BoundingBox | null =\n diff.bbox === null ? null : padBoundingBox(diff.bbox, CROP_PADDING_PX, grayA.width, grayA.height);\n\n lines.push(\n `Compared ${nameA} against ${nameB} at ${grayA.width}x${grayA.height} — ` +\n `${diff.changePercent.toFixed(2)}% of pixels differ` +\n (region === null\n ? \". The two are pixel-identical.\"\n : `, all of it within ${region.x},${region.y} ${region.width}x${region.height}.`),\n );\n\n if (region !== null) {\n overlay = await resizeForOutput(await overlayMask(b.png, diff.mask, grayA.width, grayA.height));\n overlayNote =\n `Diff overlay — B with every differing pixel tinted. ${diff.changedPixels} of ${diff.totalPixels} ` +\n `pixels changed (${diff.changePercent.toFixed(2)}%), within ` +\n `${region.x},${region.y} ${region.width}x${region.height}.`;\n }\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: [...lines, ...notes].join(\"\\n\") }];\n await pushFrame(content, a.png, `A — ${nameA}`);\n await pushFrame(content, b.png, `B — ${nameB}`);\n if (overlay) {\n content.push({ type: \"image\", data: toBase64(overlay), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: overlayNote });\n }\n return { content };\n}\n\nasync function pushFrame(content: CallToolResult[\"content\"], png: Buffer, label: string): Promise<void> {\n const resized = await resizeForOutput(png);\n const { width, height } = await getDimensions(resized);\n content.push({ type: \"image\", data: toBase64(resized), mimeType: \"image/png\" });\n content.push({ type: \"text\", text: `${label} — image ${width}x${height}` });\n}\n\n/** How a side is named in the output: where it ended up, and its title if it has one. */\nfunction describeSide(requested: string, side: Side): string {\n const where = requested === CURRENT_PAGE ? `the open page (${side.url})` : side.url;\n return side.title ? `${where} \"${side.title}\"` : where;\n}\n\n/** Cosmetic, and never allowed to fail or stall: a busy page still gets compared. */\nasync function safeTitle(page: Page): Promise<string | undefined> {\n return Promise.race([\n page.title().catch(() => undefined),\n new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), PAGE_INFO_TIMEOUT_MS)),\n ]);\n}\n\nasync function sizeOf(png: Buffer): Promise<Viewport> {\n const { width, height } = await getDimensions(png);\n return { width, height };\n}\n\nfunction isUrl(value: string): boolean {\n try {\n new URL(value);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a failed comparison. Mirrors `describeFailure` in\n * screenshot.ts, with one addition: a `page.goto` failure names which of the\n * two sides could not be opened, which is the first thing you want to know.\n */\nexport function describeCompareFailure(\n input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = \"Compare failed:\";\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (/^page\\.goto:/.test(message)) {\n const side = failingSide(input, message);\n return `${prefix} could not open ${side} — ${firstLine}`;\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n return `${prefix} ${firstLine}`;\n}\n\n/** Playwright quotes the URL it tried in a goto failure; that says which side broke. */\nfunction failingSide(input: Pick<ParsedCompareInput, \"url_a\" | \"url_b\">, message: string): string {\n if (input.url_b !== CURRENT_PAGE && message.includes(input.url_b)) return `url_b (${input.url_b})`;\n if (input.url_a !== CURRENT_PAGE && message.includes(input.url_a)) return `url_a (${input.url_a})`;\n return \"one of the two pages\";\n}\n\nexport function registerCompareTool(server: McpServer): void {\n server.registerTool(\n COMPARE_TOOL_NAME,\n {\n title: \"Compare\",\n description:\n \"Compare two pages — two URLs, or the same URL before and after a code change — and show what moved. \" +\n \"Returns both screenshots, the percentage of pixels that differ, the region they differ in, and an \" +\n `overlay of the second page with every changed pixel tinted. Pass \"${CURRENT_PAGE}\" as \\`url_a\\` to ` +\n \"compare against the page framewatch_interact has open, in whatever state your interactions left it.\",\n inputSchema: compareInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => comparePages(args),\n );\n}\n"]}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { registerInteractTool, performInteraction, INTERACT_TOOL_NAME } from "./
|
|
|
5
5
|
export { registerResponsiveTool, captureResponsive, RESPONSIVE_TOOL_NAME } from "./responsive.js";
|
|
6
6
|
export { registerAccessibilityTool, auditAccessibility, ACCESSIBILITY_TOOL_NAME } from "./accessibility.js";
|
|
7
7
|
export { registerCompareTool, comparePages, COMPARE_TOOL_NAME, CURRENT_PAGE } from "./compare.js";
|
|
8
|
+
export { registerSaveAuthTool, saveAuth, SAVE_AUTH_TOOL_NAME } from "./save-auth.js";
|
|
8
9
|
export { registerServerTools, startServer, stopServer, START_SERVER_TOOL_NAME, STOP_SERVER_TOOL_NAME, } from "./server.js";
|
|
9
10
|
/** Register every FrameWatch tool on the given server. */
|
|
10
11
|
export declare function registerAllTools(server: McpServer): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerCaptureTool } from "./capture.js";
|
|
|
3
3
|
import { registerCompareTool } from "./compare.js";
|
|
4
4
|
import { registerInteractTool } from "./interact.js";
|
|
5
5
|
import { registerResponsiveTool } from "./responsive.js";
|
|
6
|
+
import { registerSaveAuthTool } from "./save-auth.js";
|
|
6
7
|
import { registerScreenshotTool } from "./screenshot.js";
|
|
7
8
|
import { registerServerTools } from "./server.js";
|
|
8
9
|
export { registerScreenshotTool, takeScreenshot, SCREENSHOT_TOOL_NAME } from "./screenshot.js";
|
|
@@ -11,6 +12,7 @@ export { registerInteractTool, performInteraction, INTERACT_TOOL_NAME } from "./
|
|
|
11
12
|
export { registerResponsiveTool, captureResponsive, RESPONSIVE_TOOL_NAME } from "./responsive.js";
|
|
12
13
|
export { registerAccessibilityTool, auditAccessibility, ACCESSIBILITY_TOOL_NAME } from "./accessibility.js";
|
|
13
14
|
export { registerCompareTool, comparePages, COMPARE_TOOL_NAME, CURRENT_PAGE } from "./compare.js";
|
|
15
|
+
export { registerSaveAuthTool, saveAuth, SAVE_AUTH_TOOL_NAME } from "./save-auth.js";
|
|
14
16
|
export { registerServerTools, startServer, stopServer, START_SERVER_TOOL_NAME, STOP_SERVER_TOOL_NAME, } from "./server.js";
|
|
15
17
|
/** Register every FrameWatch tool on the given server. */
|
|
16
18
|
export function registerAllTools(server) {
|
|
@@ -20,6 +22,7 @@ export function registerAllTools(server) {
|
|
|
20
22
|
registerResponsiveTool(server);
|
|
21
23
|
registerAccessibilityTool(server);
|
|
22
24
|
registerCompareTool(server);
|
|
25
|
+
registerSaveAuthTool(server);
|
|
23
26
|
registerServerTools(server);
|
|
24
27
|
}
|
|
25
28
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAClG,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC5G,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAErB,0DAA0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { registerAccessibilityTool } from \"./accessibility.js\";\nimport { registerCaptureTool } from \"./capture.js\";\nimport { registerCompareTool } from \"./compare.js\";\nimport { registerInteractTool } from \"./interact.js\";\nimport { registerResponsiveTool } from \"./responsive.js\";\nimport { registerScreenshotTool } from \"./screenshot.js\";\nimport { registerServerTools } from \"./server.js\";\n\nexport { registerScreenshotTool, takeScreenshot, SCREENSHOT_TOOL_NAME } from \"./screenshot.js\";\nexport { registerCaptureTool, capturePage, CAPTURE_TOOL_NAME } from \"./capture.js\";\nexport { registerInteractTool, performInteraction, INTERACT_TOOL_NAME } from \"./interact.js\";\nexport { registerResponsiveTool, captureResponsive, RESPONSIVE_TOOL_NAME } from \"./responsive.js\";\nexport { registerAccessibilityTool, auditAccessibility, ACCESSIBILITY_TOOL_NAME } from \"./accessibility.js\";\nexport { registerCompareTool, comparePages, COMPARE_TOOL_NAME, CURRENT_PAGE } from \"./compare.js\";\nexport {\n registerServerTools,\n startServer,\n stopServer,\n START_SERVER_TOOL_NAME,\n STOP_SERVER_TOOL_NAME,\n} from \"./server.js\";\n\n/** Register every FrameWatch tool on the given server. */\nexport function registerAllTools(server: McpServer): void {\n registerScreenshotTool(server);\n registerCaptureTool(server);\n registerInteractTool(server);\n registerResponsiveTool(server);\n registerAccessibilityTool(server);\n registerCompareTool(server);\n registerServerTools(server);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAClG,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC5G,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAErB,0DAA0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { registerAccessibilityTool } from \"./accessibility.js\";\nimport { registerCaptureTool } from \"./capture.js\";\nimport { registerCompareTool } from \"./compare.js\";\nimport { registerInteractTool } from \"./interact.js\";\nimport { registerResponsiveTool } from \"./responsive.js\";\nimport { registerSaveAuthTool } from \"./save-auth.js\";\nimport { registerScreenshotTool } from \"./screenshot.js\";\nimport { registerServerTools } from \"./server.js\";\n\nexport { registerScreenshotTool, takeScreenshot, SCREENSHOT_TOOL_NAME } from \"./screenshot.js\";\nexport { registerCaptureTool, capturePage, CAPTURE_TOOL_NAME } from \"./capture.js\";\nexport { registerInteractTool, performInteraction, INTERACT_TOOL_NAME } from \"./interact.js\";\nexport { registerResponsiveTool, captureResponsive, RESPONSIVE_TOOL_NAME } from \"./responsive.js\";\nexport { registerAccessibilityTool, auditAccessibility, ACCESSIBILITY_TOOL_NAME } from \"./accessibility.js\";\nexport { registerCompareTool, comparePages, COMPARE_TOOL_NAME, CURRENT_PAGE } from \"./compare.js\";\nexport { registerSaveAuthTool, saveAuth, SAVE_AUTH_TOOL_NAME } from \"./save-auth.js\";\nexport {\n registerServerTools,\n startServer,\n stopServer,\n START_SERVER_TOOL_NAME,\n STOP_SERVER_TOOL_NAME,\n} from \"./server.js\";\n\n/** Register every FrameWatch tool on the given server. */\nexport function registerAllTools(server: McpServer): void {\n registerScreenshotTool(server);\n registerCaptureTool(server);\n registerInteractTool(server);\n registerResponsiveTool(server);\n registerAccessibilityTool(server);\n registerCompareTool(server);\n registerSaveAuthTool(server);\n registerServerTools(server);\n}\n"]}
|