@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.
- package/.claude-plugin/marketplace.json +9 -0
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -0
- package/package.json +1 -1
- package/server.js +159 -63
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
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
|
|
180
|
-
"
|
|
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
|
-
"
|
|
183
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
227
|
+
"detect_tests",
|
|
225
228
|
{
|
|
226
|
-
description: "
|
|
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
|
-
|
|
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 ({
|
|
235
|
+
async ({ url }) => {
|
|
234
236
|
try {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
+
const { job_id } = await apiPost("/api/tests/suggest", { url });
|
|
238
|
+
const result = await pollJob(job_id);
|
|
237
239
|
const lines = [];
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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: "
|
|
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
|
|
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: "
|
|
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");
|