qiksy-mcp 1.1.0 → 1.2.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.
package/AGENT-GUIDE.md ADDED
@@ -0,0 +1,111 @@
1
+ # Qiksy bridge — guide for a coding agent
2
+
3
+ Drop this file into a project (`.claude/qiksy-bridge.md`, `AGENTS.md`, or wherever your
4
+ assistant reads project notes) and the assistant knows what the bridge is, what it can
5
+ do, and — just as important — what it deliberately cannot.
6
+
7
+ Qiksy connects an agent to **the browser the human already has open**: their real
8
+ profile, their logged-in sessions, the tab they are looking at. It never launches a
9
+ browser, never switches window or tab focus, and never steals the pointer. Work happens
10
+ inside the existing (even backgrounded) tab.
11
+
12
+ ---
13
+
14
+ ## Setup, once
15
+
16
+ 1. **Install the extension** — [Qiksy on the Chrome Web Store](https://chromewebstore.google.com/detail/jcnbcbpndahhblhobhjjkhafmbkleoem).
17
+ 2. **Turn the bridge on.** Either the extension popup → *Agent bridge · MCP* → On →
18
+ *Generate* a token, or — from a Qiksy web app — press **Connect this browser** and
19
+ confirm on the page the extension opens. Both end the same way: a port (7333 by
20
+ default) and a shared token.
21
+ 3. **Point the agent at it.** Same token, same port:
22
+
23
+ ```bash
24
+ claude mcp add --transport stdio qiksy \
25
+ --env QIKSY_MCP_TOKEN=YOUR_TOKEN \
26
+ -- npx qiksy-mcp --port 7333
27
+ ```
28
+
29
+ ```json
30
+ { "mcpServers": { "qiksy": {
31
+ "command": "npx",
32
+ "args": ["qiksy-mcp", "--port", "7333"],
33
+ "env": { "QIKSY_MCP_TOKEN": "YOUR_TOKEN" }
34
+ } } }
35
+ ```
36
+
37
+ 4. **Check it.** `qa_status` should answer with the current page. "Qiksy extension is not
38
+ connected" means the popup switch is off or the token/port differ — it is not a
39
+ reason to reach for another browser driver.
40
+
41
+ ---
42
+
43
+ ## The tools
44
+
45
+ **Read-only — always available.**
46
+
47
+ | Tool | What it gives you |
48
+ |---|---|
49
+ | `qa_tabs` | Every tab the bridge can see, with `tabId`, URL, title, and which are isolated logins. Start here when several tabs are open. |
50
+ | `qa_status` | Health of one tab: URL, counts of errors, warnings, forms, failed requests. The cheap "did I break something". |
51
+ | `qa_snapshot` | The whole page as an **accessibility tree** — every control, landmark and heading with a role, a name, its state, and a stable `ref` like `[ref=e12]`. This is how you SEE the page. |
52
+ | `qa_findings` | Console errors, failed requests, a11y problems Qiksy has collected, filterable by severity. |
53
+ | `qa_export` | The `qa-export/v1` bundle: findings with selectors, failed requests with server bodies, detected form structure, repro steps. |
54
+
55
+ **Qiksy's own surfaces — never touches the page under test.**
56
+
57
+ `qa_open_panel` · `qa_run_audit` · `qa_tour` · `qa_report` · `qa_spotlight`
58
+ (`qa_spotlight` highlights an element so the human can look at the same thing you are.)
59
+
60
+ **Agent control — acts on the page. Requires Qiksy Pro AND a one-time "Agent control"
61
+ consent in the popup.** A refusal there is the human's to clear; report it in one
62
+ sentence rather than working around it.
63
+
64
+ `qa_click` · `qa_type` · `qa_type_many` · `qa_press` · `qa_wait_ready` ·
65
+ `qa_navigate` · `qa_open_isolated` · `qa_close_tab`
66
+
67
+ ---
68
+
69
+ ## How to drive it well
70
+
71
+ - **Target by `ref` from a fresh snapshot.** A `ref` is unique where a CSS selector is
72
+ ambiguous. After anything that navigates or re-renders, call `qa_wait_ready` and take
73
+ a **new** snapshot — old refs go stale.
74
+ - **Address a specific tab** with `tabId` from `qa_tabs`; omit it to act on the active
75
+ one. Tabs from `qa_open_isolated` are separate logins on the same site — that is how
76
+ you drive several accounts at once.
77
+ - **After a submit**, `qa_wait_ready` before the next call: the old document (and Qiksy
78
+ inside it) is gone, and the next call would land on a page still being replaced.
79
+ - **Autocomplete fields need a beat.** Suggestion lists arrive asynchronously (~0.4s on
80
+ travel sites). Type, wait, snapshot, then click the option you want — do not assume
81
+ the first one committed.
82
+
83
+ ## What it cannot do — say so, don't substitute
84
+
85
+ - **No screenshots.** `qa_snapshot` is a tree, not pixels. Judgements about spacing,
86
+ colour or alignment cannot be made from here.
87
+ - **No arbitrary JavaScript.** No `evaluate`, by design (a Web Store requirement). Which
88
+ means **no reading or writing localStorage** and **no measuring geometry**.
89
+ - **No `<iframe>` contents.** Payment forms (Stripe, 3-D Secure) live in frames the
90
+ content script does not enter. Fill up to them, not inside them.
91
+ - **No captcha.** Stop and hand it back.
92
+ - **Background tabs are throttled by Chrome.** Native inputs fill fine in the
93
+ background; custom popovers (Radix/MUI selects, date pickers) only render in the
94
+ foreground tab.
95
+ - **Navigation stays on the site under test.** `qa_navigate` refuses a different site
96
+ (`out-of-scope`) — the human opens that tab, then the agent works in it.
97
+
98
+ When a task genuinely needs pixels, eval or a different site, name the missing
99
+ capability out loud instead of quietly switching to another tool.
100
+
101
+ ## Several agent windows, one browser
102
+
103
+ Each MCP client starts its own copy of the server, but only one can own the loopback
104
+ port: whoever binds it is the hub and holds the extension socket, the rest relay
105
+ through it automatically. You do not need to know which one you are. If the owning
106
+ window closes, the others take the port over within a second or two — a call that fails
107
+ that way is worth one retry.
108
+
109
+ If the port is held by something that cannot share it (an older bridge, an unrelated
110
+ process), the tools say so; the fix is that process or a different `--port`, not the
111
+ popup.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "qiksy-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP bridge for the Qiksy browser extension — expose live QA findings, forms, network and session to any MCP-capable coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "qiksy-mcp": "server.mjs"
8
8
  },
9
- "files": ["server.mjs", "README.md"],
9
+ "files": ["server.mjs", "README.md", "AGENT-GUIDE.md"],
10
10
  "engines": {
11
11
  "node": ">=18"
12
12
  },
package/server.mjs CHANGED
@@ -443,7 +443,44 @@ function callHub(tool, args, timeoutMs) {
443
443
  }
444
444
 
445
445
  // ── MCP server + tools (read-only) ─────────────────────────────────────────
446
- const asText = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
446
+ /* The humour used to live only in the stderr log, where nobody looks (owner ask
447
+ * 2026-07-26: "хотелось бы рожиц"). A client draws the tool row as `Qiksy [qa_tabs]` —
448
+ * the bracketed part is the tool NAME and MCP names are `[a-zA-Z0-9_-]` only, so a face
449
+ * cannot go there. The result CAN: it rides as its own leading content block, so the
450
+ * payload block below it stays byte-identical for anything that parses it.
451
+ * Failures stay face-free on purpose — a joke on top of an error competes with the one
452
+ * sentence the human needs to act on. */
453
+ /* The trailing newline is not cosmetic slop: a client concatenates the content blocks with
454
+ * nothing between them, so without it the payload starts on the same line as the face
455
+ * (`…undocumented features{` — seen live 2026-07-26). */
456
+ const decor = () => (QUIPS_ON ? [{ type: 'text', text: `qx ${face()} ${quip()}\n` }] : []);
457
+ /* A LOADER for the calls that take a while (owner ask 2026-07-26). An audit, a report or a
458
+ * snapshot of a heavy page leaves the tool row sitting dead for seconds, and a human cannot
459
+ * tell "working" from "hung". MCP has progress notifications, so we send frames while the
460
+ * extension is busy; a client that ignores them loses nothing. Only when the caller passed a
461
+ * progressToken — otherwise there is nobody to notify. */
462
+ const BAR = ['[▰▱▱▱▱]', '[▰▰▱▱▱]', '[▰▰▰▱▱]', '[▰▰▰▰▱]', '[▰▰▰▰▰]'];
463
+ function loader(extra, label) {
464
+ const token = extra?._meta?.progressToken;
465
+ if (!token || !QUIPS_ON || typeof extra?.sendNotification !== 'function') return { stop() {} };
466
+ let i = 0;
467
+ const beat = () => {
468
+ extra
469
+ .sendNotification({
470
+ method: 'notifications/progress',
471
+ params: { progressToken: token, progress: (i % BAR.length) + 1, total: BAR.length, message: `qx ${BAR[i % BAR.length]} ${label}` },
472
+ })
473
+ .catch(() => {
474
+ /* the client hung up, or does not implement progress */
475
+ });
476
+ i++;
477
+ };
478
+ beat();
479
+ const timer = setInterval(beat, 700);
480
+ return { stop: () => clearInterval(timer) };
481
+ }
482
+
483
+ const asText = (v) => ({ content: [...decor(), { type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
447
484
  const asError = (e) => ({ isError: true, content: [{ type: 'text', text: e instanceof Error ? e.message : String(e) }] });
448
485
 
449
486
  /* The operating manual, handed to the agent at `initialize` — not left in a README
@@ -478,6 +515,10 @@ detect this and relay through it automatically. You do not need to know which on
478
515
  the user changes nothing in the popup. If the owning window closes, the rest take the port over
479
516
  on their own within a second or two, so a call that fails that way is worth one retry.
480
517
 
518
+ Every successful result starts with a short decorative line — \`qx <face> <quip>\` — and the
519
+ actual payload is the block after it. It is cosmetic: ignore it when reading data, and never
520
+ copy it into a finding, a report or a commit message. Set QIKSY_MCP_QUIPS=0 to turn it off.
521
+
481
522
  Two failures are NOT yours to fix, and each names its own cause: "extension is not connected"
482
523
  means the MCP bridge toggle in the popup is off (or its token/port differ) — only the user can
483
524
  turn it on. "Port … is held by a process that is not a Qiksy bridge" means something else owns
@@ -545,11 +586,14 @@ server.registerTool(
545
586
  'Full qa-export/v1 bundle for a page: findings (with selectors), failed requests (with server error bodies), detected form structure, recorded repro steps, env, and isolated-login name. Read this, then fix the underlying issues. Pass tabId (from qa_tabs) to target a specific tab / isolated login.',
546
587
  inputSchema: { tabId: z.number().int().optional().describe('Target tab (from qa_tabs); omit for the active tab') },
547
588
  },
548
- async ({ tabId }) => {
589
+ async ({ tabId }, extra) => {
590
+ const bar = loader(extra, 'building the QA bundle');
549
591
  try {
550
592
  return asText(await callExtension('qa_export', { tabId }, 30_000));
551
593
  } catch (e) {
552
594
  return asError(e);
595
+ } finally {
596
+ bar.stop();
553
597
  }
554
598
  },
555
599
  );
@@ -586,11 +630,14 @@ server.registerTool(
586
630
  'Run Qiksy\'s a11y/markup audit on the current DOM (unlabeled controls, missing names, WCAG AA contrast, broken images, duplicate ids, positive tabindex, …) and return the findings. Also refreshes the panel. Static, non-destructive — reads the DOM only.',
587
631
  inputSchema: { tabId: tabIdArg },
588
632
  },
589
- async ({ tabId }) => {
633
+ async ({ tabId }, extra) => {
634
+ const bar = loader(extra, 'auditing the page');
590
635
  try {
591
636
  return asText(await callExtension('qa_run_audit', { tabId }, 30_000));
592
637
  } catch (e) {
593
638
  return asError(e);
639
+ } finally {
640
+ bar.stop();
594
641
  }
595
642
  },
596
643
  );
@@ -606,11 +653,14 @@ server.registerTool(
606
653
  'Built in the live, already-authenticated page in ~milliseconds — no browser launch, no CDP. Re-run it after the page changes to refresh the refs (a ref goes stale when its element is re-rendered).',
607
654
  inputSchema: { tabId: tabIdArg },
608
655
  },
609
- async ({ tabId }) => {
656
+ async ({ tabId }, extra) => {
657
+ const bar = loader(extra, 'reading the whole page');
610
658
  try {
611
659
  return asText(await callExtension('qa_snapshot', { tabId }, 30_000));
612
660
  } catch (e) {
613
661
  return asError(e);
662
+ } finally {
663
+ bar.stop();
614
664
  }
615
665
  },
616
666
  );