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.
Files changed (127) hide show
  1. package/README.md +797 -8
  2. package/dist/constants.d.ts +264 -0
  3. package/dist/constants.js +268 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.js +4 -0
  6. package/dist/engine/browser.js.map +1 -1
  7. package/dist/engine/clicks.d.ts +221 -0
  8. package/dist/engine/clicks.js +801 -0
  9. package/dist/engine/clicks.js.map +1 -0
  10. package/dist/engine/forms.d.ts +137 -0
  11. package/dist/engine/forms.js +474 -0
  12. package/dist/engine/forms.js.map +1 -0
  13. package/dist/engine/hmr.d.ts +41 -0
  14. package/dist/engine/hmr.js +91 -0
  15. package/dist/engine/hmr.js.map +1 -0
  16. package/dist/engine/inspect.d.ts +31 -0
  17. package/dist/engine/inspect.js +383 -0
  18. package/dist/engine/inspect.js.map +1 -0
  19. package/dist/engine/interaction.d.ts +6 -1
  20. package/dist/engine/interaction.js +21 -6
  21. package/dist/engine/interaction.js.map +1 -1
  22. package/dist/engine/links.d.ts +134 -0
  23. package/dist/engine/links.js +384 -0
  24. package/dist/engine/links.js.map +1 -0
  25. package/dist/engine/mocks.d.ts +53 -0
  26. package/dist/engine/mocks.js +148 -0
  27. package/dist/engine/mocks.js.map +1 -0
  28. package/dist/engine/rtl.d.ts +129 -0
  29. package/dist/engine/rtl.js +540 -0
  30. package/dist/engine/rtl.js.map +1 -0
  31. package/dist/engine/seo.d.ts +189 -0
  32. package/dist/engine/seo.js +398 -0
  33. package/dist/engine/seo.js.map +1 -0
  34. package/dist/engine/snapshot.d.ts +29 -0
  35. package/dist/engine/snapshot.js +10 -0
  36. package/dist/engine/snapshot.js.map +1 -0
  37. package/dist/engine/vue.d.ts +54 -0
  38. package/dist/engine/vue.js +419 -0
  39. package/dist/engine/vue.js.map +1 -0
  40. package/dist/index.js +42 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/tools/accessibility.js +4 -3
  43. package/dist/tools/accessibility.js.map +1 -1
  44. package/dist/tools/api-mock.d.ts +405 -0
  45. package/dist/tools/api-mock.js +186 -0
  46. package/dist/tools/api-mock.js.map +1 -0
  47. package/dist/tools/capture.d.ts +83 -23
  48. package/dist/tools/capture.js +105 -60
  49. package/dist/tools/capture.js.map +1 -1
  50. package/dist/tools/compare.js +4 -3
  51. package/dist/tools/compare.js.map +1 -1
  52. package/dist/tools/dead-clicks.d.ts +128 -0
  53. package/dist/tools/dead-clicks.js +570 -0
  54. package/dist/tools/dead-clicks.js.map +1 -0
  55. package/dist/tools/form-test.d.ts +112 -0
  56. package/dist/tools/form-test.js +477 -0
  57. package/dist/tools/form-test.js.map +1 -0
  58. package/dist/tools/index.d.ts +16 -1
  59. package/dist/tools/index.js +42 -1
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/inspect.d.ts +78 -0
  62. package/dist/tools/inspect.js +136 -0
  63. package/dist/tools/inspect.js.map +1 -0
  64. package/dist/tools/interact.d.ts +29 -16
  65. package/dist/tools/interact.js +99 -13
  66. package/dist/tools/interact.js.map +1 -1
  67. package/dist/tools/links.d.ts +129 -0
  68. package/dist/tools/links.js +640 -0
  69. package/dist/tools/links.js.map +1 -0
  70. package/dist/tools/responsive.d.ts +6 -6
  71. package/dist/tools/responsive.js +9 -9
  72. package/dist/tools/responsive.js.map +1 -1
  73. package/dist/tools/rtl.d.ts +241 -0
  74. package/dist/tools/rtl.js +410 -0
  75. package/dist/tools/rtl.js.map +1 -0
  76. package/dist/tools/save-auth.d.ts +22 -22
  77. package/dist/tools/screenshot.js +13 -5
  78. package/dist/tools/screenshot.js.map +1 -1
  79. package/dist/tools/seo.d.ts +113 -0
  80. package/dist/tools/seo.js +281 -0
  81. package/dist/tools/seo.js.map +1 -0
  82. package/dist/tools/snapshot.d.ts +122 -0
  83. package/dist/tools/snapshot.js +183 -0
  84. package/dist/tools/snapshot.js.map +1 -0
  85. package/dist/tools/wait-for.d.ts +107 -0
  86. package/dist/tools/wait-for.js +167 -0
  87. package/dist/tools/wait-for.js.map +1 -0
  88. package/dist/utils/arabic-text.d.ts +14 -0
  89. package/dist/utils/arabic-text.js +193 -0
  90. package/dist/utils/arabic-text.js.map +1 -0
  91. package/dist/utils/budget.d.ts +41 -0
  92. package/dist/utils/budget.js +182 -0
  93. package/dist/utils/budget.js.map +1 -0
  94. package/dist/utils/format.d.ts +11 -1
  95. package/dist/utils/format.js +27 -4
  96. package/dist/utils/format.js.map +1 -1
  97. package/dist/utils/highlight.d.ts +69 -0
  98. package/dist/utils/highlight.js +181 -0
  99. package/dist/utils/highlight.js.map +1 -0
  100. package/dist/utils/link-rules.d.ts +100 -0
  101. package/dist/utils/link-rules.js +284 -0
  102. package/dist/utils/link-rules.js.map +1 -0
  103. package/dist/utils/mock-rules.d.ts +144 -0
  104. package/dist/utils/mock-rules.js +224 -0
  105. package/dist/utils/mock-rules.js.map +1 -0
  106. package/dist/utils/rtl-rules.d.ts +142 -0
  107. package/dist/utils/rtl-rules.js +296 -0
  108. package/dist/utils/rtl-rules.js.map +1 -0
  109. package/dist/utils/seo-rules.d.ts +129 -0
  110. package/dist/utils/seo-rules.js +726 -0
  111. package/dist/utils/seo-rules.js.map +1 -0
  112. package/dist/utils/snapshot-rules.d.ts +33 -0
  113. package/dist/utils/snapshot-rules.js +111 -0
  114. package/dist/utils/snapshot-rules.js.map +1 -0
  115. package/dist/utils/storage-state.d.ts +42 -1
  116. package/dist/utils/storage-state.js +115 -4
  117. package/dist/utils/storage-state.js.map +1 -1
  118. package/dist/utils/style-rules.d.ts +107 -0
  119. package/dist/utils/style-rules.js +223 -0
  120. package/dist/utils/style-rules.js.map +1 -0
  121. package/dist/utils/test-data.d.ts +75 -0
  122. package/dist/utils/test-data.js +294 -0
  123. package/dist/utils/test-data.js.map +1 -0
  124. package/dist/utils/vue-rules.d.ts +72 -0
  125. package/dist/utils/vue-rules.js +108 -0
  126. package/dist/utils/vue-rules.js.map +1 -0
  127. package/package.json +6 -4
@@ -13,8 +13,8 @@ export declare const saveAuthInteractionSchema: z.ZodEffects<z.ZodObject<{
13
13
  delta_y: z.ZodOptional<z.ZodNumber>;
14
14
  action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
15
15
  }, "strip", z.ZodTypeAny, {
16
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
17
16
  delay_ms: number;
17
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
18
18
  value?: string | undefined;
19
19
  x?: number | undefined;
20
20
  y?: number | undefined;
@@ -22,17 +22,17 @@ export declare const saveAuthInteractionSchema: z.ZodEffects<z.ZodObject<{
22
22
  delta_x?: number | undefined;
23
23
  delta_y?: number | undefined;
24
24
  }, {
25
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
25
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
26
26
  value?: string | undefined;
27
+ delay_ms?: number | undefined;
27
28
  x?: number | undefined;
28
29
  y?: number | undefined;
29
30
  selector?: string | undefined;
30
31
  delta_x?: number | undefined;
31
32
  delta_y?: number | undefined;
32
- delay_ms?: number | undefined;
33
33
  }>, {
34
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
35
34
  delay_ms: number;
35
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
36
36
  value?: string | undefined;
37
37
  x?: number | undefined;
38
38
  y?: number | undefined;
@@ -40,14 +40,14 @@ export declare const saveAuthInteractionSchema: z.ZodEffects<z.ZodObject<{
40
40
  delta_x?: number | undefined;
41
41
  delta_y?: number | undefined;
42
42
  }, {
43
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
43
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
44
44
  value?: string | undefined;
45
+ delay_ms?: number | undefined;
45
46
  x?: number | undefined;
46
47
  y?: number | undefined;
47
48
  selector?: string | undefined;
48
49
  delta_x?: number | undefined;
49
50
  delta_y?: number | undefined;
50
- delay_ms?: number | undefined;
51
51
  }>;
52
52
  export declare const saveAuthInputShape: {
53
53
  url: z.ZodString;
@@ -61,8 +61,8 @@ export declare const saveAuthInputShape: {
61
61
  delta_y: z.ZodOptional<z.ZodNumber>;
62
62
  action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
63
63
  }, "strip", z.ZodTypeAny, {
64
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
65
64
  delay_ms: number;
65
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
66
66
  value?: string | undefined;
67
67
  x?: number | undefined;
68
68
  y?: number | undefined;
@@ -70,17 +70,17 @@ export declare const saveAuthInputShape: {
70
70
  delta_x?: number | undefined;
71
71
  delta_y?: number | undefined;
72
72
  }, {
73
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
73
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
74
74
  value?: string | undefined;
75
+ delay_ms?: number | undefined;
75
76
  x?: number | undefined;
76
77
  y?: number | undefined;
77
78
  selector?: string | undefined;
78
79
  delta_x?: number | undefined;
79
80
  delta_y?: number | undefined;
80
- delay_ms?: number | undefined;
81
81
  }>, {
82
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
83
82
  delay_ms: number;
83
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
84
84
  value?: string | undefined;
85
85
  x?: number | undefined;
86
86
  y?: number | undefined;
@@ -88,14 +88,14 @@ export declare const saveAuthInputShape: {
88
88
  delta_x?: number | undefined;
89
89
  delta_y?: number | undefined;
90
90
  }, {
91
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
91
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
92
92
  value?: string | undefined;
93
+ delay_ms?: number | undefined;
93
94
  x?: number | undefined;
94
95
  y?: number | undefined;
95
96
  selector?: string | undefined;
96
97
  delta_x?: number | undefined;
97
98
  delta_y?: number | undefined;
98
- delay_ms?: number | undefined;
99
99
  }>, "many">;
100
100
  output_path: z.ZodDefault<z.ZodString>;
101
101
  wait_for: z.ZodOptional<z.ZodString>;
@@ -130,8 +130,8 @@ export declare const saveAuthInputSchema: z.ZodObject<{
130
130
  delta_y: z.ZodOptional<z.ZodNumber>;
131
131
  action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
132
132
  }, "strip", z.ZodTypeAny, {
133
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
134
133
  delay_ms: number;
134
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
135
135
  value?: string | undefined;
136
136
  x?: number | undefined;
137
137
  y?: number | undefined;
@@ -139,17 +139,17 @@ export declare const saveAuthInputSchema: z.ZodObject<{
139
139
  delta_x?: number | undefined;
140
140
  delta_y?: number | undefined;
141
141
  }, {
142
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
142
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
143
143
  value?: string | undefined;
144
+ delay_ms?: number | undefined;
144
145
  x?: number | undefined;
145
146
  y?: number | undefined;
146
147
  selector?: string | undefined;
147
148
  delta_x?: number | undefined;
148
149
  delta_y?: number | undefined;
149
- delay_ms?: number | undefined;
150
150
  }>, {
151
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
152
151
  delay_ms: number;
152
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
153
153
  value?: string | undefined;
154
154
  x?: number | undefined;
155
155
  y?: number | undefined;
@@ -157,14 +157,14 @@ export declare const saveAuthInputSchema: z.ZodObject<{
157
157
  delta_x?: number | undefined;
158
158
  delta_y?: number | undefined;
159
159
  }, {
160
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
160
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
161
161
  value?: string | undefined;
162
+ delay_ms?: number | undefined;
162
163
  x?: number | undefined;
163
164
  y?: number | undefined;
164
165
  selector?: string | undefined;
165
166
  delta_x?: number | undefined;
166
167
  delta_y?: number | undefined;
167
- delay_ms?: number | undefined;
168
168
  }>, "many">;
169
169
  output_path: z.ZodDefault<z.ZodString>;
170
170
  wait_for: z.ZodOptional<z.ZodString>;
@@ -189,9 +189,10 @@ export declare const saveAuthInputSchema: z.ZodObject<{
189
189
  }, "strip", z.ZodTypeAny, {
190
190
  url: string;
191
191
  wait_for_timeout_ms: number;
192
+ timeout_ms: number;
192
193
  interactions: {
193
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
194
194
  delay_ms: number;
195
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
195
196
  value?: string | undefined;
196
197
  x?: number | undefined;
197
198
  y?: number | undefined;
@@ -199,7 +200,6 @@ export declare const saveAuthInputSchema: z.ZodObject<{
199
200
  delta_x?: number | undefined;
200
201
  delta_y?: number | undefined;
201
202
  }[];
202
- timeout_ms: number;
203
203
  output_path: string;
204
204
  viewport?: {
205
205
  width: number;
@@ -211,14 +211,14 @@ export declare const saveAuthInputSchema: z.ZodObject<{
211
211
  }, {
212
212
  url: string;
213
213
  interactions: {
214
- action: "type" | "click" | "tap" | "key" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
214
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
215
215
  value?: string | undefined;
216
+ delay_ms?: number | undefined;
216
217
  x?: number | undefined;
217
218
  y?: number | undefined;
218
219
  selector?: string | undefined;
219
220
  delta_x?: number | undefined;
220
221
  delta_y?: number | undefined;
221
- delay_ms?: number | undefined;
222
222
  }[];
223
223
  viewport?: {
224
224
  width?: number | undefined;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { DEFAULT_SCREENSHOT_WAIT_MS, DEFAULT_VIEWPORT, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS } from "../constants.js";
3
3
  import { withPage } from "../engine/browser.js";
4
4
  import { getDimensions, resizeForOutput, toBase64 } from "../utils/image.js";
5
- import { readStorageState, storageStateField } from "../utils/storage-state.js";
5
+ import { loginFormVisible, resolveStorageState, storageStateField, withAuthNote } from "../utils/storage-state.js";
6
6
  export const SCREENSHOT_TOOL_NAME = "framewatch_screenshot";
7
7
  export const screenshotInputShape = {
8
8
  url: z
@@ -48,7 +48,8 @@ export async function takeScreenshot(rawInput) {
48
48
  const input = parsed.data;
49
49
  const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
50
50
  try {
51
- const contextOptions = input.storage_state ? { storageState: await readStorageState(input.storage_state) } : {};
51
+ const auth = await resolveStorageState(input.storage_state);
52
+ const contextOptions = auth ? { storageState: auth.state } : {};
52
53
  const shot = await withPage({ viewport, contextOptions }, async (page) => {
53
54
  const response = await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
54
55
  if (input.wait_for) {
@@ -60,7 +61,14 @@ export async function takeScreenshot(rawInput) {
60
61
  const png = input.selector
61
62
  ? await page.locator(input.selector).first().screenshot({ type: "png", timeout: input.wait_for_timeout_ms })
62
63
  : await page.screenshot({ type: "png" });
63
- return { png, title: await page.title(), finalUrl: page.url(), status: response?.status() ?? null };
64
+ return {
65
+ png,
66
+ title: await page.title(),
67
+ finalUrl: page.url(),
68
+ status: response?.status() ?? null,
69
+ // A login form on the page after restoring a session is the expiry signal.
70
+ loginVisible: auth ? await loginFormVisible(page) : false,
71
+ };
64
72
  });
65
73
  const resized = await resizeForOutput(shot.png);
66
74
  const { width, height } = await getDimensions(resized);
@@ -72,12 +80,12 @@ export async function takeScreenshot(rawInput) {
72
80
  `viewport ${viewport.width}x${viewport.height}`,
73
81
  input.selector ? `element ${input.selector}` : null,
74
82
  ].filter((p) => p !== null);
75
- return {
83
+ return withAuthNote({
76
84
  content: [
77
85
  { type: "image", data: toBase64(resized), mimeType: "image/png" },
78
86
  { type: "text", text: summaryParts.join(" — ") },
79
87
  ],
80
- };
88
+ }, auth, shot.loginVisible);
81
89
  }
82
90
  catch (error) {
83
91
  return errorResult(describeFailure(input, error));
@@ -1 +1 @@
1
- {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.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 { DEFAULT_SCREENSHOT_WAIT_MS, DEFAULT_VIEWPORT, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS } from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { getDimensions, resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { readStorageState, storageStateField } from \"../utils/storage-state.js\";\n\nexport const SCREENSHOT_TOOL_NAME = \"framewatch_screenshot\";\n\nexport const screenshotInputShape = {\n url: z\n .string()\n .url()\n .describe(\"URL to screenshot, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_SCREENSHOT_WAIT_MS)\n .describe(\"Wait time (ms) after page load before screenshot\"),\n viewport: z\n .object({\n width: z.number().int().min(1).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720)\"),\n selector: z.string().optional().describe(\"CSS selector to screenshot a specific element instead of the viewport\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before taking the screenshot\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` / `selector` to appear (must be > 0)\"),\n storage_state: storageStateField,\n};\n\nexport const screenshotInputSchema = z.object(screenshotInputShape);\nexport type ScreenshotInput = z.input<typeof screenshotInputSchema>;\ntype ParsedScreenshotInput = z.output<typeof screenshotInputSchema>;\n\n/**\n * Take a single screenshot of a page and return it as an MCP image content\n * block (base64 PNG, resized to max OUTPUT_MAX_WIDTH wide) plus a one-line\n * text summary. All failures — including invalid input — are reported as\n * `isError` results rather than thrown so the MCP client sees a useful message.\n */\nexport async function takeScreenshot(rawInput: ScreenshotInput): Promise<CallToolResult> {\n const parsed = screenshotInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Screenshot failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n try {\n const contextOptions = input.storage_state ? { storageState: await readStorageState(input.storage_state) } : {};\n const shot = await withPage({ viewport, contextOptions }, async (page) => {\n const response = await page.goto(input.url, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n\n const png = input.selector\n ? await page.locator(input.selector).first().screenshot({ type: \"png\", timeout: input.wait_for_timeout_ms })\n : await page.screenshot({ type: \"png\" });\n\n return { png, title: await page.title(), finalUrl: page.url(), status: response?.status() ?? null };\n });\n\n const resized = await resizeForOutput(shot.png);\n const { width, height } = await getDimensions(resized);\n\n const summaryParts = [\n `Screenshot of ${shot.finalUrl}`,\n shot.status !== null && shot.status >= 400 ? `HTTP ${shot.status}` : null,\n shot.title ? `\"${shot.title}\"` : null,\n `${width}x${height}`,\n `viewport ${viewport.width}x${viewport.height}`,\n input.selector ? `element ${input.selector}` : null,\n ].filter((p): p is string => p !== null);\n\n return {\n content: [\n { type: \"image\", data: toBase64(resized), mimeType: \"image/png\" },\n { type: \"text\", text: summaryParts.join(\" — \") },\n ],\n };\n } catch (error) {\n return errorResult(describeFailure(input, error));\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Matches on\n * the failing Playwright call (the message prefix) rather than on substrings\n * of user-supplied selectors, so a navigation failure is never blamed on an\n * element.\n */\nexport function describeFailure(\n input: Pick<ParsedScreenshotInput, \"url\" | \"wait_for\" | \"selector\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Screenshot of ${input.url} failed:`;\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n if (input.selector && /^locator\\.screenshot:/.test(message)) {\n return `${prefix} element \"${input.selector}\" not found or not visible within ${input.wait_for_timeout_ms}ms. ${firstLine}`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerScreenshotTool(server: McpServer): void {\n server.registerTool(\n SCREENSHOT_TOOL_NAME,\n {\n title: \"Screenshot\",\n description:\n \"Take a single screenshot of a web page (or one element on it) and return it as a PNG image. \" +\n \"Good for checking the current visual state of a running app.\",\n inputSchema: screenshotInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => takeScreenshot(args),\n );\n}\n"]}
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.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 { DEFAULT_SCREENSHOT_WAIT_MS, DEFAULT_VIEWPORT, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS } from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport { getDimensions, resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { loginFormVisible, resolveStorageState, storageStateField, withAuthNote } from \"../utils/storage-state.js\";\n\nexport const SCREENSHOT_TOOL_NAME = \"framewatch_screenshot\";\n\nexport const screenshotInputShape = {\n url: z\n .string()\n .url()\n .describe(\"URL to screenshot, e.g. http://localhost:3000 (http, https and file URLs are accepted)\"),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_SCREENSHOT_WAIT_MS)\n .describe(\"Wait time (ms) after page load before screenshot\"),\n viewport: z\n .object({\n width: z.number().int().min(1).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720)\"),\n selector: z.string().optional().describe(\"CSS selector to screenshot a specific element instead of the viewport\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before taking the screenshot\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` / `selector` to appear (must be > 0)\"),\n storage_state: storageStateField,\n};\n\nexport const screenshotInputSchema = z.object(screenshotInputShape);\nexport type ScreenshotInput = z.input<typeof screenshotInputSchema>;\ntype ParsedScreenshotInput = z.output<typeof screenshotInputSchema>;\n\n/**\n * Take a single screenshot of a page and return it as an MCP image content\n * block (base64 PNG, resized to max OUTPUT_MAX_WIDTH wide) plus a one-line\n * text summary. All failures — including invalid input — are reported as\n * `isError` results rather than thrown so the MCP client sees a useful message.\n */\nexport async function takeScreenshot(rawInput: ScreenshotInput): Promise<CallToolResult> {\n const parsed = screenshotInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Screenshot failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n try {\n const auth = await resolveStorageState(input.storage_state);\n const contextOptions = auth ? { storageState: auth.state } : {};\n const shot = await withPage({ viewport, contextOptions }, async (page) => {\n const response = await page.goto(input.url, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n if (input.wait_ms > 0) {\n await page.waitForTimeout(input.wait_ms);\n }\n\n const png = input.selector\n ? await page.locator(input.selector).first().screenshot({ type: \"png\", timeout: input.wait_for_timeout_ms })\n : await page.screenshot({ type: \"png\" });\n\n return {\n png,\n title: await page.title(),\n finalUrl: page.url(),\n status: response?.status() ?? null,\n // A login form on the page after restoring a session is the expiry signal.\n loginVisible: auth ? await loginFormVisible(page) : false,\n };\n });\n\n const resized = await resizeForOutput(shot.png);\n const { width, height } = await getDimensions(resized);\n\n const summaryParts = [\n `Screenshot of ${shot.finalUrl}`,\n shot.status !== null && shot.status >= 400 ? `HTTP ${shot.status}` : null,\n shot.title ? `\"${shot.title}\"` : null,\n `${width}x${height}`,\n `viewport ${viewport.width}x${viewport.height}`,\n input.selector ? `element ${input.selector}` : null,\n ].filter((p): p is string => p !== null);\n\n return withAuthNote(\n {\n content: [\n { type: \"image\", data: toBase64(resized), mimeType: \"image/png\" },\n { type: \"text\", text: summaryParts.join(\" — \") },\n ],\n },\n auth,\n shot.loginVisible,\n );\n } catch (error) {\n return errorResult(describeFailure(input, error));\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * Turn a Playwright/Node error into a one-line, actionable message. Matches on\n * the failing Playwright call (the message prefix) rather than on substrings\n * of user-supplied selectors, so a navigation failure is never blamed on an\n * element.\n */\nexport function describeFailure(\n input: Pick<ParsedScreenshotInput, \"url\" | \"wait_for\" | \"selector\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = `Screenshot of ${input.url} failed:`;\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `${prefix} selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms.`;\n }\n if (input.selector && /^locator\\.screenshot:/.test(message)) {\n return `${prefix} element \"${input.selector}\" not found or not visible within ${input.wait_for_timeout_ms}ms. ${firstLine}`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerScreenshotTool(server: McpServer): void {\n server.registerTool(\n SCREENSHOT_TOOL_NAME,\n {\n title: \"Screenshot\",\n description:\n \"Take a single screenshot of a web page (or one element on it) and return it as a PNG image. \" +\n \"Good for checking the current visual state of a running app.\",\n inputSchema: screenshotInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => takeScreenshot(args),\n );\n}\n"]}
@@ -0,0 +1,113 @@
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
+ import { type PageSeo } from "../engine/seo.js";
5
+ import { type SeoReport } from "../utils/seo-rules.js";
6
+ export declare const SEO_TOOL_NAME = "framewatch_seo";
7
+ export declare const seoInputShape: {
8
+ url: z.ZodString;
9
+ wait_ms: z.ZodDefault<z.ZodNumber>;
10
+ wait_for: z.ZodOptional<z.ZodString>;
11
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
12
+ check_robots: z.ZodDefault<z.ZodBoolean>;
13
+ robots_user_agent: z.ZodDefault<z.ZodString>;
14
+ check_og_image: z.ZodDefault<z.ZodBoolean>;
15
+ include_performance: z.ZodDefault<z.ZodBoolean>;
16
+ viewport: z.ZodOptional<z.ZodObject<{
17
+ width: z.ZodDefault<z.ZodNumber>;
18
+ height: z.ZodDefault<z.ZodNumber>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ width: number;
21
+ height: number;
22
+ }, {
23
+ width?: number | undefined;
24
+ height?: number | undefined;
25
+ }>>;
26
+ storage_state: z.ZodOptional<z.ZodString>;
27
+ };
28
+ export declare const seoInputSchema: z.ZodObject<{
29
+ url: z.ZodString;
30
+ wait_ms: z.ZodDefault<z.ZodNumber>;
31
+ wait_for: z.ZodOptional<z.ZodString>;
32
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
33
+ check_robots: z.ZodDefault<z.ZodBoolean>;
34
+ robots_user_agent: z.ZodDefault<z.ZodString>;
35
+ check_og_image: z.ZodDefault<z.ZodBoolean>;
36
+ include_performance: z.ZodDefault<z.ZodBoolean>;
37
+ viewport: z.ZodOptional<z.ZodObject<{
38
+ width: z.ZodDefault<z.ZodNumber>;
39
+ height: z.ZodDefault<z.ZodNumber>;
40
+ }, "strip", z.ZodTypeAny, {
41
+ width: number;
42
+ height: number;
43
+ }, {
44
+ width?: number | undefined;
45
+ height?: number | undefined;
46
+ }>>;
47
+ storage_state: z.ZodOptional<z.ZodString>;
48
+ }, "strip", z.ZodTypeAny, {
49
+ url: string;
50
+ wait_ms: number;
51
+ wait_for_timeout_ms: number;
52
+ include_performance: boolean;
53
+ check_robots: boolean;
54
+ robots_user_agent: string;
55
+ check_og_image: boolean;
56
+ viewport?: {
57
+ width: number;
58
+ height: number;
59
+ } | undefined;
60
+ wait_for?: string | undefined;
61
+ storage_state?: string | undefined;
62
+ }, {
63
+ url: string;
64
+ viewport?: {
65
+ width?: number | undefined;
66
+ height?: number | undefined;
67
+ } | undefined;
68
+ wait_ms?: number | undefined;
69
+ wait_for?: string | undefined;
70
+ wait_for_timeout_ms?: number | undefined;
71
+ storage_state?: string | undefined;
72
+ include_performance?: boolean | undefined;
73
+ check_robots?: boolean | undefined;
74
+ robots_user_agent?: string | undefined;
75
+ check_og_image?: boolean | undefined;
76
+ }>;
77
+ export type SeoInput = z.input<typeof seoInputSchema>;
78
+ /**
79
+ * Audit what a search engine — and a link preview — would make of a page.
80
+ *
81
+ * Everything is read from the *rendered* document rather than the HTML source.
82
+ * A single-page app ships an empty `<div id="app">` and fills in the title,
83
+ * the description and the structured data afterwards, so reading the source
84
+ * would report every client-rendered page as having no SEO at all, which is
85
+ * both wrong and the opposite of useful.
86
+ *
87
+ * Three things are checked that are not on the page and cannot be seen from
88
+ * it: the response headers (an `X-Robots-Tag: noindex` is invisible in the
89
+ * DOM), robots.txt (a page can be perfect and simply not crawlable), and the
90
+ * share image itself, which is fetched and measured — an `og:image` pointing
91
+ * at a 404 looks exactly like a working one until somebody shares the link.
92
+ */
93
+ export declare function auditSeo(rawInput: SeoInput): Promise<CallToolResult>;
94
+ /**
95
+ * The report as text: a verdict line, the problems up front, then every check
96
+ * in the order it was run.
97
+ *
98
+ * Passing checks are printed rather than counted. A report of four lines with
99
+ * nothing else in it is otherwise indistinguishable from an audit that only
100
+ * looked at four things.
101
+ */
102
+ export declare function formatSeoReport(page: PageSeo, report: SeoReport): string;
103
+ /**
104
+ * One actionable line for an audit that could not run. Mirrors
105
+ * `describeFailure` in screenshot.ts: match on the failing Playwright call,
106
+ * never on substrings of a user-supplied selector.
107
+ */
108
+ export declare function describeSeoFailure(input: {
109
+ url: string;
110
+ wait_for?: string;
111
+ wait_for_timeout_ms: number;
112
+ }, error: unknown): string;
113
+ export declare function registerSeoTool(server: McpServer): void;