scuttle-browser 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Axel Krantz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # Scuttle
2
+
3
+ Browser bridge for LLMs. Lets AI agents browse the web by converting pages into compact, annotated accessibility trees — no screenshots or raw HTML required.
4
+
5
+ Scuttle runs as an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server. Any MCP-compatible client (Claude Code, Claude Desktop, Cursor, etc.) can use it to navigate, read, and interact with web pages.
6
+
7
+ ## How it works
8
+
9
+ Instead of feeding raw HTML (too many tokens, too much noise) or screenshots (unreliable for spatial reasoning), Scuttle extracts the browser's **accessibility tree** — the same semantic structure used by screen readers. It then:
10
+
11
+ 1. **Prunes** invisible and decorative nodes
12
+ 2. **Assigns numeric IDs** to every interactable element (`[1] button "Submit"`, `[2] textbox "Search"`)
13
+ 3. **Labels unnamed elements** using a fallback chain: visible text → aria-label → placeholder → nearby heading → positional description
14
+ 4. **Accepts actions** by element ID — `click(1)`, `type(2, "hello")`
15
+
16
+ The result is a compact, token-efficient representation that LLMs can reason about and act on.
17
+
18
+ ```
19
+ Page: Hacker News
20
+ URL: https://news.ycombinator.com/
21
+ Interactable elements: 227
22
+ ────────────────────────────────────────────────────────────
23
+ heading "Hacker News"
24
+ [1] link "Hacker News"
25
+ [2] link "new"
26
+ [3] link "past"
27
+ ...
28
+ row
29
+ cell "1."
30
+ [12] link "Show HN: Something cool"
31
+ "Show HN: Something cool"
32
+ cell "142 points by user 3 hours ago"
33
+ [14] link "user"
34
+ [15] link "85 comments"
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ # Clone and install
41
+ git clone https://github.com/axelkrantz/scuttle.git
42
+ cd scuttle
43
+ npm install # installs dependencies + downloads Chromium
44
+ npm run build
45
+ ```
46
+
47
+ Or install globally:
48
+
49
+ ```bash
50
+ npm install -g scuttle-browser
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ ### Claude Code
56
+
57
+ Add to your project's `.mcp.json`:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "scuttle": {
63
+ "command": "node",
64
+ "args": ["/path/to/scuttle/dist/index.js"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Claude Desktop
71
+
72
+ Add to `claude_desktop_config.json`:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "scuttle": {
78
+ "command": "node",
79
+ "args": ["/path/to/scuttle/dist/index.js"]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### With npx (no clone needed)
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "scuttle": {
91
+ "command": "npx",
92
+ "args": ["-y", "scuttle-browser"]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Environment variables
99
+
100
+ | Variable | Default | Description |
101
+ |----------|---------|-------------|
102
+ | `SCUTTLE_HEADLESS` | `true` | Set to `false` to show the browser window |
103
+ | `SCUTTLE_VIEWPORT_WIDTH` | `1280` | Browser viewport width in pixels |
104
+ | `SCUTTLE_VIEWPORT_HEIGHT` | `720` | Browser viewport height in pixels |
105
+ | `SCUTTLE_TIMEOUT` | `30000` | Navigation timeout in milliseconds |
106
+ | `SCUTTLE_SETTLE_TIME` | `500` | Post-navigation settle time in ms (for SPAs that hydrate after DOM ready) |
107
+
108
+ Example with visible browser:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "scuttle": {
114
+ "command": "node",
115
+ "args": ["/path/to/scuttle/dist/index.js"],
116
+ "env": {
117
+ "SCUTTLE_HEADLESS": "false"
118
+ }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ ## Tools
125
+
126
+ ### `navigate`
127
+
128
+ Go to a URL.
129
+
130
+ ```
131
+ navigate({ url: "https://example.com" })
132
+ → "Navigated to: Example Domain\nURL: https://example.com/"
133
+ ```
134
+
135
+ ### `observe`
136
+
137
+ Get the current page state as an annotated accessibility tree. Every interactable element gets a numeric ID in brackets.
138
+
139
+ ```
140
+ observe()
141
+ → Page: Example Domain
142
+ URL: https://example.com/
143
+ Interactable elements: 1
144
+ ────────────────────────────────────────────────────────────
145
+ heading "Example Domain"
146
+ "This domain is for use in illustrative examples..."
147
+ [1] link "More information..."
148
+ ```
149
+
150
+ ### `act`
151
+
152
+ Perform actions on the page using element IDs from `observe`.
153
+
154
+ | Action | Parameters | Description |
155
+ |--------|-----------|-------------|
156
+ | `click` | `id` | Click an element |
157
+ | `type` | `id`, `text` | Clear and type text into an input |
158
+ | `select` | `id`, `text` | Select a dropdown option |
159
+ | `hover` | `id` | Hover over an element |
160
+ | `scroll` | `direction` (`"up"` or `"down"`) | Scroll the page |
161
+ | `key` | `key` (e.g., `"Enter"`, `"Tab"`) | Press a keyboard key |
162
+ | `wait` | `ms` (max 10000) | Wait for dynamic content |
163
+ | `back` | — | Browser back |
164
+ | `forward` | — | Browser forward |
165
+
166
+ ```
167
+ act({ action: "click", id: 1 })
168
+ → "Clicked link 'More information...'"
169
+
170
+ act({ action: "type", id: 5, text: "search query" })
171
+ → "Typed 'search query' into textbox 'Search'"
172
+
173
+ act({ action: "scroll", direction: "down" })
174
+ → "Scrolled down"
175
+ ```
176
+
177
+ ### `screenshot`
178
+
179
+ Take a PNG screenshot of the current viewport. Returns a base64-encoded image. Useful when the accessibility tree alone isn't enough to understand the layout.
180
+
181
+ ### `get_text`
182
+
183
+ Extract all visible text from the page. Best for reading articles, docs, or any text-heavy content without the structural overhead of `observe`.
184
+
185
+ ## Usage pattern
186
+
187
+ The typical agent loop is:
188
+
189
+ ```
190
+ 1. navigate(url) → go to a page
191
+ 2. observe() → read the page state
192
+ 3. act(...) → interact with an element
193
+ 4. observe() → read the updated state
194
+ 5. repeat 3-4 → until the task is done
195
+ ```
196
+
197
+ ## Architecture
198
+
199
+ ```
200
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
201
+ │ LLM/Agent │◄───►│ Scuttle │◄───►│ Playwright │
202
+ │ (MCP client)│ │ (MCP server) │ │ (Chromium) │
203
+ └─────────────┘ └──────────────┘ └─────────────┘
204
+
205
+ ┌─────┴─────┐
206
+ │ │
207
+ Accessibility Action
208
+ Extractor Executor
209
+ ```
210
+
211
+ - **Browser Manager** — manages Playwright browser lifecycle, navigation, and screenshots
212
+ - **Accessibility Extractor** — snapshots the accessibility tree via CDP, prunes it, assigns IDs, and handles element labeling
213
+ - **Action Executor** — maps element IDs to Playwright locators, executes actions with fallbacks
214
+
215
+ ### Element labeling strategy
216
+
217
+ When elements lack good names (looking at you, `<div class="css-1a2b3c">`), Scuttle applies a fallback chain:
218
+
219
+ 1. **Visible text** — `[5] button "Add to cart"`
220
+ 2. **Aria attributes** — `[6] textbox aria-label="Email address"`
221
+ 3. **Value** — `[7] combobox [value: "United States"]`
222
+ 4. **Context** — `[8] button (under "Account Settings")`
223
+ 5. **Role fallback** — `[9] [unnamed button]`
224
+
225
+ ## Development
226
+
227
+ ```bash
228
+ npm run dev # watch mode — recompiles on changes
229
+ npm run build # one-time build
230
+ npm start # run the MCP server
231
+ ```
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,34 @@
1
+ import { Page, Locator } from "playwright";
2
+ export interface AnnotatedNode {
3
+ id: number | null;
4
+ role: string;
5
+ name: string;
6
+ description: string;
7
+ value: string;
8
+ children: AnnotatedNode[];
9
+ }
10
+ export interface ElementRef {
11
+ id: number;
12
+ role: string;
13
+ label: string;
14
+ locator: Locator;
15
+ }
16
+ export declare class AccessibilityExtractor {
17
+ private elementMap;
18
+ private nextId;
19
+ private cdp;
20
+ private cdpPage;
21
+ reset(): void;
22
+ getElementMap(): Map<number, ElementRef>;
23
+ private getCDP;
24
+ extract(page: Page): Promise<{
25
+ tree: string;
26
+ elementCount: number;
27
+ }>;
28
+ private cdpNodesToTree;
29
+ private processNode;
30
+ private processChildren;
31
+ private buildLabel;
32
+ private buildLocator;
33
+ private serialize;
34
+ }
@@ -0,0 +1,240 @@
1
+ const INTERACTABLE_ROLES = new Set([
2
+ "link",
3
+ "button",
4
+ "textbox",
5
+ "searchbox",
6
+ "combobox",
7
+ "listbox",
8
+ "option",
9
+ "checkbox",
10
+ "radio",
11
+ "switch",
12
+ "slider",
13
+ "spinbutton",
14
+ "tab",
15
+ "menuitem",
16
+ "menuitemcheckbox",
17
+ "menuitemradio",
18
+ "treeitem",
19
+ ]);
20
+ const SKIP_ROLES = new Set([
21
+ "none",
22
+ "presentation",
23
+ "generic",
24
+ "LineBreak",
25
+ "InlineTextBox",
26
+ ]);
27
+ const STRUCTURAL_ROLES = new Set([
28
+ "group",
29
+ "list",
30
+ "listitem",
31
+ "navigation",
32
+ "main",
33
+ "complementary",
34
+ "banner",
35
+ "contentinfo",
36
+ "region",
37
+ "form",
38
+ "article",
39
+ "section",
40
+ "toolbar",
41
+ "menu",
42
+ "menubar",
43
+ "tablist",
44
+ "tabpanel",
45
+ "tree",
46
+ "grid",
47
+ "row",
48
+ "rowgroup",
49
+ "cell",
50
+ "columnheader",
51
+ "rowheader",
52
+ ]);
53
+ export class AccessibilityExtractor {
54
+ elementMap = new Map();
55
+ nextId = 1;
56
+ cdp = null;
57
+ cdpPage = null;
58
+ reset() {
59
+ this.elementMap.clear();
60
+ this.nextId = 1;
61
+ }
62
+ getElementMap() {
63
+ return this.elementMap;
64
+ }
65
+ async getCDP(page) {
66
+ // Reuse CDP session for the same page to avoid handshake overhead
67
+ if (this.cdp && this.cdpPage === page) {
68
+ return this.cdp;
69
+ }
70
+ if (this.cdp) {
71
+ await this.cdp.detach().catch(() => { });
72
+ }
73
+ this.cdp = await page.context().newCDPSession(page);
74
+ this.cdpPage = page;
75
+ return this.cdp;
76
+ }
77
+ async extract(page) {
78
+ this.reset();
79
+ const cdp = await this.getCDP(page);
80
+ const { nodes } = await cdp.send("Accessibility.getFullAXTree");
81
+ if (!nodes || nodes.length === 0) {
82
+ return { tree: "[Empty page — no accessibility tree available]", elementCount: 0 };
83
+ }
84
+ const snapshot = this.cdpNodesToTree(nodes);
85
+ if (!snapshot) {
86
+ return { tree: "[Empty page — no accessibility tree available]", elementCount: 0 };
87
+ }
88
+ const annotated = this.processNode(snapshot, page, []);
89
+ const text = this.serialize(annotated, 0);
90
+ return { tree: text, elementCount: this.elementMap.size };
91
+ }
92
+ cdpNodesToTree(nodes) {
93
+ if (nodes.length === 0)
94
+ return null;
95
+ const nodeMap = new Map();
96
+ const childIds = new Map();
97
+ for (const node of nodes) {
98
+ const axNode = {
99
+ nodeId: node.nodeId,
100
+ role: node.role?.value || "unknown",
101
+ name: node.name?.value || "",
102
+ description: node.description?.value || "",
103
+ value: node.value?.value || "",
104
+ children: [],
105
+ };
106
+ nodeMap.set(node.nodeId, axNode);
107
+ if (node.childIds) {
108
+ childIds.set(node.nodeId, node.childIds);
109
+ }
110
+ }
111
+ // Link children
112
+ for (const [parentId, kids] of childIds) {
113
+ const parent = nodeMap.get(parentId);
114
+ if (parent) {
115
+ for (const childId of kids) {
116
+ const child = nodeMap.get(childId);
117
+ if (child)
118
+ parent.children.push(child);
119
+ }
120
+ }
121
+ }
122
+ // Root is the first node
123
+ return nodeMap.get(nodes[0].nodeId) || null;
124
+ }
125
+ processNode(node, page, ancestorNames) {
126
+ const role = node.role;
127
+ const name = node.name.trim();
128
+ if (SKIP_ROLES.has(role)) {
129
+ const children = this.processChildren(node.children || [], page, ancestorNames);
130
+ if (children.length === 1)
131
+ return children[0];
132
+ if (children.length === 0)
133
+ return null;
134
+ return { id: null, role: "group", name: "", description: "", value: "", children };
135
+ }
136
+ const isInteractable = INTERACTABLE_ROLES.has(role);
137
+ // Push/pop to avoid array spread per node
138
+ if (name)
139
+ ancestorNames.push(name);
140
+ const children = this.processChildren(node.children || [], page, ancestorNames);
141
+ if (name)
142
+ ancestorNames.pop();
143
+ if (!isInteractable && STRUCTURAL_ROLES.has(role) && !name) {
144
+ if (children.length === 0)
145
+ return null;
146
+ if (children.length === 1)
147
+ return children[0];
148
+ }
149
+ let id = null;
150
+ if (isInteractable) {
151
+ id = this.nextId++;
152
+ const label = this.buildLabel(node, role, ancestorNames);
153
+ const locator = this.buildLocator(page, node, role, name);
154
+ this.elementMap.set(id, { id, role, label, locator });
155
+ }
156
+ if (role === "text" && !name && children.length === 0) {
157
+ return null;
158
+ }
159
+ return {
160
+ id,
161
+ role,
162
+ name,
163
+ description: (node.description || "").trim(),
164
+ value: (node.value || "").trim(),
165
+ children,
166
+ };
167
+ }
168
+ processChildren(children, page, ancestorNames) {
169
+ const results = [];
170
+ for (const child of children) {
171
+ const processed = this.processNode(child, page, ancestorNames);
172
+ if (processed)
173
+ results.push(processed);
174
+ }
175
+ return results;
176
+ }
177
+ buildLabel(node, role, ancestorNames) {
178
+ const name = (node.name || "").trim();
179
+ const desc = (node.description || "").trim();
180
+ // Priority 1: visible name/text
181
+ if (name)
182
+ return name;
183
+ // Priority 2: description (often aria-label or title)
184
+ if (desc)
185
+ return desc;
186
+ // Priority 3: value (for inputs with pre-filled values)
187
+ const value = (node.value || "").trim();
188
+ if (value)
189
+ return `[value: ${value}]`;
190
+ // Priority 4: contextual — use nearest ancestor name
191
+ const nearestContext = ancestorNames[ancestorNames.length - 1];
192
+ if (nearestContext)
193
+ return `(under "${nearestContext}")`;
194
+ // Priority 5: positional/role fallback
195
+ return `[unnamed ${role}]`;
196
+ }
197
+ buildLocator(page, node, role, name) {
198
+ // Use Playwright's role-based locator for best reliability
199
+ if (name) {
200
+ return page.getByRole(role, { name, exact: false });
201
+ }
202
+ return page.getByRole(role);
203
+ }
204
+ serialize(node, depth) {
205
+ if (!node)
206
+ return "";
207
+ const indent = " ".repeat(depth);
208
+ const parts = [];
209
+ // Build the line for this node
210
+ let line = indent;
211
+ if (node.id !== null) {
212
+ line += `[${node.id}] `;
213
+ }
214
+ const showRole = node.id !== null || STRUCTURAL_ROLES.has(node.role) || node.role === "heading";
215
+ if (showRole) {
216
+ line += node.role;
217
+ }
218
+ if (node.name) {
219
+ line += showRole ? ` "${node.name}"` : `"${node.name}"`;
220
+ }
221
+ if (node.description && node.description !== node.name) {
222
+ line += ` — ${node.description}`;
223
+ }
224
+ if (node.value) {
225
+ line += ` [value: "${node.value}"]`;
226
+ }
227
+ const trimmed = line.trim();
228
+ if (trimmed) {
229
+ parts.push(line);
230
+ }
231
+ // Serialize children
232
+ for (const child of node.children) {
233
+ const childStr = this.serialize(child, depth + (trimmed ? 1 : 0));
234
+ if (childStr)
235
+ parts.push(childStr);
236
+ }
237
+ return parts.join("\n");
238
+ }
239
+ }
240
+ //# sourceMappingURL=accessibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility.js","sourceRoot":"","sources":["../src/accessibility.ts"],"names":[],"mappings":"AAkBA,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,MAAM;IACN,QAAQ;IACR,SAAS;IACT,WAAW;IACX,UAAU;IACV,SAAS;IACT,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,UAAU;IACV,kBAAkB;IAClB,eAAe;IACf,UAAU;CACX,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,MAAM;IACN,cAAc;IACd,SAAS;IACT,WAAW;IACX,eAAe;CAChB,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,OAAO;IACP,MAAM;IACN,UAAU;IACV,YAAY;IACZ,MAAM;IACN,eAAe;IACf,QAAQ;IACR,aAAa;IACb,QAAQ;IACR,MAAM;IACN,SAAS;IACT,SAAS;IACT,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;IACT,UAAU;IACV,MAAM;IACN,MAAM;IACN,KAAK;IACL,UAAU;IACV,MAAM;IACN,cAAc;IACd,WAAW;CACZ,CAAC,CAAC;AAUH,MAAM,OAAO,sBAAsB;IACzB,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC3C,MAAM,GAAG,CAAC,CAAC;IACX,GAAG,GAAsB,IAAI,CAAC;IAC9B,OAAO,GAAgB,IAAI,CAAC;IAEpC,KAAK;QACH,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,IAAU;QAC7B,kEAAkE;QAClE,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,GAAG,CAAC;QAClB,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,CAAC,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAU;QACtB,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAChE,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI,EAAE,gDAAgD,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,IAAI,EAAE,gDAAgD,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;IAC5D,CAAC;IAEO,cAAc,CAAC,KAAY;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0C,CAAC;QAClE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;QAE7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG;gBACb,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,SAAS;gBACnC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;gBAC5B,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE;gBAC1C,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE;gBAC9B,QAAQ,EAAE,EAAiB;aAC5B,CAAC;YACF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACjC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,gBAAgB;QAChB,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACrC,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,MAAM,OAAO,IAAI,IAAI,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBACnC,IAAI,KAAK;wBAAE,MAAM,CAAC,QAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QAED,yBAAyB;QACzB,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;IAC9C,CAAC;IAEO,WAAW,CACjB,IAAe,EACf,IAAU,EACV,aAAuB;QAEvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAE9B,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAChF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;QACrF,CAAC;QAED,MAAM,cAAc,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEpD,0CAA0C;QAC1C,IAAI,IAAI;YAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAChF,IAAI,IAAI;YAAE,aAAa,CAAC,GAAG,EAAE,CAAC;QAE9B,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QAED,IAAI,EAAE,GAAkB,IAAI,CAAC;QAC7B,IAAI,cAAc,EAAE,CAAC;YACnB,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YACzD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAC1D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO;YACL,EAAE;YACF,IAAI;YACJ,IAAI;YACJ,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAC5C,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YAChC,QAAQ;SACT,CAAC;IACJ,CAAC;IAEO,eAAe,CACrB,QAAqB,EACrB,IAAU,EACV,aAAuB;QAEvB,MAAM,OAAO,GAAoB,EAAE,CAAC;QACpC,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAC/D,IAAI,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,UAAU,CAAC,IAAe,EAAE,IAAY,EAAE,aAAuB;QACvE,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAE7C,gCAAgC;QAChC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAEtB,sDAAsD;QACtD,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAEtB,wDAAwD;QACxD,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACxC,IAAI,KAAK;YAAE,OAAO,WAAW,KAAK,GAAG,CAAC;QAEtC,qDAAqD;QACrD,MAAM,cAAc,GAAG,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC/D,IAAI,cAAc;YAAE,OAAO,WAAW,cAAc,IAAI,CAAC;QAEzD,uCAAuC;QACvC,OAAO,YAAY,IAAI,GAAG,CAAC;IAC7B,CAAC;IAEO,YAAY,CAAC,IAAU,EAAE,IAAe,EAAE,IAAY,EAAE,IAAY;QAC1E,2DAA2D;QAC3D,IAAI,IAAI,EAAE,CAAC;YACT,OAAO,IAAI,CAAC,SAAS,CAAC,IAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAW,CAAC,CAAC;IACrC,CAAC;IAEO,SAAS,CAAC,IAA0B,EAAE,KAAa;QACzD,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,+BAA+B;QAC/B,IAAI,IAAI,GAAG,MAAM,CAAC;QAClB,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;YACrB,IAAI,IAAI,IAAI,IAAI,CAAC,EAAE,IAAI,CAAC;QAC1B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC;QAChG,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;QACpB,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC;QAC1D,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;YACvD,IAAI,IAAI,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,IAAI,aAAa,IAAI,CAAC,KAAK,IAAI,CAAC;QACtC,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,qBAAqB;QACrB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClE,IAAI,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,19 @@
1
+ import { Page } from "playwright";
2
+ import { ElementRef } from "./accessibility.js";
3
+ export type ActionResult = {
4
+ success: boolean;
5
+ message: string;
6
+ };
7
+ export declare class ActionExecutor {
8
+ private page;
9
+ private elementMap;
10
+ constructor(page: Page, elementMap: Map<number, ElementRef>);
11
+ private getElement;
12
+ click(id: number): Promise<ActionResult>;
13
+ type(id: number, text: string): Promise<ActionResult>;
14
+ selectOption(id: number, value: string): Promise<ActionResult>;
15
+ hover(id: number): Promise<ActionResult>;
16
+ scroll(direction: "up" | "down"): Promise<ActionResult>;
17
+ pressKey(key: string): Promise<ActionResult>;
18
+ wait(ms?: number): Promise<ActionResult>;
19
+ }
@@ -0,0 +1,120 @@
1
+ export class ActionExecutor {
2
+ page;
3
+ elementMap;
4
+ constructor(page, elementMap) {
5
+ this.page = page;
6
+ this.elementMap = elementMap;
7
+ }
8
+ getElement(id) {
9
+ const el = this.elementMap.get(id);
10
+ if (!el) {
11
+ throw new Error(`Element [${id}] not found. Run observe first to get the current page state.`);
12
+ }
13
+ return el;
14
+ }
15
+ async click(id) {
16
+ const el = this.getElement(id);
17
+ const loc = el.locator.first();
18
+ try {
19
+ await loc.click({ timeout: 5000 });
20
+ await this.page.waitForTimeout(500);
21
+ return { success: true, message: `Clicked ${el.role} "${el.label}"` };
22
+ }
23
+ catch (err) {
24
+ // JS fallback for elements covered by overlays or intercepted by frameworks
25
+ try {
26
+ await loc.evaluate((node) => node.click());
27
+ await this.page.waitForTimeout(500);
28
+ return {
29
+ success: true,
30
+ message: `Clicked ${el.role} "${el.label}" (via JS fallback)`,
31
+ };
32
+ }
33
+ catch {
34
+ return {
35
+ success: false,
36
+ message: `Failed to click [${id}] ${el.role} "${el.label}": ${err}`,
37
+ };
38
+ }
39
+ }
40
+ }
41
+ async type(id, text) {
42
+ const el = this.getElement(id);
43
+ const loc = el.locator.first();
44
+ try {
45
+ await loc.click({ timeout: 5000 });
46
+ await loc.fill(text);
47
+ return {
48
+ success: true,
49
+ message: `Typed "${text}" into ${el.role} "${el.label}"`,
50
+ };
51
+ }
52
+ catch (err) {
53
+ // Keyboard fallback for React-controlled inputs that reject fill()
54
+ try {
55
+ await this.page.keyboard.press("Control+A");
56
+ await this.page.keyboard.press("Backspace");
57
+ await this.page.keyboard.type(text, { delay: 50 });
58
+ return {
59
+ success: true,
60
+ message: `Typed "${text}" into ${el.role} "${el.label}" (via keyboard fallback)`,
61
+ };
62
+ }
63
+ catch {
64
+ return {
65
+ success: false,
66
+ message: `Failed to type into [${id}] ${el.role} "${el.label}": ${err}`,
67
+ };
68
+ }
69
+ }
70
+ }
71
+ async selectOption(id, value) {
72
+ const el = this.getElement(id);
73
+ try {
74
+ await el.locator.first().selectOption(value, { timeout: 5000 });
75
+ return {
76
+ success: true,
77
+ message: `Selected "${value}" in ${el.role} "${el.label}"`,
78
+ };
79
+ }
80
+ catch (err) {
81
+ return {
82
+ success: false,
83
+ message: `Failed to select in [${id}] ${el.role} "${el.label}": ${err}`,
84
+ };
85
+ }
86
+ }
87
+ async hover(id) {
88
+ const el = this.getElement(id);
89
+ try {
90
+ await el.locator.first().hover({ timeout: 5000 });
91
+ return {
92
+ success: true,
93
+ message: `Hovered over ${el.role} "${el.label}"`,
94
+ };
95
+ }
96
+ catch (err) {
97
+ return {
98
+ success: false,
99
+ message: `Failed to hover [${id}] ${el.role} "${el.label}": ${err}`,
100
+ };
101
+ }
102
+ }
103
+ async scroll(direction) {
104
+ const delta = direction === "down" ? 500 : -500;
105
+ await this.page.mouse.wheel(0, delta);
106
+ await this.page.waitForTimeout(300);
107
+ return { success: true, message: `Scrolled ${direction}` };
108
+ }
109
+ async pressKey(key) {
110
+ await this.page.keyboard.press(key);
111
+ await this.page.waitForTimeout(300);
112
+ return { success: true, message: `Pressed key "${key}"` };
113
+ }
114
+ async wait(ms = 2000) {
115
+ const capped = Math.min(ms, 10000);
116
+ await this.page.waitForTimeout(capped);
117
+ return { success: true, message: `Waited ${capped}ms` };
118
+ }
119
+ }
120
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,cAAc;IACjB,IAAI,CAAO;IACX,UAAU,CAA0B;IAE5C,YAAY,IAAU,EAAE,UAAmC;QACzD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAEO,UAAU,CAAC,EAAU;QAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,KAAK,CACb,YAAY,EAAE,+DAA+D,CAC9E,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAU;QACpB,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;YACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC;QACxE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,4EAA4E;YAC5E,IAAI,CAAC;gBACH,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAa,EAAE,EAAE,CAAE,IAAoB,CAAC,KAAK,EAAE,CAAC,CAAC;gBACrE,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;gBACpC,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE,WAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,qBAAqB;iBAC9D,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,EAAE;iBACpE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,IAAY;QACjC,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,UAAU,IAAI,UAAU,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,GAAG;aACzD,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mEAAmE;YACnE,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAC5C,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAC5C,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;gBACnD,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE,UAAU,IAAI,UAAU,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,2BAA2B;iBACjF,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,EAAE;iBACxE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,EAAU,EAAE,KAAa;QAC1C,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAChE,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,aAAa,KAAK,QAAQ,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,GAAG;aAC3D,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,wBAAwB,EAAE,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,EAAE;aACxE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAU;QACpB,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,gBAAgB,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,GAAG;aACjD,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,EAAE;aACpE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,SAAwB;QACnC,MAAM,KAAK,GAAG,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAChD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,SAAS,EAAE,EAAE,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAW;QACxB,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,GAAG,GAAG,EAAE,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAa,IAAI;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACnC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,MAAM,IAAI,EAAE,CAAC;IAC1D,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ import { Page } from "playwright";
2
+ export interface BrowserConfig {
3
+ headless: boolean;
4
+ viewportWidth: number;
5
+ viewportHeight: number;
6
+ timeout: number;
7
+ settleTime: number;
8
+ }
9
+ export declare class BrowserManager {
10
+ private browser;
11
+ private context;
12
+ private page;
13
+ private config;
14
+ constructor(config?: Partial<BrowserConfig>);
15
+ getPage(): Promise<Page>;
16
+ private pageState;
17
+ navigate(url: string): Promise<{
18
+ url: string;
19
+ title: string;
20
+ }>;
21
+ goBack(): Promise<{
22
+ url: string;
23
+ title: string;
24
+ }>;
25
+ goForward(): Promise<{
26
+ url: string;
27
+ title: string;
28
+ }>;
29
+ screenshot(): Promise<Buffer>;
30
+ close(): Promise<void>;
31
+ }
@@ -0,0 +1,79 @@
1
+ import { chromium } from "playwright";
2
+ function loadConfig() {
3
+ return {
4
+ headless: process.env.SCUTTLE_HEADLESS !== "false",
5
+ viewportWidth: parseInt(process.env.SCUTTLE_VIEWPORT_WIDTH || "1280", 10),
6
+ viewportHeight: parseInt(process.env.SCUTTLE_VIEWPORT_HEIGHT || "720", 10),
7
+ timeout: parseInt(process.env.SCUTTLE_TIMEOUT || "30000", 10),
8
+ settleTime: parseInt(process.env.SCUTTLE_SETTLE_TIME || "500", 10),
9
+ };
10
+ }
11
+ export class BrowserManager {
12
+ browser = null;
13
+ context = null;
14
+ page = null;
15
+ config;
16
+ constructor(config) {
17
+ this.config = { ...loadConfig(), ...config };
18
+ }
19
+ async getPage() {
20
+ if (!this.browser || !this.browser.isConnected()) {
21
+ this.browser = await chromium.launch({ headless: this.config.headless });
22
+ this.context = await this.browser.newContext({
23
+ viewport: {
24
+ width: this.config.viewportWidth,
25
+ height: this.config.viewportHeight,
26
+ },
27
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
28
+ });
29
+ this.page = await this.context.newPage();
30
+ }
31
+ if (!this.page || this.page.isClosed()) {
32
+ this.page = await this.context.newPage();
33
+ }
34
+ return this.page;
35
+ }
36
+ async pageState(page) {
37
+ return { url: page.url(), title: await page.title() };
38
+ }
39
+ async navigate(url) {
40
+ const page = await this.getPage();
41
+ await page.goto(url, {
42
+ waitUntil: "domcontentloaded",
43
+ timeout: this.config.timeout,
44
+ });
45
+ if (this.config.settleTime > 0) {
46
+ await page.waitForTimeout(this.config.settleTime);
47
+ }
48
+ return this.pageState(page);
49
+ }
50
+ async goBack() {
51
+ const page = await this.getPage();
52
+ await page.goBack({
53
+ waitUntil: "domcontentloaded",
54
+ timeout: this.config.timeout,
55
+ });
56
+ return this.pageState(page);
57
+ }
58
+ async goForward() {
59
+ const page = await this.getPage();
60
+ await page.goForward({
61
+ waitUntil: "domcontentloaded",
62
+ timeout: this.config.timeout,
63
+ });
64
+ return this.pageState(page);
65
+ }
66
+ async screenshot() {
67
+ const page = await this.getPage();
68
+ return await page.screenshot({ type: "png" });
69
+ }
70
+ async close() {
71
+ if (this.browser) {
72
+ await this.browser.close();
73
+ this.browser = null;
74
+ this.context = null;
75
+ this.page = null;
76
+ }
77
+ }
78
+ }
79
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { BrowserManager } from "./browser.js";
6
+ import { AccessibilityExtractor } from "./accessibility.js";
7
+ import { ActionExecutor } from "./actions.js";
8
+ const browser = new BrowserManager();
9
+ const extractor = new AccessibilityExtractor();
10
+ const server = new McpServer({
11
+ name: "scuttle",
12
+ version: "0.1.0",
13
+ });
14
+ function formatPageHeader(title, url, extra) {
15
+ const lines = [`Page: ${title}`, `URL: ${url}`];
16
+ if (extra)
17
+ lines.push(extra);
18
+ lines.push("─".repeat(60));
19
+ return lines.join("\n") + "\n";
20
+ }
21
+ function toolError(msg) {
22
+ return {
23
+ content: [{ type: "text", text: msg }],
24
+ isError: true,
25
+ };
26
+ }
27
+ server.tool("navigate", "Navigate to a URL. Returns the page title and URL after loading.", { url: z.string().describe("The URL to navigate to") }, async ({ url }) => {
28
+ try {
29
+ const result = await browser.navigate(url);
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: `Navigated to: ${result.title}\nURL: ${result.url}`,
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (err) {
40
+ return toolError(`Navigation failed: ${err}`);
41
+ }
42
+ });
43
+ server.tool("observe", `Get the current page state as an annotated accessibility tree. Each interactable element has a numeric ID in brackets like [1], [2], etc. Use these IDs with the "act" tool to interact with elements. The tree shows the semantic structure: headings, links, buttons, text inputs, etc. Call this after navigate and after every action to see the updated page state.`, {}, async () => {
44
+ try {
45
+ const page = await browser.getPage();
46
+ const url = page.url();
47
+ const [title, { tree, elementCount }] = await Promise.all([
48
+ page.title(),
49
+ extractor.extract(page),
50
+ ]);
51
+ const header = formatPageHeader(title, url, `Interactable elements: ${elementCount}`);
52
+ return { content: [{ type: "text", text: header + tree }] };
53
+ }
54
+ catch (err) {
55
+ return toolError(`Observe failed: ${err}`);
56
+ }
57
+ });
58
+ server.tool("act", `Perform an action on the current page. Available actions:
59
+ - click(id): Click an element by its numeric ID from observe
60
+ - type(id, text): Type text into an input field (clears existing text first)
61
+ - select(id, value): Select an option in a dropdown
62
+ - hover(id): Hover over an element to reveal tooltips or menus
63
+ - scroll(direction): Scroll the page "up" or "down"
64
+ - key(key): Press a keyboard key (e.g., "Enter", "Tab", "Escape", "ArrowDown")
65
+ - wait(ms): Wait for a specified time in milliseconds (max 10000ms)
66
+ - back(): Go back in browser history
67
+ - forward(): Go forward in browser history
68
+
69
+ Always call "observe" after acting to see the updated page state.`, {
70
+ action: z
71
+ .enum([
72
+ "click",
73
+ "type",
74
+ "select",
75
+ "hover",
76
+ "scroll",
77
+ "key",
78
+ "wait",
79
+ "back",
80
+ "forward",
81
+ ])
82
+ .describe("The action to perform"),
83
+ id: z
84
+ .number()
85
+ .optional()
86
+ .describe("Element ID from observe (required for click, type, select, hover)"),
87
+ text: z
88
+ .string()
89
+ .optional()
90
+ .describe("Text to type or option to select (required for type and select)"),
91
+ direction: z
92
+ .enum(["up", "down"])
93
+ .optional()
94
+ .describe('Scroll direction (required for scroll, default "down")'),
95
+ key: z
96
+ .string()
97
+ .optional()
98
+ .describe("Key to press (required for key action)"),
99
+ ms: z
100
+ .number()
101
+ .optional()
102
+ .describe("Milliseconds to wait (for wait action, default 2000)"),
103
+ }, async ({ action, id, text, direction, key, ms }) => {
104
+ try {
105
+ const page = await browser.getPage();
106
+ const executor = new ActionExecutor(page, extractor.getElementMap());
107
+ let result;
108
+ switch (action) {
109
+ case "click":
110
+ if (id === undefined)
111
+ return toolError("click requires an element id");
112
+ result = await executor.click(id);
113
+ break;
114
+ case "type":
115
+ if (id === undefined)
116
+ return toolError("type requires an element id");
117
+ if (text === undefined)
118
+ return toolError("type requires text");
119
+ result = await executor.type(id, text);
120
+ break;
121
+ case "select":
122
+ if (id === undefined)
123
+ return toolError("select requires an element id");
124
+ if (!text)
125
+ return toolError("select requires a value (pass in text param)");
126
+ result = await executor.selectOption(id, text);
127
+ break;
128
+ case "hover":
129
+ if (id === undefined)
130
+ return toolError("hover requires an element id");
131
+ result = await executor.hover(id);
132
+ break;
133
+ case "scroll":
134
+ result = await executor.scroll(direction || "down");
135
+ break;
136
+ case "key":
137
+ if (!key)
138
+ return toolError("key action requires a key name");
139
+ result = await executor.pressKey(key);
140
+ break;
141
+ case "wait":
142
+ result = await executor.wait(ms);
143
+ break;
144
+ case "back": {
145
+ const backResult = await browser.goBack();
146
+ result = {
147
+ success: true,
148
+ message: `Went back to: ${backResult.title} (${backResult.url})`,
149
+ };
150
+ break;
151
+ }
152
+ case "forward": {
153
+ const fwdResult = await browser.goForward();
154
+ result = {
155
+ success: true,
156
+ message: `Went forward to: ${fwdResult.title} (${fwdResult.url})`,
157
+ };
158
+ break;
159
+ }
160
+ }
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: result.message + (result.success ? "" : " [FAILED]"),
166
+ },
167
+ ],
168
+ isError: !result.success,
169
+ };
170
+ }
171
+ catch (err) {
172
+ return toolError(`Action failed: ${err}`);
173
+ }
174
+ });
175
+ server.tool("screenshot", "Take a screenshot of the current page viewport. Returns a PNG image. Use this when the accessibility tree alone is insufficient to understand the page layout or to verify visual state.", {}, async () => {
176
+ try {
177
+ const buf = await browser.screenshot();
178
+ return {
179
+ content: [
180
+ {
181
+ type: "image",
182
+ data: buf.toString("base64"),
183
+ mimeType: "image/png",
184
+ },
185
+ ],
186
+ };
187
+ }
188
+ catch (err) {
189
+ return toolError(`Screenshot failed: ${err}`);
190
+ }
191
+ });
192
+ server.tool("get_text", "Extract all visible text content from the current page. Useful for reading articles, documentation, or any text-heavy page without the structural markup of observe.", {}, async () => {
193
+ try {
194
+ const page = await browser.getPage();
195
+ const url = page.url();
196
+ const [title, text] = await Promise.all([
197
+ page.title(),
198
+ page.innerText("body"),
199
+ ]);
200
+ const trimmed = text.length > 50000
201
+ ? text.slice(0, 50000) + "\n\n[...truncated]"
202
+ : text;
203
+ return {
204
+ content: [
205
+ { type: "text", text: formatPageHeader(title, url) + trimmed },
206
+ ],
207
+ };
208
+ }
209
+ catch (err) {
210
+ return toolError(`get_text failed: ${err}`);
211
+ }
212
+ });
213
+ async function main() {
214
+ const transport = new StdioServerTransport();
215
+ await server.connect(transport);
216
+ const shutdown = async () => {
217
+ await browser.close();
218
+ process.exit(0);
219
+ };
220
+ process.on("SIGINT", shutdown);
221
+ process.on("SIGTERM", shutdown);
222
+ }
223
+ main().catch((err) => {
224
+ console.error("Scuttle failed to start:", err);
225
+ process.exit(1);
226
+ });
227
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "scuttle-browser",
3
+ "version": "0.1.0",
4
+ "description": "Browser bridge for LLMs — exposes web pages as annotated accessibility trees via MCP",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "scuttle": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "start": "node dist/index.js",
18
+ "dev": "tsc --watch",
19
+ "test": "node test-smoke.mjs",
20
+ "prepublishOnly": "npm run build",
21
+ "postinstall": "npx playwright install chromium || echo 'Run: npx playwright install chromium'"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "model-context-protocol",
26
+ "browser",
27
+ "web-browsing",
28
+ "llm",
29
+ "ai-agent",
30
+ "accessibility",
31
+ "playwright",
32
+ "automation"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/axelkrantz/scuttle"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.12.1",
44
+ "playwright": "^1.52.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.15.3",
48
+ "typescript": "^5.8.3"
49
+ }
50
+ }