@askqa/mcp 1.2.5 → 1.2.7

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.5",
3
+ "version": "1.2.7",
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.5",
3
+ "version": "1.2.7",
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
@@ -84,6 +84,17 @@ async function pollTestRun(testRunId, maxWaitMs = 300000) {
84
84
  throw new Error(`Test run ${testRunId} did not finish within ${maxWaitMs / 1000}s`);
85
85
  }
86
86
 
87
+ async function pollJob(jobId, maxWaitMs = 300000) {
88
+ const start = Date.now();
89
+ while (Date.now() - start < maxWaitMs) {
90
+ const job = await apiGet(`/api/jobs/${jobId}`);
91
+ if (job.status === "done") return job.result;
92
+ if (job.status === "failed") throw new Error(job.error || "Job failed");
93
+ await sleep(2000);
94
+ }
95
+ throw new Error(`Job ${jobId} did not finish within ${maxWaitMs / 1000}s`);
96
+ }
97
+
87
98
  async function fetchScreenshot(url) {
88
99
  try {
89
100
  const res = await fetch(url, {
@@ -176,16 +187,13 @@ const server = new McpServer(
176
187
  "If the latest run passed, confirm it's working. If it failed, report what failed.",
177
188
  "Only call run_test if the user explicitly asks to run a new test — checking status should use existing results.",
178
189
  "",
179
- "When the user asks to monitor a site, use list_templates to see available templates.",
180
- "There are two kinds of templates:",
181
- "",
182
- "1. Universal templates (e.g. 'quick-checks') work on any website with no configuration.",
183
- " Just call create_test with template_id and you're done.",
190
+ "When the user asks to monitor a site or set up tests, use detect_tests first.",
191
+ "detect_tests screenshots the site and asks AI to suggest 2-3 meaningful e2e tests.",
192
+ "The user then picks which tests to add. You write the code, validate with validate_test,",
193
+ "iterate until all steps pass, then save with create_test.",
184
194
  "",
185
- "2. Site-specific templates (e.g. 'shopify-cart') need to discover CSS selectors for the target site.",
186
- " Call detect_template first to probe the site and generate standalone Playwright code,",
187
- " then call create_test with that generated code (not the template_id).",
188
- " Templates that support detection have supportsDetection: true in list_templates output.",
195
+ "For universal templates (e.g. 'quick-checks') that work on any site with no config,",
196
+ "you can call create_test with template_id directly without detect_tests.",
189
197
  ].join("\n"),
190
198
  }
191
199
  );
@@ -193,7 +201,7 @@ const server = new McpServer(
193
201
  server.registerTool(
194
202
  "list_templates",
195
203
  {
196
- description: "List available test templates with usage hints. Some templates work directly with create_test (universal), others need detect_template first to discover site-specific selectors (site-specific).",
204
+ description: "List available universal test templates (e.g. 'quick-checks') that work on any site with no configuration. For custom site-specific tests, use detect_tests instead.",
197
205
  readOnlyHint: true,
198
206
  },
199
207
  async () => {
@@ -205,12 +213,7 @@ server.registerTool(
205
213
  lines.push(` Name: ${t.name}`);
206
214
  lines.push(` Description: ${t.description}`);
207
215
  lines.push(` Steps: ${t.steps.join(", ")}`);
208
- if (t.supportsCodeGeneration) {
209
- lines.push(` supportsDetection: true`);
210
- lines.push(` Usage: call detect_template → get generated code → create_test with code`);
211
- } else {
212
- lines.push(` Usage: call create_test with template_id="${t.id}" directly`);
213
- }
216
+ lines.push(` Usage: call create_test with template_id="${t.id}" directly`);
214
217
  lines.push("");
215
218
  }
216
219
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -221,66 +224,53 @@ server.registerTool(
221
224
  );
222
225
 
223
226
  server.registerTool(
224
- "detect_template",
227
+ "detect_tests",
225
228
  {
226
- description: "Run template detection against a URL to discover selectors and generate custom test code. This is the fastest way to create a custom test detection runs the template flow against the site, discovers which CSS selectors work, and generates standalone Playwright test code with those selectors baked in. Use the generated code with create_test to save it.",
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.",
227
230
  readOnlyHint: true,
228
231
  inputSchema: {
229
- template_id: z.string().describe("Template ID from list_templates (e.g. 'shopify-cart')"),
230
- url: z.string().describe("The target URL to detect against (e.g. 'https://my-store.myshopify.com')"),
232
+ url: z.string().describe("The website URL to analyze (e.g. 'https://my-store.com')"),
231
233
  },
232
234
  },
233
- async ({ template_id, url }) => {
235
+ async ({ url }) => {
234
236
  try {
235
- const result = await apiPost(`/api/tests/templates/${template_id}/detect`, { url });
236
- const content = [];
237
+ const { job_id } = await apiPost("/api/tests/suggest", { url });
238
+ const result = await pollJob(job_id);
237
239
  const lines = [];
238
240
 
239
- const statusLabel = result.status === "passed" ? "PASSED" : "FAILED";
240
- lines.push(`Detection: ${statusLabel}`);
241
- lines.push("");
242
-
243
- if (result.steps) {
244
- lines.push("Steps:");
245
- for (const step of result.steps) {
246
- const icon = step.status === "passed" ? "+" : "x";
247
- lines.push(` ${icon} ${step.name} — ${step.status}`);
248
- if (step.error) lines.push(` Error: ${step.error}`);
249
- }
241
+ if (result.page_summary) {
242
+ lines.push(`Site: ${result.page_summary}`);
250
243
  lines.push("");
251
244
  }
252
245
 
253
- if (result.selectors) {
254
- lines.push("Discovered selectors:");
255
- for (const [key, value] of Object.entries(result.selectors)) {
256
- lines.push(` ${key}: ${value}`);
257
- }
246
+ const suggestions = result.suggestions || [];
247
+ if (suggestions.length === 0) {
248
+ lines.push("No suggestions available. You can still write a custom test using screenshot_url to inspect the page.");
249
+ } else {
250
+ lines.push(`Suggested tests (${suggestions.length}):`);
258
251
  lines.push("");
259
- }
260
-
261
- if (result.code) {
262
- lines.push("Generated test code (use with create_test):");
263
- lines.push("```");
264
- lines.push(result.code);
265
- lines.push("```");
266
- }
267
-
268
- content.push({ type: "text", text: lines.join("\n") });
269
-
270
- // Fetch screenshots for each step
271
- if (result.steps) {
272
- for (const step of result.steps) {
273
- if (!step.screenshot) continue;
274
- const screenshotUrl = `${API_URL}/api/screenshots/${result.executionId}/${step.screenshot}`;
275
- const base64 = await fetchScreenshot(screenshotUrl);
276
- if (base64) {
277
- content.push({ type: "text", text: `Screenshot: ${step.name}` });
278
- content.push({ type: "image", data: base64, mimeType: "image/png" });
252
+ for (let i = 0; i < suggestions.length; i++) {
253
+ const s = suggestions[i];
254
+ lines.push(`${i + 1}. ${s.name}`);
255
+ lines.push(` What it tests: ${s.description}`);
256
+ lines.push(` Why it matters: ${s.why}`);
257
+ if (s.steps && s.steps.length > 0) {
258
+ lines.push(` Steps:`);
259
+ for (const step of s.steps) {
260
+ lines.push(` - ${step}`);
261
+ }
279
262
  }
263
+ lines.push("");
280
264
  }
265
+ lines.push("Next steps:");
266
+ lines.push("1. Ask the user which test(s) they want to add");
267
+ lines.push("2. Use screenshot_url to inspect the page and discover selectors");
268
+ lines.push("3. Write the Playwright test code");
269
+ lines.push("4. Validate with validate_test — iterate until ALL steps pass");
270
+ lines.push("5. Save with create_test");
281
271
  }
282
272
 
283
- return { content };
273
+ return { content: [{ type: "text", text: lines.join("\n") }] };
284
274
  } catch (err) {
285
275
  return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
286
276
  }
@@ -290,7 +280,7 @@ server.registerTool(
290
280
  server.registerTool(
291
281
  "create_test",
292
282
  {
293
- description: "Create a saved test. Use template_id for universal templates (e.g. 'quick-checks') that work on any site. Use code for custom tests or for site-specific templates after running detect_template. 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.",
294
284
  destructiveHint: true,
295
285
  inputSchema: {
296
286
  name: z.string().describe("A name for this test (e.g. 'Homepage health check')"),
@@ -331,7 +321,8 @@ server.registerTool(
331
321
  },
332
322
  async ({ url }) => {
333
323
  try {
334
- const result = await apiPost("/api/tests/screenshot", { url });
324
+ const { job_id } = await apiPost("/api/tests/screenshot", { url });
325
+ const result = await pollJob(job_id);
335
326
  const content = [];
336
327
 
337
328
  // Page info as structured text
@@ -384,7 +375,7 @@ server.registerTool(
384
375
  server.registerTool(
385
376
  "validate_test",
386
377
  {
387
- description: "Start here when writing a new 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 the test passes, then call create_test to save it.",
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.",
388
379
  readOnlyHint: true,
389
380
  inputSchema: {
390
381
  code: z.string().describe("Custom Playwright test code. Must define an async function test({ page, step, log })."),
@@ -1120,7 +1111,7 @@ server.registerTool(
1120
1111
  }
1121
1112
 
1122
1113
  if (run.test_code) {
1123
- lines.push("Test code (use create_test to recreate in your account):");
1114
+ lines.push("Test code (validate with validate_test first, then create_test to save):");
1124
1115
  lines.push("```javascript");
1125
1116
  lines.push(run.test_code);
1126
1117
  lines.push("```");