💬 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:
+400
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user