@askqa/mcp 1.2.7 → 1.2.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askqa",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "AskQA skills — set up notifications and monitoring for your websites",
5
5
  "mcpServers": {
6
6
  "askqa": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askqa/mcp",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "MCP server for AskQA — monitor websites with automated tests by chatting with AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/server.js CHANGED
@@ -226,7 +226,7 @@ server.registerTool(
226
226
  server.registerTool(
227
227
  "detect_tests",
228
228
  {
229
- description: "Start here when a user wants to monitor a new site. Screenshots the URL and uses AI to suggest 2-3 meaningful e2e tests tailored to that specific site (e.g. checkout flow, login, add to cart). Returns a page_summary and a list of suggestions with names, descriptions, and step sketches. After calling this: present the suggestions to the user, let them pick which ones to add, then YOU write the Playwright test code for each chosen test, validate with validate_test, iterate until it passes, then save with create_test.",
229
+ description: "Start here when a user wants to monitor a new site. Screenshots the URL and uses AI to suggest 2-3 meaningful e2e tests tailored to that specific site (e.g. checkout flow, login, add to cart). Returns a page_summary and a list of suggestions with names, descriptions, and step sketches. After calling this: present the suggestions to the user, let them pick which ones to add, then YOU write the Playwright test code for each chosen test, validate with validate_test, iterate until it passes, then save with create_test. NAVIGATION RULE: test code must only call page.goto() once, to the site root/homepage. All further navigation (to product pages, category pages, etc.) must be through real user interactions — menu hovers, link clicks, form submissions. Never use page.goto() to jump directly to a sub-page or product URL.",
230
230
  readOnlyHint: true,
231
231
  inputSchema: {
232
232
  url: z.string().describe("The website URL to analyze (e.g. 'https://my-store.com')"),
@@ -280,7 +280,7 @@ server.registerTool(
280
280
  server.registerTool(
281
281
  "create_test",
282
282
  {
283
- description: "Save a validated test. IMPORTANT: For code-based tests, only call this AFTER validate_test confirms the test passes — never save untested code. Use template_id for universal templates (e.g. 'quick-checks') that work on any site without validation. Use code for custom tests after running detect_tests → write code → validate_test. Provide template_id or code, not both.",
283
+ description: "Save a validated test. IMPORTANT: For code-based tests, only call this AFTER validate_test confirms the test passes — never save untested code. Use template_id for universal templates (e.g. 'quick-checks') that work on any site without validation. Use code for custom tests after running detect_tests → write code → validate_test. Provide template_id or code, not both. NAVIGATION RULE: test code must only call page.goto() once, to the site root/homepage. All further navigation must be through real user interactions — menu hovers, link clicks, form submissions. Never use page.goto() to jump directly to a sub-page or product URL.",
284
284
  destructiveHint: true,
285
285
  inputSchema: {
286
286
  name: z.string().describe("A name for this test (e.g. 'Homepage health check')"),
@@ -291,9 +291,10 @@ server.registerTool(
291
291
  secrets: z.record(z.string()).optional().describe("Optional key-value secrets (e.g. { email: '...', password: '...' } or { api_key: '...' }). Encrypted at rest, never returned in API responses."),
292
292
  headers: z.record(z.string()).optional().describe("Optional HTTP headers injected into requests to the target domain (e.g. { 'X-Test-Secret': 'abc' })"),
293
293
  enable_test_mode: z.boolean().optional().describe("Send X-AskQA-Secret header to the target site, enabling test mode on sites that support it (default: true)"),
294
+ test_timeout: z.coerce.number().optional().describe("Maximum seconds the scheduler is allowed to run this test (default: 120, max: 300). Set this when the test has many page navigations and validate_test needed a timeout > 120 to pass."),
294
295
  },
295
296
  },
296
- async ({ name, url, template_id, params, code, secrets, headers, enable_test_mode }) => {
297
+ async ({ name, url, template_id, params, code, secrets, headers, enable_test_mode, test_timeout }) => {
297
298
  try {
298
299
  const body = { name, url };
299
300
  if (template_id) body.template_id = template_id;
@@ -302,6 +303,7 @@ server.registerTool(
302
303
  if (secrets) body.secrets = secrets;
303
304
  if (headers) body.headers = headers;
304
305
  if (enable_test_mode !== undefined) body.enable_test_mode = enable_test_mode;
306
+ if (test_timeout !== undefined) body.test_timeout = test_timeout;
305
307
  const test = await apiPost("/api/tests/create", body);
306
308
  return { content: [{ type: "text", text: JSON.stringify(test, null, 2) }] };
307
309
  } catch (err) {
@@ -375,17 +377,35 @@ server.registerTool(
375
377
  server.registerTool(
376
378
  "validate_test",
377
379
  {
378
- description: "REQUIRED before create_test for any code-based test. Dry-runs Playwright code against a URL without saving it — returns step results, screenshots, and page structure. Steps continue even on failure for maximum debug signal. Iterate here until ALL steps pass, then call create_test to save it.",
380
+ description: `REQUIRED before create_test for any code-based test. Dry-runs Playwright code against a URL without saving it — returns step results, screenshots, and page structure. Steps continue even on failure for maximum debug signal. Iterate here until ALL steps pass, then call create_test to save it.
381
+
382
+ NAVIGATION RULE: test code must only call page.goto() once, to the site root/homepage (the url parameter). All further navigation must be through real user interactions — menu hovers, link clicks, form submissions. Never use page.goto() to jump directly to a sub-page, product URL, or collection path.
383
+
384
+ ONCE ALL STEPS PASS — before calling create_test, review the code against these quality checks and fix any violations:
385
+
386
+ 1. NO waitForLoadState after clicks on SSR sites (Shopify, Next.js, most e-commerce): these pages render HTML server-side, so elements are in the initial response. Waiting for domcontentloaded after a click adds seconds of dead time. Instead: let the next step's element waitFor() serve as the nav signal, OR use page.waitForURL(/pattern/) when you need to confirm the URL changed before running JS (e.g. page.evaluate).
387
+
388
+ 2. NO waitForTimeout — never use page.waitForTimeout(ms). Always wait for a specific condition: element visibility, URL change, or network idle.
389
+
390
+ 3. NO JS clicks — never use element.evaluate(el => el.click()). Use locator.click() which simulates real mouse events. For elements covered by sticky headers or overlays, use click({ force: true }) instead of JS clicks.
391
+
392
+ 4. DELAY-TRIGGERED POPUPS need dual guards — scroll-triggered or time-delayed popups (e.g. Klaviyo email capture) often fire after the dedicated dismiss step has already moved on. Add a second guard at the start of the next interaction step: check isVisible() and dismiss if present.
393
+
394
+ 5. POPUP DISMISS TIMEOUTS — keep popup waitFor timeouts short (4–6 s). A popup that doesn't appear within 6 s is unlikely to appear before the next interaction anyway. Don't set long timeouts just to be safe — it burns time on every run.
395
+
396
+ 6. COLLECTION CARD CLICKS — on collection/category pages, product image areas are often covered by a full-card anchor overlay (e.g. a.media_link on Shopify). If normal click fails with a coverage error, use click({ force: true }). Never use page.goto() to skip to the product URL.`,
379
397
  readOnlyHint: true,
380
398
  inputSchema: {
381
399
  code: z.string().describe("Custom Playwright test code. Must define an async function test({ page, step, log })."),
382
400
  url: z.string().describe("The target URL to test against (e.g. 'https://example.com')"),
401
+ timeout: z.coerce.number().optional().describe("Maximum seconds the test is allowed to run (default: 120, max: 300). Increase for tests with many page navigations."),
402
+ capture_trace: z.boolean().optional().describe("Enable Playwright trace recording (default: false). Enable when step output and screenshots are not enough to diagnose a failure — the trace includes full DOM snapshots, network timeline, and every action. Adds memory overhead; avoid on tests with many page navigations unless needed."),
383
403
  },
384
404
  },
385
- async ({ code, url }) => {
405
+ async ({ code, url, timeout, capture_trace }) => {
386
406
  try {
387
407
  // POST returns immediately with test_run_id; poll until done (same as run_test)
388
- const { test_run_id } = await apiPost("/api/tests/validate", { code, url });
408
+ const { test_run_id } = await apiPost("/api/tests/validate", { code, url, timeout, capture_trace });
389
409
  const testRun = await pollTestRun(test_run_id);
390
410
  const runResult = testRun.result || {};
391
411
  const content = [];
@@ -406,6 +426,9 @@ server.registerTool(
406
426
  lines.push(" Logs:");
407
427
  for (const msg of runResult.logs) lines.push(` ${msg}`);
408
428
  }
429
+ if (testRun.trace_viewer_url) {
430
+ lines.push(` Trace: ${testRun.trace_viewer_url}`);
431
+ }
409
432
  content.push({ type: "text", text: lines.join("\n") });
410
433
 
411
434
  // Include page info for debugging selectors
@@ -493,11 +516,12 @@ server.registerTool(
493
516
  secrets: z.record(z.string()).nullable().optional().describe("Updated secrets (pass null to clear). Encrypted at rest, never returned in API responses — must be provided again when updating a test that uses secrets."),
494
517
  headers: z.record(z.string()).nullable().optional().describe("Updated HTTP headers (pass null to clear)"),
495
518
  enable_test_mode: z.boolean().optional().describe("Send X-AskQA-Secret header to the target site, enabling test mode on sites that support it (default: true)"),
519
+ test_timeout: z.coerce.number().nullable().optional().describe("Maximum seconds the scheduler is allowed to run this test (default: 120, max: 300). Set when validate_test needed a timeout > 120 to pass. Pass null to reset to default."),
496
520
  healing_disabled: z.boolean().optional().describe("Set true to stop AskQA from suggesting fixes for this test (e.g. a failing test that's acceptable as-is, or one you'd rather fix yourself). Auto-clears once the test passes again. Set false to re-enable."),
497
521
  healing_note: z.string().nullable().optional().describe("Optional note explaining why healing was disabled (pass null to clear)."),
498
522
  },
499
523
  },
500
- async ({ test_id, name, url, code, template_id, params, secrets, headers, enable_test_mode, healing_disabled, healing_note }) => {
524
+ async ({ test_id, name, url, code, template_id, params, secrets, headers, enable_test_mode, test_timeout, healing_disabled, healing_note }) => {
501
525
  try {
502
526
  const body = {};
503
527
  if (name !== undefined) body.name = name;
@@ -508,6 +532,7 @@ server.registerTool(
508
532
  if (secrets !== undefined) body.secrets = secrets;
509
533
  if (headers !== undefined) body.headers = headers;
510
534
  if (enable_test_mode !== undefined) body.enable_test_mode = enable_test_mode;
535
+ if (test_timeout !== undefined) body.test_timeout = test_timeout;
511
536
  if (healing_disabled !== undefined) body.healing_disabled = healing_disabled;
512
537
  if (healing_note !== undefined) body.healing_note = healing_note;
513
538
  const test = await apiPatch(`/api/tests/${test_id}`, body);