framewatch-mcp-server 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +895 -15
- package/dist/constants.d.ts +274 -0
- package/dist/constants.js +279 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +26 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +12 -7
- package/dist/engine/interaction.js +110 -18
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +9 -2
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +90 -26
- package/dist/tools/capture.js +109 -58
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +16 -5
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +45 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +37 -18
- package/dist/tools/interact.js +113 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +10 -6
- package/dist/tools/responsive.js +21 -4
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- 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 +15 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +76 -0
- package/dist/utils/storage-state.js +195 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
package/dist/tools/interact.js
CHANGED
|
@@ -4,13 +4,27 @@ import { getSessionPage, withSessionLock } from "../engine/browser.js";
|
|
|
4
4
|
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
|
-
import {
|
|
7
|
+
import { takeSnapshot } from "../engine/snapshot.js";
|
|
8
|
+
import { describeScale, formatCardMeta } from "../utils/format.js";
|
|
9
|
+
import { refSelector } from "../utils/snapshot-rules.js";
|
|
10
|
+
import { describeAuth, loginFormVisible, resolveStorageState, storageStateField } from "../utils/storage-state.js";
|
|
11
|
+
import { DEFAULT_SNAPSHOT_MAX_CHARS, VUE_DETECT_MS, VUE_READY_TIMEOUT_MS } from "../constants.js";
|
|
12
|
+
import { hmrFor } from "../engine/hmr.js";
|
|
13
|
+
import { detectVue, waitForVueReady } from "../engine/vue.js";
|
|
14
|
+
import { describeVue } from "../utils/vue-rules.js";
|
|
15
|
+
import { describeCounts } from "./snapshot.js";
|
|
16
|
+
import { markImage } from "../utils/budget.js";
|
|
8
17
|
export const INTERACT_TOOL_NAME = "framewatch_interact";
|
|
9
18
|
export const interactInputShape = {
|
|
10
19
|
action: z
|
|
11
20
|
.enum(INTERACT_ACTIONS)
|
|
12
|
-
.describe("What to do: click, tap, type, scroll, swipe, navigate, select or hover"),
|
|
21
|
+
.describe("What to do: click, tap, type, key, scroll, swipe, navigate, select or hover"),
|
|
13
22
|
...interactionFieldShape,
|
|
23
|
+
ref: z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^e\d+$/, "a ref looks like `e8` — take one from framewatch_snapshot")
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Element ref from framewatch_snapshot (e.g. `e8`) to act on, instead of a `selector`"),
|
|
14
28
|
url: z
|
|
15
29
|
.string()
|
|
16
30
|
.url()
|
|
@@ -51,8 +65,20 @@ export const interactInputShape = {
|
|
|
51
65
|
.boolean()
|
|
52
66
|
.default(false)
|
|
53
67
|
.describe("Report paint timing and layout shifts measured around the action"),
|
|
68
|
+
include_snapshot: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.default(false)
|
|
71
|
+
.describe("Append a framewatch_snapshot of the page after the action, with fresh refs for the next call"),
|
|
72
|
+
storage_state: storageStateField,
|
|
54
73
|
};
|
|
55
|
-
export const interactInputSchema = z.object(interactInputShape).superRefine(
|
|
74
|
+
export const interactInputSchema = z.object(interactInputShape).superRefine((value, ctx) => {
|
|
75
|
+
if (value.ref !== undefined && value.selector !== undefined) {
|
|
76
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "pass either `ref` or `selector`, not both", path: ["ref"] });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// A ref is a selector Playwright understands, so the step is validated as one.
|
|
80
|
+
refineInteraction(value.ref !== undefined ? { ...value, selector: refSelector(value.ref) } : value, ctx);
|
|
81
|
+
});
|
|
56
82
|
/**
|
|
57
83
|
* Perform one interaction on the current page and show what it did.
|
|
58
84
|
*
|
|
@@ -79,9 +105,10 @@ async function runInteraction(rawInput) {
|
|
|
79
105
|
return errorResult(`Interaction failed: invalid input — ${issues}`);
|
|
80
106
|
}
|
|
81
107
|
const input = parsed.data;
|
|
108
|
+
const selector = input.ref !== undefined ? refSelector(input.ref) : input.selector;
|
|
82
109
|
const step = {
|
|
83
110
|
action: input.action,
|
|
84
|
-
...(
|
|
111
|
+
...(selector !== undefined ? { selector } : {}),
|
|
85
112
|
...(input.value !== undefined ? { value: input.value } : {}),
|
|
86
113
|
...(input.x !== undefined ? { x: input.x } : {}),
|
|
87
114
|
...(input.y !== undefined ? { y: input.y } : {}),
|
|
@@ -95,9 +122,15 @@ async function runInteraction(rawInput) {
|
|
|
95
122
|
performance: input.include_performance,
|
|
96
123
|
};
|
|
97
124
|
try {
|
|
98
|
-
|
|
125
|
+
// Cookies and storage are fixed when a context is created, so a state named
|
|
126
|
+
// here applies to the page this call opens; a session already running on
|
|
127
|
+
// the same state simply carries on (see `getSessionPage`).
|
|
128
|
+
const auth = await resolveStorageState(input.storage_state);
|
|
129
|
+
const storageState = auth ? { path: auth.path, state: auth.state } : undefined;
|
|
130
|
+
const { page, previousUrl, reopenedFor } = await getSessionPage({
|
|
99
131
|
...(input.viewport ? { viewport: input.viewport } : {}),
|
|
100
132
|
hasTouch: needsTouch([step]),
|
|
133
|
+
...(storageState ? { storageState } : {}),
|
|
101
134
|
});
|
|
102
135
|
// The layers belong to the page, not to this call (see SessionLayers), so
|
|
103
136
|
// they are installed once and emptied here — this call reports what this
|
|
@@ -106,22 +139,32 @@ async function runInteraction(rawInput) {
|
|
|
106
139
|
const layers = layersFor(page);
|
|
107
140
|
await layers.ensure(flags);
|
|
108
141
|
layers.clear();
|
|
109
|
-
// `previousUrl` means the session had to be reopened
|
|
110
|
-
// back to where the user was unless they asked
|
|
142
|
+
// `previousUrl` means the session had to be reopened (for touch, or to take
|
|
143
|
+
// a different auth state); go back to where the user was unless they asked
|
|
144
|
+
// for somewhere else.
|
|
111
145
|
const target = input.url ?? previousUrl;
|
|
112
146
|
if (target !== undefined) {
|
|
113
147
|
await page.goto(target, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
|
|
148
|
+
// A Vue app is acted on once it is mounted and its router has resolved;
|
|
149
|
+
// a plain page gets no extra wait beyond the brief look for one.
|
|
150
|
+
await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_TIMEOUT_MS });
|
|
114
151
|
}
|
|
115
152
|
if (page.url() === "about:blank") {
|
|
116
153
|
return errorResult("Interaction failed: no page is open yet — pass `url` to open one (e.g. http://localhost:3000).");
|
|
117
154
|
}
|
|
155
|
+
const vueBefore = await detectVue(page);
|
|
156
|
+
// A password field before the action and none after it is a login that
|
|
157
|
+
// just succeeded — the one moment the agent is thinking about auth.
|
|
158
|
+
const loginBefore = await loginFormVisible(page);
|
|
118
159
|
const before = await page.screenshot({ type: "png" });
|
|
119
160
|
const startedAt = Date.now();
|
|
120
|
-
await executeInteraction(page, step, { timeout_ms: input.timeout_ms });
|
|
161
|
+
const outcome = await executeInteraction(page, step, { timeout_ms: input.timeout_ms });
|
|
121
162
|
if (input.wait_ms > 0) {
|
|
122
163
|
await page.waitForTimeout(input.wait_ms);
|
|
123
164
|
}
|
|
124
165
|
const after = await page.screenshot({ type: "png" });
|
|
166
|
+
const vueAfter = vueBefore || step.action === "navigate" ? await detectVue(page) : null;
|
|
167
|
+
const loginAfter = await loginFormVisible(page);
|
|
125
168
|
const frames = [
|
|
126
169
|
{ buffer: before, timestamp_ms: 0, is_interaction: false },
|
|
127
170
|
{ buffer: after, timestamp_ms: Date.now() - startedAt, is_interaction: true },
|
|
@@ -139,9 +182,12 @@ async function runInteraction(rawInput) {
|
|
|
139
182
|
const context = layers.collect(startedAt, flags);
|
|
140
183
|
applyContext(cards, context);
|
|
141
184
|
const [beforeCard, afterCard] = cards;
|
|
185
|
+
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
142
186
|
const headline = [
|
|
143
|
-
summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0,
|
|
187
|
+
`${summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor, input.ref, outcome.note)} — ${describeScale(viewport)}`,
|
|
188
|
+
...describeRoute(vueBefore, vueAfter),
|
|
144
189
|
...(summariseContext(context, cards.length) ?? []),
|
|
190
|
+
...authLines(auth, input.url !== undefined && loginBefore, loginBefore && !loginAfter),
|
|
145
191
|
].join("\n");
|
|
146
192
|
const content = [
|
|
147
193
|
{ type: "text", text: headline },
|
|
@@ -152,19 +198,67 @@ async function runInteraction(rawInput) {
|
|
|
152
198
|
];
|
|
153
199
|
const crop = afterCard.change_region?.crop;
|
|
154
200
|
if (crop) {
|
|
155
|
-
content.push({ type: "image", data: crop, mimeType: "image/png" });
|
|
201
|
+
content.push(markImage({ type: "image", data: crop, mimeType: "image/png" }, { role: "crop" }));
|
|
202
|
+
}
|
|
203
|
+
if (input.include_snapshot) {
|
|
204
|
+
// Refs are reassigned by every snapshot, so the ones the caller used are
|
|
205
|
+
// stale the moment this runs — which is the point: these are the fresh ones.
|
|
206
|
+
const snapshot = await takeSnapshot(page, { mode: "full", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });
|
|
207
|
+
const lines = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, "", snapshot.text];
|
|
208
|
+
if (snapshot.cut_lines > 0)
|
|
209
|
+
lines.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \`selector\` or mode "interactive".`);
|
|
210
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
156
211
|
}
|
|
212
|
+
hmrFor(page).markSeen();
|
|
157
213
|
return { content };
|
|
158
214
|
}
|
|
159
215
|
catch (error) {
|
|
160
216
|
return errorResult(describeInteractFailure(input, error));
|
|
161
217
|
}
|
|
162
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The auth lines: what saved state was applied (and whether it worked, when
|
|
221
|
+
* this call opened the page), and — when a login form was there before the
|
|
222
|
+
* action and gone after it — the reminder to save the session now.
|
|
223
|
+
*/
|
|
224
|
+
function authLines(auth, expired, justSignedIn) {
|
|
225
|
+
const lines = [];
|
|
226
|
+
const applied = auth ? describeAuth(auth, expired) : null;
|
|
227
|
+
if (applied)
|
|
228
|
+
lines.push(applied);
|
|
229
|
+
if (justSignedIn) {
|
|
230
|
+
lines.push("Signed in? Save this session with framewatch_save_auth (same steps, from the login page) so every later call " +
|
|
231
|
+
"starts here instead of replaying the login.");
|
|
232
|
+
}
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The Vue line: the app and its route, and — when the action moved the
|
|
237
|
+
* router — where it went, by name.
|
|
238
|
+
*/
|
|
239
|
+
function describeRoute(before, after) {
|
|
240
|
+
const current = after ?? before;
|
|
241
|
+
if (!current)
|
|
242
|
+
return [];
|
|
243
|
+
const from = before?.route;
|
|
244
|
+
const to = after?.route;
|
|
245
|
+
if (from && to && from.path !== to.path) {
|
|
246
|
+
const name = (route) => `${route.path}${route.name ? ` (${route.name})` : ""}`;
|
|
247
|
+
return [`Vue ${current.version} — route ${name(from)} → ${name(to)}`];
|
|
248
|
+
}
|
|
249
|
+
return [describeVue(current)];
|
|
250
|
+
}
|
|
163
251
|
/** One line: what was done, where, and how much of the frame it moved. */
|
|
164
|
-
function summarise(step, url, changePercent,
|
|
165
|
-
const
|
|
252
|
+
function summarise(step, url, changePercent, reopenedFor, ref, note) {
|
|
253
|
+
const reopened = reopenedFor === "touch"
|
|
254
|
+
? " (page reopened to enable touch — page state was reset)"
|
|
255
|
+
: reopenedFor === "auth"
|
|
256
|
+
? " (page reopened with the saved auth state — page state was reset)"
|
|
257
|
+
: "";
|
|
166
258
|
const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : "no visual change";
|
|
167
|
-
|
|
259
|
+
// The step carries the ref as an `aria-ref=` selector; the caller wrote `e8`.
|
|
260
|
+
const what = (ref !== undefined ? describeInteraction(step).replaceAll(`"${refSelector(ref)}"`, ref) : describeInteraction(step)) + (note ?? "");
|
|
261
|
+
return `${what} on ${url}${reopened} — ${changed}`;
|
|
168
262
|
}
|
|
169
263
|
function errorResult(text) {
|
|
170
264
|
return { isError: true, content: [{ type: "text", text }] };
|
|
@@ -178,6 +272,10 @@ function errorResult(text) {
|
|
|
178
272
|
export function describeInteractFailure(input, error) {
|
|
179
273
|
const message = error instanceof Error ? error.message : String(error);
|
|
180
274
|
const firstLine = message.split("\n")[0];
|
|
275
|
+
if (input.ref !== undefined && message.includes(refSelector(input.ref))) {
|
|
276
|
+
return (`Interaction failed: ref ${input.ref} did not resolve — the page has changed since that snapshot, or none was ` +
|
|
277
|
+
`taken on this page. Run framewatch_snapshot again and use a ref from it. (${firstLine.replaceAll(`"${refSelector(input.ref)}"`, input.ref)})`);
|
|
278
|
+
}
|
|
181
279
|
if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
|
|
182
280
|
return ("Interaction failed: Playwright's Chromium browser is not installed. " +
|
|
183
281
|
`Run \`npx playwright install chromium\` and try again. (${firstLine})`);
|
|
@@ -193,6 +291,8 @@ export function registerInteractTool(server) {
|
|
|
193
291
|
description: "Perform one interaction (click, tap, type, scroll, swipe, hover, select, navigate) on a page and return " +
|
|
194
292
|
"before/after screenshots plus a crop of what changed. The page stays open between calls, so you can " +
|
|
195
293
|
"click, look, type and look again without replaying the whole flow — pass `url` only to open or move it. " +
|
|
294
|
+
"Target an element by `ref` from framewatch_snapshot (e.g. `e8`) instead of guessing a selector; " +
|
|
295
|
+
"`include_snapshot` returns the fresh refs after the action. " +
|
|
196
296
|
"Each call also reports the context its own action produced: console output and uncaught errors (on by " +
|
|
197
297
|
"default), plus network requests, DOM mutations and paint/layout-shift timing via `include_network`, " +
|
|
198
298
|
"`include_dom` and `include_performance` — that is how you find out why a click did nothing.",
|
|
@@ -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 { takeSnapshot } from \"../engine/snapshot.js\";\nimport type { RawFrame } from \"../types.js\";\nimport { describeScale, formatCardMeta } from \"../utils/format.js\";\nimport { refSelector } from \"../utils/snapshot-rules.js\";\nimport { describeAuth, loginFormVisible, resolveStorageState, storageStateField } from \"../utils/storage-state.js\";\nimport { DEFAULT_SNAPSHOT_MAX_CHARS, VUE_DETECT_MS, VUE_READY_TIMEOUT_MS } from \"../constants.js\";\nimport { hmrFor } from \"../engine/hmr.js\";\nimport { detectVue, waitForVueReady } from \"../engine/vue.js\";\nimport { describeVue, type VueInfo } from \"../utils/vue-rules.js\";\nimport { describeCounts } from \"./snapshot.js\";\nimport { markImage } from \"../utils/budget.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 ref: z\n .string()\n .regex(/^e\\d+$/, \"a ref looks like `e8` — take one from framewatch_snapshot\")\n .optional()\n .describe(\"Element ref from framewatch_snapshot (e.g. `e8`) to act on, instead of a `selector`\"),\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 include_snapshot: z\n .boolean()\n .default(false)\n .describe(\"Append a framewatch_snapshot of the page after the action, with fresh refs for the next call\"),\n storage_state: storageStateField,\n};\n\nexport const interactInputSchema = z.object(interactInputShape).superRefine((value, ctx) => {\n if (value.ref !== undefined && value.selector !== undefined) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: \"pass either `ref` or `selector`, not both\", path: [\"ref\"] });\n return;\n }\n // A ref is a selector Playwright understands, so the step is validated as one.\n refineInteraction(value.ref !== undefined ? { ...value, selector: refSelector(value.ref) } : value, ctx);\n});\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 selector = input.ref !== undefined ? refSelector(input.ref) : input.selector;\n const step: Interaction = {\n action: input.action,\n ...(selector !== undefined ? { 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 auth = await resolveStorageState(input.storage_state);\n const storageState = auth ? { path: auth.path, state: auth.state } : 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 // A Vue app is acted on once it is mounted and its router has resolved;\n // a plain page gets no extra wait beyond the brief look for one.\n await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_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 vueBefore = await detectVue(page);\n // A password field before the action and none after it is a login that\n // just succeeded — the one moment the agent is thinking about auth.\n const loginBefore = await loginFormVisible(page);\n const before = await page.screenshot({ type: \"png\" });\n const startedAt = Date.now();\n const outcome = 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 const vueAfter = vueBefore || step.action === \"navigate\" ? await detectVue(page) : null;\n const loginAfter = await loginFormVisible(page);\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 viewport = page.viewportSize() ?? { width: 0, height: 0 };\n const headline = [\n `${summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor, input.ref, outcome.note)} — ${describeScale(viewport)}`,\n ...describeRoute(vueBefore, vueAfter),\n ...(summariseContext(context, cards.length) ?? []),\n ...authLines(auth, input.url !== undefined && loginBefore, loginBefore && !loginAfter),\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(markImage({ type: \"image\", data: crop, mimeType: \"image/png\" }, { role: \"crop\" }));\n }\n if (input.include_snapshot) {\n // Refs are reassigned by every snapshot, so the ones the caller used are\n // stale the moment this runs — which is the point: these are the fresh ones.\n const snapshot = await takeSnapshot(page, { mode: \"full\", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });\n const lines = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, \"\", snapshot.text];\n if (snapshot.cut_lines > 0) lines.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \\`selector\\` or mode \"interactive\".`);\n content.push({ type: \"text\", text: lines.join(\"\\n\") });\n }\n hmrFor(page).markSeen();\n return { content };\n } catch (error) {\n return errorResult(describeInteractFailure(input, error));\n }\n}\n\n/**\n * The auth lines: what saved state was applied (and whether it worked, when\n * this call opened the page), and — when a login form was there before the\n * action and gone after it — the reminder to save the session now.\n */\nfunction authLines(auth: Awaited<ReturnType<typeof resolveStorageState>>, expired: boolean, justSignedIn: boolean): string[] {\n const lines: string[] = [];\n const applied = auth ? describeAuth(auth, expired) : null;\n if (applied) lines.push(applied);\n if (justSignedIn) {\n lines.push(\n \"Signed in? Save this session with framewatch_save_auth (same steps, from the login page) so every later call \" +\n \"starts here instead of replaying the login.\",\n );\n }\n return lines;\n}\n\n/**\n * The Vue line: the app and its route, and — when the action moved the\n * router — where it went, by name.\n */\nfunction describeRoute(before: VueInfo | null, after: VueInfo | null): string[] {\n const current = after ?? before;\n if (!current) return [];\n const from = before?.route;\n const to = after?.route;\n if (from && to && from.path !== to.path) {\n const name = (route: { path: string; name?: string }): string => `${route.path}${route.name ? ` (${route.name})` : \"\"}`;\n return [`Vue ${current.version} — route ${name(from)} → ${name(to)}`];\n }\n return [describeVue(current)];\n}\n\n/** One line: what was done, where, and how much of the frame it moved. */\nfunction summarise(\n step: Interaction,\n url: string,\n changePercent: number,\n reopenedFor?: \"touch\" | \"auth\",\n ref?: string,\n note?: string,\n): string {\n const reopened =\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 // The step carries the ref as an `aria-ref=` selector; the caller wrote `e8`.\n const what = (ref !== undefined ? describeInteraction(step).replaceAll(`\"${refSelector(ref)}\"`, ref) : describeInteraction(step)) + (note ?? \"\");\n return `${what} on ${url}${reopened} — ${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; ref?: string }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (input.ref !== undefined && message.includes(refSelector(input.ref))) {\n return (\n `Interaction failed: ref ${input.ref} did not resolve — the page has changed since that snapshot, or none was ` +\n `taken on this page. Run framewatch_snapshot again and use a ref from it. (${firstLine.replaceAll(`\"${refSelector(input.ref)}\"`, input.ref)})`\n );\n }\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 \"Target an element by `ref` from framewatch_snapshot (e.g. `e8`) instead of guessing a selector; \" +\n \"`include_snapshot` returns the fresh refs after the action. \" +\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"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
export declare const LINKS_TOOL_NAME = "framewatch_links";
|
|
5
|
+
export declare const linksInputShape: {
|
|
6
|
+
url: z.ZodString;
|
|
7
|
+
depth: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
check_external: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
include_resources: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
check_fragments: z.ZodDefault<z.ZodBoolean>;
|
|
11
|
+
timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
concurrency: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
max_links: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
max_pages: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
16
|
+
wait_ms: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
wait_for: z.ZodOptional<z.ZodString>;
|
|
18
|
+
wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
full_page: z.ZodDefault<z.ZodBoolean>;
|
|
20
|
+
viewport: z.ZodOptional<z.ZodObject<{
|
|
21
|
+
width: z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
height: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}, {
|
|
27
|
+
width?: number | undefined;
|
|
28
|
+
height?: number | undefined;
|
|
29
|
+
}>>;
|
|
30
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
31
|
+
};
|
|
32
|
+
export declare const linksInputSchema: z.ZodObject<{
|
|
33
|
+
url: z.ZodString;
|
|
34
|
+
depth: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
check_external: z.ZodDefault<z.ZodBoolean>;
|
|
36
|
+
include_resources: z.ZodDefault<z.ZodBoolean>;
|
|
37
|
+
check_fragments: z.ZodDefault<z.ZodBoolean>;
|
|
38
|
+
timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
39
|
+
concurrency: z.ZodDefault<z.ZodNumber>;
|
|
40
|
+
max_links: z.ZodDefault<z.ZodNumber>;
|
|
41
|
+
max_pages: z.ZodDefault<z.ZodNumber>;
|
|
42
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
43
|
+
wait_ms: z.ZodDefault<z.ZodNumber>;
|
|
44
|
+
wait_for: z.ZodOptional<z.ZodString>;
|
|
45
|
+
wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
|
|
46
|
+
full_page: z.ZodDefault<z.ZodBoolean>;
|
|
47
|
+
viewport: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
width: z.ZodDefault<z.ZodNumber>;
|
|
49
|
+
height: z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
}, "strip", z.ZodTypeAny, {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
}, {
|
|
54
|
+
width?: number | undefined;
|
|
55
|
+
height?: number | undefined;
|
|
56
|
+
}>>;
|
|
57
|
+
storage_state: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, "strip", z.ZodTypeAny, {
|
|
59
|
+
url: string;
|
|
60
|
+
wait_ms: number;
|
|
61
|
+
wait_for_timeout_ms: number;
|
|
62
|
+
timeout_ms: number;
|
|
63
|
+
full_page: boolean;
|
|
64
|
+
include_resources: boolean;
|
|
65
|
+
depth: number;
|
|
66
|
+
check_external: boolean;
|
|
67
|
+
check_fragments: boolean;
|
|
68
|
+
concurrency: number;
|
|
69
|
+
max_links: number;
|
|
70
|
+
max_pages: number;
|
|
71
|
+
viewport?: {
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
} | undefined;
|
|
75
|
+
wait_for?: string | undefined;
|
|
76
|
+
storage_state?: string | undefined;
|
|
77
|
+
selector?: string | undefined;
|
|
78
|
+
}, {
|
|
79
|
+
url: string;
|
|
80
|
+
viewport?: {
|
|
81
|
+
width?: number | undefined;
|
|
82
|
+
height?: number | undefined;
|
|
83
|
+
} | undefined;
|
|
84
|
+
wait_ms?: number | undefined;
|
|
85
|
+
wait_for?: string | undefined;
|
|
86
|
+
wait_for_timeout_ms?: number | undefined;
|
|
87
|
+
storage_state?: string | undefined;
|
|
88
|
+
timeout_ms?: number | undefined;
|
|
89
|
+
selector?: string | undefined;
|
|
90
|
+
full_page?: boolean | undefined;
|
|
91
|
+
include_resources?: boolean | undefined;
|
|
92
|
+
depth?: number | undefined;
|
|
93
|
+
check_external?: boolean | undefined;
|
|
94
|
+
check_fragments?: boolean | undefined;
|
|
95
|
+
concurrency?: number | undefined;
|
|
96
|
+
max_links?: number | undefined;
|
|
97
|
+
max_pages?: number | undefined;
|
|
98
|
+
}>;
|
|
99
|
+
export type LinksInput = z.input<typeof linksInputSchema>;
|
|
100
|
+
/**
|
|
101
|
+
* Check every link on a page — and, on request, every link on the pages it
|
|
102
|
+
* leads to — and report the ones that do not work.
|
|
103
|
+
*
|
|
104
|
+
* The page is read after it has rendered rather than from its HTML source, so
|
|
105
|
+
* a client-rendered app is checked properly instead of being reported as
|
|
106
|
+
* having no links. The checks go out through the browser context, carrying the
|
|
107
|
+
* same cookies as the page, so a link behind a login is checked as the
|
|
108
|
+
* signed-in visitor sees it.
|
|
109
|
+
*
|
|
110
|
+
* Three things separate this from a HEAD loop. A HEAD that fails is retried as
|
|
111
|
+
* a GET before anything is called broken, because a great many servers refuse
|
|
112
|
+
* the method and serve the URL perfectly well. A status that means "I will not
|
|
113
|
+
* answer that" (401, 403, 429) is reported apart from one that means "there is
|
|
114
|
+
* nothing here", because a report that cries wolf gets ignored. And a
|
|
115
|
+
* same-page `#fragment` is checked against the document, which is the one
|
|
116
|
+
* broken link no HTTP check can ever find.
|
|
117
|
+
*/
|
|
118
|
+
export declare function checkLinks(rawInput: LinksInput): Promise<CallToolResult>;
|
|
119
|
+
/**
|
|
120
|
+
* One actionable line for a check that could not run. Mirrors `describeFailure`
|
|
121
|
+
* in screenshot.ts: match on the failing Playwright call, never on substrings
|
|
122
|
+
* of a user-supplied selector.
|
|
123
|
+
*/
|
|
124
|
+
export declare function describeLinksFailure(input: {
|
|
125
|
+
url: string;
|
|
126
|
+
wait_for?: string;
|
|
127
|
+
wait_for_timeout_ms: number;
|
|
128
|
+
}, error: unknown): string;
|
|
129
|
+
export declare function registerLinksTool(server: McpServer): void;
|