💬 Commit message: Update 2026-02-11 12:18:05, 14 files, 1838 lines

📁 Files changed: 14
📝 Lines changed: 1838

  • CLAUDE.md
  • README.md
  • TODO.md
  • browser.d.ts
  • browser.d.ts.map
  • browser.js
  • browser.js.map
  • mcp.js
  • mcp.js.map
  • types.d.ts
  • types.d.ts.map
  • browser.ts
  • mcp.ts
  • types.ts
This commit is contained in:
Adam Ladachowski
2026-02-11 12:18:06 +01:00
parent f7df5d8f54
commit f5df8c67f3
14 changed files with 1790 additions and 48 deletions
+64
View File
@@ -62,6 +62,27 @@ Commands are typed objects with a `cmd` discriminator:
All commands return `{ ok: true, ...data }` or `{ ok: false, error: string }`.
### Command Categories
| Category | Commands |
|----------|----------|
| Navigation | `goto`, `back`, `forward`, `reload`, `wait` |
| Interaction | `click`, `type`, `hover`, `select`, `keys`, `scroll`, `upload` |
| Content | `query`, `screenshot`, `url`, `html`, `eval` |
| Debugging | `console`, `errors`, `network`, `intercept`, `metrics`, `a11y` |
| Storage | `cookies`, `storage`, `dialog`, `session_save`, `session_restore` |
| Viewport | `viewport`, `emulate` |
| Image | `favicon`, `convert`, `resize`, `crop`, `compress`, `thumbnail` |
### Event Listeners
The browser automatically captures events when launched:
- **Console**: `page.on('console')` - Captures all console messages
- **Errors**: `page.on('pageerror')` - Captures uncaught exceptions
- **Network**: `page.on('request/response/requestfailed')` - Captures all network activity
- **Dialogs**: `page.on('dialog')` - Handles alert/confirm/prompt dialogs
## CLI Options
Key flags: `-s <port>` (server mode), `-q <selector>` (query), `-c <selector>` (click), `-t <sel>=<text>` (type), `-i` (interactive), `--headed` (visible browser).
@@ -81,3 +102,46 @@ browse-mcp
```
The MCP server (`src/mcp.ts`) exposes all browser commands as tools. It uses stdio transport and auto-launches the browser on first command.
### Debugging Tools
```bash
# Get console logs
curl localhost:13373 -d '{"cmd":"console","level":"error"}'
# Get page errors
curl localhost:13373 -d '{"cmd":"errors"}'
# Get network requests (all or failed only)
curl localhost:13373 -d '{"cmd":"network","filter":"failed"}'
# Block requests matching pattern
curl localhost:13373 -d '{"cmd":"intercept","action":"block","pattern":"**/*ads*"}'
# Mock API response
curl localhost:13373 -d '{"cmd":"intercept","action":"mock","pattern":"**/api/*","status":200,"body":"{\"mock\":true}"}'
# Get performance metrics
curl localhost:13373 -d '{"cmd":"metrics","resources":true}'
# Get accessibility tree
curl localhost:13373 -d '{"cmd":"a11y"}'
# Emulate mobile device
curl localhost:13373 -d '{"cmd":"emulate","device":"iPhone 13"}'
```
### MCP Resources
Resources can be accessed via `@` mentions in Claude Code:
| Resource | Description |
|----------|-------------|
| `browser://state` | Current browser state |
| `browser://console` | Console messages |
| `browser://network` | All network requests |
| `browser://network/failed` | Failed requests only |
| `browser://errors` | Page errors |
| `browser://a11y` | Accessibility tree |
| `browser://html` | Page HTML (truncated) |
| `browser://screenshot` | Page screenshot |
+113 -5
View File
@@ -145,9 +145,79 @@ Add to Claude Code's MCP config (`~/.claude/settings.json`):
}
```
**Available Tools:** `goto`, `click`, `type`, `query`, `screenshot`, `url`, `html`, `back`, `forward`, `reload`, `wait`, `eval`
### MCP Tools Reference
**Image Processing Tools:** `favicon`, `convert`, `resize`, `crop`, `compress`, `thumbnail`
**Navigation & Interaction:**
| Tool | Description |
|------|-------------|
| `goto` | Navigate to a URL |
| `click` | Click on an element |
| `type` | Type text into an input field |
| `hover` | Hover over an element |
| `select` | Select option(s) in a dropdown |
| `keys` | Send keyboard shortcuts (e.g., "Enter", "Control+a") |
| `scroll` | Scroll page or element into view |
| `upload` | Upload files to a file input |
| `back`, `forward`, `reload` | Browser navigation |
| `wait` | Wait for a specified time |
**Debugging & Inspection:**
| Tool | Description |
|------|-------------|
| `console` | Get captured console messages (log, warn, error, etc.) |
| `errors` | Get page errors (uncaught exceptions) |
| `network` | Get captured network requests/responses |
| `intercept` | Block or mock network requests |
| `metrics` | Get performance metrics and DOM statistics |
| `a11y` | Get accessibility tree snapshot |
**Page Content:**
| Tool | Description |
|------|-------------|
| `query` | Query elements by CSS selector |
| `screenshot` | Take a screenshot |
| `url` | Get current URL and title |
| `html` | Get page HTML content |
| `eval` | Execute JavaScript in browser context |
**Storage & Session:**
| Tool | Description |
|------|-------------|
| `cookies` | Get, set, delete, or clear cookies |
| `storage` | Access localStorage or sessionStorage |
| `dialog` | Configure how browser dialogs are handled |
| `session_save` | Save session state to file |
| `session_restore` | Restore session from file |
**Viewport & Emulation:**
| Tool | Description |
|------|-------------|
| `viewport` | Resize browser viewport |
| `emulate` | Emulate a mobile device |
**Image Processing:**
| Tool | Description |
|------|-------------|
| `favicon` | Generate favicon set from image |
| `convert` | Convert image format |
| `resize` | Resize image |
| `crop` | Crop image |
| `compress` | Compress image |
| `thumbnail` | Create thumbnail |
### MCP Resources
| Resource | Description |
|----------|-------------|
| `browser://state` | Browser state (URL, title, launched) |
| `browser://html` | Page HTML (truncated to 10KB) |
| `browser://html/full` | Complete page HTML |
| `browser://console` | Captured console messages |
| `browser://network` | All network requests |
| `browser://network/failed` | Failed requests only |
| `browser://errors` | Page errors |
| `browser://a11y` | Accessibility tree |
| `browser://screenshot` | Page screenshot as base64 PNG |
## Programmatic Usage
@@ -182,18 +252,56 @@ const server = await startServer({ port: 3000, headless: false });
### ClaudeBrowser
**Lifecycle:**
- `launch()` - Launch the browser
- `close()` - Close the browser
- `newPage()` - Open new page
**Navigation:**
- `goto(url)` - Navigate to URL
- `back()` / `forward()` / `reload()` - Browser navigation
- `wait(ms)` - Wait for timeout
**Interaction:**
- `click(selector)` - Click element
- `type(selector, text)` - Type into input
- `hover(selector)` - Hover over element
- `select(selector, value)` - Select dropdown option(s)
- `keys(keys)` - Press keyboard keys
- `scroll(selector?, x?, y?)` - Scroll page or element
- `upload(selector, files)` - Upload files
**Content:**
- `query(selector)` - Query elements, returns attributes
- `screenshot(path?, fullPage?)` - Take screenshot
- `getUrl()` - Get current URL and title
- `getHtml(full?)` - Get page HTML
- `back()` / `forward()` / `reload()` - Navigation
- `wait(ms)` - Wait for timeout
- `newPage()` - Open new page
- `eval(script)` - Execute JavaScript
**Debugging:**
- `getConsole(level?, clear?)` - Get console messages
- `getErrors(clear?)` - Get page errors
- `getNetwork(filter?, clear?)` - Get network requests
- `getMetrics(includeResources?)` - Get performance metrics
- `getA11y(selector?)` - Get accessibility tree
**Storage:**
- `getCookies(name?)` - Get cookies
- `setCookie(name, value, url?)` - Set cookie
- `deleteCookie(name)` / `clearCookies()` - Remove cookies
- `getStorage(type, key?)` - Get localStorage/sessionStorage
- `setStorage(type, key, value)` - Set storage item
- `deleteStorage(type, key)` / `clearStorage(type)` - Remove storage
**Interception:**
- `addIntercept(pattern, action, response?)` - Block or mock requests
- `clearIntercepts()` - Remove all intercepts
**Viewport:**
- `setViewport(width, height)` - Resize viewport
- `emulate(device)` - Emulate device (e.g., 'iPhone 13')
**Commands:**
- `executeCommand(cmd)` - Execute a command object
### BrowserServer
+16 -16
View File
@@ -2,7 +2,7 @@
## Phase 1: Core Debugging (Console & Errors)
- [x] Step 1.1: Add Console Command
- [ ] Step 1.2: Add Page Errors Command
- [x] Step 1.2: Add Page Errors Command
## Phase 2: Network Monitoring
- [x] Step 2.1: Add Network Logging
@@ -10,30 +10,30 @@
- [x] Step 2.3: Add Request Interception
## Phase 3: Performance & Metrics
- [ ] Step 3.1: Add Performance Metrics
- [ ] Step 3.2: Add Resource Timing
- [x] Step 3.1: Add Performance Metrics
- [x] Step 3.2: Add Resource Timing
## Phase 4: Accessibility
- [ ] Step 4.1: Add Accessibility Snapshot
- [x] Step 4.1: Add Accessibility Snapshot
## Phase 5: Dialog Handling
- [ ] Step 5.1: Add Dialog Command
- [x] Step 5.1: Add Dialog Command
## Phase 6: Storage & Cookies
- [ ] Step 6.1: Add Cookies Command
- [ ] Step 6.2: Add Storage Command
- [x] Step 6.1: Add Cookies Command
- [x] Step 6.2: Add Storage Command
## Phase 7: Advanced Interactions
- [ ] Step 7.1: Add Hover Command
- [ ] Step 7.2: Add Select Command
- [ ] Step 7.3: Add Keys Command
- [ ] Step 7.4: Add Upload Command
- [ ] Step 7.5: Add Scroll Command
- [x] Step 7.1: Add Hover Command
- [x] Step 7.2: Add Select Command
- [x] Step 7.3: Add Keys Command
- [x] Step 7.4: Add Upload Command
- [x] Step 7.5: Add Scroll Command
## Phase 8: Viewport & Emulation
- [ ] Step 8.1: Add Viewport Command
- [ ] Step 8.2: Add Emulate Command
- [x] Step 8.1: Add Viewport Command
- [x] Step 8.2: Add Emulate Command
## Phase 9: Documentation
- [ ] Step 9.1: Update README
- [ ] Step 9.2: Update CLAUDE.md
- [x] Step 9.1: Update README
- [x] Step 9.2: Update CLAUDE.md
+51 -1
View File
@@ -1,5 +1,5 @@
import { type BrowserContext, type Page } from 'playwright';
import type { BrowserCommand, BrowserOptions, CommandResponse, ConsoleMessage, ElementInfo, NetworkEntry } from './types.js';
import type { A11yNode, BrowserCommand, BrowserOptions, CommandResponse, ConsoleMessage, DialogEntry, ElementInfo, MetricsData, NetworkEntry, PageError } from './types.js';
export declare class ClaudeBrowser {
private browser;
private context;
@@ -7,9 +7,14 @@ export declare class ClaudeBrowser {
private options;
private consoleMessages;
private networkEntries;
private pageErrors;
private dialogHistory;
private dialogConfig;
private interceptPatterns;
constructor(options?: BrowserOptions);
launch(): Promise<void>;
private setupErrorListener;
private setupDialogListener;
private setupConsoleListener;
private setupNetworkListener;
close(): Promise<void>;
@@ -52,13 +57,58 @@ export declare class ClaudeBrowser {
clearConsole(): void;
getNetwork(filter?: string, clear?: boolean): NetworkEntry[];
clearNetwork(): void;
getErrors(clear?: boolean): PageError[];
clearErrors(): void;
getMetrics(includeResources?: boolean): Promise<MetricsData>;
getA11y(selector?: string): Promise<A11yNode | null>;
getDialogs(): DialogEntry[];
clearDialogs(): void;
setDialogConfig(config: {
autoAccept?: boolean;
autoDismiss?: boolean;
text?: string;
}): void;
getDialogConfig(): {
autoAccept: boolean;
autoDismiss: boolean;
};
addIntercept(pattern: string, action: 'block' | 'mock', response?: {
status?: number;
body?: string;
contentType?: string;
}): Promise<void>;
private handleIntercept;
clearIntercepts(): Promise<void>;
getInterceptPatterns(): string[];
getCookies(name?: string): Promise<Array<{
name: string;
value: string;
domain: string;
path: string;
}>>;
setCookie(name: string, value: string, url?: string): Promise<void>;
deleteCookie(name: string): Promise<void>;
clearCookies(): Promise<void>;
getStorage(type: 'local' | 'session', key?: string): Promise<Record<string, string>>;
setStorage(type: 'local' | 'session', key: string, value: string): Promise<void>;
deleteStorage(type: 'local' | 'session', key: string): Promise<void>;
clearStorage(type: 'local' | 'session'): Promise<void>;
hover(selector: string): Promise<void>;
select(selector: string, value: string | string[]): Promise<string[]>;
keys(keys: string): Promise<void>;
upload(selector: string, files: string[]): Promise<void>;
scroll(selector?: string, x?: number, y?: number): Promise<void>;
setViewport(width: number, height: number): Promise<{
width: number;
height: number;
}>;
emulate(device: string): Promise<{
width: number;
height: number;
}>;
private handleDialogCommand;
private handleCookiesCommand;
private handleStorageCommand;
executeCommand(cmd: BrowserCommand): Promise<CommandResponse>;
}
//# sourceMappingURL=browser.d.ts.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
+390 -20
View File
@@ -8,6 +8,9 @@ export class ClaudeBrowser {
options;
consoleMessages = [];
networkEntries = [];
pageErrors = [];
dialogHistory = [];
dialogConfig = { autoAccept: false, autoDismiss: false, promptText: '' };
interceptPatterns = new Map();
constructor(options = {}) {
this.options = {
@@ -27,6 +30,41 @@ export class ClaudeBrowser {
this.page = await this.context.newPage();
this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page);
this.setupErrorListener(this.page);
this.setupDialogListener(this.page);
}
setupErrorListener(page) {
page.on('pageerror', (error) => {
this.pageErrors.push({
message: error.message,
stack: error.stack,
timestamp: Date.now(),
});
});
}
setupDialogListener(page) {
page.on('dialog', async (dialog) => {
const entry = {
type: dialog.type(),
message: dialog.message(),
defaultValue: dialog.defaultValue() || undefined,
timestamp: Date.now(),
};
if (this.dialogConfig.autoAccept) {
await dialog.accept(this.dialogConfig.promptText || undefined);
entry.response = this.dialogConfig.promptText || true;
}
else if (this.dialogConfig.autoDismiss) {
await dialog.dismiss();
entry.response = false;
}
else {
// Default: accept to prevent blocking
await dialog.accept();
entry.response = true;
}
this.dialogHistory.push(entry);
});
}
setupConsoleListener(page) {
page.on('console', (msg) => {
@@ -180,6 +218,8 @@ export class ClaudeBrowser {
this.page = await this.context.newPage();
this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page);
this.setupErrorListener(this.page);
this.setupDialogListener(this.page);
}
async eval(script) {
const page = this.ensurePage();
@@ -216,29 +256,142 @@ export class ClaudeBrowser {
clearNetwork() {
this.networkEntries = [];
}
getErrors(clear = false) {
const errors = this.pageErrors;
if (clear) {
this.pageErrors = [];
}
return errors;
}
clearErrors() {
this.pageErrors = [];
}
async getMetrics(includeResources = false) {
const page = this.ensurePage();
const metricsScript = `(() => {
const timing = performance.timing;
const navigationStart = timing.navigationStart;
const paintEntries = performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(e => e.name === 'first-paint');
const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
return {
timing: {
domContentLoaded: timing.domContentLoadedEventEnd - navigationStart,
load: timing.loadEventEnd - navigationStart,
firstPaint: firstPaint ? firstPaint.startTime : undefined,
firstContentfulPaint: fcp ? fcp.startTime : undefined,
},
dom: {
nodes: document.getElementsByTagName('*').length,
scripts: document.getElementsByTagName('script').length,
stylesheets: document.getElementsByTagName('link').length,
images: document.getElementsByTagName('img').length,
},
};
})()`;
const metrics = (await page.evaluate(metricsScript));
if (includeResources) {
const resourcesScript = `(() => {
return performance.getEntriesByType('resource').map(entry => ({
name: entry.name,
type: entry.initiatorType,
duration: Math.round(entry.duration),
size: entry.transferSize || 0,
}));
})()`;
const resources = (await page.evaluate(resourcesScript));
return { ...metrics, resources };
}
return metrics;
}
async getA11y(selector) {
const page = this.ensurePage();
// Build a11y tree using ARIA attributes and semantic roles
const selectorArg = selector ? JSON.stringify(selector) : 'null';
const script = `((selector) => {
function getA11yNode(el) {
const role = el.getAttribute('role') || getImplicitRole(el);
const name = getAccessibleName(el);
const node = { role, name: name || undefined };
const value = el.value || el.getAttribute('aria-valuenow');
if (value) node.value = String(value);
const desc = el.getAttribute('aria-describedby');
if (desc) {
const descEl = document.getElementById(desc);
if (descEl) node.description = descEl.textContent?.trim();
}
const children = [];
for (const child of el.children) {
if (isAccessible(child)) children.push(getA11yNode(child));
}
if (children.length) node.children = children;
return node;
}
function getImplicitRole(el) {
const tag = el.tagName.toLowerCase();
const roleMap = { button:'button', a:'link', input:'textbox', img:'img',
h1:'heading', h2:'heading', h3:'heading', h4:'heading', nav:'navigation',
main:'main', footer:'contentinfo', header:'banner', aside:'complementary',
form:'form', table:'table', ul:'list', ol:'list', li:'listitem' };
return roleMap[tag] || 'generic';
}
function getAccessibleName(el) {
return el.getAttribute('aria-label') || el.getAttribute('alt')
|| el.getAttribute('title') || (el.tagName === 'INPUT' ? el.placeholder : null)
|| el.textContent?.trim().slice(0, 100);
}
function isAccessible(el) {
if (el.nodeType !== 1) return false;
if (el.getAttribute('aria-hidden') === 'true') return false;
const style = getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
}
const root = selector ? document.querySelector(selector) : document.body;
return root ? getA11yNode(root) : null;
})(${selectorArg})`;
return page.evaluate(script);
}
getDialogs() {
return this.dialogHistory;
}
clearDialogs() {
this.dialogHistory = [];
}
setDialogConfig(config) {
if (config.autoAccept !== undefined)
this.dialogConfig.autoAccept = config.autoAccept;
if (config.autoDismiss !== undefined)
this.dialogConfig.autoDismiss = config.autoDismiss;
if (config.text !== undefined)
this.dialogConfig.promptText = config.text;
}
getDialogConfig() {
return { autoAccept: this.dialogConfig.autoAccept, autoDismiss: this.dialogConfig.autoDismiss };
}
async addIntercept(pattern, action, response) {
const page = this.ensurePage();
this.interceptPatterns.set(pattern, { action, response });
await page.route(pattern, async (route) => {
const config = this.interceptPatterns.get(pattern);
if (!config) {
await route.continue();
return;
}
if (config.action === 'block') {
await route.abort();
}
else if (config.action === 'mock' && config.response) {
await route.fulfill({
status: config.response.status || 200,
contentType: config.response.contentType || 'application/json',
body: config.response.body || '',
});
}
else {
await route.continue();
}
});
await page.route(pattern, (route) => this.handleIntercept(pattern, route));
}
async handleIntercept(pattern, route) {
const config = this.interceptPatterns.get(pattern);
if (!config) {
await route.continue();
return;
}
if (config.action === 'block') {
await route.abort();
return;
}
if (config.action === 'mock' && config.response) {
await route.fulfill({
status: config.response.status || 200,
contentType: config.response.contentType || 'application/json',
body: config.response.body || '',
});
return;
}
await route.continue();
}
async clearIntercepts() {
const page = this.ensurePage();
@@ -250,6 +403,177 @@ export class ClaudeBrowser {
getInterceptPatterns() {
return Array.from(this.interceptPatterns.keys());
}
// Phase 6: Cookies & Storage
async getCookies(name) {
const context = this.getContext();
if (!context)
throw new Error('Browser not launched');
const cookies = await context.cookies();
const filtered = name ? cookies.filter((c) => c.name === name) : cookies;
return filtered.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path }));
}
async setCookie(name, value, url) {
const context = this.getContext();
if (!context)
throw new Error('Browser not launched');
const page = this.ensurePage();
await context.addCookies([{ name, value, url: url || page.url() }]);
}
async deleteCookie(name) {
const context = this.getContext();
if (!context)
throw new Error('Browser not launched');
const cookies = await context.cookies();
const toKeep = cookies.filter((c) => c.name !== name);
await context.clearCookies();
if (toKeep.length > 0)
await context.addCookies(toKeep);
}
async clearCookies() {
const context = this.getContext();
if (!context)
throw new Error('Browser not launched');
await context.clearCookies();
}
async getStorage(type, key) {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
const script = key
? `({ [${JSON.stringify(key)}]: ${storage}.getItem(${JSON.stringify(key)}) || '' })`
: `Object.fromEntries(Array.from({ length: ${storage}.length }, (_, i) => {
const k = ${storage}.key(i); return [k, ${storage}.getItem(k)];
}))`;
return page.evaluate(script);
}
async setStorage(type, key, value) {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
}
async deleteStorage(type, key) {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.removeItem(${JSON.stringify(key)})`);
}
async clearStorage(type) {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.clear()`);
}
// Phase 7: Advanced Interactions
async hover(selector) {
const page = this.ensurePage();
await page.hover(selector);
}
async select(selector, value) {
const page = this.ensurePage();
return page.selectOption(selector, value);
}
async keys(keys) {
const page = this.ensurePage();
await page.keyboard.press(keys);
}
async upload(selector, files) {
const page = this.ensurePage();
await page.setInputFiles(selector, files);
}
async scroll(selector, x, y) {
const page = this.ensurePage();
if (selector) {
await page.locator(selector).scrollIntoViewIfNeeded();
}
else {
await page.evaluate(`window.scrollTo(${x || 0}, ${y || 0})`);
}
}
// Phase 8: Viewport & Emulation
async setViewport(width, height) {
const page = this.ensurePage();
await page.setViewportSize({ width, height });
return { width, height };
}
async emulate(device) {
const page = this.ensurePage();
const { devices } = await import('playwright');
const deviceConfig = devices[device];
if (!deviceConfig)
throw new Error(`Unknown device: ${device}. Try 'iPhone 13', 'Pixel 5', etc.`);
await page.setViewportSize(deviceConfig.viewport);
return deviceConfig.viewport;
}
handleDialogCommand(cmd) {
switch (cmd.action) {
case 'status':
return { ok: true, dialogs: this.getDialogs(), dialogConfig: this.getDialogConfig() };
case 'accept':
this.setDialogConfig({ autoAccept: true, autoDismiss: false, text: cmd.text });
return { ok: true, dialogConfig: this.getDialogConfig() };
case 'dismiss':
this.setDialogConfig({ autoAccept: false, autoDismiss: true });
return { ok: true, dialogConfig: this.getDialogConfig() };
case 'config':
this.setDialogConfig({
autoAccept: cmd.autoAccept,
autoDismiss: cmd.autoDismiss,
text: cmd.text,
});
return { ok: true, dialogConfig: this.getDialogConfig() };
default:
return { ok: false, error: 'Unknown dialog action' };
}
}
async handleCookiesCommand(cmd) {
switch (cmd.action) {
case 'get': {
const cookies = await this.getCookies(cmd.name);
return { ok: true, cookies, count: cookies.length };
}
case 'set': {
if (!cmd.name || !cmd.value)
return { ok: false, error: 'Name and value required' };
await this.setCookie(cmd.name, cmd.value, cmd.url);
return { ok: true };
}
case 'delete': {
if (!cmd.name)
return { ok: false, error: 'Name required' };
await this.deleteCookie(cmd.name);
return { ok: true };
}
case 'clear': {
await this.clearCookies();
return { ok: true };
}
default:
return { ok: false, error: 'Unknown cookies action' };
}
}
async handleStorageCommand(cmd) {
switch (cmd.action) {
case 'get': {
const storage = await this.getStorage(cmd.type, cmd.key);
return { ok: true, storage, count: Object.keys(storage).length };
}
case 'set': {
if (!cmd.key || cmd.value === undefined)
return { ok: false, error: 'Key and value required' };
await this.setStorage(cmd.type, cmd.key, cmd.value);
return { ok: true };
}
case 'delete': {
if (!cmd.key)
return { ok: false, error: 'Key required' };
await this.deleteStorage(cmd.type, cmd.key);
return { ok: true };
}
case 'clear': {
await this.clearStorage(cmd.type);
return { ok: true };
}
default:
return { ok: false, error: 'Unknown storage action' };
}
}
async executeCommand(cmd) {
try {
switch (cmd.cmd) {
@@ -332,6 +656,52 @@ export class ClaudeBrowser {
await this.addIntercept(cmd.pattern, cmd.action, cmd.response);
return { ok: true, patterns: this.getInterceptPatterns() };
}
case 'errors': {
const errors = this.getErrors(cmd.clear);
return { ok: true, count: errors.length, errors };
}
case 'metrics': {
const metrics = await this.getMetrics(cmd.resources);
return { ok: true, metrics };
}
case 'a11y': {
const a11y = await this.getA11y(cmd.selector);
return { ok: true, a11y: a11y || undefined };
}
case 'dialog':
return this.handleDialogCommand(cmd);
case 'cookies':
return this.handleCookiesCommand(cmd);
case 'storage':
return this.handleStorageCommand(cmd);
case 'hover': {
await this.hover(cmd.selector);
return { ok: true };
}
case 'select': {
const selected = await this.select(cmd.selector, cmd.value);
return { ok: true, selected };
}
case 'keys': {
await this.keys(cmd.keys);
return { ok: true };
}
case 'upload': {
await this.upload(cmd.selector, cmd.files);
return { ok: true };
}
case 'scroll': {
await this.scroll(cmd.selector, cmd.x, cmd.y);
return { ok: true };
}
case 'viewport': {
const viewport = await this.setViewport(cmd.width, cmd.height);
return { ok: true, viewport };
}
case 'emulate': {
const viewport = await this.emulate(cmd.device);
return { ok: true, viewport };
}
case 'favicon': {
const result = await image.createFavicon(cmd.input, cmd.outputDir);
return { ok: true, files: result.files, outputDir: result.outputDir };
+1 -1
View File
File diff suppressed because one or more lines are too long
Vendored
+186
View File
@@ -162,6 +162,141 @@ server.tool('intercept', 'Block or mock network requests matching a pattern', {
await browser.addIntercept(pattern, action, response);
return textResult(JSON.stringify({ ok: true, action, pattern, patterns: browser.getInterceptPatterns() }));
}));
// Page errors
server.tool('errors', 'Get captured page errors (uncaught exceptions and unhandled promise rejections)', {
clear: z.boolean().optional().default(false).describe('Clear errors after retrieving'),
}, withLogging('errors', async ({ clear }) => {
await ensureLaunched();
const errors = browser.getErrors(clear);
return textResult(JSON.stringify({ ok: true, count: errors.length, errors }));
}));
// Performance metrics
server.tool('metrics', 'Get page performance metrics and DOM statistics', {
resources: z
.boolean()
.optional()
.default(false)
.describe('Include individual resource timing entries'),
}, withLogging('metrics', async ({ resources }) => {
await ensureLaunched();
const metrics = await browser.getMetrics(resources);
return textResult(JSON.stringify({ ok: true, metrics }));
}));
// Accessibility
server.tool('a11y', 'Get accessibility tree snapshot for the page or a specific element', {
selector: z.string().optional().describe('CSS selector to get subtree for specific element'),
}, withLogging('a11y', async ({ selector }) => {
await ensureLaunched();
const a11y = await browser.getA11y(selector);
return textResult(JSON.stringify({ ok: true, a11y }));
}));
// Dialog handling
server.tool('dialog', 'Configure how browser dialogs (alert, confirm, prompt) are handled', {
action: z
.enum(['status', 'accept', 'dismiss', 'config'])
.describe('Action: status (get history), accept (auto-accept), dismiss (auto-dismiss), config (set both)'),
text: z.string().optional().describe('Text to enter for prompt dialogs when accepting'),
autoAccept: z.boolean().optional().describe('Auto-accept dialogs (for config action)'),
autoDismiss: z.boolean().optional().describe('Auto-dismiss dialogs (for config action)'),
}, withLogging('dialog', async ({ action, text, autoAccept, autoDismiss }) => {
await ensureLaunched();
if (action === 'status') {
return textResult(JSON.stringify({
ok: true,
dialogs: browser.getDialogs(),
config: browser.getDialogConfig(),
}));
}
if (action === 'accept') {
browser.setDialogConfig({ autoAccept: true, autoDismiss: false, text });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
}
if (action === 'dismiss') {
browser.setDialogConfig({ autoAccept: false, autoDismiss: true });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
}
browser.setDialogConfig({ autoAccept, autoDismiss, text });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
}));
// Cookies
server.tool('cookies', 'Get, set, delete, or clear browser cookies', {
action: z.enum(['get', 'set', 'delete', 'clear']).describe('Action to perform'),
name: z.string().optional().describe('Cookie name (for get/set/delete)'),
value: z.string().optional().describe('Cookie value (for set)'),
url: z.string().optional().describe('URL for cookie (for set, defaults to current page)'),
}, withLogging('cookies', async ({ action, name, value, url }) => {
await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'cookies', action, name, value, url });
return textResult(JSON.stringify(result));
}));
// Storage
server.tool('storage', 'Get, set, delete, or clear localStorage or sessionStorage', {
type: z.enum(['local', 'session']).describe('Storage type'),
action: z.enum(['get', 'set', 'delete', 'clear']).describe('Action to perform'),
key: z.string().optional().describe('Storage key'),
value: z.string().optional().describe('Value to set'),
}, withLogging('storage', async ({ type, action, key, value }) => {
await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'storage', type, action, key, value });
return textResult(JSON.stringify(result));
}));
// Hover
server.tool('hover', 'Hover over an element to trigger hover states', { selector: z.string().describe('CSS selector of element to hover') }, withLogging('hover', async ({ selector }) => {
await ensureLaunched();
await browser.hover(selector);
return textResult(JSON.stringify({ ok: true }));
}));
// Select
server.tool('select', 'Select option(s) in a dropdown/select element', {
selector: z.string().describe('CSS selector of select element'),
value: z.union([z.string(), z.array(z.string())]).describe('Value(s) to select'),
}, withLogging('select', async ({ selector, value }) => {
await ensureLaunched();
const selected = await browser.select(selector, value);
return textResult(JSON.stringify({ ok: true, selected }));
}));
// Keys
server.tool('keys', 'Send keyboard keys or shortcuts (e.g., "Enter", "Control+a", "Escape")', { keys: z.string().describe('Key or key combination to press') }, withLogging('keys', async ({ keys }) => {
await ensureLaunched();
await browser.keys(keys);
return textResult(JSON.stringify({ ok: true }));
}));
// Upload
server.tool('upload', 'Upload files to a file input element', {
selector: z.string().describe('CSS selector of file input'),
files: z.array(z.string()).describe('Array of file paths to upload'),
}, withLogging('upload', async ({ selector, files }) => {
await ensureLaunched();
await browser.upload(selector, files);
return textResult(JSON.stringify({ ok: true }));
}));
// Scroll
server.tool('scroll', 'Scroll the page or an element into view', {
selector: z.string().optional().describe('CSS selector to scroll into view'),
x: z.number().optional().describe('X position to scroll to (if no selector)'),
y: z.number().optional().describe('Y position to scroll to (if no selector)'),
}, withLogging('scroll', async ({ selector, x, y }) => {
await ensureLaunched();
await browser.scroll(selector, x, y);
return textResult(JSON.stringify({ ok: true }));
}));
// Viewport
server.tool('viewport', 'Resize the browser viewport', {
width: z.number().describe('Viewport width in pixels'),
height: z.number().describe('Viewport height in pixels'),
}, withLogging('viewport', async ({ width, height }) => {
await ensureLaunched();
const viewport = await browser.setViewport(width, height);
return textResult(JSON.stringify({ ok: true, viewport }));
}));
// Emulate
server.tool('emulate', 'Emulate a mobile device (viewport, user agent, touch)', {
device: z.string().describe('Device name (e.g., "iPhone 13", "Pixel 5", "iPad Pro")'),
}, withLogging('emulate', async ({ device }) => {
await ensureLaunched();
const viewport = await browser.emulate(device);
return textResult(JSON.stringify({ ok: true, device, viewport }));
}));
// Utility
server.tool('wait', 'Wait for a specified time in milliseconds', { ms: z.number().optional().default(1000) }, withLogging('wait', async ({ ms }) => {
await ensureLaunched();
@@ -468,6 +603,57 @@ server.resource('Failed Requests', 'browser://network/failed', {
],
};
});
// Resource: browser://errors - Captured page errors
server.resource('Page Errors', 'browser://errors', {
description: 'Uncaught exceptions and unhandled promise rejections',
mimeType: 'application/json',
}, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://errors',
mimeType: 'application/json',
text: JSON.stringify({ launched: false, errors: [] }),
},
],
};
}
const errors = browser.getErrors(false);
return {
contents: [
{
uri: 'browser://errors',
mimeType: 'application/json',
text: JSON.stringify({ launched: true, count: errors.length, errors }),
},
],
};
});
// Resource: browser://a11y - Accessibility tree snapshot
server.resource('Accessibility Tree', 'browser://a11y', { description: 'Accessibility tree snapshot of the current page', mimeType: 'application/json' }, async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://a11y',
mimeType: 'application/json',
text: JSON.stringify({ launched: false, a11y: null }),
},
],
};
}
const a11y = await browser.getA11y();
return {
contents: [
{
uri: 'browser://a11y',
mimeType: 'application/json',
text: JSON.stringify({ launched: true, a11y }),
},
],
};
});
// Resource: browser://screenshot - Current page screenshot (base64)
server.resource('Page Screenshot', 'browser://screenshot', { description: 'Screenshot of the current page as base64 PNG', mimeType: 'image/png' }, async () => {
if (!launched) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+128 -1
View File
@@ -142,7 +142,114 @@ export interface InterceptCommand {
contentType?: string;
};
}
export type BrowserCommand = GotoCommand | ClickCommand | TypeCommand | QueryCommand | ScreenshotCommand | UrlCommand | HtmlCommand | BackCommand | ForwardCommand | ReloadCommand | WaitCommand | NewPageCommand | CloseCommand | EvalCommand | FaviconCommand | ConvertCommand | ResizeCommand | CropCommand | CompressCommand | ThumbnailCommand | ConsoleCommand | NetworkCommand | InterceptCommand;
export interface PageError {
message: string;
stack?: string;
timestamp: number;
}
export interface ErrorsCommand {
cmd: 'errors';
clear?: boolean;
}
export interface MetricsData {
timing: {
domContentLoaded?: number;
load?: number;
firstPaint?: number;
firstContentfulPaint?: number;
};
dom: {
nodes: number;
scripts: number;
stylesheets: number;
images: number;
};
resources?: Array<{
name: string;
type: string;
duration: number;
size: number;
}>;
}
export interface MetricsCommand {
cmd: 'metrics';
resources?: boolean;
}
export interface A11yNode {
role: string;
name?: string;
value?: string;
description?: string;
children?: A11yNode[];
}
export interface A11yCommand {
cmd: 'a11y';
selector?: string;
}
export interface DialogEntry {
type: 'alert' | 'confirm' | 'prompt' | 'beforeunload';
message: string;
defaultValue?: string;
response?: string | boolean;
timestamp: number;
}
export interface DialogCommand {
cmd: 'dialog';
action: 'status' | 'accept' | 'dismiss' | 'config';
text?: string;
autoAccept?: boolean;
autoDismiss?: boolean;
}
export interface CookiesCommand {
cmd: 'cookies';
action: 'get' | 'set' | 'delete' | 'clear';
name?: string;
value?: string;
domain?: string;
path?: string;
url?: string;
}
export interface StorageCommand {
cmd: 'storage';
type: 'local' | 'session';
action: 'get' | 'set' | 'delete' | 'clear';
key?: string;
value?: string;
}
export interface HoverCommand {
cmd: 'hover';
selector: string;
}
export interface SelectCommand {
cmd: 'select';
selector: string;
value: string | string[];
}
export interface KeysCommand {
cmd: 'keys';
keys: string;
}
export interface UploadCommand {
cmd: 'upload';
selector: string;
files: string[];
}
export interface ScrollCommand {
cmd: 'scroll';
selector?: string;
x?: number;
y?: number;
}
export interface ViewportCommand {
cmd: 'viewport';
width: number;
height: number;
}
export interface EmulateCommand {
cmd: 'emulate';
device: string;
}
export type BrowserCommand = GotoCommand | ClickCommand | TypeCommand | QueryCommand | ScreenshotCommand | UrlCommand | HtmlCommand | BackCommand | ForwardCommand | ReloadCommand | WaitCommand | NewPageCommand | CloseCommand | EvalCommand | FaviconCommand | ConvertCommand | ResizeCommand | CropCommand | CompressCommand | ThumbnailCommand | ConsoleCommand | NetworkCommand | InterceptCommand | ErrorsCommand | MetricsCommand | A11yCommand | DialogCommand | CookiesCommand | StorageCommand | HoverCommand | SelectCommand | KeysCommand | UploadCommand | ScrollCommand | ViewportCommand | EmulateCommand;
export interface SuccessResponse {
ok: true;
url?: string;
@@ -161,6 +268,26 @@ export interface SuccessResponse {
messages?: ConsoleMessage[];
requests?: NetworkEntry[];
patterns?: string[];
errors?: PageError[];
metrics?: MetricsData;
a11y?: A11yNode;
dialogs?: DialogEntry[];
dialogConfig?: {
autoAccept: boolean;
autoDismiss: boolean;
};
cookies?: Array<{
name: string;
value: string;
domain: string;
path: string;
}>;
storage?: Record<string, string>;
viewport?: {
width: number;
height: number;
};
selected?: string[];
}
export interface ErrorResponse {
ok: false;
+1 -1
View File
File diff suppressed because one or more lines are too long
+400
View File
@@ -2,12 +2,19 @@ import { resolve } from 'node:path';
import { type Browser, type BrowserContext, type Page, type Route, webkit } from 'playwright';
import * as image from './image.js';
import type {
A11yNode,
BrowserCommand,
BrowserOptions,
CommandResponse,
ConsoleMessage,
CookiesCommand,
DialogCommand,
DialogEntry,
ElementInfo,
MetricsData,
NetworkEntry,
PageError,
StorageCommand,
} from './types.js';
export class ClaudeBrowser {
@@ -17,6 +24,9 @@ export class ClaudeBrowser {
private options: Required<BrowserOptions>;
private consoleMessages: ConsoleMessage[] = [];
private networkEntries: NetworkEntry[] = [];
private pageErrors: PageError[] = [];
private dialogHistory: DialogEntry[] = [];
private dialogConfig = { autoAccept: false, autoDismiss: false, promptText: '' };
private interceptPatterns: Map<
string,
{
@@ -44,6 +54,43 @@ export class ClaudeBrowser {
this.page = await this.context.newPage();
this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page);
this.setupErrorListener(this.page);
this.setupDialogListener(this.page);
}
private setupErrorListener(page: Page): void {
page.on('pageerror', (error) => {
this.pageErrors.push({
message: error.message,
stack: error.stack,
timestamp: Date.now(),
});
});
}
private setupDialogListener(page: Page): void {
page.on('dialog', async (dialog) => {
const entry: DialogEntry = {
type: dialog.type() as DialogEntry['type'],
message: dialog.message(),
defaultValue: dialog.defaultValue() || undefined,
timestamp: Date.now(),
};
if (this.dialogConfig.autoAccept) {
await dialog.accept(this.dialogConfig.promptText || undefined);
entry.response = this.dialogConfig.promptText || true;
} else if (this.dialogConfig.autoDismiss) {
await dialog.dismiss();
entry.response = false;
} else {
// Default: accept to prevent blocking
await dialog.accept();
entry.response = true;
}
this.dialogHistory.push(entry);
});
}
private setupConsoleListener(page: Page): void {
@@ -219,6 +266,8 @@ export class ClaudeBrowser {
this.page = await this.context.newPage();
this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page);
this.setupErrorListener(this.page);
this.setupDialogListener(this.page);
}
async eval(script: string): Promise<unknown> {
@@ -260,6 +309,129 @@ export class ClaudeBrowser {
this.networkEntries = [];
}
getErrors(clear = false): PageError[] {
const errors = this.pageErrors;
if (clear) {
this.pageErrors = [];
}
return errors;
}
clearErrors(): void {
this.pageErrors = [];
}
async getMetrics(includeResources = false): Promise<MetricsData> {
const page = this.ensurePage();
const metricsScript = `(() => {
const timing = performance.timing;
const navigationStart = timing.navigationStart;
const paintEntries = performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(e => e.name === 'first-paint');
const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
return {
timing: {
domContentLoaded: timing.domContentLoadedEventEnd - navigationStart,
load: timing.loadEventEnd - navigationStart,
firstPaint: firstPaint ? firstPaint.startTime : undefined,
firstContentfulPaint: fcp ? fcp.startTime : undefined,
},
dom: {
nodes: document.getElementsByTagName('*').length,
scripts: document.getElementsByTagName('script').length,
stylesheets: document.getElementsByTagName('link').length,
images: document.getElementsByTagName('img').length,
},
};
})()`;
const metrics = (await page.evaluate(metricsScript)) as MetricsData;
if (includeResources) {
const resourcesScript = `(() => {
return performance.getEntriesByType('resource').map(entry => ({
name: entry.name,
type: entry.initiatorType,
duration: Math.round(entry.duration),
size: entry.transferSize || 0,
}));
})()`;
const resources = (await page.evaluate(resourcesScript)) as MetricsData['resources'];
return { ...metrics, resources };
}
return metrics;
}
async getA11y(selector?: string): Promise<A11yNode | null> {
const page = this.ensurePage();
// Build a11y tree using ARIA attributes and semantic roles
const selectorArg = selector ? JSON.stringify(selector) : 'null';
const script = `((selector) => {
function getA11yNode(el) {
const role = el.getAttribute('role') || getImplicitRole(el);
const name = getAccessibleName(el);
const node = { role, name: name || undefined };
const value = el.value || el.getAttribute('aria-valuenow');
if (value) node.value = String(value);
const desc = el.getAttribute('aria-describedby');
if (desc) {
const descEl = document.getElementById(desc);
if (descEl) node.description = descEl.textContent?.trim();
}
const children = [];
for (const child of el.children) {
if (isAccessible(child)) children.push(getA11yNode(child));
}
if (children.length) node.children = children;
return node;
}
function getImplicitRole(el) {
const tag = el.tagName.toLowerCase();
const roleMap = { button:'button', a:'link', input:'textbox', img:'img',
h1:'heading', h2:'heading', h3:'heading', h4:'heading', nav:'navigation',
main:'main', footer:'contentinfo', header:'banner', aside:'complementary',
form:'form', table:'table', ul:'list', ol:'list', li:'listitem' };
return roleMap[tag] || 'generic';
}
function getAccessibleName(el) {
return el.getAttribute('aria-label') || el.getAttribute('alt')
|| el.getAttribute('title') || (el.tagName === 'INPUT' ? el.placeholder : null)
|| el.textContent?.trim().slice(0, 100);
}
function isAccessible(el) {
if (el.nodeType !== 1) return false;
if (el.getAttribute('aria-hidden') === 'true') return false;
const style = getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden';
}
const root = selector ? document.querySelector(selector) : document.body;
return root ? getA11yNode(root) : null;
})(${selectorArg})`;
return page.evaluate(script) as Promise<A11yNode | null>;
}
getDialogs(): DialogEntry[] {
return this.dialogHistory;
}
clearDialogs(): void {
this.dialogHistory = [];
}
setDialogConfig(config: { autoAccept?: boolean; autoDismiss?: boolean; text?: string }): void {
if (config.autoAccept !== undefined) this.dialogConfig.autoAccept = config.autoAccept;
if (config.autoDismiss !== undefined) this.dialogConfig.autoDismiss = config.autoDismiss;
if (config.text !== undefined) this.dialogConfig.promptText = config.text;
}
getDialogConfig(): { autoAccept: boolean; autoDismiss: boolean } {
return { autoAccept: this.dialogConfig.autoAccept, autoDismiss: this.dialogConfig.autoDismiss };
}
async addIntercept(
pattern: string,
action: 'block' | 'mock',
@@ -303,6 +475,188 @@ export class ClaudeBrowser {
return Array.from(this.interceptPatterns.keys());
}
// Phase 6: Cookies & Storage
async getCookies(
name?: string
): Promise<Array<{ name: string; value: string; domain: string; path: string }>> {
const context = this.getContext();
if (!context) throw new Error('Browser not launched');
const cookies = await context.cookies();
const filtered = name ? cookies.filter((c) => c.name === name) : cookies;
return filtered.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path }));
}
async setCookie(name: string, value: string, url?: string): Promise<void> {
const context = this.getContext();
if (!context) throw new Error('Browser not launched');
const page = this.ensurePage();
await context.addCookies([{ name, value, url: url || page.url() }]);
}
async deleteCookie(name: string): Promise<void> {
const context = this.getContext();
if (!context) throw new Error('Browser not launched');
const cookies = await context.cookies();
const toKeep = cookies.filter((c) => c.name !== name);
await context.clearCookies();
if (toKeep.length > 0) await context.addCookies(toKeep);
}
async clearCookies(): Promise<void> {
const context = this.getContext();
if (!context) throw new Error('Browser not launched');
await context.clearCookies();
}
async getStorage(type: 'local' | 'session', key?: string): Promise<Record<string, string>> {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
const script = key
? `({ [${JSON.stringify(key)}]: ${storage}.getItem(${JSON.stringify(key)}) || '' })`
: `Object.fromEntries(Array.from({ length: ${storage}.length }, (_, i) => {
const k = ${storage}.key(i); return [k, ${storage}.getItem(k)];
}))`;
return page.evaluate(script) as Promise<Record<string, string>>;
}
async setStorage(type: 'local' | 'session', key: string, value: string): Promise<void> {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
}
async deleteStorage(type: 'local' | 'session', key: string): Promise<void> {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.removeItem(${JSON.stringify(key)})`);
}
async clearStorage(type: 'local' | 'session'): Promise<void> {
const page = this.ensurePage();
const storage = type === 'local' ? 'localStorage' : 'sessionStorage';
await page.evaluate(`${storage}.clear()`);
}
// Phase 7: Advanced Interactions
async hover(selector: string): Promise<void> {
const page = this.ensurePage();
await page.hover(selector);
}
async select(selector: string, value: string | string[]): Promise<string[]> {
const page = this.ensurePage();
return page.selectOption(selector, value);
}
async keys(keys: string): Promise<void> {
const page = this.ensurePage();
await page.keyboard.press(keys);
}
async upload(selector: string, files: string[]): Promise<void> {
const page = this.ensurePage();
await page.setInputFiles(selector, files);
}
async scroll(selector?: string, x?: number, y?: number): Promise<void> {
const page = this.ensurePage();
if (selector) {
await page.locator(selector).scrollIntoViewIfNeeded();
} else {
await page.evaluate(`window.scrollTo(${x || 0}, ${y || 0})`);
}
}
// Phase 8: Viewport & Emulation
async setViewport(width: number, height: number): Promise<{ width: number; height: number }> {
const page = this.ensurePage();
await page.setViewportSize({ width, height });
return { width, height };
}
async emulate(device: string): Promise<{ width: number; height: number }> {
const page = this.ensurePage();
const { devices } = await import('playwright');
const deviceConfig = devices[device];
if (!deviceConfig)
throw new Error(`Unknown device: ${device}. Try 'iPhone 13', 'Pixel 5', etc.`);
await page.setViewportSize(deviceConfig.viewport);
return deviceConfig.viewport;
}
private handleDialogCommand(cmd: DialogCommand): CommandResponse {
switch (cmd.action) {
case 'status':
return { ok: true, dialogs: this.getDialogs(), dialogConfig: this.getDialogConfig() };
case 'accept':
this.setDialogConfig({ autoAccept: true, autoDismiss: false, text: cmd.text });
return { ok: true, dialogConfig: this.getDialogConfig() };
case 'dismiss':
this.setDialogConfig({ autoAccept: false, autoDismiss: true });
return { ok: true, dialogConfig: this.getDialogConfig() };
case 'config':
this.setDialogConfig({
autoAccept: cmd.autoAccept,
autoDismiss: cmd.autoDismiss,
text: cmd.text,
});
return { ok: true, dialogConfig: this.getDialogConfig() };
default:
return { ok: false, error: 'Unknown dialog action' };
}
}
private async handleCookiesCommand(cmd: CookiesCommand): Promise<CommandResponse> {
switch (cmd.action) {
case 'get': {
const cookies = await this.getCookies(cmd.name);
return { ok: true, cookies, count: cookies.length };
}
case 'set': {
if (!cmd.name || !cmd.value) return { ok: false, error: 'Name and value required' };
await this.setCookie(cmd.name, cmd.value, cmd.url);
return { ok: true };
}
case 'delete': {
if (!cmd.name) return { ok: false, error: 'Name required' };
await this.deleteCookie(cmd.name);
return { ok: true };
}
case 'clear': {
await this.clearCookies();
return { ok: true };
}
default:
return { ok: false, error: 'Unknown cookies action' };
}
}
private async handleStorageCommand(cmd: StorageCommand): Promise<CommandResponse> {
switch (cmd.action) {
case 'get': {
const storage = await this.getStorage(cmd.type, cmd.key);
return { ok: true, storage, count: Object.keys(storage).length };
}
case 'set': {
if (!cmd.key || cmd.value === undefined)
return { ok: false, error: 'Key and value required' };
await this.setStorage(cmd.type, cmd.key, cmd.value);
return { ok: true };
}
case 'delete': {
if (!cmd.key) return { ok: false, error: 'Key required' };
await this.deleteStorage(cmd.type, cmd.key);
return { ok: true };
}
case 'clear': {
await this.clearStorage(cmd.type);
return { ok: true };
}
default:
return { ok: false, error: 'Unknown storage action' };
}
}
async executeCommand(cmd: BrowserCommand): Promise<CommandResponse> {
try {
switch (cmd.cmd) {
@@ -385,6 +739,52 @@ export class ClaudeBrowser {
await this.addIntercept(cmd.pattern, cmd.action, cmd.response);
return { ok: true, patterns: this.getInterceptPatterns() };
}
case 'errors': {
const errors = this.getErrors(cmd.clear);
return { ok: true, count: errors.length, errors };
}
case 'metrics': {
const metrics = await this.getMetrics(cmd.resources);
return { ok: true, metrics };
}
case 'a11y': {
const a11y = await this.getA11y(cmd.selector);
return { ok: true, a11y: a11y || undefined };
}
case 'dialog':
return this.handleDialogCommand(cmd);
case 'cookies':
return this.handleCookiesCommand(cmd);
case 'storage':
return this.handleStorageCommand(cmd);
case 'hover': {
await this.hover(cmd.selector);
return { ok: true };
}
case 'select': {
const selected = await this.select(cmd.selector, cmd.value);
return { ok: true, selected };
}
case 'keys': {
await this.keys(cmd.keys);
return { ok: true };
}
case 'upload': {
await this.upload(cmd.selector, cmd.files);
return { ok: true };
}
case 'scroll': {
await this.scroll(cmd.selector, cmd.x, cmd.y);
return { ok: true };
}
case 'viewport': {
const viewport = await this.setViewport(cmd.width, cmd.height);
return { ok: true, viewport };
}
case 'emulate': {
const viewport = await this.emulate(cmd.device);
return { ok: true, viewport };
}
case 'favicon': {
const result = await image.createFavicon(cmd.input, cmd.outputDir);
return { ok: true, files: result.files, outputDir: result.outputDir };
+280
View File
@@ -259,6 +259,223 @@ server.tool(
})
);
// Page errors
server.tool(
'errors',
'Get captured page errors (uncaught exceptions and unhandled promise rejections)',
{
clear: z.boolean().optional().default(false).describe('Clear errors after retrieving'),
},
withLogging('errors', async ({ clear }) => {
await ensureLaunched();
const errors = browser.getErrors(clear);
return textResult(JSON.stringify({ ok: true, count: errors.length, errors }));
})
);
// Performance metrics
server.tool(
'metrics',
'Get page performance metrics and DOM statistics',
{
resources: z
.boolean()
.optional()
.default(false)
.describe('Include individual resource timing entries'),
},
withLogging('metrics', async ({ resources }) => {
await ensureLaunched();
const metrics = await browser.getMetrics(resources);
return textResult(JSON.stringify({ ok: true, metrics }));
})
);
// Accessibility
server.tool(
'a11y',
'Get accessibility tree snapshot for the page or a specific element',
{
selector: z.string().optional().describe('CSS selector to get subtree for specific element'),
},
withLogging('a11y', async ({ selector }) => {
await ensureLaunched();
const a11y = await browser.getA11y(selector);
return textResult(JSON.stringify({ ok: true, a11y }));
})
);
// Dialog handling
server.tool(
'dialog',
'Configure how browser dialogs (alert, confirm, prompt) are handled',
{
action: z
.enum(['status', 'accept', 'dismiss', 'config'])
.describe(
'Action: status (get history), accept (auto-accept), dismiss (auto-dismiss), config (set both)'
),
text: z.string().optional().describe('Text to enter for prompt dialogs when accepting'),
autoAccept: z.boolean().optional().describe('Auto-accept dialogs (for config action)'),
autoDismiss: z.boolean().optional().describe('Auto-dismiss dialogs (for config action)'),
},
withLogging('dialog', async ({ action, text, autoAccept, autoDismiss }) => {
await ensureLaunched();
if (action === 'status') {
return textResult(
JSON.stringify({
ok: true,
dialogs: browser.getDialogs(),
config: browser.getDialogConfig(),
})
);
}
if (action === 'accept') {
browser.setDialogConfig({ autoAccept: true, autoDismiss: false, text });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
}
if (action === 'dismiss') {
browser.setDialogConfig({ autoAccept: false, autoDismiss: true });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
}
browser.setDialogConfig({ autoAccept, autoDismiss, text });
return textResult(JSON.stringify({ ok: true, config: browser.getDialogConfig() }));
})
);
// Cookies
server.tool(
'cookies',
'Get, set, delete, or clear browser cookies',
{
action: z.enum(['get', 'set', 'delete', 'clear']).describe('Action to perform'),
name: z.string().optional().describe('Cookie name (for get/set/delete)'),
value: z.string().optional().describe('Cookie value (for set)'),
url: z.string().optional().describe('URL for cookie (for set, defaults to current page)'),
},
withLogging('cookies', async ({ action, name, value, url }) => {
await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'cookies', action, name, value, url });
return textResult(JSON.stringify(result));
})
);
// Storage
server.tool(
'storage',
'Get, set, delete, or clear localStorage or sessionStorage',
{
type: z.enum(['local', 'session']).describe('Storage type'),
action: z.enum(['get', 'set', 'delete', 'clear']).describe('Action to perform'),
key: z.string().optional().describe('Storage key'),
value: z.string().optional().describe('Value to set'),
},
withLogging('storage', async ({ type, action, key, value }) => {
await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'storage', type, action, key, value });
return textResult(JSON.stringify(result));
})
);
// Hover
server.tool(
'hover',
'Hover over an element to trigger hover states',
{ selector: z.string().describe('CSS selector of element to hover') },
withLogging('hover', async ({ selector }) => {
await ensureLaunched();
await browser.hover(selector);
return textResult(JSON.stringify({ ok: true }));
})
);
// Select
server.tool(
'select',
'Select option(s) in a dropdown/select element',
{
selector: z.string().describe('CSS selector of select element'),
value: z.union([z.string(), z.array(z.string())]).describe('Value(s) to select'),
},
withLogging('select', async ({ selector, value }) => {
await ensureLaunched();
const selected = await browser.select(selector, value);
return textResult(JSON.stringify({ ok: true, selected }));
})
);
// Keys
server.tool(
'keys',
'Send keyboard keys or shortcuts (e.g., "Enter", "Control+a", "Escape")',
{ keys: z.string().describe('Key or key combination to press') },
withLogging('keys', async ({ keys }) => {
await ensureLaunched();
await browser.keys(keys);
return textResult(JSON.stringify({ ok: true }));
})
);
// Upload
server.tool(
'upload',
'Upload files to a file input element',
{
selector: z.string().describe('CSS selector of file input'),
files: z.array(z.string()).describe('Array of file paths to upload'),
},
withLogging('upload', async ({ selector, files }) => {
await ensureLaunched();
await browser.upload(selector, files);
return textResult(JSON.stringify({ ok: true }));
})
);
// Scroll
server.tool(
'scroll',
'Scroll the page or an element into view',
{
selector: z.string().optional().describe('CSS selector to scroll into view'),
x: z.number().optional().describe('X position to scroll to (if no selector)'),
y: z.number().optional().describe('Y position to scroll to (if no selector)'),
},
withLogging('scroll', async ({ selector, x, y }) => {
await ensureLaunched();
await browser.scroll(selector, x, y);
return textResult(JSON.stringify({ ok: true }));
})
);
// Viewport
server.tool(
'viewport',
'Resize the browser viewport',
{
width: z.number().describe('Viewport width in pixels'),
height: z.number().describe('Viewport height in pixels'),
},
withLogging('viewport', async ({ width, height }) => {
await ensureLaunched();
const viewport = await browser.setViewport(width, height);
return textResult(JSON.stringify({ ok: true, viewport }));
})
);
// Emulate
server.tool(
'emulate',
'Emulate a mobile device (viewport, user agent, touch)',
{
device: z.string().describe('Device name (e.g., "iPhone 13", "Pixel 5", "iPad Pro")'),
},
withLogging('emulate', async ({ device }) => {
await ensureLaunched();
const viewport = await browser.emulate(device);
return textResult(JSON.stringify({ ok: true, device, viewport }));
})
);
// Utility
server.tool(
'wait',
@@ -681,6 +898,69 @@ server.resource(
}
);
// Resource: browser://errors - Captured page errors
server.resource(
'Page Errors',
'browser://errors',
{
description: 'Uncaught exceptions and unhandled promise rejections',
mimeType: 'application/json',
},
async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://errors',
mimeType: 'application/json',
text: JSON.stringify({ launched: false, errors: [] }),
},
],
};
}
const errors = browser.getErrors(false);
return {
contents: [
{
uri: 'browser://errors',
mimeType: 'application/json',
text: JSON.stringify({ launched: true, count: errors.length, errors }),
},
],
};
}
);
// Resource: browser://a11y - Accessibility tree snapshot
server.resource(
'Accessibility Tree',
'browser://a11y',
{ description: 'Accessibility tree snapshot of the current page', mimeType: 'application/json' },
async () => {
if (!launched) {
return {
contents: [
{
uri: 'browser://a11y',
mimeType: 'application/json',
text: JSON.stringify({ launched: false, a11y: null }),
},
],
};
}
const a11y = await browser.getA11y();
return {
contents: [
{
uri: 'browser://a11y',
mimeType: 'application/json',
text: JSON.stringify({ launched: true, a11y }),
},
],
};
}
);
// Resource: browser://screenshot - Current page screenshot (base64)
server.resource(
'Page Screenshot',
+158 -1
View File
@@ -171,6 +171,133 @@ export interface InterceptCommand {
};
}
export interface PageError {
message: string;
stack?: string;
timestamp: number;
}
export interface ErrorsCommand {
cmd: 'errors';
clear?: boolean;
}
export interface MetricsData {
timing: {
domContentLoaded?: number;
load?: number;
firstPaint?: number;
firstContentfulPaint?: number;
};
dom: {
nodes: number;
scripts: number;
stylesheets: number;
images: number;
};
resources?: Array<{
name: string;
type: string;
duration: number;
size: number;
}>;
}
export interface MetricsCommand {
cmd: 'metrics';
resources?: boolean;
}
export interface A11yNode {
role: string;
name?: string;
value?: string;
description?: string;
children?: A11yNode[];
}
export interface A11yCommand {
cmd: 'a11y';
selector?: string;
}
export interface DialogEntry {
type: 'alert' | 'confirm' | 'prompt' | 'beforeunload';
message: string;
defaultValue?: string;
response?: string | boolean;
timestamp: number;
}
export interface DialogCommand {
cmd: 'dialog';
action: 'status' | 'accept' | 'dismiss' | 'config';
text?: string;
autoAccept?: boolean;
autoDismiss?: boolean;
}
// Phase 6: Storage & Cookies
export interface CookiesCommand {
cmd: 'cookies';
action: 'get' | 'set' | 'delete' | 'clear';
name?: string;
value?: string;
domain?: string;
path?: string;
url?: string;
}
export interface StorageCommand {
cmd: 'storage';
type: 'local' | 'session';
action: 'get' | 'set' | 'delete' | 'clear';
key?: string;
value?: string;
}
// Phase 7: Advanced Interactions
export interface HoverCommand {
cmd: 'hover';
selector: string;
}
export interface SelectCommand {
cmd: 'select';
selector: string;
value: string | string[];
}
export interface KeysCommand {
cmd: 'keys';
keys: string;
}
export interface UploadCommand {
cmd: 'upload';
selector: string;
files: string[];
}
export interface ScrollCommand {
cmd: 'scroll';
selector?: string;
x?: number;
y?: number;
}
// Phase 8: Viewport & Emulation
export interface ViewportCommand {
cmd: 'viewport';
width: number;
height: number;
}
export interface EmulateCommand {
cmd: 'emulate';
device: string;
}
export type BrowserCommand =
| GotoCommand
| ClickCommand
@@ -194,7 +321,20 @@ export type BrowserCommand =
| ThumbnailCommand
| ConsoleCommand
| NetworkCommand
| InterceptCommand;
| InterceptCommand
| ErrorsCommand
| MetricsCommand
| A11yCommand
| DialogCommand
| CookiesCommand
| StorageCommand
| HoverCommand
| SelectCommand
| KeysCommand
| UploadCommand
| ScrollCommand
| ViewportCommand
| EmulateCommand;
// Response types
export interface SuccessResponse {
@@ -218,6 +358,23 @@ export interface SuccessResponse {
// Network fields
requests?: NetworkEntry[];
patterns?: string[];
// Error fields
errors?: PageError[];
// Metrics fields
metrics?: MetricsData;
// Accessibility fields
a11y?: A11yNode;
// Dialog fields
dialogs?: DialogEntry[];
dialogConfig?: { autoAccept: boolean; autoDismiss: boolean };
// Cookie fields
cookies?: Array<{ name: string; value: string; domain: string; path: string }>;
// Storage fields
storage?: Record<string, string>;
// Viewport fields
viewport?: { width: number; height: number };
// Selected values
selected?: string[];
}
export interface ErrorResponse {