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.
Files changed (133) hide show
  1. package/README.md +895 -15
  2. package/dist/constants.d.ts +274 -0
  3. package/dist/constants.js +279 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.d.ts +20 -4
  6. package/dist/engine/browser.js +26 -9
  7. package/dist/engine/browser.js.map +1 -1
  8. package/dist/engine/clicks.d.ts +221 -0
  9. package/dist/engine/clicks.js +801 -0
  10. package/dist/engine/clicks.js.map +1 -0
  11. package/dist/engine/forms.d.ts +137 -0
  12. package/dist/engine/forms.js +474 -0
  13. package/dist/engine/forms.js.map +1 -0
  14. package/dist/engine/hmr.d.ts +41 -0
  15. package/dist/engine/hmr.js +91 -0
  16. package/dist/engine/hmr.js.map +1 -0
  17. package/dist/engine/inspect.d.ts +31 -0
  18. package/dist/engine/inspect.js +383 -0
  19. package/dist/engine/inspect.js.map +1 -0
  20. package/dist/engine/interaction.d.ts +12 -7
  21. package/dist/engine/interaction.js +110 -18
  22. package/dist/engine/interaction.js.map +1 -1
  23. package/dist/engine/links.d.ts +134 -0
  24. package/dist/engine/links.js +384 -0
  25. package/dist/engine/links.js.map +1 -0
  26. package/dist/engine/mocks.d.ts +53 -0
  27. package/dist/engine/mocks.js +148 -0
  28. package/dist/engine/mocks.js.map +1 -0
  29. package/dist/engine/rtl.d.ts +129 -0
  30. package/dist/engine/rtl.js +540 -0
  31. package/dist/engine/rtl.js.map +1 -0
  32. package/dist/engine/seo.d.ts +189 -0
  33. package/dist/engine/seo.js +398 -0
  34. package/dist/engine/seo.js.map +1 -0
  35. package/dist/engine/snapshot.d.ts +29 -0
  36. package/dist/engine/snapshot.js +10 -0
  37. package/dist/engine/snapshot.js.map +1 -0
  38. package/dist/engine/vue.d.ts +54 -0
  39. package/dist/engine/vue.js +419 -0
  40. package/dist/engine/vue.js.map +1 -0
  41. package/dist/index.js +45 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/tools/accessibility.d.ts +4 -0
  44. package/dist/tools/accessibility.js +9 -2
  45. package/dist/tools/accessibility.js.map +1 -1
  46. package/dist/tools/api-mock.d.ts +405 -0
  47. package/dist/tools/api-mock.js +186 -0
  48. package/dist/tools/api-mock.js.map +1 -0
  49. package/dist/tools/capture.d.ts +90 -26
  50. package/dist/tools/capture.js +109 -58
  51. package/dist/tools/capture.js.map +1 -1
  52. package/dist/tools/compare.d.ts +4 -0
  53. package/dist/tools/compare.js +16 -5
  54. package/dist/tools/compare.js.map +1 -1
  55. package/dist/tools/dead-clicks.d.ts +128 -0
  56. package/dist/tools/dead-clicks.js +570 -0
  57. package/dist/tools/dead-clicks.js.map +1 -0
  58. package/dist/tools/form-test.d.ts +112 -0
  59. package/dist/tools/form-test.js +477 -0
  60. package/dist/tools/form-test.js.map +1 -0
  61. package/dist/tools/index.d.ts +17 -1
  62. package/dist/tools/index.js +45 -1
  63. package/dist/tools/index.js.map +1 -1
  64. package/dist/tools/inspect.d.ts +78 -0
  65. package/dist/tools/inspect.js +136 -0
  66. package/dist/tools/inspect.js.map +1 -0
  67. package/dist/tools/interact.d.ts +37 -18
  68. package/dist/tools/interact.js +113 -13
  69. package/dist/tools/interact.js.map +1 -1
  70. package/dist/tools/links.d.ts +129 -0
  71. package/dist/tools/links.js +640 -0
  72. package/dist/tools/links.js.map +1 -0
  73. package/dist/tools/responsive.d.ts +10 -6
  74. package/dist/tools/responsive.js +21 -4
  75. package/dist/tools/responsive.js.map +1 -1
  76. package/dist/tools/rtl.d.ts +241 -0
  77. package/dist/tools/rtl.js +410 -0
  78. package/dist/tools/rtl.js.map +1 -0
  79. package/dist/tools/save-auth.d.ts +263 -0
  80. package/dist/tools/save-auth.js +253 -0
  81. package/dist/tools/save-auth.js.map +1 -0
  82. package/dist/tools/screenshot.d.ts +4 -0
  83. package/dist/tools/screenshot.js +15 -4
  84. package/dist/tools/screenshot.js.map +1 -1
  85. package/dist/tools/seo.d.ts +113 -0
  86. package/dist/tools/seo.js +281 -0
  87. package/dist/tools/seo.js.map +1 -0
  88. package/dist/tools/snapshot.d.ts +122 -0
  89. package/dist/tools/snapshot.js +183 -0
  90. package/dist/tools/snapshot.js.map +1 -0
  91. package/dist/tools/wait-for.d.ts +107 -0
  92. package/dist/tools/wait-for.js +167 -0
  93. package/dist/tools/wait-for.js.map +1 -0
  94. package/dist/utils/arabic-text.d.ts +14 -0
  95. package/dist/utils/arabic-text.js +193 -0
  96. package/dist/utils/arabic-text.js.map +1 -0
  97. package/dist/utils/budget.d.ts +41 -0
  98. package/dist/utils/budget.js +182 -0
  99. package/dist/utils/budget.js.map +1 -0
  100. package/dist/utils/format.d.ts +11 -1
  101. package/dist/utils/format.js +27 -4
  102. package/dist/utils/format.js.map +1 -1
  103. package/dist/utils/highlight.d.ts +69 -0
  104. package/dist/utils/highlight.js +181 -0
  105. package/dist/utils/highlight.js.map +1 -0
  106. package/dist/utils/link-rules.d.ts +100 -0
  107. package/dist/utils/link-rules.js +284 -0
  108. package/dist/utils/link-rules.js.map +1 -0
  109. package/dist/utils/mock-rules.d.ts +144 -0
  110. package/dist/utils/mock-rules.js +224 -0
  111. package/dist/utils/mock-rules.js.map +1 -0
  112. package/dist/utils/rtl-rules.d.ts +142 -0
  113. package/dist/utils/rtl-rules.js +296 -0
  114. package/dist/utils/rtl-rules.js.map +1 -0
  115. package/dist/utils/seo-rules.d.ts +129 -0
  116. package/dist/utils/seo-rules.js +726 -0
  117. package/dist/utils/seo-rules.js.map +1 -0
  118. package/dist/utils/snapshot-rules.d.ts +33 -0
  119. package/dist/utils/snapshot-rules.js +111 -0
  120. package/dist/utils/snapshot-rules.js.map +1 -0
  121. package/dist/utils/storage-state.d.ts +76 -0
  122. package/dist/utils/storage-state.js +195 -0
  123. package/dist/utils/storage-state.js.map +1 -0
  124. package/dist/utils/style-rules.d.ts +107 -0
  125. package/dist/utils/style-rules.js +223 -0
  126. package/dist/utils/style-rules.js.map +1 -0
  127. package/dist/utils/test-data.d.ts +75 -0
  128. package/dist/utils/test-data.js +294 -0
  129. package/dist/utils/test-data.js.map +1 -0
  130. package/dist/utils/vue-rules.d.ts +72 -0
  131. package/dist/utils/vue-rules.js +108 -0
  132. package/dist/utils/vue-rules.js.map +1 -0
  133. package/package.json +6 -4
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_SNAPSHOT_MAX_CHARS, DEFAULT_SNAPSHOT_WAIT_MS, MAX_SNAPSHOT_MAX_CHARS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, VUE_READY_TIMEOUT_MS, } from "../constants.js";
3
+ import { getSessionPage, withSessionLock } from "../engine/browser.js";
4
+ import { hmrFor } from "../engine/hmr.js";
5
+ import { takeSnapshot } from "../engine/snapshot.js";
6
+ import { componentTree, detectVue, waitForVueReady } from "../engine/vue.js";
7
+ import { describeVue, formatComponentTree } from "../utils/vue-rules.js";
8
+ import { describeScale } from "../utils/format.js";
9
+ import { resizeForOutput, toBase64 } from "../utils/image.js";
10
+ import { loginFormVisible, resolveStorageState, storageStateField, withAuthNote } from "../utils/storage-state.js";
11
+ export const SNAPSHOT_TOOL_NAME = "framewatch_snapshot";
12
+ export const snapshotInputShape = {
13
+ url: z
14
+ .string()
15
+ .url()
16
+ .optional()
17
+ .describe("Open this URL first. Omit to read the page left open by framewatch_interact / framewatch_inspect."),
18
+ selector: z.string().optional().describe("Only this container, e.g. `main` or `#checkout`"),
19
+ mode: z
20
+ .enum(["full", "interactive"])
21
+ .default("full")
22
+ .describe("`full` is the whole tree with headings and text; `interactive` is a flat list of what can be clicked, typed into or chosen"),
23
+ max_chars: z
24
+ .number()
25
+ .int()
26
+ .min(200)
27
+ .max(MAX_SNAPSHOT_MAX_CHARS)
28
+ .default(DEFAULT_SNAPSHOT_MAX_CHARS)
29
+ .describe("Cut the tree past this many characters (a note says how many lines went)"),
30
+ include_screenshot: z.boolean().default(false).describe("Also return a screenshot of the page as it was read"),
31
+ include_components: z
32
+ .boolean()
33
+ .default(false)
34
+ .describe("On a Vue app: also return the component tree (names and nesting) from the root"),
35
+ wait_ms: z
36
+ .number()
37
+ .int()
38
+ .min(0)
39
+ .default(DEFAULT_SNAPSHOT_WAIT_MS)
40
+ .describe("Settle time (ms) after opening `url`. On a Vue app this is a ceiling: the page is read as soon as the app " +
41
+ "is mounted and its router ready"),
42
+ wait_for: z.string().optional().describe("CSS selector to wait for (visible) before reading"),
43
+ wait_for_timeout_ms: z.number().int().min(1).default(SELECTOR_TIMEOUT_MS).describe("Max time (ms) to wait for `wait_for`"),
44
+ viewport: z
45
+ .object({
46
+ width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),
47
+ height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),
48
+ })
49
+ .optional()
50
+ .describe("Resize the page to this first (defaults to leaving it as it is)"),
51
+ storage_state: storageStateField,
52
+ };
53
+ export const snapshotInputSchema = z.object(snapshotInputShape);
54
+ /**
55
+ * Read the page as a tree of named elements, on the interact session's page so
56
+ * the refs it hands out are the ones the next `framewatch_interact` or
57
+ * `framewatch_inspect` can use.
58
+ */
59
+ export function snapshotPage(rawInput) {
60
+ return withSessionLock(() => runSnapshot(rawInput));
61
+ }
62
+ async function runSnapshot(rawInput) {
63
+ const parsed = snapshotInputSchema.safeParse(rawInput);
64
+ if (!parsed.success) {
65
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
66
+ return errorResult(`Snapshot failed: invalid input — ${issues}`);
67
+ }
68
+ const input = parsed.data;
69
+ try {
70
+ const opened = await openSessionPage(input, "Snapshot");
71
+ if ("error" in opened)
72
+ return errorResult(opened.error);
73
+ const { page, vue, auth, login_visible } = opened;
74
+ const snapshot = await takeSnapshot(page, {
75
+ ...(input.selector ? { selector: input.selector } : {}),
76
+ mode: input.mode,
77
+ max_chars: input.max_chars,
78
+ });
79
+ const viewport = page.viewportSize() ?? { width: 0, height: 0 };
80
+ const title = await page.title().catch(() => "");
81
+ const lines = [
82
+ `Snapshot of ${page.url()}${title ? ` — "${title}"` : ""} — ${describeScale(viewport).replace(/, images.*$/, "")} — ${describeCounts(snapshot)}${vue ? ` — ${describeVue(vue)}` : ""}`,
83
+ ...(input.include_screenshot ? [describeScale(viewport)] : []),
84
+ ...(input.selector ? [`Scope: ${input.selector}`] : []),
85
+ REF_HINT,
86
+ "",
87
+ snapshot.text,
88
+ ...(snapshot.cut_lines > 0
89
+ ? [`… ${snapshot.cut_lines} more lines cut — raise \`max_chars\`, pass \`selector\`, or use mode "interactive".`]
90
+ : []),
91
+ ];
92
+ if (input.include_components) {
93
+ lines.push("", ...(await describeComponents(page, vue)));
94
+ }
95
+ const content = [{ type: "text", text: lines.join("\n") }];
96
+ if (input.include_screenshot) {
97
+ const png = await page.screenshot({ type: "png" });
98
+ content.unshift({ type: "image", data: toBase64(await resizeForOutput(png)), mimeType: "image/png" });
99
+ }
100
+ hmrFor(page).markSeen();
101
+ return withAuthNote({ content }, auth, login_visible);
102
+ }
103
+ catch (error) {
104
+ return errorResult(describeSessionFailure("Snapshot", input, error));
105
+ }
106
+ }
107
+ const REF_HINT = "Refs: pass one as `ref` to framewatch_interact (to act on it) or framewatch_inspect (to measure it). " +
108
+ "They stay valid until the page changes — snapshot again after an action that re-rendered. " +
109
+ 'In a framewatch_capture script, target the same element with a selector such as role=button[name="Sign in"].';
110
+ export function describeCounts(snapshot) {
111
+ return `${snapshot.elements} elements, ${snapshot.interactive} interactive`;
112
+ }
113
+ /** The component tree, or why there is none. */
114
+ async function describeComponents(page, vue) {
115
+ if (!vue)
116
+ return ["Components: no Vue app on this page"];
117
+ if (vue.major < 3)
118
+ return ["Components: Vue 2 — component details need Vue 3"];
119
+ if (vue.production)
120
+ return ["Components: production build — no component data on elements"];
121
+ const tree = await componentTree(page);
122
+ if (!tree)
123
+ return ["Components: could not be read"];
124
+ return formatComponentTree(tree.root, tree.total);
125
+ }
126
+ export async function openSessionPage(input, toolName) {
127
+ const options = {};
128
+ if (input.viewport)
129
+ options.viewport = input.viewport;
130
+ const auth = await resolveStorageState(input.storage_state);
131
+ if (auth)
132
+ options.storageState = { path: auth.path, state: auth.state };
133
+ const { page, previousUrl } = await getSessionPage(options);
134
+ const target = input.url ?? previousUrl;
135
+ if (target !== undefined) {
136
+ await page.goto(target, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
137
+ if (input.wait_for) {
138
+ await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS });
139
+ }
140
+ // `wait_ms` is a ceiling, not a sleep: a Vue app is read the moment it is
141
+ // mounted and its router has resolved; a page without one gets the full wait.
142
+ const { vue } = await waitForVueReady(page, { detect_ms: input.wait_ms ?? 0, ready_ms: VUE_READY_TIMEOUT_MS });
143
+ if (page.url() === "about:blank")
144
+ return { error: noPage(toolName) };
145
+ return { page, vue, auth, login_visible: auth && input.url !== undefined ? await loginFormVisible(page) : false };
146
+ }
147
+ if (page.url() === "about:blank")
148
+ return { error: noPage(toolName) };
149
+ return { page, vue: await detectVue(page), auth, login_visible: false };
150
+ }
151
+ function noPage(toolName) {
152
+ return `${toolName} failed: no page is open yet — pass \`url\` to open one (e.g. http://localhost:3000).`;
153
+ }
154
+ export function describeSessionFailure(toolName, input, error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ const firstLine = message.split("\n")[0];
157
+ if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
158
+ return `${toolName} failed: Playwright's Chromium browser is not installed. Run \`npx playwright install chromium\` and try again. (${firstLine})`;
159
+ }
160
+ if (/^page\.goto:/.test(message)) {
161
+ return `${toolName} failed: could not open ${input.url ?? "the page"} — ${firstLine}`;
162
+ }
163
+ if (input.wait_for && /^page\.waitForSelector:/.test(message)) {
164
+ return `${toolName} failed: selector "${input.wait_for}" did not become visible within ${input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS}ms.`;
165
+ }
166
+ return `${toolName} failed: ${firstLine}`;
167
+ }
168
+ function errorResult(text) {
169
+ return { isError: true, content: [{ type: "text", text }] };
170
+ }
171
+ export function registerSnapshotTool(server) {
172
+ server.registerTool(SNAPSHOT_TOOL_NAME, {
173
+ title: "Snapshot",
174
+ description: "Read the page as a tree of named elements — every heading, field, button and link with its accessible name " +
175
+ "and a short ref like `e8` — so you know what is on the page and what to call it instead of guessing selectors " +
176
+ "from a screenshot. Pass a ref as `ref` to framewatch_interact to act on that element, or to framewatch_inspect " +
177
+ "to measure it. Works on the page framewatch_interact keeps open; pass `url` to open one. " +
178
+ "Use mode `interactive` for a short list of only what can be acted on.",
179
+ inputSchema: snapshotInputShape,
180
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
181
+ }, async (args) => snapshotPage(args));
182
+ }
183
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/tools/snapshot.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_SNAPSHOT_MAX_CHARS,\n DEFAULT_SNAPSHOT_WAIT_MS,\n MAX_SNAPSHOT_MAX_CHARS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n VUE_READY_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { getSessionPage, withSessionLock, type SessionOptions } from \"../engine/browser.js\";\nimport { hmrFor } from \"../engine/hmr.js\";\nimport { takeSnapshot, type PageSnapshot } from \"../engine/snapshot.js\";\nimport { componentTree, detectVue, waitForVueReady } from \"../engine/vue.js\";\nimport { describeVue, formatComponentTree, type VueInfo } from \"../utils/vue-rules.js\";\nimport { describeScale } from \"../utils/format.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { loginFormVisible, resolveStorageState, storageStateField, withAuthNote, type ResolvedAuth } from \"../utils/storage-state.js\";\nimport type { Viewport } from \"../types.js\";\n\nexport const SNAPSHOT_TOOL_NAME = \"framewatch_snapshot\";\n\nexport const snapshotInputShape = {\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to read the page left open by framewatch_interact / framewatch_inspect.\"),\n selector: z.string().optional().describe(\"Only this container, e.g. `main` or `#checkout`\"),\n mode: z\n .enum([\"full\", \"interactive\"])\n .default(\"full\")\n .describe(\"`full` is the whole tree with headings and text; `interactive` is a flat list of what can be clicked, typed into or chosen\"),\n max_chars: z\n .number()\n .int()\n .min(200)\n .max(MAX_SNAPSHOT_MAX_CHARS)\n .default(DEFAULT_SNAPSHOT_MAX_CHARS)\n .describe(\"Cut the tree past this many characters (a note says how many lines went)\"),\n include_screenshot: z.boolean().default(false).describe(\"Also return a screenshot of the page as it was read\"),\n include_components: z\n .boolean()\n .default(false)\n .describe(\"On a Vue app: also return the component tree (names and nesting) from the root\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_SNAPSHOT_WAIT_MS)\n .describe(\n \"Settle time (ms) after opening `url`. On a Vue app this is a ceiling: the page is read as soon as the app \" +\n \"is mounted and its router ready\",\n ),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before reading\"),\n wait_for_timeout_ms: z.number().int().min(1).default(SELECTOR_TIMEOUT_MS).describe(\"Max time (ms) to wait for `wait_for`\"),\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 first (defaults to leaving it as it is)\"),\n storage_state: storageStateField,\n};\n\nexport const snapshotInputSchema = z.object(snapshotInputShape);\nexport type SnapshotInput = z.input<typeof snapshotInputSchema>;\n\n/**\n * Read the page as a tree of named elements, on the interact session's page so\n * the refs it hands out are the ones the next `framewatch_interact` or\n * `framewatch_inspect` can use.\n */\nexport function snapshotPage(rawInput: SnapshotInput): Promise<CallToolResult> {\n return withSessionLock(() => runSnapshot(rawInput));\n}\n\nasync function runSnapshot(rawInput: SnapshotInput): Promise<CallToolResult> {\n const parsed = snapshotInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Snapshot failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n try {\n const opened = await openSessionPage(input, \"Snapshot\");\n if (\"error\" in opened) return errorResult(opened.error);\n const { page, vue, auth, login_visible } = opened;\n\n const snapshot = await takeSnapshot(page, {\n ...(input.selector ? { selector: input.selector } : {}),\n mode: input.mode,\n max_chars: input.max_chars,\n });\n\n const viewport = page.viewportSize() ?? { width: 0, height: 0 };\n const title = await page.title().catch(() => \"\");\n const lines = [\n `Snapshot of ${page.url()}${title ? ` — \"${title}\"` : \"\"} — ${describeScale(viewport).replace(/, images.*$/, \"\")} — ${describeCounts(snapshot)}${vue ? ` — ${describeVue(vue)}` : \"\"}`,\n ...(input.include_screenshot ? [describeScale(viewport)] : []),\n ...(input.selector ? [`Scope: ${input.selector}`] : []),\n REF_HINT,\n \"\",\n snapshot.text,\n ...(snapshot.cut_lines > 0\n ? [`… ${snapshot.cut_lines} more lines cut — raise \\`max_chars\\`, pass \\`selector\\`, or use mode \"interactive\".`]\n : []),\n ];\n if (input.include_components) {\n lines.push(\"\", ...(await describeComponents(page, vue)));\n }\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: lines.join(\"\\n\") }];\n if (input.include_screenshot) {\n const png = await page.screenshot({ type: \"png\" });\n content.unshift({ type: \"image\", data: toBase64(await resizeForOutput(png)), mimeType: \"image/png\" });\n }\n hmrFor(page).markSeen();\n return withAuthNote({ content }, auth, login_visible);\n } catch (error) {\n return errorResult(describeSessionFailure(\"Snapshot\", input, error));\n }\n}\n\nconst REF_HINT =\n \"Refs: pass one as `ref` to framewatch_interact (to act on it) or framewatch_inspect (to measure it). \" +\n \"They stay valid until the page changes — snapshot again after an action that re-rendered. \" +\n 'In a framewatch_capture script, target the same element with a selector such as role=button[name=\"Sign in\"].';\n\nexport function describeCounts(snapshot: Pick<PageSnapshot, \"elements\" | \"interactive\">): string {\n return `${snapshot.elements} elements, ${snapshot.interactive} interactive`;\n}\n\n/** The component tree, or why there is none. */\nasync function describeComponents(page: Awaited<ReturnType<typeof getSessionPage>>[\"page\"], vue: VueInfo | null): Promise<string[]> {\n if (!vue) return [\"Components: no Vue app on this page\"];\n if (vue.major < 3) return [\"Components: Vue 2 — component details need Vue 3\"];\n if (vue.production) return [\"Components: production build — no component data on elements\"];\n const tree = await componentTree(page);\n if (!tree) return [\"Components: could not be read\"];\n return formatComponentTree(tree.root, tree.total);\n}\n\n/** Input the session-page tools share: where to open, how long to wait, what size. */\nexport interface SessionPageInput {\n url?: string;\n wait_ms?: number;\n wait_for?: string;\n wait_for_timeout_ms?: number;\n viewport?: Viewport;\n storage_state?: string;\n}\n\n/**\n * The session page, opened at `url` when one is given. Shared by snapshot and\n * inspect, which both read the page interact drives; `toolName` is only for\n * the error wording.\n */\nexport interface OpenedSessionPage {\n page: Awaited<ReturnType<typeof getSessionPage>>[\"page\"];\n vue: VueInfo | null;\n /** The saved auth in force for this call, if any. */\n auth: ResolvedAuth | null;\n /** Whether the page showed a login form after opening — only checked when `url` was given. */\n login_visible: boolean;\n}\n\nexport async function openSessionPage(input: SessionPageInput, toolName: string): Promise<OpenedSessionPage | { error: string }> {\n const options: SessionOptions = {};\n if (input.viewport) options.viewport = input.viewport;\n const auth = await resolveStorageState(input.storage_state);\n if (auth) options.storageState = { path: auth.path, state: auth.state };\n\n const { page, previousUrl } = await getSessionPage(options);\n const target = input.url ?? previousUrl;\n if (target !== undefined) {\n await page.goto(target, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS });\n }\n // `wait_ms` is a ceiling, not a sleep: a Vue app is read the moment it is\n // mounted and its router has resolved; a page without one gets the full wait.\n const { vue } = await waitForVueReady(page, { detect_ms: input.wait_ms ?? 0, ready_ms: VUE_READY_TIMEOUT_MS });\n if (page.url() === \"about:blank\") return { error: noPage(toolName) };\n return { page, vue, auth, login_visible: auth && input.url !== undefined ? await loginFormVisible(page) : false };\n }\n if (page.url() === \"about:blank\") return { error: noPage(toolName) };\n return { page, vue: await detectVue(page), auth, login_visible: false };\n}\n\nfunction noPage(toolName: string): string {\n return `${toolName} failed: no page is open yet — pass \\`url\\` to open one (e.g. http://localhost:3000).`;\n}\n\nexport function describeSessionFailure(toolName: string, input: { url?: string; wait_for?: string; wait_for_timeout_ms?: number }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return `${toolName} failed: Playwright's Chromium browser is not installed. Run \\`npx playwright install chromium\\` and try again. (${firstLine})`;\n }\n if (/^page\\.goto:/.test(message)) {\n return `${toolName} failed: could not open ${input.url ?? \"the page\"} — ${firstLine}`;\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${toolName} failed: selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms ?? SELECTOR_TIMEOUT_MS}ms.`;\n }\n return `${toolName} failed: ${firstLine}`;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\nexport function registerSnapshotTool(server: McpServer): void {\n server.registerTool(\n SNAPSHOT_TOOL_NAME,\n {\n title: \"Snapshot\",\n description:\n \"Read the page as a tree of named elements — every heading, field, button and link with its accessible name \" +\n \"and a short ref like `e8` — so you know what is on the page and what to call it instead of guessing selectors \" +\n \"from a screenshot. Pass a ref as `ref` to framewatch_interact to act on that element, or to framewatch_inspect \" +\n \"to measure it. Works on the page framewatch_interact keeps open; pass `url` to open one. \" +\n \"Use mode `interactive` for a short list of only what can be acted on.\",\n inputSchema: snapshotInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => snapshotPage(args),\n );\n}\n"]}
@@ -0,0 +1,107 @@
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 WAIT_FOR_TOOL_NAME = "framewatch_wait_for";
5
+ export declare const waitForInputShape: {
6
+ url: z.ZodOptional<z.ZodString>;
7
+ until: z.ZodDefault<z.ZodEnum<["hot_update", "vue_ready", "selector", "network_idle"]>>;
8
+ selector: z.ZodOptional<z.ZodString>;
9
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
10
+ include_screenshot: z.ZodDefault<z.ZodBoolean>;
11
+ include_snapshot: z.ZodDefault<z.ZodBoolean>;
12
+ wait_ms: z.ZodDefault<z.ZodNumber>;
13
+ viewport: z.ZodOptional<z.ZodObject<{
14
+ width: z.ZodNumber;
15
+ height: z.ZodNumber;
16
+ }, "strip", z.ZodTypeAny, {
17
+ width: number;
18
+ height: number;
19
+ }, {
20
+ width: number;
21
+ height: number;
22
+ }>>;
23
+ storage_state: z.ZodOptional<z.ZodString>;
24
+ };
25
+ export declare const waitForInputSchema: z.ZodEffects<z.ZodObject<{
26
+ url: z.ZodOptional<z.ZodString>;
27
+ until: z.ZodDefault<z.ZodEnum<["hot_update", "vue_ready", "selector", "network_idle"]>>;
28
+ selector: z.ZodOptional<z.ZodString>;
29
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
30
+ include_screenshot: z.ZodDefault<z.ZodBoolean>;
31
+ include_snapshot: z.ZodDefault<z.ZodBoolean>;
32
+ wait_ms: z.ZodDefault<z.ZodNumber>;
33
+ viewport: z.ZodOptional<z.ZodObject<{
34
+ width: z.ZodNumber;
35
+ height: z.ZodNumber;
36
+ }, "strip", z.ZodTypeAny, {
37
+ width: number;
38
+ height: number;
39
+ }, {
40
+ width: number;
41
+ height: number;
42
+ }>>;
43
+ storage_state: z.ZodOptional<z.ZodString>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ wait_ms: number;
46
+ timeout_ms: number;
47
+ include_screenshot: boolean;
48
+ include_snapshot: boolean;
49
+ until: "selector" | "hot_update" | "vue_ready" | "network_idle";
50
+ url?: string | undefined;
51
+ viewport?: {
52
+ width: number;
53
+ height: number;
54
+ } | undefined;
55
+ storage_state?: string | undefined;
56
+ selector?: string | undefined;
57
+ }, {
58
+ url?: string | undefined;
59
+ viewport?: {
60
+ width: number;
61
+ height: number;
62
+ } | undefined;
63
+ wait_ms?: number | undefined;
64
+ storage_state?: string | undefined;
65
+ timeout_ms?: number | undefined;
66
+ selector?: string | undefined;
67
+ include_screenshot?: boolean | undefined;
68
+ include_snapshot?: boolean | undefined;
69
+ until?: "selector" | "hot_update" | "vue_ready" | "network_idle" | undefined;
70
+ }>, {
71
+ wait_ms: number;
72
+ timeout_ms: number;
73
+ include_screenshot: boolean;
74
+ include_snapshot: boolean;
75
+ until: "selector" | "hot_update" | "vue_ready" | "network_idle";
76
+ url?: string | undefined;
77
+ viewport?: {
78
+ width: number;
79
+ height: number;
80
+ } | undefined;
81
+ storage_state?: string | undefined;
82
+ selector?: string | undefined;
83
+ }, {
84
+ url?: string | undefined;
85
+ viewport?: {
86
+ width: number;
87
+ height: number;
88
+ } | undefined;
89
+ wait_ms?: number | undefined;
90
+ storage_state?: string | undefined;
91
+ timeout_ms?: number | undefined;
92
+ selector?: string | undefined;
93
+ include_screenshot?: boolean | undefined;
94
+ include_snapshot?: boolean | undefined;
95
+ until?: "selector" | "hot_update" | "vue_ready" | "network_idle" | undefined;
96
+ }>;
97
+ export type WaitForInput = z.input<typeof waitForInputSchema>;
98
+ /**
99
+ * Wait for the session page to reach a state worth looking at, then look.
100
+ *
101
+ * The one that earns the tool its place is `hot_update`: after the agent
102
+ * saves a file, Vite patches the open page in place, and this returns the
103
+ * moment that patch has landed — no reload, no replayed flow, no guessed
104
+ * sleep. The other conditions replace the guessed sleeps elsewhere.
105
+ */
106
+ export declare function waitFor(rawInput: WaitForInput): Promise<CallToolResult>;
107
+ export declare function registerWaitForTool(server: McpServer): void;
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_SNAPSHOT_MAX_CHARS, DEFAULT_SNAPSHOT_WAIT_MS, DEFAULT_WAIT_FOR_TIMEOUT_MS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MAX_WAIT_FOR_TIMEOUT_MS, VUE_DETECT_MS, VUE_READY_TIMEOUT_MS, } from "../constants.js";
3
+ import { withSessionLock } from "../engine/browser.js";
4
+ import { hasViteClient, hmrFor } from "../engine/hmr.js";
5
+ import { takeSnapshot } from "../engine/snapshot.js";
6
+ import { detectVue, waitForVueReady } from "../engine/vue.js";
7
+ import { describeScale } from "../utils/format.js";
8
+ import { resizeForOutput, toBase64 } from "../utils/image.js";
9
+ import { storageStateField, withAuthNote } from "../utils/storage-state.js";
10
+ import { describeVue } from "../utils/vue-rules.js";
11
+ import { describeCounts, describeSessionFailure, openSessionPage } from "./snapshot.js";
12
+ export const WAIT_FOR_TOOL_NAME = "framewatch_wait_for";
13
+ export const waitForInputShape = {
14
+ url: z
15
+ .string()
16
+ .url()
17
+ .optional()
18
+ .describe("Open this URL first. Omit to wait on the page left open by framewatch_interact / framewatch_snapshot."),
19
+ until: z
20
+ .enum(["hot_update", "vue_ready", "selector", "network_idle"])
21
+ .default("hot_update")
22
+ .describe("`hot_update`: Vite applied a hot update (or full reload) newer than the last tool call on this page — " +
23
+ "use it right after saving a file. `vue_ready`: a Vue app is mounted and its router has resolved. " +
24
+ "`selector`: `selector` is visible. `network_idle`: no requests for 500ms."),
25
+ selector: z.string().optional().describe("For `until: selector` — the element to wait for"),
26
+ timeout_ms: z
27
+ .number()
28
+ .int()
29
+ .min(1)
30
+ .max(MAX_WAIT_FOR_TIMEOUT_MS)
31
+ .default(DEFAULT_WAIT_FOR_TIMEOUT_MS)
32
+ .describe("Give up after this long"),
33
+ include_screenshot: z.boolean().default(true).describe("Return a screenshot once the condition holds"),
34
+ include_snapshot: z.boolean().default(false).describe("Also return a framewatch_snapshot with fresh refs"),
35
+ wait_ms: z.number().int().min(0).default(DEFAULT_SNAPSHOT_WAIT_MS).describe("Settle time (ms) after opening `url`"),
36
+ viewport: z
37
+ .object({
38
+ width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH),
39
+ height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT),
40
+ })
41
+ .optional()
42
+ .describe("Resize the page to this first (defaults to leaving it as it is)"),
43
+ storage_state: storageStateField,
44
+ };
45
+ export const waitForInputSchema = z.object(waitForInputShape).superRefine((value, ctx) => {
46
+ if (value.until === "selector" && !value.selector) {
47
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "`until: selector` needs a `selector`", path: ["selector"] });
48
+ }
49
+ });
50
+ /**
51
+ * Wait for the session page to reach a state worth looking at, then look.
52
+ *
53
+ * The one that earns the tool its place is `hot_update`: after the agent
54
+ * saves a file, Vite patches the open page in place, and this returns the
55
+ * moment that patch has landed — no reload, no replayed flow, no guessed
56
+ * sleep. The other conditions replace the guessed sleeps elsewhere.
57
+ */
58
+ export function waitFor(rawInput) {
59
+ return withSessionLock(() => runWaitFor(rawInput));
60
+ }
61
+ async function runWaitFor(rawInput) {
62
+ const parsed = waitForInputSchema.safeParse(rawInput);
63
+ if (!parsed.success) {
64
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
65
+ return errorResult(`Wait failed: invalid input — ${issues}`);
66
+ }
67
+ const input = parsed.data;
68
+ try {
69
+ const opened = await openSessionPage(input, "Wait");
70
+ if ("error" in opened)
71
+ return errorResult(opened.error);
72
+ const { page, auth, login_visible } = opened;
73
+ const watcher = hmrFor(page);
74
+ const since = watcher.lastSeen;
75
+ const started = Date.now();
76
+ const elapsed = () => Date.now() - started;
77
+ let headline;
78
+ let vue = null;
79
+ switch (input.until) {
80
+ case "hot_update": {
81
+ const event = await watcher.waitForEvent(since, input.timeout_ms);
82
+ if (!event) {
83
+ if (!watcher.connected && !(await hasViteClient(page))) {
84
+ return errorResult(`Wait failed: ${page.url()} has no Vite dev-server connection (no \`[vite] connected.\` in the console and no /@vite/client script), so there will never be a hot update to wait for. Is this the dev server, or a built page?`);
85
+ }
86
+ const latest = watcher.latest;
87
+ const last = latest ? ` — last one landed ${started - latest.t}ms before this call (${latest.path})` : "";
88
+ return errorResult(`Wait failed: No hot update within ${input.timeout_ms}ms${last}. Save the file, then call again.`);
89
+ }
90
+ const landed = event.t - started;
91
+ if (event.kind === "reload") {
92
+ await page.waitForLoadState("load", { timeout: input.timeout_ms }).catch(() => undefined);
93
+ }
94
+ vue = (await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_TIMEOUT_MS })).vue;
95
+ const what = event.kind === "reload" ? "Full reload" : "Hot update";
96
+ const kind = event.kind === "css" ? " (css)" : "";
97
+ headline = `${what} landed after ${Math.max(0, landed)}ms${kind}${event.path ? `: ${event.path}` : ""}`;
98
+ break;
99
+ }
100
+ case "vue_ready": {
101
+ const result = await waitForVueReady(page, { detect_ms: input.timeout_ms, ready_ms: input.timeout_ms });
102
+ if (!result.vue)
103
+ return errorResult(`Wait failed: No Vue app mounted on ${page.url()} within ${input.timeout_ms}ms.`);
104
+ vue = result.vue;
105
+ headline = `Vue ready after ${result.waited_ms}ms`;
106
+ break;
107
+ }
108
+ case "selector": {
109
+ try {
110
+ await page.waitForSelector(input.selector, { state: "visible", timeout: input.timeout_ms });
111
+ }
112
+ catch {
113
+ return errorResult(`Wait failed: "${input.selector}" did not appear within ${input.timeout_ms}ms on ${page.url()}.`);
114
+ }
115
+ headline = `"${input.selector}" appeared after ${elapsed()}ms`;
116
+ vue = await detectVue(page);
117
+ break;
118
+ }
119
+ case "network_idle": {
120
+ try {
121
+ await page.waitForLoadState("networkidle", { timeout: input.timeout_ms });
122
+ }
123
+ catch {
124
+ return errorResult(`Wait failed: the network did not go idle within ${input.timeout_ms}ms on ${page.url()}.`);
125
+ }
126
+ headline = `Network idle after ${elapsed()}ms`;
127
+ vue = await detectVue(page);
128
+ break;
129
+ }
130
+ }
131
+ const viewport = page.viewportSize() ?? { width: 0, height: 0 };
132
+ const lines = [`${headline}${vue ? ` — ${describeVue(vue)}` : ""} — ${page.url()} — ${describeScale(viewport)}`];
133
+ const content = [{ type: "text", text: lines.join("\n") }];
134
+ if (input.include_screenshot) {
135
+ const png = await page.screenshot({ type: "png" });
136
+ content.push({ type: "image", data: toBase64(await resizeForOutput(png)), mimeType: "image/png" });
137
+ }
138
+ if (input.include_snapshot) {
139
+ const snapshot = await takeSnapshot(page, { mode: "full", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });
140
+ const text = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, "", snapshot.text];
141
+ if (snapshot.cut_lines > 0)
142
+ text.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \`selector\` or mode "interactive".`);
143
+ content.push({ type: "text", text: text.join("\n") });
144
+ }
145
+ watcher.markSeen();
146
+ return withAuthNote({ content }, auth, login_visible);
147
+ }
148
+ catch (error) {
149
+ return errorResult(describeSessionFailure("Wait", input, error));
150
+ }
151
+ }
152
+ function errorResult(text) {
153
+ return { isError: true, content: [{ type: "text", text }] };
154
+ }
155
+ export function registerWaitForTool(server) {
156
+ server.registerTool(WAIT_FOR_TOOL_NAME, {
157
+ title: "Wait for",
158
+ description: "Wait for the open page to reach a state worth looking at, then screenshot it. After you save a file, " +
159
+ "`until: hot_update` returns the moment Vite has patched the page in place (or reloaded it) — no reload, " +
160
+ "no replayed flow, no guessed sleep — and names the file that changed. `vue_ready` waits for a Vue app to " +
161
+ "mount and its router to resolve; `selector` for an element; `network_idle` for requests to stop. " +
162
+ "Works on the page framewatch_interact / framewatch_snapshot keep open; `include_snapshot` returns fresh refs.",
163
+ inputSchema: waitForInputShape,
164
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
165
+ }, async (args) => waitFor(args));
166
+ }
167
+ //# sourceMappingURL=wait-for.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-for.js","sourceRoot":"","sources":["../../src/tools/wait-for.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_SNAPSHOT_MAX_CHARS,\n DEFAULT_SNAPSHOT_WAIT_MS,\n DEFAULT_WAIT_FOR_TIMEOUT_MS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n MAX_WAIT_FOR_TIMEOUT_MS,\n VUE_DETECT_MS,\n VUE_READY_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withSessionLock } from \"../engine/browser.js\";\nimport { hasViteClient, hmrFor } from \"../engine/hmr.js\";\nimport { takeSnapshot } from \"../engine/snapshot.js\";\nimport { detectVue, waitForVueReady } from \"../engine/vue.js\";\nimport { describeScale } from \"../utils/format.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { storageStateField, withAuthNote } from \"../utils/storage-state.js\";\nimport { describeVue, type VueInfo } from \"../utils/vue-rules.js\";\nimport { describeCounts, describeSessionFailure, openSessionPage } from \"./snapshot.js\";\n\nexport const WAIT_FOR_TOOL_NAME = \"framewatch_wait_for\";\n\nexport const waitForInputShape = {\n url: z\n .string()\n .url()\n .optional()\n .describe(\"Open this URL first. Omit to wait on the page left open by framewatch_interact / framewatch_snapshot.\"),\n until: z\n .enum([\"hot_update\", \"vue_ready\", \"selector\", \"network_idle\"])\n .default(\"hot_update\")\n .describe(\n \"`hot_update`: Vite applied a hot update (or full reload) newer than the last tool call on this page — \" +\n \"use it right after saving a file. `vue_ready`: a Vue app is mounted and its router has resolved. \" +\n \"`selector`: `selector` is visible. `network_idle`: no requests for 500ms.\",\n ),\n selector: z.string().optional().describe(\"For `until: selector` — the element to wait for\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .max(MAX_WAIT_FOR_TIMEOUT_MS)\n .default(DEFAULT_WAIT_FOR_TIMEOUT_MS)\n .describe(\"Give up after this long\"),\n include_screenshot: z.boolean().default(true).describe(\"Return a screenshot once the condition holds\"),\n include_snapshot: z.boolean().default(false).describe(\"Also return a framewatch_snapshot with fresh refs\"),\n wait_ms: z.number().int().min(0).default(DEFAULT_SNAPSHOT_WAIT_MS).describe(\"Settle time (ms) after opening `url`\"),\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 first (defaults to leaving it as it is)\"),\n storage_state: storageStateField,\n};\n\nexport const waitForInputSchema = z.object(waitForInputShape).superRefine((value, ctx) => {\n if (value.until === \"selector\" && !value.selector) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: \"`until: selector` needs a `selector`\", path: [\"selector\"] });\n }\n});\nexport type WaitForInput = z.input<typeof waitForInputSchema>;\n\n/**\n * Wait for the session page to reach a state worth looking at, then look.\n *\n * The one that earns the tool its place is `hot_update`: after the agent\n * saves a file, Vite patches the open page in place, and this returns the\n * moment that patch has landed — no reload, no replayed flow, no guessed\n * sleep. The other conditions replace the guessed sleeps elsewhere.\n */\nexport function waitFor(rawInput: WaitForInput): Promise<CallToolResult> {\n return withSessionLock(() => runWaitFor(rawInput));\n}\n\nasync function runWaitFor(rawInput: WaitForInput): Promise<CallToolResult> {\n const parsed = waitForInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Wait failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n try {\n const opened = await openSessionPage(input, \"Wait\");\n if (\"error\" in opened) return errorResult(opened.error);\n const { page, auth, login_visible } = opened;\n const watcher = hmrFor(page);\n const since = watcher.lastSeen;\n const started = Date.now();\n const elapsed = (): number => Date.now() - started;\n\n let headline: string;\n let vue: VueInfo | null = null;\n\n switch (input.until) {\n case \"hot_update\": {\n const event = await watcher.waitForEvent(since, input.timeout_ms);\n if (!event) {\n if (!watcher.connected && !(await hasViteClient(page))) {\n return errorResult(\n `Wait failed: ${page.url()} has no Vite dev-server connection (no \\`[vite] connected.\\` in the console and no /@vite/client script), so there will never be a hot update to wait for. Is this the dev server, or a built page?`,\n );\n }\n const latest = watcher.latest;\n const last = latest ? ` — last one landed ${started - latest.t}ms before this call (${latest.path})` : \"\";\n return errorResult(`Wait failed: No hot update within ${input.timeout_ms}ms${last}. Save the file, then call again.`);\n }\n const landed = event.t - started;\n if (event.kind === \"reload\") {\n await page.waitForLoadState(\"load\", { timeout: input.timeout_ms }).catch(() => undefined);\n }\n vue = (await waitForVueReady(page, { detect_ms: VUE_DETECT_MS, ready_ms: VUE_READY_TIMEOUT_MS })).vue;\n const what = event.kind === \"reload\" ? \"Full reload\" : \"Hot update\";\n const kind = event.kind === \"css\" ? \" (css)\" : \"\";\n headline = `${what} landed after ${Math.max(0, landed)}ms${kind}${event.path ? `: ${event.path}` : \"\"}`;\n break;\n }\n case \"vue_ready\": {\n const result = await waitForVueReady(page, { detect_ms: input.timeout_ms, ready_ms: input.timeout_ms });\n if (!result.vue) return errorResult(`Wait failed: No Vue app mounted on ${page.url()} within ${input.timeout_ms}ms.`);\n vue = result.vue;\n headline = `Vue ready after ${result.waited_ms}ms`;\n break;\n }\n case \"selector\": {\n try {\n await page.waitForSelector(input.selector!, { state: \"visible\", timeout: input.timeout_ms });\n } catch {\n return errorResult(`Wait failed: \"${input.selector}\" did not appear within ${input.timeout_ms}ms on ${page.url()}.`);\n }\n headline = `\"${input.selector}\" appeared after ${elapsed()}ms`;\n vue = await detectVue(page);\n break;\n }\n case \"network_idle\": {\n try {\n await page.waitForLoadState(\"networkidle\", { timeout: input.timeout_ms });\n } catch {\n return errorResult(`Wait failed: the network did not go idle within ${input.timeout_ms}ms on ${page.url()}.`);\n }\n headline = `Network idle after ${elapsed()}ms`;\n vue = await detectVue(page);\n break;\n }\n }\n\n const viewport = page.viewportSize() ?? { width: 0, height: 0 };\n const lines = [`${headline}${vue ? ` — ${describeVue(vue)}` : \"\"} — ${page.url()} — ${describeScale(viewport)}`];\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: lines.join(\"\\n\") }];\n if (input.include_screenshot) {\n const png = await page.screenshot({ type: \"png\" });\n content.push({ type: \"image\", data: toBase64(await resizeForOutput(png)), mimeType: \"image/png\" });\n }\n if (input.include_snapshot) {\n const snapshot = await takeSnapshot(page, { mode: \"full\", max_chars: DEFAULT_SNAPSHOT_MAX_CHARS });\n const text = [`Snapshot — ${describeCounts(snapshot)} (refs valid until the page changes)`, \"\", snapshot.text];\n if (snapshot.cut_lines > 0) text.push(`… ${snapshot.cut_lines} more lines cut — use framewatch_snapshot with \\`selector\\` or mode \"interactive\".`);\n content.push({ type: \"text\", text: text.join(\"\\n\") });\n }\n watcher.markSeen();\n return withAuthNote({ content }, auth, login_visible);\n } catch (error) {\n return errorResult(describeSessionFailure(\"Wait\", input, error));\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\nexport function registerWaitForTool(server: McpServer): void {\n server.registerTool(\n WAIT_FOR_TOOL_NAME,\n {\n title: \"Wait for\",\n description:\n \"Wait for the open page to reach a state worth looking at, then screenshot it. After you save a file, \" +\n \"`until: hot_update` returns the moment Vite has patched the page in place (or reloaded it) — no reload, \" +\n \"no replayed flow, no guessed sleep — and names the file that changed. `vue_ready` waits for a Vue app to \" +\n \"mount and its router to resolve; `selector` for an element; `network_idle` for requests to stop. \" +\n \"Works on the page framewatch_interact / framewatch_snapshot keep open; `include_snapshot` returns fresh refs.\",\n inputSchema: waitForInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => waitFor(args),\n );\n}\n"]}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build Arabic text about as long as `original`.
3
+ *
4
+ * `seed` makes the choice deterministic and varied: the same string in the
5
+ * same place gets the same replacement on every run (so two runs of the tool
6
+ * are comparable, and the LTR and RTL passes agree), while two different
7
+ * elements of the same length do not all get the same word.
8
+ *
9
+ * A string that is only digits and punctuation keeps its shape and is merely
10
+ * transliterated — see `ARABIC_DIGITS`.
11
+ */
12
+ export declare function arabicFor(original: string, seed?: number): string;
13
+ /** Latin digits to Arabic-Indic, leaving everything else alone. */
14
+ export declare function toArabicDigits(value: string): string;