@askqa/mcp 1.0.8 → 1.1.0

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,5 +1,17 @@
1
1
  {
2
2
  "name": "askqa",
3
- "version": "1.0.8",
4
- "description": "AskQA skills — set up notifications and monitoring for your websites"
3
+ "version": "1.1.0",
4
+ "description": "AskQA skills — set up notifications and monitoring for your websites",
5
+ "mcpServers": {
6
+ "askqa": {
7
+ "command": "node",
8
+ "args": [
9
+ "${CLAUDE_PLUGIN_ROOT}/server.js"
10
+ ],
11
+ "env": {
12
+ "AUTOQA_API_URL": "",
13
+ "AUTOQA_API_KEY": ""
14
+ }
15
+ }
16
+ }
5
17
  }
package/README.md CHANGED
@@ -73,3 +73,19 @@ The AI will use `screenshot_url` to inspect the page, write custom Playwright co
73
73
  > "Why is my checkout test failing?"
74
74
 
75
75
  The AI will call `get_test_results` and `get_test_screenshots` to analyze the failure.
76
+
77
+ ## Privacy Policy
78
+
79
+ This extension only makes HTTPS requests to the AskQA API. It does not access files on your computer or collect analytics.
80
+
81
+ For full details, see the [Privacy Policy](https://askqa.ai/privacy).
82
+
83
+ ## Support
84
+
85
+ - **Website**: [askqa.ai](https://askqa.ai)
86
+ - **Issues**: [GitHub Issues](https://github.com/askqa-ai/askqa-plugins/issues)
87
+ - **Email**: support@askqa.ai
88
+
89
+ ## License
90
+
91
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askqa/mcp",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
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
@@ -97,10 +97,11 @@ async function fetchScreenshot(url) {
97
97
  }
98
98
  }
99
99
 
100
- function buildTestRunText(testRun) {
100
+ function buildTestRunText(testRun, testName) {
101
101
  const icon = testRun.status === "completed" ? "✓" : testRun.status === "failed" ? "✗" : "…";
102
+ const testLabel = testName ? `${testName} (#${testRun.test_id})` : `${testRun.test_id}`;
102
103
  const lines = [
103
- `${icon} Run #${testRun.id} | Test: ${testRun.test_id} | ${testRun.trigger_type} | ${testRun.status}`,
104
+ `${icon} Run #${testRun.id} | ${testLabel} | ${testRun.trigger_type} | ${testRun.status}`,
104
105
  ];
105
106
 
106
107
  if (testRun.result?.durationMs) {
@@ -179,6 +180,7 @@ server.registerTool(
179
180
  "list_templates",
180
181
  {
181
182
  description: "List available test templates. Returns template IDs, names, descriptions, and steps.",
183
+ readOnlyHint: true,
182
184
  },
183
185
  async () => {
184
186
  try {
@@ -194,20 +196,25 @@ server.registerTool(
194
196
  "create_test",
195
197
  {
196
198
  description: "Create a saved test. Use template_id for built-in templates, or code for custom Playwright tests. Provide one or the other, not both.",
199
+ destructiveHint: true,
197
200
  inputSchema: {
198
201
  name: z.string().describe("A name for this test (e.g. 'Homepage health check')"),
199
202
  url: z.string().describe("The target URL to test (e.g. 'https://example.com')"),
200
203
  template_id: z.string().optional().describe("Template ID from list_templates (e.g. 'quick-checks'). Omit if using code."),
201
204
  params: z.record(z.string()).optional().describe("Optional template parameters"),
202
- code: z.string().optional().describe("Custom Playwright test code. Must define an async function test({ page, step, log }). Omit if using template_id."),
205
+ code: z.string().optional().describe("Custom Playwright test code. Must define an async function test({ page, step, log, secrets }). Omit if using template_id."),
206
+ 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."),
207
+ headers: z.record(z.string()).optional().describe("Optional HTTP headers injected into requests to the target domain (e.g. { 'X-Test-Secret': 'abc' })"),
203
208
  },
204
209
  },
205
- async ({ name, url, template_id, params, code }) => {
210
+ async ({ name, url, template_id, params, code, secrets, headers }) => {
206
211
  try {
207
212
  const body = { name, url };
208
213
  if (template_id) body.template_id = template_id;
209
214
  if (params) body.params = params;
210
215
  if (code) body.code = code;
216
+ if (secrets) body.secrets = secrets;
217
+ if (headers) body.headers = headers;
211
218
  const test = await apiPost("/api/tests/create", body);
212
219
  return { content: [{ type: "text", text: JSON.stringify(test, null, 2) }] };
213
220
  } catch (err) {
@@ -220,6 +227,7 @@ server.registerTool(
220
227
  "screenshot_url",
221
228
  {
222
229
  description: "Take a screenshot of a URL and extract page structure (links, buttons, inputs, headings with selectors). Use this BEFORE writing custom test code to see the page layout and discover available selectors.",
230
+ readOnlyHint: true,
223
231
  inputSchema: {
224
232
  url: z.string().describe("The URL to screenshot (e.g. 'https://example.com')"),
225
233
  },
@@ -280,6 +288,7 @@ server.registerTool(
280
288
  "validate_test",
281
289
  {
282
290
  description: "Dry-run custom Playwright test code against a URL. Returns execution results, screenshots, and page structure for debugging. Steps continue even on failure to maximize debug signal. Use this to iterate on code before calling create_test.",
291
+ readOnlyHint: true,
283
292
  inputSchema: {
284
293
  code: z.string().describe("Custom Playwright test code. Must define an async function test({ page, step, log })."),
285
294
  url: z.string().describe("The target URL to test against (e.g. 'https://example.com')"),
@@ -349,6 +358,7 @@ server.registerTool(
349
358
  "list_tests",
350
359
  {
351
360
  description: "List all saved tests for the current organization. Returns id, name, url, template_id, status, and last_run summary (id, status, completed_at). This is the best starting point when checking if a feature or site is working — find the relevant test, then use get_test_results to see details.",
361
+ readOnlyHint: true,
352
362
  },
353
363
  async () => {
354
364
  try {
@@ -364,6 +374,7 @@ server.registerTool(
364
374
  "get_test",
365
375
  {
366
376
  description: "Get full details of a test by ID, including code for custom tests. Use list_tests to find the test ID first.",
377
+ readOnlyHint: true,
367
378
  inputSchema: {
368
379
  test_id: z.coerce.number().describe("The test ID (from list_tests or create_test)"),
369
380
  },
@@ -382,6 +393,7 @@ server.registerTool(
382
393
  "update_test",
383
394
  {
384
395
  description: "Update an existing test's name, URL, code, or other properties. Only provided fields are changed.",
396
+ destructiveHint: true,
385
397
  inputSchema: {
386
398
  test_id: z.coerce.number().describe("The test ID to update (from list_tests or get_test)"),
387
399
  name: z.string().optional().describe("New test name"),
@@ -389,9 +401,11 @@ server.registerTool(
389
401
  code: z.string().optional().describe("Updated custom Playwright test code"),
390
402
  template_id: z.string().optional().describe("Updated template ID"),
391
403
  params: z.record(z.string()).optional().describe("Updated template parameters"),
404
+ secrets: z.record(z.string()).nullable().optional().describe("Updated secrets (pass null to clear)"),
405
+ headers: z.record(z.string()).nullable().optional().describe("Updated HTTP headers (pass null to clear)"),
392
406
  },
393
407
  },
394
- async ({ test_id, name, url, code, template_id, params }) => {
408
+ async ({ test_id, name, url, code, template_id, params, secrets, headers }) => {
395
409
  try {
396
410
  const body = {};
397
411
  if (name !== undefined) body.name = name;
@@ -399,6 +413,8 @@ server.registerTool(
399
413
  if (code !== undefined) body.code = code;
400
414
  if (template_id !== undefined) body.template_id = template_id;
401
415
  if (params !== undefined) body.params = params;
416
+ if (secrets !== undefined) body.secrets = secrets;
417
+ if (headers !== undefined) body.headers = headers;
402
418
  const test = await apiPatch(`/api/tests/${test_id}`, body);
403
419
  return { content: [{ type: "text", text: JSON.stringify(test, null, 2) }] };
404
420
  } catch (err) {
@@ -411,6 +427,7 @@ server.registerTool(
411
427
  "delete_test",
412
428
  {
413
429
  description: "Permanently delete a test and its associated schedules. IMPORTANT: Always call this first WITHOUT confirm to see what will be deleted, show that to the user, and only call again with confirm=true after the user explicitly agrees.",
430
+ destructiveHint: true,
414
431
  inputSchema: {
415
432
  test_id: z.coerce.number().describe("The test ID to delete (from list_tests)"),
416
433
  confirm: z.boolean().optional().describe("Set to true to actually delete. Omit or false to preview what will be deleted."),
@@ -461,16 +478,18 @@ server.registerTool(
461
478
  "run_test",
462
479
  {
463
480
  description: "Run a saved test by ID. Waits for the test to finish and returns full results with step details.",
481
+ destructiveHint: true,
464
482
  inputSchema: {
465
483
  test_id: z.coerce.number().describe("The test ID to run (from create_test or list_tests)"),
466
484
  },
467
485
  },
468
486
  async ({ test_id }) => {
469
487
  try {
488
+ const test = await apiGet(`/api/tests/${test_id}`);
470
489
  const { test_run_id } = await apiPost(`/api/tests/${test_id}/run`, {});
471
490
  console.error(`Started test run ${test_run_id} for test ${test_id}`);
472
491
  const testRun = await pollTestRun(test_run_id);
473
- const text = buildTestRunText(testRun);
492
+ const text = buildTestRunText(testRun, test.name);
474
493
  return { content: [{ type: "text", text }] };
475
494
  } catch (err) {
476
495
  return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
@@ -482,6 +501,7 @@ server.registerTool(
482
501
  "get_test_screenshots",
483
502
  {
484
503
  description: "Get screenshots from a test run. Returns only images. Use after run_test or get_test_results to view screenshots.",
504
+ readOnlyHint: true,
485
505
  inputSchema: {
486
506
  test_run_id: z.coerce.number().describe("The test run ID (from run_test or get_test_results)"),
487
507
  },
@@ -507,6 +527,7 @@ server.registerTool(
507
527
  "schedule_test",
508
528
  {
509
529
  description: "Create a recurring schedule for a saved test. The test will run automatically at the specified interval.",
530
+ destructiveHint: true,
510
531
  inputSchema: {
511
532
  test_id: z.coerce.number().describe("The test ID to schedule (from create_test or list_tests)"),
512
533
  interval: z.enum(["every_minute", "hourly", "every_6_hours", "every_12_hours", "daily", "weekly"])
@@ -534,6 +555,7 @@ server.registerTool(
534
555
  "list_schedules",
535
556
  {
536
557
  description: "List all test schedules for the current organization, including last run status and next run time.",
558
+ readOnlyHint: true,
537
559
  },
538
560
  async () => {
539
561
  try {
@@ -563,6 +585,7 @@ server.registerTool(
563
585
  "update_schedule",
564
586
  {
565
587
  description: "Pause or resume a test schedule.",
588
+ destructiveHint: true,
566
589
  inputSchema: {
567
590
  schedule_id: z.coerce.number().describe("The schedule ID (from list_schedules)"),
568
591
  enabled: z.boolean().describe("true to resume, false to pause"),
@@ -588,6 +611,7 @@ server.registerTool(
588
611
  "delete_schedule",
589
612
  {
590
613
  description: "Permanently delete a test schedule. Historical run results are preserved.",
614
+ destructiveHint: true,
591
615
  inputSchema: {
592
616
  schedule_id: z.coerce.number().describe("The schedule ID to delete (from list_schedules)"),
593
617
  },
@@ -606,6 +630,7 @@ server.registerTool(
606
630
  "get_test_results",
607
631
  {
608
632
  description: "Get recent test run results with step-by-step details. Use this to answer questions like 'is X working?' — filter by test_id to see the latest runs for a specific test. Shows status, timing, step pass/fail, errors, and screenshot links.",
633
+ readOnlyHint: true,
609
634
  inputSchema: {
610
635
  test_id: z.coerce.number().optional().describe("Filter by test ID (optional — omit to see all runs)"),
611
636
  limit: z.coerce.number().optional().describe("Max results to return (default: 10)"),
@@ -620,9 +645,18 @@ server.registerTool(
620
645
  if (!data.test_runs.length) {
621
646
  return { content: [{ type: "text", text: "No test runs found." }] };
622
647
  }
648
+
649
+ // Build test name map
650
+ const testIds = [...new Set(data.test_runs.map((r) => r.test_id))];
651
+ const nameMap = {};
652
+ const testsData = await apiGet("/api/tests/list");
653
+ for (const t of testsData.tests) {
654
+ if (testIds.includes(t.id)) nameMap[t.id] = t.name;
655
+ }
656
+
623
657
  const lines = [];
624
658
  for (const run of data.test_runs) {
625
- lines.push(buildTestRunText(run));
659
+ lines.push(buildTestRunText(run, nameMap[run.test_id]));
626
660
  lines.push("");
627
661
  }
628
662
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -638,6 +672,7 @@ server.registerTool(
638
672
  "add_notification_channel",
639
673
  {
640
674
  description: "Add a notification channel to receive alerts when scheduled tests fail. Supports email, Telegram, and Slack. For Telegram, get a chat ID from https://t.me/userinfobot. For Slack, create an Incoming Webhook in your Slack workspace settings.",
675
+ destructiveHint: true,
641
676
  inputSchema: {
642
677
  channel_type: z.enum(["email", "telegram", "slack"]).describe("The notification channel type"),
643
678
  email_address: z.string().optional().describe("Email address for email channels (required when channel_type is email)"),
@@ -696,6 +731,7 @@ server.registerTool(
696
731
  "list_notification_channels",
697
732
  {
698
733
  description: "List all notification channels configured for the current organization.",
734
+ readOnlyHint: true,
699
735
  },
700
736
  async () => {
701
737
  try {
@@ -727,6 +763,7 @@ server.registerTool(
727
763
  "remove_notification_channel",
728
764
  {
729
765
  description: "Remove a notification channel. Use list_notification_channels to find the channel ID.",
766
+ destructiveHint: true,
730
767
  inputSchema: {
731
768
  channel_id: z.coerce.number().describe("The channel ID to remove (from list_notification_channels)"),
732
769
  },
@@ -745,6 +782,7 @@ server.registerTool(
745
782
  "test_notification_channel",
746
783
  {
747
784
  description: "Send a test notification to verify a channel is working correctly.",
785
+ destructiveHint: true,
748
786
  inputSchema: {
749
787
  channel_id: z.coerce.number().describe("The channel ID to test (from list_notification_channels)"),
750
788
  },
@@ -759,6 +797,38 @@ server.registerTool(
759
797
  }
760
798
  );
761
799
 
800
+ server.registerTool(
801
+ "get_org_secret",
802
+ {
803
+ description: "Get the org's X-AskQA-Secret header value. This token is automatically sent with every test request so your backend can detect monitoring traffic and enable test mode (e.g. Stripe sandbox).",
804
+ readOnlyHint: true,
805
+ },
806
+ async () => {
807
+ try {
808
+ const data = await apiGet("/api/org/secret");
809
+ return { content: [{ type: "text", text: `Your X-AskQA-Secret header value: ${data.askqa_secret}\n\nThis is sent automatically with every test request to your target domain. Configure your backend to check for this header to enable test mode.` }] };
810
+ } catch (err) {
811
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
812
+ }
813
+ }
814
+ );
815
+
816
+ server.registerTool(
817
+ "rotate_org_secret",
818
+ {
819
+ description: "Generate a new X-AskQA-Secret header value. The old value will stop working immediately — update your backend configuration.",
820
+ destructiveHint: true,
821
+ },
822
+ async () => {
823
+ try {
824
+ const data = await apiPost("/api/org/secret/rotate", {});
825
+ return { content: [{ type: "text", text: `New X-AskQA-Secret header value: ${data.askqa_secret}\n\nUpdate your backend configuration to use this new value. The old value no longer works.` }] };
826
+ } catch (err) {
827
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
828
+ }
829
+ }
830
+ );
831
+
762
832
  const transport = new StdioServerTransport();
763
833
  await server.connect(transport);
764
834
  console.error("AskQA MCP server running on stdio");
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: setup-mcp
3
+ description: Configure AskQA MCP server with API key
4
+ ---
5
+
6
+ # Set Up AskQA MCP Server
7
+
8
+ Use this skill to automatically configure the AskQA MCP server in your Claude settings.
9
+
10
+ ## Step 1: Get API Key
11
+
12
+ Ask the user if they already have an AskQA API key. If not, direct them to:
13
+ - Sign up at https://askqa.ai
14
+ - Get their API key from https://askqa.ai/account
15
+
16
+ Once they have the key, ask them to provide it.
17
+
18
+ ## Step 2: Detect Environment
19
+
20
+ Check if the user is in a git repository by looking for a `.git` directory in the current working directory or parent directories.
21
+
22
+ Use the Bash tool:
23
+ ```bash
24
+ git rev-parse --git-dir 2>/dev/null
25
+ ```
26
+
27
+ If this succeeds, we're in a git repo. If it fails, we're in Desktop/global mode.
28
+
29
+ ## Step 3: Determine Config Location
30
+
31
+ **If in a git repository:**
32
+ - Config location: `.mcp.json` in the project root (project-specific)
33
+ - This configuration will only apply when working in this repository
34
+
35
+ **If not in a git repository (Desktop mode):**
36
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
37
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
38
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
39
+ - This configuration will apply globally for the user
40
+
41
+ Tell the user which location will be used.
42
+
43
+ ## Step 4: Read Existing Config
44
+
45
+ Use the Read tool to check if the config file exists and read its contents.
46
+
47
+ If the file doesn't exist, start with an empty config:
48
+ ```json
49
+ {
50
+ "mcpServers": {}
51
+ }
52
+ ```
53
+
54
+ If the file exists, parse it as JSON.
55
+
56
+ ## Step 5: Update Config
57
+
58
+ Add or update the `askqa` MCP server entry:
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "askqa": {
64
+ "command": "node",
65
+ "args": ["${CLAUDE_PLUGIN_ROOT}/askqa/server.js"],
66
+ "env": {
67
+ "AUTOQA_API_URL": "https://api.askqa.ai",
68
+ "AUTOQA_API_KEY": "aq_..."
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ **Important notes:**
76
+ - Use `${CLAUDE_PLUGIN_ROOT}` variable - it resolves to the plugin installation directory
77
+ - Replace `"aq_..."` with the actual API key the user provided
78
+ - Preserve any other MCP servers already configured
79
+ - If `askqa` already exists, update it with the new API key
80
+
81
+ ## Step 6: Write Config
82
+
83
+ Use the Write tool to save the updated config back to the file.
84
+
85
+ **For project config (`.mcp.json`):**
86
+ - Write the config file to the project root
87
+
88
+ **For Desktop config:**
89
+ - Write directly to the Desktop config file location
90
+
91
+ ## Step 7: Confirm
92
+
93
+ After writing the config, confirm with the user:
94
+
95
+ **If project config:**
96
+ ```
97
+ ✓ AskQA MCP server configured in .mcp.json
98
+
99
+ The MCP server is now available in this repository. Restart Claude Code to load it.
100
+
101
+ Try these commands:
102
+ - list_tests - List your saved tests
103
+ - create_test - Create a new test
104
+ ```
105
+
106
+ **If Desktop config:**
107
+ ```
108
+ ✓ AskQA MCP server configured globally
109
+
110
+ The MCP server is now available in Claude Desktop. Restart Claude Desktop to load it.
111
+
112
+ You can now use AskQA tools in any conversation.
113
+ ```
114
+
115
+ ## Error Handling
116
+
117
+ If any step fails:
118
+ - **Permission error**: Tell user they may need to run with appropriate permissions
119
+ - **Invalid API key format**: Check that it starts with `aq_`
120
+ - **Invalid JSON**: If existing config is malformed, warn user and ask if they want to replace it
121
+
122
+ ## Additional Tips
123
+
124
+ After successful setup, remind the user:
125
+ - They can verify the setup by restarting and running `list_tests`
126
+ - They can update the API key anytime by running this skill again
127
+ - Project configs (`.mcp.json`) can be committed to git for team sharing (but shouldn't include the API key - use environment variables instead for team setups)
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: setup-slack
3
+ description: Configure Slack notifications for AskQA test failures
4
+ ---
5
+
1
6
  # Set Up Slack Notifications
2
7
 
3
8
  Use this skill to configure Slack notifications for AskQA test failures. When a scheduled test fails, AskQA will post a message to your Slack channel with the test name, failed steps, and a link to the full results.
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: shopify-add-to-cart
3
+ description: Set up automated monitoring for a Shopify store's add-to-cart flow
4
+ ---
5
+
1
6
  # Monitor Shopify Add to Cart
2
7
 
3
8
  Use this skill to set up automated monitoring for a Shopify store's add-to-cart flow. When the button breaks — due to an app update, theme change, or platform rollout — AskQA catches it within minutes and sends an alert with a screenshot.