@askqa/mcp 1.0.8 → 1.1.1

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.1",
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.1",
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,27 @@ 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' })"),
208
+ 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)"),
203
209
  },
204
210
  },
205
- async ({ name, url, template_id, params, code }) => {
211
+ async ({ name, url, template_id, params, code, secrets, headers, enable_test_mode }) => {
206
212
  try {
207
213
  const body = { name, url };
208
214
  if (template_id) body.template_id = template_id;
209
215
  if (params) body.params = params;
210
216
  if (code) body.code = code;
217
+ if (secrets) body.secrets = secrets;
218
+ if (headers) body.headers = headers;
219
+ if (enable_test_mode !== undefined) body.enable_test_mode = enable_test_mode;
211
220
  const test = await apiPost("/api/tests/create", body);
212
221
  return { content: [{ type: "text", text: JSON.stringify(test, null, 2) }] };
213
222
  } catch (err) {
@@ -220,6 +229,7 @@ server.registerTool(
220
229
  "screenshot_url",
221
230
  {
222
231
  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.",
232
+ readOnlyHint: true,
223
233
  inputSchema: {
224
234
  url: z.string().describe("The URL to screenshot (e.g. 'https://example.com')"),
225
235
  },
@@ -280,6 +290,7 @@ server.registerTool(
280
290
  "validate_test",
281
291
  {
282
292
  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.",
293
+ readOnlyHint: true,
283
294
  inputSchema: {
284
295
  code: z.string().describe("Custom Playwright test code. Must define an async function test({ page, step, log })."),
285
296
  url: z.string().describe("The target URL to test against (e.g. 'https://example.com')"),
@@ -349,6 +360,7 @@ server.registerTool(
349
360
  "list_tests",
350
361
  {
351
362
  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.",
363
+ readOnlyHint: true,
352
364
  },
353
365
  async () => {
354
366
  try {
@@ -364,6 +376,7 @@ server.registerTool(
364
376
  "get_test",
365
377
  {
366
378
  description: "Get full details of a test by ID, including code for custom tests. Use list_tests to find the test ID first.",
379
+ readOnlyHint: true,
367
380
  inputSchema: {
368
381
  test_id: z.coerce.number().describe("The test ID (from list_tests or create_test)"),
369
382
  },
@@ -382,6 +395,7 @@ server.registerTool(
382
395
  "update_test",
383
396
  {
384
397
  description: "Update an existing test's name, URL, code, or other properties. Only provided fields are changed.",
398
+ destructiveHint: true,
385
399
  inputSchema: {
386
400
  test_id: z.coerce.number().describe("The test ID to update (from list_tests or get_test)"),
387
401
  name: z.string().optional().describe("New test name"),
@@ -389,9 +403,12 @@ server.registerTool(
389
403
  code: z.string().optional().describe("Updated custom Playwright test code"),
390
404
  template_id: z.string().optional().describe("Updated template ID"),
391
405
  params: z.record(z.string()).optional().describe("Updated template parameters"),
406
+ secrets: z.record(z.string()).nullable().optional().describe("Updated secrets (pass null to clear)"),
407
+ headers: z.record(z.string()).nullable().optional().describe("Updated HTTP headers (pass null to clear)"),
408
+ 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)"),
392
409
  },
393
410
  },
394
- async ({ test_id, name, url, code, template_id, params }) => {
411
+ async ({ test_id, name, url, code, template_id, params, secrets, headers, enable_test_mode }) => {
395
412
  try {
396
413
  const body = {};
397
414
  if (name !== undefined) body.name = name;
@@ -399,6 +416,9 @@ server.registerTool(
399
416
  if (code !== undefined) body.code = code;
400
417
  if (template_id !== undefined) body.template_id = template_id;
401
418
  if (params !== undefined) body.params = params;
419
+ if (secrets !== undefined) body.secrets = secrets;
420
+ if (headers !== undefined) body.headers = headers;
421
+ if (enable_test_mode !== undefined) body.enable_test_mode = enable_test_mode;
402
422
  const test = await apiPatch(`/api/tests/${test_id}`, body);
403
423
  return { content: [{ type: "text", text: JSON.stringify(test, null, 2) }] };
404
424
  } catch (err) {
@@ -411,6 +431,7 @@ server.registerTool(
411
431
  "delete_test",
412
432
  {
413
433
  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.",
434
+ destructiveHint: true,
414
435
  inputSchema: {
415
436
  test_id: z.coerce.number().describe("The test ID to delete (from list_tests)"),
416
437
  confirm: z.boolean().optional().describe("Set to true to actually delete. Omit or false to preview what will be deleted."),
@@ -461,16 +482,18 @@ server.registerTool(
461
482
  "run_test",
462
483
  {
463
484
  description: "Run a saved test by ID. Waits for the test to finish and returns full results with step details.",
485
+ destructiveHint: true,
464
486
  inputSchema: {
465
487
  test_id: z.coerce.number().describe("The test ID to run (from create_test or list_tests)"),
466
488
  },
467
489
  },
468
490
  async ({ test_id }) => {
469
491
  try {
492
+ const test = await apiGet(`/api/tests/${test_id}`);
470
493
  const { test_run_id } = await apiPost(`/api/tests/${test_id}/run`, {});
471
494
  console.error(`Started test run ${test_run_id} for test ${test_id}`);
472
495
  const testRun = await pollTestRun(test_run_id);
473
- const text = buildTestRunText(testRun);
496
+ const text = buildTestRunText(testRun, test.name);
474
497
  return { content: [{ type: "text", text }] };
475
498
  } catch (err) {
476
499
  return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
@@ -482,6 +505,7 @@ server.registerTool(
482
505
  "get_test_screenshots",
483
506
  {
484
507
  description: "Get screenshots from a test run. Returns only images. Use after run_test or get_test_results to view screenshots.",
508
+ readOnlyHint: true,
485
509
  inputSchema: {
486
510
  test_run_id: z.coerce.number().describe("The test run ID (from run_test or get_test_results)"),
487
511
  },
@@ -507,6 +531,7 @@ server.registerTool(
507
531
  "schedule_test",
508
532
  {
509
533
  description: "Create a recurring schedule for a saved test. The test will run automatically at the specified interval.",
534
+ destructiveHint: true,
510
535
  inputSchema: {
511
536
  test_id: z.coerce.number().describe("The test ID to schedule (from create_test or list_tests)"),
512
537
  interval: z.enum(["every_minute", "hourly", "every_6_hours", "every_12_hours", "daily", "weekly"])
@@ -534,6 +559,7 @@ server.registerTool(
534
559
  "list_schedules",
535
560
  {
536
561
  description: "List all test schedules for the current organization, including last run status and next run time.",
562
+ readOnlyHint: true,
537
563
  },
538
564
  async () => {
539
565
  try {
@@ -563,6 +589,7 @@ server.registerTool(
563
589
  "update_schedule",
564
590
  {
565
591
  description: "Pause or resume a test schedule.",
592
+ destructiveHint: true,
566
593
  inputSchema: {
567
594
  schedule_id: z.coerce.number().describe("The schedule ID (from list_schedules)"),
568
595
  enabled: z.boolean().describe("true to resume, false to pause"),
@@ -588,6 +615,7 @@ server.registerTool(
588
615
  "delete_schedule",
589
616
  {
590
617
  description: "Permanently delete a test schedule. Historical run results are preserved.",
618
+ destructiveHint: true,
591
619
  inputSchema: {
592
620
  schedule_id: z.coerce.number().describe("The schedule ID to delete (from list_schedules)"),
593
621
  },
@@ -606,6 +634,7 @@ server.registerTool(
606
634
  "get_test_results",
607
635
  {
608
636
  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.",
637
+ readOnlyHint: true,
609
638
  inputSchema: {
610
639
  test_id: z.coerce.number().optional().describe("Filter by test ID (optional — omit to see all runs)"),
611
640
  limit: z.coerce.number().optional().describe("Max results to return (default: 10)"),
@@ -620,9 +649,18 @@ server.registerTool(
620
649
  if (!data.test_runs.length) {
621
650
  return { content: [{ type: "text", text: "No test runs found." }] };
622
651
  }
652
+
653
+ // Build test name map
654
+ const testIds = [...new Set(data.test_runs.map((r) => r.test_id))];
655
+ const nameMap = {};
656
+ const testsData = await apiGet("/api/tests/list");
657
+ for (const t of testsData.tests) {
658
+ if (testIds.includes(t.id)) nameMap[t.id] = t.name;
659
+ }
660
+
623
661
  const lines = [];
624
662
  for (const run of data.test_runs) {
625
- lines.push(buildTestRunText(run));
663
+ lines.push(buildTestRunText(run, nameMap[run.test_id]));
626
664
  lines.push("");
627
665
  }
628
666
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -638,6 +676,7 @@ server.registerTool(
638
676
  "add_notification_channel",
639
677
  {
640
678
  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.",
679
+ destructiveHint: true,
641
680
  inputSchema: {
642
681
  channel_type: z.enum(["email", "telegram", "slack"]).describe("The notification channel type"),
643
682
  email_address: z.string().optional().describe("Email address for email channels (required when channel_type is email)"),
@@ -696,6 +735,7 @@ server.registerTool(
696
735
  "list_notification_channels",
697
736
  {
698
737
  description: "List all notification channels configured for the current organization.",
738
+ readOnlyHint: true,
699
739
  },
700
740
  async () => {
701
741
  try {
@@ -727,6 +767,7 @@ server.registerTool(
727
767
  "remove_notification_channel",
728
768
  {
729
769
  description: "Remove a notification channel. Use list_notification_channels to find the channel ID.",
770
+ destructiveHint: true,
730
771
  inputSchema: {
731
772
  channel_id: z.coerce.number().describe("The channel ID to remove (from list_notification_channels)"),
732
773
  },
@@ -745,6 +786,7 @@ server.registerTool(
745
786
  "test_notification_channel",
746
787
  {
747
788
  description: "Send a test notification to verify a channel is working correctly.",
789
+ destructiveHint: true,
748
790
  inputSchema: {
749
791
  channel_id: z.coerce.number().describe("The channel ID to test (from list_notification_channels)"),
750
792
  },
@@ -759,6 +801,38 @@ server.registerTool(
759
801
  }
760
802
  );
761
803
 
804
+ server.registerTool(
805
+ "get_org_secret",
806
+ {
807
+ 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).",
808
+ readOnlyHint: true,
809
+ },
810
+ async () => {
811
+ try {
812
+ const data = await apiGet("/api/org/secret");
813
+ 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.` }] };
814
+ } catch (err) {
815
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
816
+ }
817
+ }
818
+ );
819
+
820
+ server.registerTool(
821
+ "rotate_org_secret",
822
+ {
823
+ description: "Generate a new X-AskQA-Secret header value. The old value will stop working immediately — update your backend configuration.",
824
+ destructiveHint: true,
825
+ },
826
+ async () => {
827
+ try {
828
+ const data = await apiPost("/api/org/secret/rotate", {});
829
+ 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.` }] };
830
+ } catch (err) {
831
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
832
+ }
833
+ }
834
+ );
835
+
762
836
  const transport = new StdioServerTransport();
763
837
  await server.connect(transport);
764
838
  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.