@askqa/mcp 1.2.8 → 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.8",
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.8",
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
@@ -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,18 +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. 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.",
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')"),
383
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."),
384
403
  },
385
404
  },
386
- async ({ code, url, timeout }) => {
405
+ async ({ code, url, timeout, capture_trace }) => {
387
406
  try {
388
407
  // POST returns immediately with test_run_id; poll until done (same as run_test)
389
- const { test_run_id } = await apiPost("/api/tests/validate", { code, url, timeout });
408
+ const { test_run_id } = await apiPost("/api/tests/validate", { code, url, timeout, capture_trace });
390
409
  const testRun = await pollTestRun(test_run_id);
391
410
  const runResult = testRun.result || {};
392
411
  const content = [];
@@ -497,11 +516,12 @@ server.registerTool(
497
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."),
498
517
  headers: z.record(z.string()).nullable().optional().describe("Updated HTTP headers (pass null to clear)"),
499
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."),
500
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."),
501
521
  healing_note: z.string().nullable().optional().describe("Optional note explaining why healing was disabled (pass null to clear)."),
502
522
  },
503
523
  },
504
- 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 }) => {
505
525
  try {
506
526
  const body = {};
507
527
  if (name !== undefined) body.name = name;
@@ -512,6 +532,7 @@ server.registerTool(
512
532
  if (secrets !== undefined) body.secrets = secrets;
513
533
  if (headers !== undefined) body.headers = headers;
514
534
  if (enable_test_mode !== undefined) body.enable_test_mode = enable_test_mode;
535
+ if (test_timeout !== undefined) body.test_timeout = test_timeout;
515
536
  if (healing_disabled !== undefined) body.healing_disabled = healing_disabled;
516
537
  if (healing_note !== undefined) body.healing_note = healing_note;
517
538
  const test = await apiPatch(`/api/tests/${test_id}`, body);