framewatch-mcp-server 0.1.1 → 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 +797 -8
- package/dist/constants.d.ts +264 -0
- package/dist/constants.js +268 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.js +4 -0
- 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 +6 -1
- package/dist/engine/interaction.js +21 -6
- 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 +42 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.js +4 -3
- 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 +83 -23
- package/dist/tools/capture.js +105 -60
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.js +4 -3
- 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 +16 -1
- package/dist/tools/index.js +42 -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 +29 -16
- package/dist/tools/interact.js +99 -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 +6 -6
- package/dist/tools/responsive.js +9 -9
- 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 +22 -22
- package/dist/tools/screenshot.js +13 -5
- 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 +42 -1
- package/dist/utils/storage-state.js +115 -4
- package/dist/utils/storage-state.js.map +1 -1
- 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,14 +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 {
|
|
8
|
-
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";
|
|
9
17
|
export const INTERACT_TOOL_NAME = "framewatch_interact";
|
|
10
18
|
export const interactInputShape = {
|
|
11
19
|
action: z
|
|
12
20
|
.enum(INTERACT_ACTIONS)
|
|
13
21
|
.describe("What to do: click, tap, type, key, scroll, swipe, navigate, select or hover"),
|
|
14
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`"),
|
|
15
28
|
url: z
|
|
16
29
|
.string()
|
|
17
30
|
.url()
|
|
@@ -52,9 +65,20 @@ export const interactInputShape = {
|
|
|
52
65
|
.boolean()
|
|
53
66
|
.default(false)
|
|
54
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"),
|
|
55
72
|
storage_state: storageStateField,
|
|
56
73
|
};
|
|
57
|
-
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
|
+
});
|
|
58
82
|
/**
|
|
59
83
|
* Perform one interaction on the current page and show what it did.
|
|
60
84
|
*
|
|
@@ -81,9 +105,10 @@ async function runInteraction(rawInput) {
|
|
|
81
105
|
return errorResult(`Interaction failed: invalid input — ${issues}`);
|
|
82
106
|
}
|
|
83
107
|
const input = parsed.data;
|
|
108
|
+
const selector = input.ref !== undefined ? refSelector(input.ref) : input.selector;
|
|
84
109
|
const step = {
|
|
85
110
|
action: input.action,
|
|
86
|
-
...(
|
|
111
|
+
...(selector !== undefined ? { selector } : {}),
|
|
87
112
|
...(input.value !== undefined ? { value: input.value } : {}),
|
|
88
113
|
...(input.x !== undefined ? { x: input.x } : {}),
|
|
89
114
|
...(input.y !== undefined ? { y: input.y } : {}),
|
|
@@ -100,9 +125,8 @@ async function runInteraction(rawInput) {
|
|
|
100
125
|
// Cookies and storage are fixed when a context is created, so a state named
|
|
101
126
|
// here applies to the page this call opens; a session already running on
|
|
102
127
|
// the same state simply carries on (see `getSessionPage`).
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
: undefined;
|
|
128
|
+
const auth = await resolveStorageState(input.storage_state);
|
|
129
|
+
const storageState = auth ? { path: auth.path, state: auth.state } : undefined;
|
|
106
130
|
const { page, previousUrl, reopenedFor } = await getSessionPage({
|
|
107
131
|
...(input.viewport ? { viewport: input.viewport } : {}),
|
|
108
132
|
hasTouch: needsTouch([step]),
|
|
@@ -121,17 +145,26 @@ async function runInteraction(rawInput) {
|
|
|
121
145
|
const target = input.url ?? previousUrl;
|
|
122
146
|
if (target !== undefined) {
|
|
123
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 });
|
|
124
151
|
}
|
|
125
152
|
if (page.url() === "about:blank") {
|
|
126
153
|
return errorResult("Interaction failed: no page is open yet — pass `url` to open one (e.g. http://localhost:3000).");
|
|
127
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);
|
|
128
159
|
const before = await page.screenshot({ type: "png" });
|
|
129
160
|
const startedAt = Date.now();
|
|
130
|
-
await executeInteraction(page, step, { timeout_ms: input.timeout_ms });
|
|
161
|
+
const outcome = await executeInteraction(page, step, { timeout_ms: input.timeout_ms });
|
|
131
162
|
if (input.wait_ms > 0) {
|
|
132
163
|
await page.waitForTimeout(input.wait_ms);
|
|
133
164
|
}
|
|
134
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);
|
|
135
168
|
const frames = [
|
|
136
169
|
{ buffer: before, timestamp_ms: 0, is_interaction: false },
|
|
137
170
|
{ buffer: after, timestamp_ms: Date.now() - startedAt, is_interaction: true },
|
|
@@ -149,9 +182,12 @@ async function runInteraction(rawInput) {
|
|
|
149
182
|
const context = layers.collect(startedAt, flags);
|
|
150
183
|
applyContext(cards, context);
|
|
151
184
|
const [beforeCard, afterCard] = cards;
|
|
185
|
+
const viewport = page.viewportSize() ?? { width: 0, height: 0 };
|
|
152
186
|
const headline = [
|
|
153
|
-
summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor)
|
|
187
|
+
`${summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor, input.ref, outcome.note)} — ${describeScale(viewport)}`,
|
|
188
|
+
...describeRoute(vueBefore, vueAfter),
|
|
154
189
|
...(summariseContext(context, cards.length) ?? []),
|
|
190
|
+
...authLines(auth, input.url !== undefined && loginBefore, loginBefore && !loginAfter),
|
|
155
191
|
].join("\n");
|
|
156
192
|
const content = [
|
|
157
193
|
{ type: "text", text: headline },
|
|
@@ -162,23 +198,67 @@ async function runInteraction(rawInput) {
|
|
|
162
198
|
];
|
|
163
199
|
const crop = afterCard.change_region?.crop;
|
|
164
200
|
if (crop) {
|
|
165
|
-
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") });
|
|
166
211
|
}
|
|
212
|
+
hmrFor(page).markSeen();
|
|
167
213
|
return { content };
|
|
168
214
|
}
|
|
169
215
|
catch (error) {
|
|
170
216
|
return errorResult(describeInteractFailure(input, error));
|
|
171
217
|
}
|
|
172
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
|
+
}
|
|
173
251
|
/** One line: what was done, where, and how much of the frame it moved. */
|
|
174
|
-
function summarise(step, url, changePercent, reopenedFor) {
|
|
175
|
-
const
|
|
252
|
+
function summarise(step, url, changePercent, reopenedFor, ref, note) {
|
|
253
|
+
const reopened = reopenedFor === "touch"
|
|
176
254
|
? " (page reopened to enable touch — page state was reset)"
|
|
177
255
|
: reopenedFor === "auth"
|
|
178
256
|
? " (page reopened with the saved auth state — page state was reset)"
|
|
179
257
|
: "";
|
|
180
258
|
const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : "no visual change";
|
|
181
|
-
|
|
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}`;
|
|
182
262
|
}
|
|
183
263
|
function errorResult(text) {
|
|
184
264
|
return { isError: true, content: [{ type: "text", text }] };
|
|
@@ -192,6 +272,10 @@ function errorResult(text) {
|
|
|
192
272
|
export function describeInteractFailure(input, error) {
|
|
193
273
|
const message = error instanceof Error ? error.message : String(error);
|
|
194
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
|
+
}
|
|
195
279
|
if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
|
|
196
280
|
return ("Interaction failed: Playwright's Chromium browser is not installed. " +
|
|
197
281
|
`Run \`npx playwright install chromium\` and try again. (${firstLine})`);
|
|
@@ -207,6 +291,8 @@ export function registerInteractTool(server) {
|
|
|
207
291
|
description: "Perform one interaction (click, tap, type, scroll, swipe, hover, select, navigate) on a page and return " +
|
|
208
292
|
"before/after screenshots plus a crop of what changed. The page stays open between calls, so you can " +
|
|
209
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. " +
|
|
210
296
|
"Each call also reports the context its own action produced: console output and uncaught errors (on by " +
|
|
211
297
|
"default), plus network requests, DOM mutations and paint/layout-shift timing via `include_network`, " +
|
|
212
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\";\nimport { readStorageState, storageStateField } from \"../utils/storage-state.js\";\n\nexport const INTERACT_TOOL_NAME = \"framewatch_interact\";\n\nexport const interactInputShape = {\n action: z\n .enum(INTERACT_ACTIONS)\n .describe(\"What to do: click, tap, type, key, scroll, swipe, navigate, select or hover\"),\n ...interactionFieldShape,\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to act on the page left open by the previous call.\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_INTERACT_WAIT_MS)\n .describe(\"Wait time (ms) after the action before the 'after' screenshot, so animations can settle\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for the target element\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),\n })\n .optional()\n .describe(\"Resize the page to this before acting (defaults to leaving it as it is)\"),\n include_console: z\n .boolean()\n .default(true)\n .describe(\"Report console logs, uncaught errors and unhandled rejections the action caused\"),\n include_network: z\n .boolean()\n .default(false)\n .describe(\"Report network requests (method, url, status, duration) the action caused\"),\n include_dom: z\n .boolean()\n .default(false)\n .describe(\"Report the DOM mutations the action caused — which elements were added, removed or restyled\"),\n include_performance: z\n .boolean()\n .default(false)\n .describe(\"Report paint timing and layout shifts measured around the action\"),\n storage_state: storageStateField,\n};\n\nexport const interactInputSchema = z.object(interactInputShape).superRefine(refineInteraction);\nexport type InteractInput = z.input<typeof interactInputSchema>;\n\n/**\n * Perform one interaction on the current page and show what it did.\n *\n * Unlike every other FrameWatch tool this one is stateful on purpose: the page\n * stays open between calls (see `getSessionPage`), which is what makes\n * click → look → type → look iteration possible. Pass `url` to open or move\n * the page; omit it to keep working on what is already there.\n *\n * The result is a two-frame diff card sequence — before, after, and the\n * change region between them — reusing the same diff engine as\n * `framewatch_capture` so the change bbox and crop mean the same thing in\n * both tools.\n *\n * Calls are serialised through `withSessionLock`, which every tool that\n * touches the session page shares: one page, one hand.\n */\nexport function performInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n return withSessionLock(() => runInteraction(rawInput));\n}\n\nasync function runInteraction(rawInput: InteractInput): Promise<CallToolResult> {\n const parsed = interactInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Interaction failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const step: Interaction = {\n action: input.action,\n ...(input.selector !== undefined ? { selector: input.selector } : {}),\n ...(input.value !== undefined ? { value: input.value } : {}),\n ...(input.x !== undefined ? { x: input.x } : {}),\n ...(input.y !== undefined ? { y: input.y } : {}),\n ...(input.delta_x !== undefined ? { delta_x: input.delta_x } : {}),\n ...(input.delta_y !== undefined ? { delta_y: input.delta_y } : {}),\n };\n\n const flags: LayerFlags = {\n console: input.include_console,\n network: input.include_network,\n dom: input.include_dom,\n performance: input.include_performance,\n };\n\n try {\n // Cookies and storage are fixed when a context is created, so a state named\n // here applies to the page this call opens; a session already running on\n // the same state simply carries on (see `getSessionPage`).\n const storageState = input.storage_state\n ? { path: input.storage_state, state: await readStorageState(input.storage_state) }\n : undefined;\n\n const { page, previousUrl, reopenedFor } = await getSessionPage({\n ...(input.viewport ? { viewport: input.viewport } : {}),\n hasTouch: needsTouch([step]),\n ...(storageState ? { storageState } : {}),\n });\n\n // The layers belong to the page, not to this call (see SessionLayers), so\n // they are installed once and emptied here — this call reports what this\n // action caused, not what the last twenty did. Both happen before the\n // navigation, so a page opened by `url` has its load watched too.\n const layers = layersFor(page);\n await layers.ensure(flags);\n layers.clear();\n\n // `previousUrl` means the session had to be reopened (for touch, or to take\n // a different auth state); go back to where the user was unless they asked\n // for somewhere else.\n const target = input.url ?? previousUrl;\n if (target !== undefined) {\n await page.goto(target, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n }\n if (page.url() === \"about:blank\") {\n return errorResult(\n \"Interaction failed: no page is open yet — pass `url` to open one (e.g. http://localhost:3000).\",\n );\n }\n\n const before = await page.screenshot({ type: \"png\" });\n const startedAt = Date.now();\n await executeInteraction(page, step, { timeout_ms: input.timeout_ms });\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n const after = await page.screenshot({ type: \"png\" });\n\n const frames: RawFrame[] = [\n { buffer: before, timestamp_ms: 0, is_interaction: false },\n { buffer: after, timestamp_ms: Date.now() - startedAt, is_interaction: true },\n ];\n // sensitivity 0 keeps both frames however small the change is — the point\n // of this tool is to show what one action did, including \"nothing\".\n const { cards } = await buildDiffCards(frames, { sensitivity: 0, max_frames: 2 });\n if (cards.length < 2) {\n return errorResult(`Interaction failed: ${describeInteraction(step)} ran, but the screenshots could not be compared.`);\n }\n\n // `startedAt` is the instant of the \"before\" frame, which puts the split\n // exactly where it belongs: everything from opening the page lands on the\n // before card (negative, i.e. \"how we got here\") and everything the action\n // caused lands on the after card.\n const context = layers.collect(startedAt, flags);\n applyContext(cards, context);\n\n const [beforeCard, afterCard] = cards;\n const headline = [\n summarise(step, page.url(), afterCard.change_region?.change_percent ?? 0, reopenedFor),\n ...(summariseContext(context, cards.length) ?? []),\n ].join(\"\\n\");\n\n const content: CallToolResult[\"content\"] = [\n { type: \"text\", text: headline },\n { type: \"image\", data: beforeCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `Before — ${formatCardMeta(beforeCard)}` },\n { type: \"image\", data: afterCard.full_frame, mimeType: \"image/png\" },\n { type: \"text\", text: `After — ${formatCardMeta(afterCard)}` },\n ];\n const crop = afterCard.change_region?.crop;\n if (crop) {\n content.push({ type: \"image\", data: crop, mimeType: \"image/png\" });\n }\n return { content };\n } catch (error) {\n return errorResult(describeInteractFailure(input, error));\n }\n}\n\n/** One line: what was done, where, and how much of the frame it moved. */\nfunction summarise(step: Interaction, url: string, changePercent: number, reopenedFor?: \"touch\" | \"auth\"): string {\n const note =\n reopenedFor === \"touch\"\n ? \" (page reopened to enable touch — page state was reset)\"\n : reopenedFor === \"auth\"\n ? \" (page reopened with the saved auth state — page state was reset)\"\n : \"\";\n const changed = changePercent > 0 ? `${changePercent.toFixed(1)}% of the frame changed` : \"no visual change\";\n return `${describeInteraction(step)} on ${url}${note} — ${changed}`;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a failure into one actionable line. Interaction errors already name the\n * step and the reason (see `executeInteraction`), so they are passed through\n * with the page appended for context; everything else is a navigation or\n * browser problem, worded as in the other tools.\n */\nexport function describeInteractFailure(input: { url?: string }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n \"Interaction failed: Playwright's Chromium browser is not installed. \" +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (/^page\\.goto:/.test(message)) {\n return `Interaction failed: could not open ${input.url ?? \"the page\"} — ${firstLine}`;\n }\n return input.url ? `${firstLine} (page: ${input.url})` : firstLine;\n}\n\nexport function registerInteractTool(server: McpServer): void {\n server.registerTool(\n INTERACT_TOOL_NAME,\n {\n title: \"Interact\",\n description:\n \"Perform one interaction (click, tap, type, scroll, swipe, hover, select, navigate) on a page and return \" +\n \"before/after screenshots plus a crop of what changed. The page stays open between calls, so you can \" +\n \"click, look, type and look again without replaying the whole flow — pass `url` only to open or move it. \" +\n \"Each call also reports the context its own action produced: console output and uncaught errors (on by \" +\n \"default), plus network requests, DOM mutations and paint/layout-shift timing via `include_network`, \" +\n \"`include_dom` and `include_performance` — that is how you find out why a click did nothing.\",\n inputSchema: interactInputShape,\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => performInteraction(args),\n );\n}\n"]}
|
|
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;
|