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,263 @@
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 SAVE_AUTH_TOOL_NAME = "framewatch_save_auth";
5
+ /** One step of the login flow. Same shape as a capture script, with a settle time that suits a form. */
6
+ export declare const saveAuthInteractionSchema: z.ZodEffects<z.ZodObject<{
7
+ delay_ms: z.ZodDefault<z.ZodNumber>;
8
+ selector: z.ZodOptional<z.ZodString>;
9
+ value: z.ZodOptional<z.ZodString>;
10
+ x: z.ZodOptional<z.ZodNumber>;
11
+ y: z.ZodOptional<z.ZodNumber>;
12
+ delta_x: z.ZodOptional<z.ZodNumber>;
13
+ delta_y: z.ZodOptional<z.ZodNumber>;
14
+ action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ delay_ms: number;
17
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
18
+ value?: string | undefined;
19
+ x?: number | undefined;
20
+ y?: number | undefined;
21
+ selector?: string | undefined;
22
+ delta_x?: number | undefined;
23
+ delta_y?: number | undefined;
24
+ }, {
25
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
26
+ value?: string | undefined;
27
+ delay_ms?: number | undefined;
28
+ x?: number | undefined;
29
+ y?: number | undefined;
30
+ selector?: string | undefined;
31
+ delta_x?: number | undefined;
32
+ delta_y?: number | undefined;
33
+ }>, {
34
+ delay_ms: number;
35
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
36
+ value?: string | undefined;
37
+ x?: number | undefined;
38
+ y?: number | undefined;
39
+ selector?: string | undefined;
40
+ delta_x?: number | undefined;
41
+ delta_y?: number | undefined;
42
+ }, {
43
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
44
+ value?: string | undefined;
45
+ delay_ms?: number | undefined;
46
+ x?: number | undefined;
47
+ y?: number | undefined;
48
+ selector?: string | undefined;
49
+ delta_x?: number | undefined;
50
+ delta_y?: number | undefined;
51
+ }>;
52
+ export declare const saveAuthInputShape: {
53
+ url: z.ZodString;
54
+ interactions: z.ZodArray<z.ZodEffects<z.ZodObject<{
55
+ delay_ms: z.ZodDefault<z.ZodNumber>;
56
+ selector: z.ZodOptional<z.ZodString>;
57
+ value: z.ZodOptional<z.ZodString>;
58
+ x: z.ZodOptional<z.ZodNumber>;
59
+ y: z.ZodOptional<z.ZodNumber>;
60
+ delta_x: z.ZodOptional<z.ZodNumber>;
61
+ delta_y: z.ZodOptional<z.ZodNumber>;
62
+ action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
63
+ }, "strip", z.ZodTypeAny, {
64
+ delay_ms: number;
65
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
66
+ value?: string | undefined;
67
+ x?: number | undefined;
68
+ y?: number | undefined;
69
+ selector?: string | undefined;
70
+ delta_x?: number | undefined;
71
+ delta_y?: number | undefined;
72
+ }, {
73
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
74
+ value?: string | undefined;
75
+ delay_ms?: number | undefined;
76
+ x?: number | undefined;
77
+ y?: number | undefined;
78
+ selector?: string | undefined;
79
+ delta_x?: number | undefined;
80
+ delta_y?: number | undefined;
81
+ }>, {
82
+ delay_ms: number;
83
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
84
+ value?: string | undefined;
85
+ x?: number | undefined;
86
+ y?: number | undefined;
87
+ selector?: string | undefined;
88
+ delta_x?: number | undefined;
89
+ delta_y?: number | undefined;
90
+ }, {
91
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
92
+ value?: string | undefined;
93
+ delay_ms?: number | undefined;
94
+ x?: number | undefined;
95
+ y?: number | undefined;
96
+ selector?: string | undefined;
97
+ delta_x?: number | undefined;
98
+ delta_y?: number | undefined;
99
+ }>, "many">;
100
+ output_path: z.ZodDefault<z.ZodString>;
101
+ wait_for: z.ZodOptional<z.ZodString>;
102
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
103
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
104
+ viewport: z.ZodOptional<z.ZodObject<{
105
+ width: z.ZodDefault<z.ZodNumber>;
106
+ height: z.ZodDefault<z.ZodNumber>;
107
+ is_mobile: z.ZodDefault<z.ZodBoolean>;
108
+ has_touch: z.ZodOptional<z.ZodBoolean>;
109
+ }, "strip", z.ZodTypeAny, {
110
+ width: number;
111
+ height: number;
112
+ is_mobile: boolean;
113
+ has_touch?: boolean | undefined;
114
+ }, {
115
+ width?: number | undefined;
116
+ height?: number | undefined;
117
+ is_mobile?: boolean | undefined;
118
+ has_touch?: boolean | undefined;
119
+ }>>;
120
+ };
121
+ export declare const saveAuthInputSchema: z.ZodObject<{
122
+ url: z.ZodString;
123
+ interactions: z.ZodArray<z.ZodEffects<z.ZodObject<{
124
+ delay_ms: z.ZodDefault<z.ZodNumber>;
125
+ selector: z.ZodOptional<z.ZodString>;
126
+ value: z.ZodOptional<z.ZodString>;
127
+ x: z.ZodOptional<z.ZodNumber>;
128
+ y: z.ZodOptional<z.ZodNumber>;
129
+ delta_x: z.ZodOptional<z.ZodNumber>;
130
+ delta_y: z.ZodOptional<z.ZodNumber>;
131
+ action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
132
+ }, "strip", z.ZodTypeAny, {
133
+ delay_ms: number;
134
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
135
+ value?: string | undefined;
136
+ x?: number | undefined;
137
+ y?: number | undefined;
138
+ selector?: string | undefined;
139
+ delta_x?: number | undefined;
140
+ delta_y?: number | undefined;
141
+ }, {
142
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
143
+ value?: string | undefined;
144
+ delay_ms?: number | undefined;
145
+ x?: number | undefined;
146
+ y?: number | undefined;
147
+ selector?: string | undefined;
148
+ delta_x?: number | undefined;
149
+ delta_y?: number | undefined;
150
+ }>, {
151
+ delay_ms: number;
152
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
153
+ value?: string | undefined;
154
+ x?: number | undefined;
155
+ y?: number | undefined;
156
+ selector?: string | undefined;
157
+ delta_x?: number | undefined;
158
+ delta_y?: number | undefined;
159
+ }, {
160
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
161
+ value?: string | undefined;
162
+ delay_ms?: number | undefined;
163
+ x?: number | undefined;
164
+ y?: number | undefined;
165
+ selector?: string | undefined;
166
+ delta_x?: number | undefined;
167
+ delta_y?: number | undefined;
168
+ }>, "many">;
169
+ output_path: z.ZodDefault<z.ZodString>;
170
+ wait_for: z.ZodOptional<z.ZodString>;
171
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
172
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
173
+ viewport: z.ZodOptional<z.ZodObject<{
174
+ width: z.ZodDefault<z.ZodNumber>;
175
+ height: z.ZodDefault<z.ZodNumber>;
176
+ is_mobile: z.ZodDefault<z.ZodBoolean>;
177
+ has_touch: z.ZodOptional<z.ZodBoolean>;
178
+ }, "strip", z.ZodTypeAny, {
179
+ width: number;
180
+ height: number;
181
+ is_mobile: boolean;
182
+ has_touch?: boolean | undefined;
183
+ }, {
184
+ width?: number | undefined;
185
+ height?: number | undefined;
186
+ is_mobile?: boolean | undefined;
187
+ has_touch?: boolean | undefined;
188
+ }>>;
189
+ }, "strip", z.ZodTypeAny, {
190
+ url: string;
191
+ wait_for_timeout_ms: number;
192
+ timeout_ms: number;
193
+ interactions: {
194
+ delay_ms: number;
195
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
196
+ value?: string | undefined;
197
+ x?: number | undefined;
198
+ y?: number | undefined;
199
+ selector?: string | undefined;
200
+ delta_x?: number | undefined;
201
+ delta_y?: number | undefined;
202
+ }[];
203
+ output_path: string;
204
+ viewport?: {
205
+ width: number;
206
+ height: number;
207
+ is_mobile: boolean;
208
+ has_touch?: boolean | undefined;
209
+ } | undefined;
210
+ wait_for?: string | undefined;
211
+ }, {
212
+ url: string;
213
+ interactions: {
214
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
215
+ value?: string | undefined;
216
+ delay_ms?: number | undefined;
217
+ x?: number | undefined;
218
+ y?: number | undefined;
219
+ selector?: string | undefined;
220
+ delta_x?: number | undefined;
221
+ delta_y?: number | undefined;
222
+ }[];
223
+ viewport?: {
224
+ width?: number | undefined;
225
+ height?: number | undefined;
226
+ is_mobile?: boolean | undefined;
227
+ has_touch?: boolean | undefined;
228
+ } | undefined;
229
+ wait_for?: string | undefined;
230
+ wait_for_timeout_ms?: number | undefined;
231
+ timeout_ms?: number | undefined;
232
+ output_path?: string | undefined;
233
+ }>;
234
+ export type SaveAuthInput = z.input<typeof saveAuthInputSchema>;
235
+ /**
236
+ * Run a login flow once and save what it produced, so no other tool has to
237
+ * replay it.
238
+ *
239
+ * The saved file is Playwright's storage state — cookies plus per-origin
240
+ * localStorage — which every other FrameWatch tool takes as `storage_state`.
241
+ * Two rules make it trustworthy:
242
+ *
243
+ * - Nothing is written unless the flow finished, including `wait_for`. A
244
+ * state file that is not signed in is worse than no file at all: every
245
+ * later call would load it and quietly get the login screen back.
246
+ * - The final frame is always returned, success or failure. When a flow
247
+ * breaks, the picture of where it stopped is the thing that explains why.
248
+ *
249
+ * There is no auto-refresh. When a saved session expires the login screen
250
+ * simply appears in the next capture, which is the clearest possible signal to
251
+ * run this tool again.
252
+ */
253
+ export declare function saveAuth(rawInput: SaveAuthInput): Promise<CallToolResult>;
254
+ /**
255
+ * One actionable line for a failure that happened outside the flow itself
256
+ * (launching the browser, opening the URL, writing the file). Mirrors
257
+ * `describeFailure` in screenshot.ts.
258
+ */
259
+ export declare function describeSaveAuthFailure(input: {
260
+ url: string;
261
+ output_path: string;
262
+ }, error: unknown): string;
263
+ export declare function registerSaveAuthTool(server: McpServer): void;
@@ -0,0 +1,253 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_AUTH_STATE_PATH, DEFAULT_VIEWPORT, MAX_INTERACTIONS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, NAVIGATION_TIMEOUT_MS, SAVE_AUTH_STEP_DELAY_MS, SAVE_AUTH_WAIT_FOR_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, } from "../constants.js";
3
+ import { withPage } from "../engine/browser.js";
4
+ import { CAPTURE_ACTIONS, describeInteraction, executeInteraction, interactionFieldShape, needsTouch, refineInteraction, } from "../engine/interaction.js";
5
+ import { resizeForOutput, toBase64 } from "../utils/image.js";
6
+ import { storageStateSummary, writeStorageState } from "../utils/storage-state.js";
7
+ export const SAVE_AUTH_TOOL_NAME = "framewatch_save_auth";
8
+ /** One step of the login flow. Same shape as a capture script, with a settle time that suits a form. */
9
+ export const saveAuthInteractionSchema = z
10
+ .object({
11
+ action: z
12
+ .enum(CAPTURE_ACTIONS)
13
+ .describe("What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate"),
14
+ ...interactionFieldShape,
15
+ delay_ms: z
16
+ .number()
17
+ .int()
18
+ .min(0)
19
+ .default(SAVE_AUTH_STEP_DELAY_MS)
20
+ .describe("Wait this long (ms) before performing this step, so the previous one can settle"),
21
+ })
22
+ .superRefine(refineInteraction);
23
+ export const saveAuthInputShape = {
24
+ url: z
25
+ .string()
26
+ .url()
27
+ .describe("Where the flow starts — the login page or the gate, e.g. http://localhost:3000"),
28
+ interactions: z
29
+ .array(saveAuthInteractionSchema)
30
+ .max(MAX_INTERACTIONS)
31
+ .describe("The login/setup steps to run, in order. Use `key` with `Enter` (or a `\\n` at the end of a typed " +
32
+ "value) to submit a form."),
33
+ output_path: z
34
+ .string()
35
+ .min(1)
36
+ .default(DEFAULT_AUTH_STATE_PATH)
37
+ .describe("Where to write the state file. Relative paths are resolved against the server's working directory."),
38
+ wait_for: z
39
+ .string()
40
+ .optional()
41
+ .describe("CSS selector that proves the flow worked, e.g. '.feed' or '.dashboard'. Strongly recommended: without " +
42
+ "it a flow that silently failed still saves a signed-out state."),
43
+ wait_for_timeout_ms: z
44
+ .number()
45
+ .int()
46
+ .min(1)
47
+ .default(SAVE_AUTH_WAIT_FOR_TIMEOUT_MS)
48
+ .describe("Max time (ms) to wait for `wait_for` after the last step (must be > 0)"),
49
+ timeout_ms: z
50
+ .number()
51
+ .int()
52
+ .min(1)
53
+ .default(SELECTOR_TIMEOUT_MS)
54
+ .describe("Max time (ms) one step may spend waiting for its target element"),
55
+ viewport: z
56
+ .object({
57
+ width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),
58
+ height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),
59
+ is_mobile: z
60
+ .boolean()
61
+ .default(false)
62
+ .describe("Emulate a phone (mobile viewport meta handling). Set it if the app serves a separate mobile UI."),
63
+ has_touch: z
64
+ .boolean()
65
+ .optional()
66
+ .describe("Give the page touch events. Defaults to on when the flow taps or swipes, or when `is_mobile` is set."),
67
+ })
68
+ .optional()
69
+ .describe("Viewport for the flow (defaults to 1280x720). A phone-shaped app wants 390x844 with `is_mobile`."),
70
+ };
71
+ export const saveAuthInputSchema = z.object(saveAuthInputShape);
72
+ /**
73
+ * Run a login flow once and save what it produced, so no other tool has to
74
+ * replay it.
75
+ *
76
+ * The saved file is Playwright's storage state — cookies plus per-origin
77
+ * localStorage — which every other FrameWatch tool takes as `storage_state`.
78
+ * Two rules make it trustworthy:
79
+ *
80
+ * - Nothing is written unless the flow finished, including `wait_for`. A
81
+ * state file that is not signed in is worse than no file at all: every
82
+ * later call would load it and quietly get the login screen back.
83
+ * - The final frame is always returned, success or failure. When a flow
84
+ * breaks, the picture of where it stopped is the thing that explains why.
85
+ *
86
+ * There is no auto-refresh. When a saved session expires the login screen
87
+ * simply appears in the next capture, which is the clearest possible signal to
88
+ * run this tool again.
89
+ */
90
+ export async function saveAuth(rawInput) {
91
+ const parsed = saveAuthInputSchema.safeParse(rawInput);
92
+ if (!parsed.success) {
93
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
94
+ return errorResult(`Saving auth state failed: invalid input — ${issues}`);
95
+ }
96
+ const input = parsed.data;
97
+ const viewport = {
98
+ width: input.viewport?.width ?? DEFAULT_VIEWPORT.width,
99
+ height: input.viewport?.height ?? DEFAULT_VIEWPORT.height,
100
+ };
101
+ // Touch follows the script, as it does in framewatch_capture: `hasTouch`
102
+ // changes what feature detection sees, so it is not turned on for a flow
103
+ // that never touches anything.
104
+ const hasTouch = input.viewport?.has_touch ?? (input.viewport?.is_mobile === true || needsTouch(input.interactions));
105
+ const contextOptions = {
106
+ ...(hasTouch ? { hasTouch: true } : {}),
107
+ ...(input.viewport?.is_mobile ? { isMobile: true } : {}),
108
+ };
109
+ try {
110
+ const outcome = await withPage({ viewport, contextOptions }, async (page, context) => {
111
+ const completed = [];
112
+ await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
113
+ for (const step of input.interactions) {
114
+ try {
115
+ await executeInteraction(page, step, { timeout_ms: input.timeout_ms });
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ return { failure: message, completed, png: await safeScreenshot(page), finalUrl: safely(() => page.url()) };
120
+ }
121
+ completed.push(describeInteraction(step));
122
+ }
123
+ if (input.wait_for) {
124
+ try {
125
+ await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
126
+ }
127
+ catch {
128
+ return {
129
+ failure: `the flow ran, but "${input.wait_for}" never became visible within ${input.wait_for_timeout_ms}ms, ` +
130
+ "so it did not sign in",
131
+ completed,
132
+ png: await safeScreenshot(page),
133
+ finalUrl: safely(() => page.url()),
134
+ };
135
+ }
136
+ }
137
+ return {
138
+ state: await context.storageState(),
139
+ completed,
140
+ png: await safeScreenshot(page),
141
+ finalUrl: safely(() => page.url()),
142
+ };
143
+ });
144
+ return outcome.state
145
+ ? await succeed(input, outcome, outcome.state)
146
+ : fail(`Saving auth state failed: ${outcome.failure}`, outcome);
147
+ }
148
+ catch (error) {
149
+ return errorResult(describeSaveAuthFailure(input, error));
150
+ }
151
+ }
152
+ /** Write the state and describe what is in it — and what to do with it next. */
153
+ async function succeed(input, outcome, state) {
154
+ await writeStorageState(state, input.output_path);
155
+ const lines = [`Saved auth state to ${input.output_path} — ${storageStateSummary(state)}.`];
156
+ if (outcome.completed.length > 0) {
157
+ lines.push(`Ran ${outcome.completed.length} step${outcome.completed.length === 1 ? "" : "s"}: ${outcome.completed.join("; ")}`);
158
+ }
159
+ if (outcome.finalUrl)
160
+ lines.push(`Ended on ${outcome.finalUrl}`);
161
+ if (state.cookies.length === 0 && state.origins.every((origin) => origin.localStorage.length === 0)) {
162
+ lines.push("Nothing was stored, so this file will not keep you signed in. Check that the flow really completed " +
163
+ "(add `wait_for`), and note that sessions kept only in sessionStorage or in memory cannot be saved.");
164
+ }
165
+ else {
166
+ lines.push(`Pass storage_state: "${input.output_path}" to framewatch_screenshot, framewatch_capture, ` +
167
+ "framewatch_interact, framewatch_responsive, framewatch_accessibility or framewatch_compare to start " +
168
+ "past this flow. The file holds live session credentials — keep it out of version control.");
169
+ }
170
+ return { content: await withFrame(outcome.png, lines.join("\n")) };
171
+ }
172
+ /** The failure, plus the frame it happened on. */
173
+ function fail(message, outcome) {
174
+ const lines = [message];
175
+ if (outcome.completed.length > 0)
176
+ lines.push(`Completed before it stopped: ${outcome.completed.join("; ")}`);
177
+ if (outcome.finalUrl)
178
+ lines.push(`Page at that moment: ${outcome.finalUrl}`);
179
+ lines.push("Nothing was written — a state file that is not signed in would make every later call fail silently.");
180
+ const content = [{ type: "text", text: lines.join("\n") }];
181
+ return { isError: true, content: pushFrame(content, outcome.png) };
182
+ }
183
+ async function withFrame(png, text) {
184
+ const content = [];
185
+ if (png) {
186
+ content.push({ type: "image", data: toBase64(await resizeForOutput(png)), mimeType: "image/png" });
187
+ }
188
+ content.push({ type: "text", text });
189
+ return content;
190
+ }
191
+ /** The failure frame is attached raw-ish: it is evidence, and resizing it can itself fail. */
192
+ function pushFrame(content, png) {
193
+ if (png)
194
+ content.push({ type: "image", data: png.toString("base64"), mimeType: "image/png" });
195
+ return content;
196
+ }
197
+ /** A screenshot is always worth having and never worth failing over. */
198
+ async function safeScreenshot(page) {
199
+ try {
200
+ if (page.isClosed())
201
+ return undefined;
202
+ return await page.screenshot({ type: "png" });
203
+ }
204
+ catch {
205
+ return undefined;
206
+ }
207
+ }
208
+ function safely(read) {
209
+ try {
210
+ return read();
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ }
216
+ function errorResult(text) {
217
+ return { isError: true, content: [{ type: "text", text }] };
218
+ }
219
+ /**
220
+ * One actionable line for a failure that happened outside the flow itself
221
+ * (launching the browser, opening the URL, writing the file). Mirrors
222
+ * `describeFailure` in screenshot.ts.
223
+ */
224
+ export function describeSaveAuthFailure(input, error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ const firstLine = message.split("\n")[0];
227
+ const prefix = "Saving auth state failed:";
228
+ if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
229
+ return (`${prefix} Playwright's Chromium browser is not installed. ` +
230
+ `Run \`npx playwright install chromium\` and try again. (${firstLine})`);
231
+ }
232
+ if (/^page\.goto:/.test(message)) {
233
+ return `${prefix} could not open ${input.url} — ${firstLine}`;
234
+ }
235
+ if (/EACCES|EPERM|ENOTDIR|EISDIR/.test(message)) {
236
+ return `${prefix} could not write ${input.output_path} — ${firstLine}`;
237
+ }
238
+ return `${prefix} ${firstLine}`;
239
+ }
240
+ export function registerSaveAuthTool(server) {
241
+ server.registerTool(SAVE_AUTH_TOOL_NAME, {
242
+ title: "Save auth",
243
+ description: "Run a login or gate flow once and save the browser state it produces (cookies and localStorage) to a " +
244
+ "file. Every other FrameWatch tool takes that file as `storage_state` and opens the page already signed " +
245
+ "in, so an app behind a login can be tested without replaying the flow on every call. Give `wait_for` a " +
246
+ "selector that only exists once signed in: nothing is written unless it appears, so a state file always " +
247
+ "means a real session. When a saved session expires the login screen shows up in the next capture — run " +
248
+ "this again then.",
249
+ inputSchema: saveAuthInputShape,
250
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
251
+ }, async (args) => saveAuth(args));
252
+ }
253
+ //# sourceMappingURL=save-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save-auth.js","sourceRoot":"","sources":["../../src/tools/save-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { BrowserContextOptions, Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_AUTH_STATE_PATH,\n DEFAULT_VIEWPORT,\n MAX_INTERACTIONS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SAVE_AUTH_STEP_DELAY_MS,\n SAVE_AUTH_WAIT_FOR_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport {\n CAPTURE_ACTIONS,\n describeInteraction,\n executeInteraction,\n interactionFieldShape,\n needsTouch,\n refineInteraction,\n} from \"../engine/interaction.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { storageStateSummary, writeStorageState, type StorageState } from \"../utils/storage-state.js\";\n\nexport const SAVE_AUTH_TOOL_NAME = \"framewatch_save_auth\";\n\n/** One step of the login flow. Same shape as a capture script, with a settle time that suits a form. */\nexport const saveAuthInteractionSchema = z\n .object({\n action: z\n .enum(CAPTURE_ACTIONS)\n .describe(\"What to do: click, tap, type, key, scroll, swipe, hover, select, wait or navigate\"),\n ...interactionFieldShape,\n delay_ms: z\n .number()\n .int()\n .min(0)\n .default(SAVE_AUTH_STEP_DELAY_MS)\n .describe(\"Wait this long (ms) before performing this step, so the previous one can settle\"),\n })\n .superRefine(refineInteraction);\n\nexport const saveAuthInputShape = {\n url: z\n .string()\n .url()\n .describe(\"Where the flow starts — the login page or the gate, e.g. http://localhost:3000\"),\n interactions: z\n .array(saveAuthInteractionSchema)\n .max(MAX_INTERACTIONS)\n .describe(\n \"The login/setup steps to run, in order. Use `key` with `Enter` (or a `\\\\n` at the end of a typed \" +\n \"value) to submit a form.\",\n ),\n output_path: z\n .string()\n .min(1)\n .default(DEFAULT_AUTH_STATE_PATH)\n .describe(\"Where to write the state file. Relative paths are resolved against the server's working directory.\"),\n wait_for: z\n .string()\n .optional()\n .describe(\n \"CSS selector that proves the flow worked, e.g. '.feed' or '.dashboard'. Strongly recommended: without \" +\n \"it a flow that silently failed still saves a signed-out state.\",\n ),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SAVE_AUTH_WAIT_FOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` after the last step (must be > 0)\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) one step may spend waiting for its target element\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n is_mobile: z\n .boolean()\n .default(false)\n .describe(\"Emulate a phone (mobile viewport meta handling). Set it if the app serves a separate mobile UI.\"),\n has_touch: z\n .boolean()\n .optional()\n .describe(\"Give the page touch events. Defaults to on when the flow taps or swipes, or when `is_mobile` is set.\"),\n })\n .optional()\n .describe(\"Viewport for the flow (defaults to 1280x720). A phone-shaped app wants 390x844 with `is_mobile`.\"),\n};\n\nexport const saveAuthInputSchema = z.object(saveAuthInputShape);\nexport type SaveAuthInput = z.input<typeof saveAuthInputSchema>;\n\n/** What the browser side of the run produced: either a state, or the step that stopped it. */\ninterface Outcome {\n png?: Buffer;\n finalUrl?: string;\n state?: StorageState;\n failure?: string;\n completed: string[];\n}\n\n/**\n * Run a login flow once and save what it produced, so no other tool has to\n * replay it.\n *\n * The saved file is Playwright's storage state — cookies plus per-origin\n * localStorage — which every other FrameWatch tool takes as `storage_state`.\n * Two rules make it trustworthy:\n *\n * - Nothing is written unless the flow finished, including `wait_for`. A\n * state file that is not signed in is worse than no file at all: every\n * later call would load it and quietly get the login screen back.\n * - The final frame is always returned, success or failure. When a flow\n * breaks, the picture of where it stopped is the thing that explains why.\n *\n * There is no auto-refresh. When a saved session expires the login screen\n * simply appears in the next capture, which is the clearest possible signal to\n * run this tool again.\n */\nexport async function saveAuth(rawInput: SaveAuthInput): Promise<CallToolResult> {\n const parsed = saveAuthInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Saving auth state failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n const viewport = {\n width: input.viewport?.width ?? DEFAULT_VIEWPORT.width,\n height: input.viewport?.height ?? DEFAULT_VIEWPORT.height,\n };\n // Touch follows the script, as it does in framewatch_capture: `hasTouch`\n // changes what feature detection sees, so it is not turned on for a flow\n // that never touches anything.\n const hasTouch = input.viewport?.has_touch ?? (input.viewport?.is_mobile === true || needsTouch(input.interactions));\n const contextOptions: BrowserContextOptions = {\n ...(hasTouch ? { hasTouch: true } : {}),\n ...(input.viewport?.is_mobile ? { isMobile: true } : {}),\n };\n\n try {\n const outcome = await withPage({ viewport, contextOptions }, async (page, context): Promise<Outcome> => {\n const completed: string[] = [];\n await page.goto(input.url, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n\n for (const step of input.interactions) {\n try {\n await executeInteraction(page, step, { timeout_ms: input.timeout_ms });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { failure: message, completed, png: await safeScreenshot(page), finalUrl: safely(() => page.url()) };\n }\n completed.push(describeInteraction(step));\n }\n\n if (input.wait_for) {\n try {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n } catch {\n return {\n failure:\n `the flow ran, but \"${input.wait_for}\" never became visible within ${input.wait_for_timeout_ms}ms, ` +\n \"so it did not sign in\",\n completed,\n png: await safeScreenshot(page),\n finalUrl: safely(() => page.url()),\n };\n }\n }\n\n return {\n state: await context.storageState(),\n completed,\n png: await safeScreenshot(page),\n finalUrl: safely(() => page.url()),\n };\n });\n\n return outcome.state\n ? await succeed(input, outcome, outcome.state)\n : fail(`Saving auth state failed: ${outcome.failure}`, outcome);\n } catch (error) {\n return errorResult(describeSaveAuthFailure(input, error));\n }\n}\n\n/** Write the state and describe what is in it — and what to do with it next. */\nasync function succeed(\n input: z.output<typeof saveAuthInputSchema>,\n outcome: Outcome,\n state: StorageState,\n): Promise<CallToolResult> {\n await writeStorageState(state, input.output_path);\n\n const lines = [`Saved auth state to ${input.output_path} — ${storageStateSummary(state)}.`];\n if (outcome.completed.length > 0) {\n lines.push(`Ran ${outcome.completed.length} step${outcome.completed.length === 1 ? \"\" : \"s\"}: ${outcome.completed.join(\"; \")}`);\n }\n if (outcome.finalUrl) lines.push(`Ended on ${outcome.finalUrl}`);\n\n if (state.cookies.length === 0 && state.origins.every((origin) => origin.localStorage.length === 0)) {\n lines.push(\n \"Nothing was stored, so this file will not keep you signed in. Check that the flow really completed \" +\n \"(add `wait_for`), and note that sessions kept only in sessionStorage or in memory cannot be saved.\",\n );\n } else {\n lines.push(\n `Pass storage_state: \"${input.output_path}\" to framewatch_screenshot, framewatch_capture, ` +\n \"framewatch_interact, framewatch_responsive, framewatch_accessibility or framewatch_compare to start \" +\n \"past this flow. The file holds live session credentials — keep it out of version control.\",\n );\n }\n\n return { content: await withFrame(outcome.png, lines.join(\"\\n\")) };\n}\n\n/** The failure, plus the frame it happened on. */\nfunction fail(message: string, outcome: Outcome): CallToolResult {\n const lines = [message];\n if (outcome.completed.length > 0) lines.push(`Completed before it stopped: ${outcome.completed.join(\"; \")}`);\n if (outcome.finalUrl) lines.push(`Page at that moment: ${outcome.finalUrl}`);\n lines.push(\"Nothing was written — a state file that is not signed in would make every later call fail silently.\");\n\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: lines.join(\"\\n\") }];\n return { isError: true, content: pushFrame(content, outcome.png) };\n}\n\nasync function withFrame(png: Buffer | undefined, text: string): Promise<CallToolResult[\"content\"]> {\n const content: CallToolResult[\"content\"] = [];\n if (png) {\n content.push({ type: \"image\", data: toBase64(await resizeForOutput(png)), mimeType: \"image/png\" });\n }\n content.push({ type: \"text\", text });\n return content;\n}\n\n/** The failure frame is attached raw-ish: it is evidence, and resizing it can itself fail. */\nfunction pushFrame(content: CallToolResult[\"content\"], png: Buffer | undefined): CallToolResult[\"content\"] {\n if (png) content.push({ type: \"image\", data: png.toString(\"base64\"), mimeType: \"image/png\" });\n return content;\n}\n\n/** A screenshot is always worth having and never worth failing over. */\nasync function safeScreenshot(page: Page): Promise<Buffer | undefined> {\n try {\n if (page.isClosed()) return undefined;\n return await page.screenshot({ type: \"png\" });\n } catch {\n return undefined;\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a failure that happened outside the flow itself\n * (launching the browser, opening the URL, writing the file). Mirrors\n * `describeFailure` in screenshot.ts.\n */\nexport function describeSaveAuthFailure(input: { url: string; output_path: string }, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const prefix = \"Saving auth state failed:\";\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n `${prefix} Playwright's Chromium browser is not installed. ` +\n `Run \\`npx playwright install chromium\\` and try again. (${firstLine})`\n );\n }\n if (/^page\\.goto:/.test(message)) {\n return `${prefix} could not open ${input.url} — ${firstLine}`;\n }\n if (/EACCES|EPERM|ENOTDIR|EISDIR/.test(message)) {\n return `${prefix} could not write ${input.output_path} — ${firstLine}`;\n }\n return `${prefix} ${firstLine}`;\n}\n\nexport function registerSaveAuthTool(server: McpServer): void {\n server.registerTool(\n SAVE_AUTH_TOOL_NAME,\n {\n title: \"Save auth\",\n description:\n \"Run a login or gate flow once and save the browser state it produces (cookies and localStorage) to a \" +\n \"file. Every other FrameWatch tool takes that file as `storage_state` and opens the page already signed \" +\n \"in, so an app behind a login can be tested without replaying the flow on every call. Give `wait_for` a \" +\n \"selector that only exists once signed in: nothing is written unless it appears, so a state file always \" +\n \"means a real session. When a saved session expires the login screen shows up in the next capture — run \" +\n \"this again then.\",\n inputSchema: saveAuthInputShape,\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async (args) => saveAuth(args),\n );\n}\n"]}
@@ -18,6 +18,7 @@ export declare const screenshotInputShape: {
18
18
  selector: z.ZodOptional<z.ZodString>;
19
19
  wait_for: z.ZodOptional<z.ZodString>;
20
20
  wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
21
+ storage_state: z.ZodOptional<z.ZodString>;
21
22
  };
22
23
  export declare const screenshotInputSchema: z.ZodObject<{
23
24
  url: z.ZodString;
@@ -35,6 +36,7 @@ export declare const screenshotInputSchema: z.ZodObject<{
35
36
  selector: z.ZodOptional<z.ZodString>;
36
37
  wait_for: z.ZodOptional<z.ZodString>;
37
38
  wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
39
+ storage_state: z.ZodOptional<z.ZodString>;
38
40
  }, "strip", z.ZodTypeAny, {
39
41
  url: string;
40
42
  wait_ms: number;
@@ -44,6 +46,7 @@ export declare const screenshotInputSchema: z.ZodObject<{
44
46
  height: number;
45
47
  } | undefined;
46
48
  wait_for?: string | undefined;
49
+ storage_state?: string | undefined;
47
50
  selector?: string | undefined;
48
51
  }, {
49
52
  url: string;
@@ -54,6 +57,7 @@ export declare const screenshotInputSchema: z.ZodObject<{
54
57
  wait_ms?: number | undefined;
55
58
  wait_for?: string | undefined;
56
59
  wait_for_timeout_ms?: number | undefined;
60
+ storage_state?: string | undefined;
57
61
  selector?: string | undefined;
58
62
  }>;
59
63
  export type ScreenshotInput = z.input<typeof screenshotInputSchema>;
@@ -2,6 +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 { loginFormVisible, resolveStorageState, storageStateField, withAuthNote } from "../utils/storage-state.js";
5
6
  export const SCREENSHOT_TOOL_NAME = "framewatch_screenshot";
6
7
  export const screenshotInputShape = {
7
8
  url: z
@@ -29,6 +30,7 @@ export const screenshotInputShape = {
29
30
  .min(1)
30
31
  .default(SELECTOR_TIMEOUT_MS)
31
32
  .describe("Max time (ms) to wait for `wait_for` / `selector` to appear (must be > 0)"),
33
+ storage_state: storageStateField,
32
34
  };
33
35
  export const screenshotInputSchema = z.object(screenshotInputShape);
34
36
  /**
@@ -46,7 +48,9 @@ export async function takeScreenshot(rawInput) {
46
48
  const input = parsed.data;
47
49
  const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
48
50
  try {
49
- const shot = await withPage({ viewport }, async (page) => {
51
+ const auth = await resolveStorageState(input.storage_state);
52
+ const contextOptions = auth ? { storageState: auth.state } : {};
53
+ const shot = await withPage({ viewport, contextOptions }, async (page) => {
50
54
  const response = await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
51
55
  if (input.wait_for) {
52
56
  await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
@@ -57,7 +61,14 @@ export async function takeScreenshot(rawInput) {
57
61
  const png = input.selector
58
62
  ? await page.locator(input.selector).first().screenshot({ type: "png", timeout: input.wait_for_timeout_ms })
59
63
  : await page.screenshot({ type: "png" });
60
- 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
+ };
61
72
  });
62
73
  const resized = await resizeForOutput(shot.png);
63
74
  const { width, height } = await getDimensions(resized);
@@ -69,12 +80,12 @@ export async function takeScreenshot(rawInput) {
69
80
  `viewport ${viewport.width}x${viewport.height}`,
70
81
  input.selector ? `element ${input.selector}` : null,
71
82
  ].filter((p) => p !== null);
72
- return {
83
+ return withAuthNote({
73
84
  content: [
74
85
  { type: "image", data: toBase64(resized), mimeType: "image/png" },
75
86
  { type: "text", text: summaryParts.join(" — ") },
76
87
  ],
77
- };
88
+ }, auth, shot.loginVisible);
78
89
  }
79
90
  catch (error) {
80
91
  return errorResult(describeFailure(input, error));