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,405 @@
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 API_MOCK_TOOL_NAME = "framewatch_api_mock";
5
+ /** What one mock answers with, when it answers at all. */
6
+ export declare const mockResponseSchema: z.ZodObject<{
7
+ status: z.ZodOptional<z.ZodNumber>;
8
+ body: z.ZodOptional<z.ZodUnknown>;
9
+ delay_ms: z.ZodOptional<z.ZodNumber>;
10
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ status?: number | undefined;
13
+ body?: unknown;
14
+ headers?: Record<string, string> | undefined;
15
+ delay_ms?: number | undefined;
16
+ }, {
17
+ status?: number | undefined;
18
+ body?: unknown;
19
+ headers?: Record<string, string> | undefined;
20
+ delay_ms?: number | undefined;
21
+ }>;
22
+ /** One rule: what to intercept, and what to do with it. */
23
+ export declare const mockSchema: z.ZodObject<{
24
+ url_pattern: z.ZodString;
25
+ scenario: z.ZodOptional<z.ZodEnum<["empty", "error", "unauthorized", "not_found", "slow", "malformed", "offline"]>>;
26
+ response: z.ZodOptional<z.ZodObject<{
27
+ status: z.ZodOptional<z.ZodNumber>;
28
+ body: z.ZodOptional<z.ZodUnknown>;
29
+ delay_ms: z.ZodOptional<z.ZodNumber>;
30
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
31
+ }, "strip", z.ZodTypeAny, {
32
+ status?: number | undefined;
33
+ body?: unknown;
34
+ headers?: Record<string, string> | undefined;
35
+ delay_ms?: number | undefined;
36
+ }, {
37
+ status?: number | undefined;
38
+ body?: unknown;
39
+ headers?: Record<string, string> | undefined;
40
+ delay_ms?: number | undefined;
41
+ }>>;
42
+ abort: z.ZodOptional<z.ZodEnum<["failed", "timedout", "connectionrefused", "internetdisconnected"]>>;
43
+ times: z.ZodOptional<z.ZodNumber>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ url_pattern: string;
46
+ response?: {
47
+ status?: number | undefined;
48
+ body?: unknown;
49
+ headers?: Record<string, string> | undefined;
50
+ delay_ms?: number | undefined;
51
+ } | undefined;
52
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
53
+ times?: number | undefined;
54
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
55
+ }, {
56
+ url_pattern: string;
57
+ response?: {
58
+ status?: number | undefined;
59
+ body?: unknown;
60
+ headers?: Record<string, string> | undefined;
61
+ delay_ms?: number | undefined;
62
+ } | undefined;
63
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
64
+ times?: number | undefined;
65
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
66
+ }>;
67
+ export declare const apiMockInputShape: {
68
+ mocks: z.ZodArray<z.ZodObject<{
69
+ url_pattern: z.ZodString;
70
+ scenario: z.ZodOptional<z.ZodEnum<["empty", "error", "unauthorized", "not_found", "slow", "malformed", "offline"]>>;
71
+ response: z.ZodOptional<z.ZodObject<{
72
+ status: z.ZodOptional<z.ZodNumber>;
73
+ body: z.ZodOptional<z.ZodUnknown>;
74
+ delay_ms: z.ZodOptional<z.ZodNumber>;
75
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
76
+ }, "strip", z.ZodTypeAny, {
77
+ status?: number | undefined;
78
+ body?: unknown;
79
+ headers?: Record<string, string> | undefined;
80
+ delay_ms?: number | undefined;
81
+ }, {
82
+ status?: number | undefined;
83
+ body?: unknown;
84
+ headers?: Record<string, string> | undefined;
85
+ delay_ms?: number | undefined;
86
+ }>>;
87
+ abort: z.ZodOptional<z.ZodEnum<["failed", "timedout", "connectionrefused", "internetdisconnected"]>>;
88
+ times: z.ZodOptional<z.ZodNumber>;
89
+ }, "strip", z.ZodTypeAny, {
90
+ url_pattern: string;
91
+ response?: {
92
+ status?: number | undefined;
93
+ body?: unknown;
94
+ headers?: Record<string, string> | undefined;
95
+ delay_ms?: number | undefined;
96
+ } | undefined;
97
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
98
+ times?: number | undefined;
99
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
100
+ }, {
101
+ url_pattern: string;
102
+ response?: {
103
+ status?: number | undefined;
104
+ body?: unknown;
105
+ headers?: Record<string, string> | undefined;
106
+ delay_ms?: number | undefined;
107
+ } | undefined;
108
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
109
+ times?: number | undefined;
110
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
111
+ }>, "many">;
112
+ block_unmatched: z.ZodDefault<z.ZodBoolean>;
113
+ include_network: z.ZodDefault<z.ZodBoolean>;
114
+ url: z.ZodString;
115
+ duration_ms: z.ZodDefault<z.ZodNumber>;
116
+ sensitivity: z.ZodDefault<z.ZodNumber>;
117
+ max_frames: z.ZodDefault<z.ZodNumber>;
118
+ interval_ms: z.ZodDefault<z.ZodNumber>;
119
+ viewport: z.ZodOptional<z.ZodObject<{
120
+ width: z.ZodDefault<z.ZodNumber>;
121
+ height: z.ZodDefault<z.ZodNumber>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ width: number;
124
+ height: number;
125
+ }, {
126
+ width?: number | undefined;
127
+ height?: number | undefined;
128
+ }>>;
129
+ wait_for: z.ZodOptional<z.ZodString>;
130
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
131
+ interactions: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
132
+ delay_ms: z.ZodDefault<z.ZodNumber>;
133
+ selector: z.ZodOptional<z.ZodString>;
134
+ value: z.ZodOptional<z.ZodString>;
135
+ x: z.ZodOptional<z.ZodNumber>;
136
+ y: z.ZodOptional<z.ZodNumber>;
137
+ delta_x: z.ZodOptional<z.ZodNumber>;
138
+ delta_y: z.ZodOptional<z.ZodNumber>;
139
+ action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
140
+ }, "strip", z.ZodTypeAny, {
141
+ delay_ms: number;
142
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
143
+ value?: string | undefined;
144
+ x?: number | undefined;
145
+ y?: number | undefined;
146
+ selector?: string | undefined;
147
+ delta_x?: number | undefined;
148
+ delta_y?: number | undefined;
149
+ }, {
150
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
151
+ value?: string | undefined;
152
+ delay_ms?: number | undefined;
153
+ x?: number | undefined;
154
+ y?: number | undefined;
155
+ selector?: string | undefined;
156
+ delta_x?: number | undefined;
157
+ delta_y?: number | undefined;
158
+ }>, {
159
+ delay_ms: number;
160
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
161
+ value?: string | undefined;
162
+ x?: number | undefined;
163
+ y?: number | undefined;
164
+ selector?: string | undefined;
165
+ delta_x?: number | undefined;
166
+ delta_y?: number | undefined;
167
+ }, {
168
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
169
+ value?: string | undefined;
170
+ delay_ms?: number | undefined;
171
+ x?: number | undefined;
172
+ y?: number | undefined;
173
+ selector?: string | undefined;
174
+ delta_x?: number | undefined;
175
+ delta_y?: number | undefined;
176
+ }>, "many">>;
177
+ interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
178
+ include_console: z.ZodDefault<z.ZodBoolean>;
179
+ include_dom: z.ZodDefault<z.ZodBoolean>;
180
+ include_performance: z.ZodDefault<z.ZodBoolean>;
181
+ storage_state: z.ZodOptional<z.ZodString>;
182
+ };
183
+ export declare const apiMockInputSchema: z.ZodObject<{
184
+ mocks: z.ZodArray<z.ZodObject<{
185
+ url_pattern: z.ZodString;
186
+ scenario: z.ZodOptional<z.ZodEnum<["empty", "error", "unauthorized", "not_found", "slow", "malformed", "offline"]>>;
187
+ response: z.ZodOptional<z.ZodObject<{
188
+ status: z.ZodOptional<z.ZodNumber>;
189
+ body: z.ZodOptional<z.ZodUnknown>;
190
+ delay_ms: z.ZodOptional<z.ZodNumber>;
191
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
192
+ }, "strip", z.ZodTypeAny, {
193
+ status?: number | undefined;
194
+ body?: unknown;
195
+ headers?: Record<string, string> | undefined;
196
+ delay_ms?: number | undefined;
197
+ }, {
198
+ status?: number | undefined;
199
+ body?: unknown;
200
+ headers?: Record<string, string> | undefined;
201
+ delay_ms?: number | undefined;
202
+ }>>;
203
+ abort: z.ZodOptional<z.ZodEnum<["failed", "timedout", "connectionrefused", "internetdisconnected"]>>;
204
+ times: z.ZodOptional<z.ZodNumber>;
205
+ }, "strip", z.ZodTypeAny, {
206
+ url_pattern: string;
207
+ response?: {
208
+ status?: number | undefined;
209
+ body?: unknown;
210
+ headers?: Record<string, string> | undefined;
211
+ delay_ms?: number | undefined;
212
+ } | undefined;
213
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
214
+ times?: number | undefined;
215
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
216
+ }, {
217
+ url_pattern: string;
218
+ response?: {
219
+ status?: number | undefined;
220
+ body?: unknown;
221
+ headers?: Record<string, string> | undefined;
222
+ delay_ms?: number | undefined;
223
+ } | undefined;
224
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
225
+ times?: number | undefined;
226
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
227
+ }>, "many">;
228
+ block_unmatched: z.ZodDefault<z.ZodBoolean>;
229
+ include_network: z.ZodDefault<z.ZodBoolean>;
230
+ url: z.ZodString;
231
+ duration_ms: z.ZodDefault<z.ZodNumber>;
232
+ sensitivity: z.ZodDefault<z.ZodNumber>;
233
+ max_frames: z.ZodDefault<z.ZodNumber>;
234
+ interval_ms: z.ZodDefault<z.ZodNumber>;
235
+ viewport: z.ZodOptional<z.ZodObject<{
236
+ width: z.ZodDefault<z.ZodNumber>;
237
+ height: z.ZodDefault<z.ZodNumber>;
238
+ }, "strip", z.ZodTypeAny, {
239
+ width: number;
240
+ height: number;
241
+ }, {
242
+ width?: number | undefined;
243
+ height?: number | undefined;
244
+ }>>;
245
+ wait_for: z.ZodOptional<z.ZodString>;
246
+ wait_for_timeout_ms: z.ZodDefault<z.ZodNumber>;
247
+ interactions: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
248
+ delay_ms: z.ZodDefault<z.ZodNumber>;
249
+ selector: z.ZodOptional<z.ZodString>;
250
+ value: z.ZodOptional<z.ZodString>;
251
+ x: z.ZodOptional<z.ZodNumber>;
252
+ y: z.ZodOptional<z.ZodNumber>;
253
+ delta_x: z.ZodOptional<z.ZodNumber>;
254
+ delta_y: z.ZodOptional<z.ZodNumber>;
255
+ action: z.ZodEnum<["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"]>;
256
+ }, "strip", z.ZodTypeAny, {
257
+ delay_ms: number;
258
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
259
+ value?: string | undefined;
260
+ x?: number | undefined;
261
+ y?: number | undefined;
262
+ selector?: string | undefined;
263
+ delta_x?: number | undefined;
264
+ delta_y?: number | undefined;
265
+ }, {
266
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
267
+ value?: string | undefined;
268
+ delay_ms?: number | undefined;
269
+ x?: number | undefined;
270
+ y?: number | undefined;
271
+ selector?: string | undefined;
272
+ delta_x?: number | undefined;
273
+ delta_y?: number | undefined;
274
+ }>, {
275
+ delay_ms: number;
276
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
277
+ value?: string | undefined;
278
+ x?: number | undefined;
279
+ y?: number | undefined;
280
+ selector?: string | undefined;
281
+ delta_x?: number | undefined;
282
+ delta_y?: number | undefined;
283
+ }, {
284
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
285
+ value?: string | undefined;
286
+ delay_ms?: number | undefined;
287
+ x?: number | undefined;
288
+ y?: number | undefined;
289
+ selector?: string | undefined;
290
+ delta_x?: number | undefined;
291
+ delta_y?: number | undefined;
292
+ }>, "many">>;
293
+ interaction_timeout_ms: z.ZodDefault<z.ZodNumber>;
294
+ include_console: z.ZodDefault<z.ZodBoolean>;
295
+ include_dom: z.ZodDefault<z.ZodBoolean>;
296
+ include_performance: z.ZodDefault<z.ZodBoolean>;
297
+ storage_state: z.ZodOptional<z.ZodString>;
298
+ }, "strip", z.ZodTypeAny, {
299
+ url: string;
300
+ wait_for_timeout_ms: number;
301
+ block_unmatched: boolean;
302
+ duration_ms: number;
303
+ sensitivity: number;
304
+ max_frames: number;
305
+ interval_ms: number;
306
+ interaction_timeout_ms: number;
307
+ include_console: boolean;
308
+ include_network: boolean;
309
+ include_dom: boolean;
310
+ include_performance: boolean;
311
+ mocks: {
312
+ url_pattern: string;
313
+ response?: {
314
+ status?: number | undefined;
315
+ body?: unknown;
316
+ headers?: Record<string, string> | undefined;
317
+ delay_ms?: number | undefined;
318
+ } | undefined;
319
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
320
+ times?: number | undefined;
321
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
322
+ }[];
323
+ viewport?: {
324
+ width: number;
325
+ height: number;
326
+ } | undefined;
327
+ wait_for?: string | undefined;
328
+ storage_state?: string | undefined;
329
+ interactions?: {
330
+ delay_ms: number;
331
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
332
+ value?: string | undefined;
333
+ x?: number | undefined;
334
+ y?: number | undefined;
335
+ selector?: string | undefined;
336
+ delta_x?: number | undefined;
337
+ delta_y?: number | undefined;
338
+ }[] | undefined;
339
+ }, {
340
+ url: string;
341
+ mocks: {
342
+ url_pattern: string;
343
+ response?: {
344
+ status?: number | undefined;
345
+ body?: unknown;
346
+ headers?: Record<string, string> | undefined;
347
+ delay_ms?: number | undefined;
348
+ } | undefined;
349
+ abort?: "failed" | "timedout" | "connectionrefused" | "internetdisconnected" | undefined;
350
+ times?: number | undefined;
351
+ scenario?: "error" | "offline" | "empty" | "unauthorized" | "not_found" | "slow" | "malformed" | undefined;
352
+ }[];
353
+ viewport?: {
354
+ width?: number | undefined;
355
+ height?: number | undefined;
356
+ } | undefined;
357
+ wait_for?: string | undefined;
358
+ wait_for_timeout_ms?: number | undefined;
359
+ storage_state?: string | undefined;
360
+ block_unmatched?: boolean | undefined;
361
+ duration_ms?: number | undefined;
362
+ sensitivity?: number | undefined;
363
+ max_frames?: number | undefined;
364
+ interval_ms?: number | undefined;
365
+ interactions?: {
366
+ action: "type" | "key" | "click" | "tap" | "scroll" | "swipe" | "hover" | "select" | "wait" | "navigate";
367
+ value?: string | undefined;
368
+ delay_ms?: number | undefined;
369
+ x?: number | undefined;
370
+ y?: number | undefined;
371
+ selector?: string | undefined;
372
+ delta_x?: number | undefined;
373
+ delta_y?: number | undefined;
374
+ }[] | undefined;
375
+ interaction_timeout_ms?: number | undefined;
376
+ include_console?: boolean | undefined;
377
+ include_network?: boolean | undefined;
378
+ include_dom?: boolean | undefined;
379
+ include_performance?: boolean | undefined;
380
+ }>;
381
+ export type ApiMockInput = z.input<typeof apiMockInputSchema>;
382
+ type ParsedApiMockInput = z.output<typeof apiMockInputSchema>;
383
+ /**
384
+ * Show the page the answers it is never given in development.
385
+ *
386
+ * Intercepts the requests named in `mocks`, answers them however the caller
387
+ * says — empty, broken, slow, unauthorised, dead — and records the page's
388
+ * reaction as diff cards, exactly as `framewatch_capture` would. It is the
389
+ * same recorder, differ and layers; the only difference is what the network
390
+ * says back.
391
+ *
392
+ * The report leads with what each mock actually did, because the failure this
393
+ * tool is most likely to hit is the silent one: a pattern that matches nothing,
394
+ * a page that renders perfectly on real data, and a test that proved nothing at
395
+ * all.
396
+ */
397
+ export declare function mockApi(rawInput: ApiMockInput): Promise<CallToolResult>;
398
+ /**
399
+ * One actionable line for a run that could not happen. Mirrors
400
+ * `describeCaptureFailure`: match on the failing Playwright call, never on
401
+ * substrings of a user-supplied selector.
402
+ */
403
+ export declare function describeApiMockFailure(input: Pick<ParsedApiMockInput, "url" | "wait_for" | "wait_for_timeout_ms">, error: unknown): string;
404
+ export declare function registerApiMockTool(server: McpServer): void;
405
+ export {};
@@ -0,0 +1,186 @@
1
+ import { z } from "zod";
2
+ import { MAX_MOCKS, MAX_MOCK_BODY_BYTES, MAX_MOCK_DELAY_MS } from "../constants.js";
3
+ import { MockRouter } from "../engine/mocks.js";
4
+ import { formatDiffCards } from "../utils/format.js";
5
+ import { ABORT_REASONS, MOCK_SCENARIOS, formatMockReport, resolveMock, } from "../utils/mock-rules.js";
6
+ import { authLines, captureInputShape, runCapture, summariseContext } from "./capture.js";
7
+ export const API_MOCK_TOOL_NAME = "framewatch_api_mock";
8
+ /** What one mock answers with, when it answers at all. */
9
+ export const mockResponseSchema = z.object({
10
+ status: z.number().int().min(100).max(599).optional().describe("HTTP status to answer with (default 200)"),
11
+ body: z
12
+ .unknown()
13
+ .optional()
14
+ .describe("The response body. An object or array is sent as JSON; a string is sent exactly as written, which is " +
15
+ "how you simulate an API that returns broken JSON."),
16
+ delay_ms: z
17
+ .number()
18
+ .int()
19
+ .min(0)
20
+ .max(MAX_MOCK_DELAY_MS)
21
+ .optional()
22
+ .describe("Hold the response back this long, so the recording shows the waiting state"),
23
+ headers: z.record(z.string()).optional().describe("Response headers. A `content-type` here overrides the one the body implies."),
24
+ });
25
+ /** One rule: what to intercept, and what to do with it. */
26
+ export const mockSchema = z.object({
27
+ url_pattern: z
28
+ .string()
29
+ .min(1)
30
+ .describe("Glob matched against the whole URL, e.g. `**/api/products*`. A bare path like `/api/products` matches " +
31
+ "nothing — it needs the leading `**`."),
32
+ scenario: z
33
+ .enum(MOCK_SCENARIOS)
34
+ .optional()
35
+ .describe("Shorthand for a common state: `empty` (200, `[]`), `error` (500), `unauthorized` (401), `not_found` " +
36
+ "(404), `slow` (200 after 5s), `malformed` (200 with a body that is not JSON), `offline` (the request " +
37
+ "fails outright). Anything you also put in `response` wins over the shorthand."),
38
+ response: mockResponseSchema.optional(),
39
+ abort: z
40
+ .enum(ABORT_REASONS)
41
+ .optional()
42
+ .describe("Fail the request instead of answering it — what the page sees when the network is down"),
43
+ times: z
44
+ .number()
45
+ .int()
46
+ .min(1)
47
+ .optional()
48
+ .describe("Only apply to the first N matching requests; later ones fall through to the next mock, or to the real server"),
49
+ });
50
+ export const apiMockInputShape = {
51
+ ...captureInputShape,
52
+ mocks: z
53
+ .array(mockSchema)
54
+ .min(1)
55
+ .max(MAX_MOCKS)
56
+ .describe("The requests to intercept. When two patterns match the same request, the first one listed wins."),
57
+ block_unmatched: z
58
+ .boolean()
59
+ .default(false)
60
+ .describe("Fail every request no mock matched, instead of letting it reach the real server. Turns the run into " +
61
+ "'what does this page do with no backend at all'."),
62
+ // Louder than in framewatch_capture, and deliberately: the whole point here
63
+ // is what the page did with the answers it was given, so the requests
64
+ // themselves are part of the result rather than an extra.
65
+ include_network: z
66
+ .boolean()
67
+ .default(true)
68
+ .describe("Attach network requests (method, url, status, duration) to the frames they settled between"),
69
+ };
70
+ export const apiMockInputSchema = z.object(apiMockInputShape);
71
+ /**
72
+ * Show the page the answers it is never given in development.
73
+ *
74
+ * Intercepts the requests named in `mocks`, answers them however the caller
75
+ * says — empty, broken, slow, unauthorised, dead — and records the page's
76
+ * reaction as diff cards, exactly as `framewatch_capture` would. It is the
77
+ * same recorder, differ and layers; the only difference is what the network
78
+ * says back.
79
+ *
80
+ * The report leads with what each mock actually did, because the failure this
81
+ * tool is most likely to hit is the silent one: a pattern that matches nothing,
82
+ * a page that renders perfectly on real data, and a test that proved nothing at
83
+ * all.
84
+ */
85
+ export async function mockApi(rawInput) {
86
+ const parsed = apiMockInputSchema.safeParse(rawInput);
87
+ if (!parsed.success) {
88
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
89
+ return errorResult(`API mock run failed: invalid input — ${issues}`);
90
+ }
91
+ const input = parsed.data;
92
+ const mocks = input.mocks.map((mock) => resolveMock(mock));
93
+ const oversized = tooLarge(mocks);
94
+ if (oversized)
95
+ return errorResult(`API mock run of ${input.url} failed: ${oversized}`);
96
+ const router = new MockRouter({ mocks, block_unmatched: input.block_unmatched });
97
+ let run;
98
+ try {
99
+ run = await runCapture(input, {
100
+ prepare: (page) => router.install(page),
101
+ finish: async () => router.dispose(),
102
+ });
103
+ }
104
+ catch (error) {
105
+ router.dispose();
106
+ return errorResult(describeApiMockFailure(input, error));
107
+ }
108
+ const report = formatMockReport({
109
+ activity: router.activity,
110
+ unmatched: router.unmatched,
111
+ block_unmatched: input.block_unmatched,
112
+ // The length that was asked for, not the wall clock the recorder measured:
113
+ // "longer than the 5000ms recording" is what the caller can act on.
114
+ duration_ms: input.duration_ms,
115
+ });
116
+ return formatDiffCards({
117
+ cards: run.cards,
118
+ total_frames: run.total_frames,
119
+ duration_ms: run.duration_ms,
120
+ url: run.url,
121
+ final_url: run.final_url,
122
+ title: run.title,
123
+ dropped: run.dropped,
124
+ interactions: run.interactions,
125
+ viewport: run.viewport,
126
+ notes: [...report, ...(summariseContext(run.context, run.cards.length) ?? []), ...authLines(run)],
127
+ });
128
+ }
129
+ /**
130
+ * The first mock whose body will not fit, if any. Bodies are held in Node,
131
+ * pushed into the browser and echoed in the report, so an unbounded one is a
132
+ * way to wedge all three at once.
133
+ */
134
+ function tooLarge(mocks) {
135
+ for (let i = 0; i < mocks.length; i++) {
136
+ const mock = mocks[i];
137
+ if (mock.kind !== "fulfill")
138
+ continue;
139
+ const bytes = Buffer.byteLength(mock.body, "utf8");
140
+ if (bytes > MAX_MOCK_BODY_BYTES) {
141
+ return (`mock ${i + 1} (\`${mock.url_pattern}\`) has a ${bytes}-byte body, which is too large — ` +
142
+ `the limit is ${MAX_MOCK_BODY_BYTES} bytes.`);
143
+ }
144
+ }
145
+ return undefined;
146
+ }
147
+ function errorResult(text) {
148
+ return { isError: true, content: [{ type: "text", text }] };
149
+ }
150
+ /**
151
+ * One actionable line for a run that could not happen. Mirrors
152
+ * `describeCaptureFailure`: match on the failing Playwright call, never on
153
+ * substrings of a user-supplied selector.
154
+ */
155
+ export function describeApiMockFailure(input, error) {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ const line = message.split("\n")[0];
158
+ const prefix = `API mock run of ${input.url} failed:`;
159
+ if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
160
+ return (`${prefix} Playwright's Chromium browser is not installed. ` +
161
+ `Run \`npx playwright install chromium\` and try again. (${line})`);
162
+ }
163
+ if (input.wait_for && /^page\.waitForSelector:/.test(message)) {
164
+ return `${prefix} selector "${input.wait_for}" did not become visible within ${input.wait_for_timeout_ms}ms.`;
165
+ }
166
+ if (/^page\.goto:/.test(message)) {
167
+ return `${prefix} the page could not be opened — ${line}`;
168
+ }
169
+ return `${prefix} ${line}`;
170
+ }
171
+ export function registerApiMockTool(server) {
172
+ server.registerTool(API_MOCK_TOOL_NAME, {
173
+ title: "API mock",
174
+ description: "Answer the page's API calls yourself and record what it does with the answer. Use it to see the " +
175
+ "states you cannot reach on real data: an empty list, a 500, a 401, a response that takes five " +
176
+ "seconds, a body that is not valid JSON, or a request that fails outright. `scenario` covers each of " +
177
+ "those in a word; `response` sets the status, body, headers and delay by hand. Returns the same diff " +
178
+ "cards as framewatch_capture, plus a report of what each mock actually served — including the ones " +
179
+ "that matched nothing, which is how you find out a pattern was wrong instead of trusting a page that " +
180
+ "looked fine. Requests no mock matched reach the real server and are named; `block_unmatched` cuts " +
181
+ "them off instead.",
182
+ inputSchema: apiMockInputShape,
183
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
184
+ }, async (args) => mockApi(args));
185
+ }
186
+ //# sourceMappingURL=api-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-mock.js","sourceRoot":"","sources":["../../src/tools/api-mock.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 { MAX_MOCKS, MAX_MOCK_BODY_BYTES, MAX_MOCK_DELAY_MS } from \"../constants.js\";\nimport { MockRouter } from \"../engine/mocks.js\";\nimport { formatDiffCards } from \"../utils/format.js\";\nimport {\n ABORT_REASONS,\n MOCK_SCENARIOS,\n formatMockReport,\n resolveMock,\n type ResolvedMock,\n} from \"../utils/mock-rules.js\";\nimport { authLines, captureInputShape, runCapture, summariseContext, type CaptureRun } from \"./capture.js\";\n\nexport const API_MOCK_TOOL_NAME = \"framewatch_api_mock\";\n\n/** What one mock answers with, when it answers at all. */\nexport const mockResponseSchema = z.object({\n status: z.number().int().min(100).max(599).optional().describe(\"HTTP status to answer with (default 200)\"),\n body: z\n .unknown()\n .optional()\n .describe(\n \"The response body. An object or array is sent as JSON; a string is sent exactly as written, which is \" +\n \"how you simulate an API that returns broken JSON.\",\n ),\n delay_ms: z\n .number()\n .int()\n .min(0)\n .max(MAX_MOCK_DELAY_MS)\n .optional()\n .describe(\"Hold the response back this long, so the recording shows the waiting state\"),\n headers: z.record(z.string()).optional().describe(\"Response headers. A `content-type` here overrides the one the body implies.\"),\n});\n\n/** One rule: what to intercept, and what to do with it. */\nexport const mockSchema = z.object({\n url_pattern: z\n .string()\n .min(1)\n .describe(\n \"Glob matched against the whole URL, e.g. `**/api/products*`. A bare path like `/api/products` matches \" +\n \"nothing — it needs the leading `**`.\",\n ),\n scenario: z\n .enum(MOCK_SCENARIOS)\n .optional()\n .describe(\n \"Shorthand for a common state: `empty` (200, `[]`), `error` (500), `unauthorized` (401), `not_found` \" +\n \"(404), `slow` (200 after 5s), `malformed` (200 with a body that is not JSON), `offline` (the request \" +\n \"fails outright). Anything you also put in `response` wins over the shorthand.\",\n ),\n response: mockResponseSchema.optional(),\n abort: z\n .enum(ABORT_REASONS)\n .optional()\n .describe(\"Fail the request instead of answering it — what the page sees when the network is down\"),\n times: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Only apply to the first N matching requests; later ones fall through to the next mock, or to the real server\"),\n});\n\nexport const apiMockInputShape = {\n ...captureInputShape,\n mocks: z\n .array(mockSchema)\n .min(1)\n .max(MAX_MOCKS)\n .describe(\"The requests to intercept. When two patterns match the same request, the first one listed wins.\"),\n block_unmatched: z\n .boolean()\n .default(false)\n .describe(\n \"Fail every request no mock matched, instead of letting it reach the real server. Turns the run into \" +\n \"'what does this page do with no backend at all'.\",\n ),\n // Louder than in framewatch_capture, and deliberately: the whole point here\n // is what the page did with the answers it was given, so the requests\n // themselves are part of the result rather than an extra.\n include_network: z\n .boolean()\n .default(true)\n .describe(\"Attach network requests (method, url, status, duration) to the frames they settled between\"),\n};\n\nexport const apiMockInputSchema = z.object(apiMockInputShape);\nexport type ApiMockInput = z.input<typeof apiMockInputSchema>;\ntype ParsedApiMockInput = z.output<typeof apiMockInputSchema>;\n\n/**\n * Show the page the answers it is never given in development.\n *\n * Intercepts the requests named in `mocks`, answers them however the caller\n * says — empty, broken, slow, unauthorised, dead — and records the page's\n * reaction as diff cards, exactly as `framewatch_capture` would. It is the\n * same recorder, differ and layers; the only difference is what the network\n * says back.\n *\n * The report leads with what each mock actually did, because the failure this\n * tool is most likely to hit is the silent one: a pattern that matches nothing,\n * a page that renders perfectly on real data, and a test that proved nothing at\n * all.\n */\nexport async function mockApi(rawInput: ApiMockInput): Promise<CallToolResult> {\n const parsed = apiMockInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`API mock run failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n\n const mocks = input.mocks.map((mock) => resolveMock(mock));\n const oversized = tooLarge(mocks);\n if (oversized) return errorResult(`API mock run of ${input.url} failed: ${oversized}`);\n\n const router = new MockRouter({ mocks, block_unmatched: input.block_unmatched });\n\n let run: CaptureRun;\n try {\n run = await runCapture(input, {\n prepare: (page) => router.install(page),\n finish: async () => router.dispose(),\n });\n } catch (error) {\n router.dispose();\n return errorResult(describeApiMockFailure(input, error));\n }\n\n const report = formatMockReport({\n activity: router.activity,\n unmatched: router.unmatched,\n block_unmatched: input.block_unmatched,\n // The length that was asked for, not the wall clock the recorder measured:\n // \"longer than the 5000ms recording\" is what the caller can act on.\n duration_ms: input.duration_ms,\n });\n\n return formatDiffCards({\n cards: run.cards,\n total_frames: run.total_frames,\n duration_ms: run.duration_ms,\n url: run.url,\n final_url: run.final_url,\n title: run.title,\n dropped: run.dropped,\n interactions: run.interactions,\n viewport: run.viewport,\n notes: [...report, ...(summariseContext(run.context, run.cards.length) ?? []), ...authLines(run)],\n });\n}\n\n/**\n * The first mock whose body will not fit, if any. Bodies are held in Node,\n * pushed into the browser and echoed in the report, so an unbounded one is a\n * way to wedge all three at once.\n */\nfunction tooLarge(mocks: ResolvedMock[]): string | undefined {\n for (let i = 0; i < mocks.length; i++) {\n const mock = mocks[i];\n if (mock.kind !== \"fulfill\") continue;\n const bytes = Buffer.byteLength(mock.body, \"utf8\");\n if (bytes > MAX_MOCK_BODY_BYTES) {\n return (\n `mock ${i + 1} (\\`${mock.url_pattern}\\`) has a ${bytes}-byte body, which is too large — ` +\n `the limit is ${MAX_MOCK_BODY_BYTES} bytes.`\n );\n }\n }\n return undefined;\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a run that could not happen. Mirrors\n * `describeCaptureFailure`: match on the failing Playwright call, never on\n * substrings of a user-supplied selector.\n */\nexport function describeApiMockFailure(\n input: Pick<ParsedApiMockInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const line = message.split(\"\\n\")[0];\n const prefix = `API mock run 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. (${line})`\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 (/^page\\.goto:/.test(message)) {\n return `${prefix} the page could not be opened — ${line}`;\n }\n return `${prefix} ${line}`;\n}\n\nexport function registerApiMockTool(server: McpServer): void {\n server.registerTool(\n API_MOCK_TOOL_NAME,\n {\n title: \"API mock\",\n description:\n \"Answer the page's API calls yourself and record what it does with the answer. Use it to see the \" +\n \"states you cannot reach on real data: an empty list, a 500, a 401, a response that takes five \" +\n \"seconds, a body that is not valid JSON, or a request that fails outright. `scenario` covers each of \" +\n \"those in a word; `response` sets the status, body, headers and delay by hand. Returns the same diff \" +\n \"cards as framewatch_capture, plus a report of what each mock actually served — including the ones \" +\n \"that matched nothing, which is how you find out a pattern was wrong instead of trusting a page that \" +\n \"looked fine. Requests no mock matched reach the real server and are named; `block_unmatched` cuts \" +\n \"them off instead.\",\n inputSchema: apiMockInputShape,\n annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => mockApi(args),\n );\n}\n"]}