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 +111 -0
- package/package.json +2 -2
- package/server.mjs +54 -4
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.
|
|
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
|
-
|
|
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
|
);
|