@askqa/mcp 1.2.3 → 1.2.6

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "plugins": [
3
+ {
4
+ "name": "askqa",
5
+ "version": "1.2.3",
6
+ "description": "AskQA skills — set up notifications and monitoring for your websites"
7
+ }
8
+ ]
9
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askqa",
3
- "version": "1.2.3",
3
+ "version": "1.2.6",
4
4
  "description": "AskQA skills — set up notifications and monitoring for your websites",
5
5
  "mcpServers": {
6
6
  "askqa": {
package/README.md CHANGED
@@ -54,6 +54,9 @@ Get your API key from [askqa.ai](https://askqa.ai) after signing in.
54
54
  | `list_notification_channels` | List notification channels |
55
55
  | `remove_notification_channel` | Remove a notification channel |
56
56
  | `test_notification_channel` | Send a test notification |
57
+ | `share_test_run` | Make a test run publicly shareable, returns a share URL |
58
+ | `unshare_test_run` | Revoke public sharing for a test run |
59
+ | `get_shared_run` | Fetch a shared run by URL or token (no API key needed); includes test code to recreate it |
57
60
 
58
61
  ## Example Conversations
59
62
 
@@ -75,6 +78,18 @@ The AI will use `screenshot_url` to inspect the page, write custom Playwright co
75
78
 
76
79
  The AI will call `get_test_results` and `get_test_screenshots` to analyze the failure.
77
80
 
81
+ **Share a test run:**
82
+
83
+ > "Share my latest checkout run with the client"
84
+
85
+ The AI will call `get_test_results` to find the run, then `share_test_run` to generate a public link. Use `unshare_test_run` to revoke access later.
86
+
87
+ **Copy a shared test into your account:**
88
+
89
+ > "I got this link — can you set up the same test for my site? https://askqa.ai/r/..."
90
+
91
+ The AI will call `get_shared_run` to read the test code from the shared link (no API key needed), then `create_test` to add it to your account.
92
+
78
93
  ## Privacy Policy
79
94
 
80
95
  This extension only makes HTTPS requests to the AskQA API. It does not access files on your computer or collect analytics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askqa/mcp",
3
- "version": "1.2.3",
3
+ "version": "1.2.6",
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:",
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.",
181
194
  "",
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.",
184
- "",
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 })."),
@@ -1077,6 +1068,111 @@ server.registerTool(
1077
1068
  }
1078
1069
  );
1079
1070
 
1071
+ server.registerTool(
1072
+ "get_shared_run",
1073
+ {
1074
+ description:
1075
+ "Fetch a publicly shared test run by its share URL or token. No API key required — anyone with the link can call this. " +
1076
+ "Returns run results, step details, and the original test config (test_code, test_template_id, test_params, test_url) " +
1077
+ "so you can recreate the test in your own account using create_test.",
1078
+ readOnlyHint: true,
1079
+ inputSchema: {
1080
+ share_url_or_token: z.string().describe("The share URL (https://askqa.ai/r/TOKEN) or just the token"),
1081
+ },
1082
+ },
1083
+ async ({ share_url_or_token }) => {
1084
+ try {
1085
+ const token = share_url_or_token.includes("/")
1086
+ ? share_url_or_token.split("/").pop()
1087
+ : share_url_or_token;
1088
+
1089
+ const res = await fetch(`${API_URL}/api/test-runs/shared/${encodeURIComponent(token)}`);
1090
+ if (res.status === 404) {
1091
+ return { content: [{ type: "text", text: "Shared run not found. The link may be invalid or sharing may have been revoked." }], isError: true };
1092
+ }
1093
+ if (!res.ok) {
1094
+ throw new Error(`API ${res.status}: ${await res.text()}`);
1095
+ }
1096
+ const run = await res.json();
1097
+
1098
+ const lines = [
1099
+ `Run #${run.id} — ${run.test_name || "Unnamed test"} | ${run.status}`,
1100
+ `URL: ${run.test_url || "unknown"}`,
1101
+ `Duration: ${run.result?.durationMs ? (run.result.durationMs / 1000).toFixed(1) + "s" : "N/A"}`,
1102
+ "",
1103
+ ];
1104
+
1105
+ if (run.result?.steps?.length) {
1106
+ lines.push("Steps:");
1107
+ for (const s of run.result.steps) {
1108
+ lines.push(` ${s.status === "passed" ? "✓" : "✗"} ${s.name}${s.error ? " — " + s.error : ""}`);
1109
+ }
1110
+ lines.push("");
1111
+ }
1112
+
1113
+ if (run.test_code) {
1114
+ lines.push("Test code (validate with validate_test first, then create_test to save):");
1115
+ lines.push("```javascript");
1116
+ lines.push(run.test_code);
1117
+ lines.push("```");
1118
+ } else if (run.test_template_id) {
1119
+ lines.push(`Template: ${run.test_template_id}`);
1120
+ if (run.test_params && Object.keys(run.test_params).length) {
1121
+ lines.push(`Params: ${JSON.stringify(run.test_params)}`);
1122
+ }
1123
+ }
1124
+
1125
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1126
+ } catch (err) {
1127
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
1128
+ }
1129
+ }
1130
+ );
1131
+
1132
+ server.registerTool(
1133
+ "share_test_run",
1134
+ {
1135
+ description: "Make a test run publicly shareable. Returns a share_url that anyone can open without logging in. Calling again on an already-shared run returns the same URL. Use unshare_test_run to revoke access.",
1136
+ inputSchema: {
1137
+ test_run_id: z.coerce.number().describe("The test run ID to share"),
1138
+ },
1139
+ },
1140
+ async ({ test_run_id }) => {
1141
+ try {
1142
+ const data = await apiPost(`/api/test-runs/${test_run_id}/share`, {});
1143
+ return { content: [{ type: "text", text: `Share URL: ${data.share_url}\n\nAnyone with this link can view the test run results without logging in.` }] };
1144
+ } catch (err) {
1145
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
1146
+ }
1147
+ }
1148
+ );
1149
+
1150
+ server.registerTool(
1151
+ "unshare_test_run",
1152
+ {
1153
+ description: "Revoke public sharing for a test run. The share URL will stop working immediately.",
1154
+ destructiveHint: true,
1155
+ inputSchema: {
1156
+ test_run_id: z.coerce.number().describe("The test run ID to stop sharing"),
1157
+ },
1158
+ },
1159
+ async ({ test_run_id }) => {
1160
+ try {
1161
+ const res = await fetch(`${API_URL}/api/test-runs/${test_run_id}/share`, {
1162
+ method: "DELETE",
1163
+ headers: { Authorization: `Bearer ${API_KEY}` },
1164
+ });
1165
+ if (!res.ok && res.status !== 204) {
1166
+ const text = await res.text();
1167
+ throw new Error(`API ${res.status}: ${text}`);
1168
+ }
1169
+ return { content: [{ type: "text", text: `Sharing disabled for run #${test_run_id}. The public link no longer works.` }] };
1170
+ } catch (err) {
1171
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
1172
+ }
1173
+ }
1174
+ );
1175
+
1080
1176
  const transport = new StdioServerTransport();
1081
1177
  await server.connect(transport);
1082
1178
  console.error("AskQA MCP server running on stdio");