#
tokens: 48276/50000 74/82 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/justasmonkev/mcp-accessibility-scanner?page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .idea
│   ├── .gitignore
│   ├── copilot.data.migration.agent.xml
│   ├── copilot.data.migration.edit.xml
│   ├── modules.xml
│   ├── New folder (3).iml
│   └── vcs.xml
├── cli.js
├── config.d.ts
├── eslint.config.mjs
├── glama.json
├── index.d.ts
├── index.js
├── LICENSE
├── NOTICE.md
├── package-lock.json
├── package.json
├── README.md
├── server.json
├── src
│   ├── actions.d.ts
│   ├── browserContextFactory.ts
│   ├── browserServerBackend.ts
│   ├── config.ts
│   ├── context.ts
│   ├── DEPS.list
│   ├── extension
│   │   ├── cdpRelay.ts
│   │   ├── DEPS.list
│   │   ├── extensionContextFactory.ts
│   │   └── protocol.ts
│   ├── external-modules.d.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── DEPS.list
│   │   ├── http.ts
│   │   ├── inProcessTransport.ts
│   │   ├── manualPromise.ts
│   │   ├── mdb.ts
│   │   ├── proxyBackend.ts
│   │   ├── README.md
│   │   ├── server.ts
│   │   └── tool.ts
│   ├── program.ts
│   ├── response.ts
│   ├── sessionLog.ts
│   ├── tab.ts
│   ├── tools
│   │   ├── common.ts
│   │   ├── console.ts
│   │   ├── DEPS.list
│   │   ├── dialogs.ts
│   │   ├── evaluate.ts
│   │   ├── files.ts
│   │   ├── form.ts
│   │   ├── install.ts
│   │   ├── keyboard.ts
│   │   ├── mouse.ts
│   │   ├── navigate.ts
│   │   ├── network.ts
│   │   ├── pdf.ts
│   │   ├── screenshot.ts
│   │   ├── snapshot.ts
│   │   ├── tabs.ts
│   │   ├── tool.ts
│   │   ├── utils.ts
│   │   ├── verify.ts
│   │   └── wait.ts
│   ├── tools.ts
│   ├── utils
│   │   ├── codegen.ts
│   │   ├── fileUtils.ts
│   │   ├── guid.ts
│   │   ├── log.ts
│   │   └── package.ts
│   └── vscode
│       ├── DEPS.list
│       ├── host.ts
│       └── main.ts
├── tests
│   ├── config.test.ts
│   ├── context.test.ts
│   ├── response.test.ts
│   ├── tab.test.ts
│   ├── tool-definitions.test.ts
│   ├── tools-common.test.ts
│   ├── tools-console.test.ts
│   ├── tools-navigate.test.ts
│   ├── tools-network.test.ts
│   ├── tools-tabs.test.ts
│   ├── tools-utils.test.ts
│   └── utils.test.ts
├── tsconfig.all.json
├── tsconfig.json
├── utils
│   └── copyright.js
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------

```
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
/tmp
/out-tsc

/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.pnp
.pnp.js

.vscode/*

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
build/
lib/
coverage/

```

--------------------------------------------------------------------------------
/src/mcp/README.md:
--------------------------------------------------------------------------------

```markdown
- Generic MCP utils, no dependencies on anything.

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown

# MCP Accessibility Scanner 🔍

[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/justasmonkev-mcp-accessibility-scanner-badge.png)](https://mseep.ai/app/justasmonkev-mcp-accessibility-scanner)

A Model Context Protocol (MCP) server that provides automated web accessibility scanning using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, capture annotated screenshots, and generate detailed accessibility reports.
A powerful Model Context Protocol (MCP) server that provides automated web accessibility scanning and browser automation using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, interact with web pages, manage persistent browser sessions, and generate detailed accessibility reports with visual annotations.

## Features

### Accessibility Scanning
✅ Full WCAG 2.0/2.1/2.2 compliance checking (A, AA, AAA levels)  
📄 Detailed JSON reports with remediation guidance  
🎯 Support for specific violation categories (color contrast, ARIA, forms, keyboard navigation, etc.)  

### Browser Automation
🖱️ Click, hover, and drag elements using accessibility snapshots  
⌨️ Type text and handle keyboard inputs  
🔍 Capture page snapshots to discover all interactive elements  
📸 Take screenshots and save PDFs  
🎯 Support for both element-based and coordinate-based interactions  

### Advanced Features
📑 Tab management for multi-page workflows  
🌐 Monitor console messages and network requests  
⏱️ Wait for dynamic content to load  
📁 Handle file uploads and browser dialogs  
🔄 Navigate through browser history

## Installation

You can install the package using any of these methods:

Using npm:
```bash
npm install -g mcp-accessibility-scanner
```

### Installation in VS Code

Install the Accessibility Scanner in VS Code using the VS Code CLI:

For VS Code:
```bash
code --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
```

For VS Code Insiders:
```bash
code-insiders --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
```

## Configuration

Here's the Claude Desktop configuration:

```json
{
  "mcpServers": {
    "accessibility-scanner": {
      "command": "npx",
      "args": ["-y", "mcp-accessibility-scanner"]
    }
  }
}
```

### Advanced Configuration

You can pass a configuration file to customize Playwright behavior:

```json
{
  "mcpServers": {
    "accessibility-scanner": {
      "command": "npx",
      "args": ["-y", "mcp-accessibility-scanner", "--config", "/path/to/config.json"]
    }
  }
}
```

#### Configuration Options

Create a `config.json` file with the following options:

```json
{
  "browser": {
    "browserName": "chromium",
    "launchOptions": {
      "headless": true,
      "channel": "chrome"
    }
  },
  "timeouts": {
    "navigationTimeout": 60000,
    "defaultTimeout": 5000
  },
  "network": {
    "allowedOrigins": ["example.com", "trusted-site.com"],
    "blockedOrigins": ["ads.example.com"]
  }
}
```

**Available Options:**

- `browser.browserName`: Browser to use (`chromium`, `firefox`, `webkit`)
- `browser.launchOptions.headless`: Run browser in headless mode (default: `true` on Linux without display, `false` otherwise)
- `browser.launchOptions.channel`: Browser channel (`chrome`, `chrome-beta`, `msedge`, etc.)
- `timeouts.navigationTimeout`: Maximum time for page navigation in milliseconds (default: `60000`)
- `timeouts.defaultTimeout`: Default timeout for Playwright operations in milliseconds (default: `5000`)
- `network.allowedOrigins`: List of origins to allow (blocks all others if specified)
- `network.blockedOrigins`: List of origins to block

## Available Tools

The MCP server provides comprehensive browser automation and accessibility scanning tools:

### Core Accessibility Tool

#### `scan_page`
Performs a comprehensive accessibility scan on the current page using Axe-core.

**Parameters:**
- `violationsTag`: Array of WCAG/violation tags to check

**Supported Violation Tags:**
- WCAG standards: `wcag2a`, `wcag2aa`, `wcag2aaa`, `wcag21a`, `wcag21aa`, `wcag21aaa`, `wcag22a`, `wcag22aa`, `wcag22aaa`
- Section 508: `section508`
- Categories: `cat.aria`, `cat.color`, `cat.forms`, `cat.keyboard`, `cat.language`, `cat.name-role-value`, `cat.parsing`, `cat.semantics`, `cat.sensory-and-visual-cues`, `cat.structure`, `cat.tables`, `cat.text-alternatives`, `cat.time-and-media`

### Navigation Tools

#### `browser_navigate`
Navigate to a URL.
- Parameters: `url` (string)

#### `browser_navigate_back`
Go back to the previous page.

#### `browser_navigate_forward`
Go forward to the next page.

### Page Interaction Tools

#### `browser_snapshot`
Capture accessibility snapshot of the current page (better than screenshot for analysis).

#### `browser_click`
Perform click on a web page element.
- Parameters: `element` (description), `ref` (element reference), `doubleClick` (optional)

#### `browser_type`
Type text into editable element.
- Parameters: `element`, `ref`, `text`, `submit` (optional), `slowly` (optional)

#### `browser_hover`
Hover over element on page.
- Parameters: `element`, `ref`

#### `browser_drag`
Perform drag and drop between two elements.
- Parameters: `startElement`, `startRef`, `endElement`, `endRef`

#### `browser_select_option`
Select an option in a dropdown.
- Parameters: `element`, `ref`, `values` (array)

#### `browser_press_key`
Press a key on the keyboard.
- Parameters: `key` (e.g., 'ArrowLeft' or 'a')

### Screenshot & Visual Tools

#### `browser_take_screenshot`
Take a screenshot of the current page.
- Parameters: `raw` (optional), `filename` (optional), `element` (optional), `ref` (optional)

#### `browser_pdf_save`
Save page as PDF.
- Parameters: `filename` (optional, defaults to `page-{timestamp}.pdf`)

### Browser Management

#### `browser_close`
Close the page.

#### `browser_resize`
Resize the browser window.
- Parameters: `width`, `height`

### Tab Management

#### `browser_tab_list`
List all open browser tabs.

#### `browser_tab_new`
Open a new tab.
- Parameters: `url` (optional)

#### `browser_tab_select`
Select a tab by index.
- Parameters: `index`

#### `browser_tab_close`
Close a tab.
- Parameters: `index` (optional, closes current tab if not provided)

### Information & Monitoring Tools

#### `browser_console_messages`
Returns all console messages from the page.

#### `browser_network_requests`
Returns all network requests since loading the page.

### Utility Tools

#### `browser_wait_for`
Wait for text to appear/disappear or time to pass.
- Parameters: `time` (optional), `text` (optional), `textGone` (optional)

#### `browser_handle_dialog`
Handle browser dialogs (alerts, confirms, prompts).
- Parameters: `accept` (boolean), `promptText` (optional)

#### `browser_file_upload`
Upload files to the page.
- Parameters: `paths` (array of absolute file paths)

### Vision Mode Tools (Coordinate-based Interaction)

#### `browser_screen_capture`
Take a screenshot for coordinate-based interaction.

#### `browser_screen_move_mouse`
Move mouse to specific coordinates.
- Parameters: `element`, `x`, `y`

#### `browser_screen_click`
Click at specific coordinates.
- Parameters: `element`, `x`, `y`

#### `browser_screen_drag`
Drag from one coordinate to another.
- Parameters: `element`, `startX`, `startY`, `endX`, `endY`

#### `browser_screen_type`
Type text (coordinate-independent).
- Parameters: `text`, `submit` (optional)

## Usage Examples

### Basic Accessibility Scan
```
1. Navigate to example.com using browser_navigate
2. Run scan_page with violationsTag: ["wcag21aa"]
```

### Color Contrast Check
```
1. Use browser_navigate to go to example.com
2. Run scan_page with violationsTag: ["cat.color"]
```

### Multi-step Workflow
```
1. Navigate to example.com with browser_navigate
2. Take a browser_snapshot to see available elements
3. Click the "Sign In" button using browser_click
4. Type "[email protected]" using browser_type
5. Run scan_page on the login page
6. Take a browser_take_screenshot to capture the final state
```

### Page Analysis
```
1. Navigate to example.com
2. Use browser_snapshot to capture all interactive elements
3. Review console messages with browser_console_messages
4. Check network activity with browser_network_requests
```

### Tab Management
```
1. Open a new tab with browser_tab_new
2. Navigate to different pages in each tab
3. Switch between tabs using browser_tab_select
4. List all tabs with browser_tab_list
```

### Waiting for Dynamic Content
```
1. Navigate to a page
2. Use browser_wait_for to wait for specific text to appear
3. Interact with the dynamically loaded content
```

**Note:** Most interaction tools require element references from browser_snapshot. Always capture a snapshot before attempting to interact with page elements.

## Development

Clone and set up the project:
```bash
git clone https://github.com/JustasMonkev/mcp-accessibility-scanner.git
cd mcp-accessibility-scanner
npm install
```

## License

MIT


```

--------------------------------------------------------------------------------
/tsconfig.all.json:
--------------------------------------------------------------------------------

```json
{
  "extends": "./tsconfig.json",
  "include": ["**/*.ts", "**/*.js"],
}

```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
{
	"$schema": "https://glama.ai/mcp/schemas/server.json",
	"maintainers": ["JustasMonkev"]
}

```

--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/.idea/copilot.data.migration.edit.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="EditMigrationStateService">
    <option name="migrationStatus" value="COMPLETED" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/.idea/copilot.data.migration.agent.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="AgentMigrationStateService">
    <option name="migrationStatus" value="COMPLETED" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/New folder (3).iml" filepath="$PROJECT_DIR$/.idea/New folder (3).iml" />
    </modules>
  </component>
</project>
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    // --- Module & JS Target ---
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",

    // --- Output ---
    "rootDir": "src",
    "outDir": "./lib",

    // --- Developer Experience & Interop ---
    "strict": true,
    "sourceMap": true, // <-- ADDED: Crucial for debugging!
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": [
    "src"
  ]
}

```

--------------------------------------------------------------------------------
/NOTICE.md:
--------------------------------------------------------------------------------

```markdown
# NOTICE

## mcp-accessibility-scanner

Copyright (c) 2024 Justas Monkev

This project is licensed under the MIT License.

## Third-Party Code Attribution

This project includes code adapted from:

### Microsoft Playwright MCP
- Copyright (c) Microsoft Corporation
- Licensed under the Apache License, Version 2.0
- Original source: https://github.com/microsoft/playwright-mcp

Adapted code has been modified to:
- Convert from ESM to CommonJS module format
- Integrate with axe-core accessibility scanning
- Work with existing Playwright instances rather than spawning new ones

```

--------------------------------------------------------------------------------
/utils/copyright.js:
--------------------------------------------------------------------------------

```javascript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

```

--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import './lib/program.js';

```

--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
  "name": "io.github.JustasMonkev/mcp-accessibility-scanner",
  "description": "MCP server for automated web accessibility scanning with Playwright and Axe-core.",
  "status": "active",
  "repository": {
    "url": "https://github.com/JustasMonkev/mcp-accessibility-scanner",
    "source": "github"
  },
  "version": "1.1.1",
  "packages": [
    {
      "registry_type": "npm",
      "registry_base_url": "https://registry.npmjs.org",
      "identifier": "mcp-accessibility-scanner",
      "version": "1.1.1",
      "transport": { "type": "stdio" },
      "environment_variables": []
    }
  ]
}

```

--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { createConnection } from './lib/index.js';
export { createConnection };

```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: ['**']
  pull_request:
    branches: ['**']

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [24]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run typecheck
        run: npx tsc --noEmit

      - name: Run lint
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

```

--------------------------------------------------------------------------------
/src/utils/log.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debug from 'debug';

const errorsDebug = debug('pw:mcp:errors');

export function logUnhandledError(error: unknown) {
  errorsDebug(error);
}

export const testDebug = debug('pw:mcp:test');

```

--------------------------------------------------------------------------------
/src/utils/guid.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import crypto from 'crypto';

export function createGuid(): string {
  return crypto.randomBytes(16).toString('hex');
}

export function createHash(data: string): string {
  return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
}

```

--------------------------------------------------------------------------------
/src/utils/package.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import fs from 'fs';
import path from 'path';
import url from 'url';

const __filename = url.fileURLToPath(import.meta.url);
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8'));

```

--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Config } from './config.js';
import type { BrowserContext } from 'playwright';

export type Connection = {
  server: Server;
  close(): Promise<void>;
};

export declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Connection>;
export {};

```

--------------------------------------------------------------------------------
/src/tools/console.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

const console = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_console_messages',
    title: 'Get console messages',
    description: 'Returns all console messages',
    inputSchema: z.object({}),
    type: 'readOnly',
  },
  handle: async (tab, params, response) => {
    tab.consoleMessages().map(message => response.addResult(message.toString()));
  },
});

export default [
  console,
];

```

--------------------------------------------------------------------------------
/src/extension/protocol.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Whenever the commands/events change, the version must be updated. The latest
// extension version should be compatible with the old MCP clients.
export const VERSION = 1;

export type ExtensionCommand = {
  'attachToTab': {
    params: {};
  };
  'forwardCDPCommand': {
    params: {
      method: string,
      sessionId?: string
      params?: any,
    };
  };
};

export type ExtensionEvents = {
  'forwardCDPEvent': {
    params: {
      method: string,
      sessionId?: string
      params?: any,
    };
  };
};

```

--------------------------------------------------------------------------------
/src/mcp/tool.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { zodToJsonSchema } from 'zod-to-json-schema';

import type { z } from 'zod';
import type * as mcpServer from './server.js';

export type ToolSchema<Input extends z.Schema> = {
  name: string;
  title: string;
  description: string;
  inputSchema: Input;
  type: 'readOnly' | 'destructive';
};

export function toMcpTool(tool: ToolSchema<any>): mcpServer.Tool {
  return {
    name: tool.name,
    description: tool.description,
    inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as mcpServer.Tool['inputSchema'],
    annotations: {
      title: tool.title,
      readOnlyHint: tool.type === 'readOnly',
      destructiveHint: tool.type === 'destructive',
      openWorldHint: true,
    },
  };
}

export function defineToolSchema<Input extends z.Schema>(tool: ToolSchema<Input>): ToolSchema<Input> {
  return tool;
}

```

--------------------------------------------------------------------------------
/src/tools/pdf.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

import * as javascript from '../utils/codegen.js';

const pdfSchema = z.object({
  filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
});

const pdf = defineTabTool({
  capability: 'pdf',

  schema: {
    name: 'browser_pdf_save',
    title: 'Save as PDF',
    description: 'Save page as PDF',
    inputSchema: pdfSchema,
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`);
    response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
    response.addResult(`Saved page as ${fileName}`);
    await tab.page.pdf({ path: fileName });
  },
});

export default [
  pdf,
];

```

--------------------------------------------------------------------------------
/src/tools/network.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

import type * as playwright from 'playwright';

const requests = defineTabTool({
  capability: 'core',

  schema: {
    name: 'browser_network_requests',
    title: 'List network requests',
    description: 'Returns all network requests since loading the page',
    inputSchema: z.object({}),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const requests = tab.requests();
    [...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res)));
  },
});

function renderRequest(request: playwright.Request, response: playwright.Response | null) {
  const result: string[] = [];
  result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
  if (response)
    result.push(`=> [${response.status()}] ${response.statusText()}`);
  return result.join(' ');
}

export default [
  requests,
];

```

--------------------------------------------------------------------------------
/src/utils/fileUtils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import os from 'node:os';
import path from 'node:path';

export function cacheDir() {
  let cacheDirectory: string;
  if (process.platform === 'linux')
    cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
  else if (process.platform === 'darwin')
    cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
  else if (process.platform === 'win32')
    cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
  else
    throw new Error('Unsupported platform: ' + process.platform);
  return path.join(cacheDirectory, 'ms-playwright');
}

export function sanitizeForFilePath(s: string) {
  const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
  const separator = s.lastIndexOf('.');
  if (separator === -1)
    return sanitize(s);
  return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1));
}

```

--------------------------------------------------------------------------------
/src/tools/files.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

const uploadFile = defineTabTool({
  capability: 'core',

  schema: {
    name: 'browser_file_upload',
    title: 'Upload files',
    description: 'Upload one or multiple files',
    inputSchema: z.object({
      paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
    if (!modalState)
      throw new Error('No file chooser visible');

    response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);

    tab.clearModalState(modalState);
    await tab.waitForCompletion(async () => {
      await modalState.fileChooser.setFiles(params.paths);
    });
  },
  clearsModalState: 'fileChooser',
});

export default [
  uploadFile,
];

```

--------------------------------------------------------------------------------
/src/tools/dialogs.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

const handleDialog = defineTabTool({
  capability: 'core',

  schema: {
    name: 'browser_handle_dialog',
    title: 'Handle a dialog',
    description: 'Handle a dialog',
    inputSchema: z.object({
      accept: z.boolean().describe('Whether to accept the dialog.'),
      promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const dialogState = tab.modalStates().find(state => state.type === 'dialog');
    if (!dialogState)
      throw new Error('No dialog visible');

    tab.clearModalState(dialogState);
    await tab.waitForCompletion(async () => {
      if (params.accept)
        await dialogState.dialog.accept(params.promptText);
      else
        await dialogState.dialog.dismiss();
    });
  },

  clearsModalState: 'dialog',
});

export default [
  handleDialog,
];

```

--------------------------------------------------------------------------------
/src/tools/navigate.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTool, defineTabTool } from './tool.js';

const navigate = defineTool({
  capability: 'core',

  schema: {
    name: 'browser_navigate',
    title: 'Navigate to a URL',
    description: 'Navigate to a URL',
    inputSchema: z.object({
      url: z.string().describe('The URL to navigate to'),
    }),
    type: 'destructive',
  },

  handle: async (context, params, response) => {
    const tab = await context.ensureTab();
    await tab.navigate(params.url);

    response.setIncludeSnapshot();
    response.addCode(`await page.goto('${params.url}');`);
  },
});

const goBack = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_navigate_back',
    title: 'Go back',
    description: 'Go back to the previous page',
    inputSchema: z.object({}),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    await tab.page.goBack();
    response.setIncludeSnapshot();
    response.addCode(`await page.goBack();`);
  },
});

export default [
  navigate,
  goBack,
];

```

--------------------------------------------------------------------------------
/src/tools/common.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool, defineTool } from './tool.js';

const close = defineTool({
  capability: 'core',
  schema: {
    name: 'browser_close',
    title: 'Close browser',
    description: 'Close the page',
    inputSchema: z.object({}),
    type: 'readOnly',
  },

  handle: async (context, params, response) => {
    await context.closeBrowserContext();
    response.setIncludeTabs();
    response.addCode(`await page.close()`);
  },
});

const resize = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_resize',
    title: 'Resize browser window',
    description: 'Resize the browser window',
    inputSchema: z.object({
      width: z.number().describe('Width of the browser window'),
      height: z.number().describe('Height of the browser window'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);

    await tab.waitForCompletion(async () => {
      await tab.page.setViewportSize({ width: params.width, height: params.height });
    });
  },
});

export default [
  close,
  resize
];

```

--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import common from './tools/common.js';
import console from './tools/console.js';
import dialogs from './tools/dialogs.js';
import evaluate from './tools/evaluate.js';
import files from './tools/files.js';
import form from './tools/form.js';
import install from './tools/install.js';
import keyboard from './tools/keyboard.js';
import mouse from './tools/mouse.js';
import navigate from './tools/navigate.js';
import network from './tools/network.js';
import pdf from './tools/pdf.js';
import snapshot from './tools/snapshot.js';
import tabs from './tools/tabs.js';
import screenshot from './tools/screenshot.js';
import wait from './tools/wait.js';
import verify from './tools/verify.js';

import type { Tool } from './tools/tool.js';
import type { FullConfig } from './config.js';

export const allTools: Tool<any>[] = [
  ...common,
  ...console,
  ...dialogs,
  ...evaluate,
  ...files,
  ...form,
  ...install,
  ...keyboard,
  ...navigate,
  ...network,
  ...mouse,
  ...pdf,
  ...screenshot,
  ...snapshot,
  ...tabs,
  ...wait,
  ...verify,
];

export function filteredTools(config: FullConfig) {
  return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
}

```

--------------------------------------------------------------------------------
/src/tools/install.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { fork } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import { z } from 'zod';
import { defineTool } from './tool.js';


const install = defineTool({
  capability: 'core-install',
  schema: {
    name: 'browser_install',
    title: 'Install the browser specified in the config',
    description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
    inputSchema: z.object({}),
    type: 'destructive',
  },

  handle: async (context, params, response) => {
    const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
    const cliUrl = import.meta.resolve('playwright/package.json');
    const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
    const child = fork(cliPath, ['install', channel], {
      stdio: 'pipe',
    });
    const output: string[] = [];
    child.stdout?.on('data', data => output.push(data.toString()));
    child.stderr?.on('data', data => output.push(data.toString()));
    await new Promise<void>((resolve, reject) => {
      child.on('close', code => {
        if (code === 0)
          resolve();
        else
          reject(new Error(`Failed to install browser: ${output.join('')}`));
      });
    });
    response.setIncludeTabs();
  },
});

export default [
  install,
];

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BrowserServerBackend } from './browserServerBackend.js';
import { resolveConfig } from './config.js';
import { contextFactory } from './browserContextFactory.js';
import * as mcpServer from './mcp/server.js';
import { packageJSON } from './utils/package.js';

import type { Config } from '../config.js';
import type { BrowserContext } from 'playwright';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';

export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
  const config = await resolveConfig(userConfig);
  const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config);
  return mcpServer.createServer('Playwright', packageJSON.version, new BrowserServerBackend(config, factory), false);
}

class SimpleBrowserContextFactory implements BrowserContextFactory {
  name = 'custom';
  description = 'Connect to a browser using a custom context getter';

  private readonly _contextGetter: () => Promise<BrowserContext>;

  constructor(contextGetter: () => Promise<BrowserContext>) {
    this._contextGetter = contextGetter;
  }

  async createContext(): Promise<{ browserContext: BrowserContext, close: () => Promise<void> }> {
    const browserContext = await this._contextGetter();
    return {
      browserContext,
      close: () => browserContext.close()
    };
  }
}

```

--------------------------------------------------------------------------------
/src/utils/codegen.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// adapted from:
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts

// NOTE: this function should not be used to escape any selectors.
export function escapeWithQuotes(text: string, char: string = '\'') {
  const stringified = JSON.stringify(text);
  const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
  if (char === '\'')
    return char + escapedText.replace(/[']/g, '\\\'') + char;
  if (char === '"')
    return char + escapedText.replace(/["]/g, '\\"') + char;
  if (char === '`')
    return char + escapedText.replace(/[`]/g, '\\`') + char;
  throw new Error('Invalid escape char');
}

export function quote(text: string) {
  return escapeWithQuotes(text, '\'');
}

export function formatObject(value: any, indent = '  '): string {
  if (typeof value === 'string')
    return quote(value);
  if (Array.isArray(value))
    return `[${value.map(o => formatObject(o)).join(', ')}]`;
  if (typeof value === 'object') {
    const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
    if (!keys.length)
      return '{}';
    const tokens: string[] = [];
    for (const key of keys)
      tokens.push(`${key}: ${formatObject(value[key])}`);
    return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
  }
  return String(value);
}

```

--------------------------------------------------------------------------------
/src/tools/evaluate.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';

import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

import type * as playwright from 'playwright';

const evaluateSchema = z.object({
  function: z.string().describe('() => { /* code */ } or (element) => { /* code */ } when element is provided'),
  element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
  ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
});

const evaluate = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_evaluate',
    title: 'Evaluate JavaScript',
    description: 'Evaluate JavaScript expression on page or element',
    inputSchema: evaluateSchema,
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    let locator: playwright.Locator | undefined;
    if (params.ref && params.element) {
      locator = await tab.refLocator({ ref: params.ref, element: params.element });
      response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
    } else {
      response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
    }

    await tab.waitForCompletion(async () => {
      const receiver = locator ?? tab.page as any;
      const result = await receiver._evaluateFunction(params.function);
      response.addResult(JSON.stringify(result, null, 2) || 'undefined');
    });
  },
});

export default [
  evaluate,
];

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
	"name": "mcp-accessibility-scanner",
	"version": "2.0.3",
	"mcpName": "io.github.JustasMonkev/mcp-accessibility-scanner",
	"description": "A Model Context Protocol (MCP) server for performing automated accessibility scans of web pages using Playwright and Axe-core",
	"type": "module",
	"exports": {
		"./package.json": "./package.json",
		".": {
			"types": "./index.d.ts",
			"default": "./index.js"
		}
	},
	"bin": {
		"mcp-server-playwright": "cli.js"
	},
	"files": [
		"lib/**/*",
		"README.md",
		"LICENSE",
		"NOTICE.md"
	],
	"scripts": {
		"build": "tsc --project tsconfig.json",
		"lint": "eslint . && tsc --noEmit",
		"watch": "tsc --watch",
		"run-server": "node lib/browserServer.js",
		"clean": "rm -rf lib",
		"npm-publish": "npm run clean && npm run build && npm publish",
		"test": "vitest run",
		"test:watch": "vitest",
		"test:ui": "vitest --ui",
		"test:coverage": "vitest run --coverage"
	},
	"dependencies": {
		"@axe-core/playwright": "^4.11.0",
		"@cfworker/json-schema": "^4.1.1",
		"@modelcontextprotocol/sdk": "^1.24.0",
		"commander": "^14.0.1",
		"debug": "^4.4.3",
		"dotenv": "^17.2.2",
		"mime": "^4.1.0",
		"playwright": "^1.55.0",
		"playwright-core": "^1.55.0",
		"ws": "^8.18.3",
		"zod-to-json-schema": "^3.24.6"
	},
	"devDependencies": {
		"@eslint/eslintrc": "^3.3.1",
		"@eslint/js": "^9.36.0",
		"@playwright/test": "^1.55.0",
		"@stylistic/eslint-plugin": "^5.3.1",
		"@types/chrome": "^0.1.12",
		"@types/debug": "^4.1.12",
		"@types/node": "^24.5.2",
		"@types/ws": "^8.18.1",
		"@typescript-eslint/eslint-plugin": "^8.44.0",
		"@typescript-eslint/parser": "^8.44.0",
		"@typescript-eslint/utils": "^8.44.0",
		"@vitest/coverage-v8": "^4.0.7",
		"@vitest/ui": "^4.0.7",
		"eslint": "^9.35.0",
		"eslint-plugin-import": "^2.32.0",
		"eslint-plugin-notice": "^1.0.0",
		"happy-dom": "^20.0.10",
		"ts-node": "^10.9.2",
		"typescript": "^5.9.2",
		"vitest": "^4.0.7"
	},
	"keywords": [
		"mcp",
		"accessibility",
		"a11y",
		"wcag",
		"axe-core",
		"playwright",
		"claude",
		"model-context-protocol"
	],
	"author": "",
	"license": "MIT",
	"repository": {
		"type": "git",
		"url": "git+https://github.com/JustasMonkev/mcp-accessibility-scanner.git"
	},
	"engines": {
		"node": ">=16.0.0"
	},
	"publishConfig": {
		"access": "public"
	}
}

```

--------------------------------------------------------------------------------
/tests/utils.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect } from 'vitest';
import { createGuid, createHash } from '../src/utils/guid.js';

describe('Utils', () => {
  describe('createGuid', () => {
    it('should generate unique identifiers', () => {
      const id1 = createGuid();
      const id2 = createGuid();
      const id3 = createGuid();

      expect(id1).not.toBe(id2);
      expect(id2).not.toBe(id3);
      expect(id1).not.toBe(id3);
    });

    it('should return strings', () => {
      const id = createGuid();
      expect(typeof id).toBe('string');
    });

    it('should generate non-empty strings', () => {
      const id = createGuid();
      expect(id.length).toBeGreaterThan(0);
    });

    it('should generate hex strings', () => {
      const ids = Array.from({ length: 10 }, () => createGuid());

      ids.forEach(id => {
        expect(id).toMatch(/^[a-f0-9]+$/);
        expect(id.length).toBe(32); // 16 bytes = 32 hex chars
      });
    });
  });

  describe('createHash', () => {
    it('should generate hash from data', () => {
      const hash = createHash('test data');
      expect(hash).toBeDefined();
      expect(typeof hash).toBe('string');
    });

    it('should generate consistent hashes', () => {
      const hash1 = createHash('test');
      const hash2 = createHash('test');
      expect(hash1).toBe(hash2);
    });

    it('should generate different hashes for different data', () => {
      const hash1 = createHash('test1');
      const hash2 = createHash('test2');
      expect(hash1).not.toBe(hash2);
    });

    it('should generate 7 character hashes', () => {
      const hash = createHash('test data');
      expect(hash.length).toBe(7);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tools/wait.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTool } from './tool.js';

const wait = defineTool({
  capability: 'core',

  schema: {
    name: 'browser_wait_for',
    title: 'Wait for',
    description: 'Wait for text to appear or disappear or a specified time to pass',
    inputSchema: z.object({
      time: z.number().optional().describe('The time to wait in seconds'),
      text: z.string().optional().describe('The text to wait for'),
      textGone: z.string().optional().describe('The text to wait for to disappear'),
    }),
    type: 'readOnly',
  },

  handle: async (context, params, response) => {
    if (!params.text && !params.textGone && !params.time)
      throw new Error('Either time, text or textGone must be provided');

    if (params.time) {
      response.addCode(`await new Promise(f => setTimeout(f, ${params.time!} * 1000));`);
      await new Promise(f => setTimeout(f, Math.min(30000, params.time! * 1000)));
    }

    const tab = context.currentTabOrDie();
    const locator = params.text ? tab.page.getByText(params.text).first() : undefined;
    const goneLocator = params.textGone ? tab.page.getByText(params.textGone).first() : undefined;

    if (goneLocator) {
      response.addCode(`await page.getByText(${JSON.stringify(params.textGone)}).first().waitFor({ state: 'hidden' });`);
      await goneLocator.waitFor({ state: 'hidden' });
    }

    if (locator) {
      response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
      await locator.waitFor({ state: 'visible' });
    }

    response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
    response.setIncludeSnapshot();
  },
});

export default [
  wait,
];

```

--------------------------------------------------------------------------------
/src/tools/form.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';

import { defineTabTool } from './tool.js';
import { generateLocator } from './utils.js';
import * as javascript from '../utils/codegen.js';

const fillForm = defineTabTool({
  capability: 'core',

  schema: {
    name: 'browser_fill_form',
    title: 'Fill form',
    description: 'Fill multiple form fields',
    inputSchema: z.object({
      fields: z.array(z.object({
        name: z.string().describe('Human-readable field name'),
        type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
        ref: z.string().describe('Exact target field reference from the page snapshot'),
        value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
      })).describe('Fields to fill in'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    for (const field of params.fields) {
      const locator = await tab.refLocator({ element: field.name, ref: field.ref });
      const locatorSource = `await page.${await generateLocator(locator)}`;
      if (field.type === 'textbox' || field.type === 'slider') {
        await locator.fill(field.value);
        response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`);
      } else if (field.type === 'checkbox' || field.type === 'radio') {
        await locator.setChecked(field.value === 'true');
        response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`);
      } else if (field.type === 'combobox') {
        await locator.selectOption({ label: field.value });
        response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`);
      }
    }
  },
});

export default [
  fillForm,
];

```

--------------------------------------------------------------------------------
/src/tools/tool.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type { z } from 'zod';
import type { Context } from '../context.js';
import type * as playwright from 'playwright';
import type { ToolCapability } from '../../config.js';
import type { Tab } from '../tab.js';
import type { Response } from '../response.js';
import type { ToolSchema } from '../mcp/tool.js';

export type FileUploadModalState = {
  type: 'fileChooser';
  description: string;
  fileChooser: playwright.FileChooser;
};

export type DialogModalState = {
  type: 'dialog';
  description: string;
  dialog: playwright.Dialog;
};

export type ModalState = FileUploadModalState | DialogModalState;

export type Tool<Input extends z.Schema = z.Schema> = {
  capability: ToolCapability;
  schema: ToolSchema<Input>;
  handle: (context: Context, params: z.output<Input>, response: Response) => Promise<void>;
};

export function defineTool<Input extends z.Schema>(tool: Tool<Input>): Tool<Input> {
  return tool;
}

export type TabTool<Input extends z.Schema = z.Schema> = {
  capability: ToolCapability;
  schema: ToolSchema<Input>;
  clearsModalState?: ModalState['type'];
  handle: (tab: Tab, params: z.output<Input>, response: Response) => Promise<void>;
};

export function defineTabTool<Input extends z.Schema>(tool: TabTool<Input>): Tool<Input> {
  return {
    ...tool,
    handle: async (context, params, response) => {
      const tab = context.currentTabOrDie();
      const modalStates = tab.modalStates().map(state => state.type);
      if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
        response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
      else if (!tool.clearsModalState && modalStates.length)
        response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
      else
        return tool.handle(tab, params, response);
    },
  };
}

```

--------------------------------------------------------------------------------
/src/extension/extensionContextFactory.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debug from 'debug';
import * as playwright from 'playwright';
import { startHttpServer } from '../mcp/http.js';
import { CDPRelayServer } from './cdpRelay.js';

import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';

const debugLogger = debug('pw:mcp:relay');

export class ExtensionContextFactory implements BrowserContextFactory {
  private _browserChannel: string;
  private _userDataDir?: string;
  private _executablePath?: string;

  constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) {
    this._browserChannel = browserChannel;
    this._userDataDir = userDataDir;
    this._executablePath = executablePath;
  }

  async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
    const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName);
    return {
      browserContext: browser.contexts()[0],
      close: async () => {
        debugLogger('close() called for browser context');
        await browser.close();
      }
    };
  }

  private async _obtainBrowser(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<playwright.Browser> {
    const relay = await this._startRelay(abortSignal);
    await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
    return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
  }

  private async _startRelay(abortSignal: AbortSignal) {
    const httpServer = await startHttpServer({});
    if (abortSignal.aborted) {
      httpServer.close();
      throw new Error(abortSignal.reason);
    }
    const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath);
    abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
    debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
    return cdpRelayServer;
  }
}

```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/**',
        'lib/**',
        '**/*.d.ts',
        '**/*.config.*',
        '**/mockData/**',
        'tests/**',
        'src/extension/**', // Extension code typically needs browser context
        'src/vscode/**', // VSCode specific code
        'src/browserServerBackend.ts', // Complex backend initialization
        'src/program.ts', // CLI program entry point
        'src/index.ts', // Entry point
        'src/browserContextFactory.ts', // Requires real Playwright browser launch
        'src/sessionLog.ts', // File system operations
        'src/tools.ts', // Server initialization
        'src/config.ts', // Requires file system reading and environment parsing
        'src/context.ts', // Requires complex async Playwright browser lifecycle
        'src/tab.ts', // Requires complex Playwright page navigation and lifecycle
        'src/mcp/**', // MCP server infrastructure requires server setup
        // Tools requiring complex Playwright mocking or integration tests
        'src/tools/dialogs.ts',
        'src/tools/evaluate.ts',
        'src/tools/files.ts',
        'src/tools/form.ts',
        'src/tools/install.ts',
        'src/tools/keyboard.ts',
        'src/tools/mouse.ts',
        'src/tools/pdf.ts',
        'src/tools/screenshot.ts',
        'src/tools/snapshot.ts',
        'src/tools/verify.ts',
        'src/tools/wait.ts',
        'src/utils/codegen.ts', // Code generation utilities
        'src/utils/package.ts', // Simple package.json wrapper
        'src/utils/fileUtils.ts', // File system operations
      ],
      include: ['src/**/*.ts'],
      all: true,
      // Adjusted thresholds based on testable code without modifying source
      // Focused on: response, tool definitions, config, context basics, tested tools, utils
      lines: 90,
      functions: 90,
      branches: 90,
      statements: 90,
    },
    testTimeout: 30000,
    hookTimeout: 30000,
  },
});

```

--------------------------------------------------------------------------------
/src/tools/utils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// @ts-ignore
import { asLocator } from 'playwright-core/lib/utils';

import type * as playwright from 'playwright';
import type { Tab } from '../tab.js';

export async function waitForCompletion<R>(tab: Tab, callback: () => Promise<R>): Promise<R> {
  const requests = new Set<playwright.Request>();
  let frameNavigated = false;
  let waitCallback: () => void = () => {};
  const waitBarrier = new Promise<void>(f => { waitCallback = f; });

  const requestListener = (request: playwright.Request) => requests.add(request);
  const requestFinishedListener = (request: playwright.Request) => {
    requests.delete(request);
    if (!requests.size)
      waitCallback();
  };

  const frameNavigateListener = (frame: playwright.Frame) => {
    if (frame.parentFrame())
      return;
    frameNavigated = true;
    dispose();
    clearTimeout(timeout);
    void tab.waitForLoadState('load').then(waitCallback);
  };

  const onTimeout = () => {
    dispose();
    waitCallback();
  };

  tab.page.on('request', requestListener);
  tab.page.on('requestfinished', requestFinishedListener);
  tab.page.on('framenavigated', frameNavigateListener);
  const timeout = setTimeout(onTimeout, 10000);

  const dispose = () => {
    tab.page.off('request', requestListener);
    tab.page.off('requestfinished', requestFinishedListener);
    tab.page.off('framenavigated', frameNavigateListener);
    clearTimeout(timeout);
  };

  try {
    const result = await callback();
    if (!requests.size && !frameNavigated)
      waitCallback();
    await waitBarrier;
    await tab.waitForTimeout(1000);
    return result;
  } finally {
    dispose();
  }
}

export async function generateLocator(locator: playwright.Locator): Promise<string> {
  try {
    const { resolvedSelector } = await (locator as any)._resolveSelector();
    return asLocator('javascript', resolvedSelector);
  } catch (e) {
    throw new Error('Ref not found, likely because element was removed. Use browser_snapshot to see what elements are currently on the page.');
  }
}

export async function callOnPageNoTrace<T>(page: playwright.Page, callback: (page: playwright.Page) => Promise<T>): Promise<T> {
  return await (page as any)._wrapApiCall(() => callback(page), { internal: true });
}

```

--------------------------------------------------------------------------------
/src/vscode/main.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as mcpServer from '../mcp/server.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js';
import type { FullConfig } from '../config.js';
import type { BrowserContext } from 'playwright-core';

class VSCodeBrowserContextFactory implements BrowserContextFactory {
  name = 'vscode';
  description = 'Connect to a browser running in the Playwright VS Code extension';

  constructor(private _config: FullConfig, private _playwright: typeof import('playwright'), private _connectionString: string) {}

  async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal): Promise<{ browserContext: BrowserContext; close: () => Promise<void>; }> {
    let launchOptions: any = this._config.browser.launchOptions;
    if (this._config.browser.userDataDir) {
      launchOptions = {
        ...launchOptions,
        ...this._config.browser.contextOptions,
        userDataDir: this._config.browser.userDataDir,
      };
    }
    const connectionString = new URL(this._connectionString);
    connectionString.searchParams.set('launch-options', JSON.stringify(launchOptions));

    const browserType = this._playwright.chromium; // it could also be firefox or webkit, we just need some browser type to call `connect` on
    const browser = await browserType.connect(connectionString.toString());

    const context = browser.contexts()[0] ?? await browser.newContext(this._config.browser.contextOptions);

    return {
      browserContext: context,
      close: async () => {
        await browser.close();
      }
    };
  }
}

async function main(config: FullConfig, connectionString: string, lib: string) {
  const playwright = await import(lib).then(mod => mod.default ?? mod);
  const factory = new VSCodeBrowserContextFactory(config, playwright, connectionString);
  await mcpServer.connect(
      {
        name: 'Playwright MCP',
        nameInConfig: 'playwright-vscode',
        create: () => new BrowserServerBackend(config, factory),
        version: 'unused'
      },
      new StdioServerTransport(),
      false
  );
}

await main(
    JSON.parse(process.argv[2]),
    process.argv[3],
    process.argv[4]
);

```

--------------------------------------------------------------------------------
/src/tools/keyboard.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';

import { defineTabTool } from './tool.js';
import { elementSchema } from './snapshot.js';
import { generateLocator } from './utils.js';
import * as javascript from '../utils/codegen.js';

const pressKey = defineTabTool({
  capability: 'core',

  schema: {
    name: 'browser_press_key',
    title: 'Press a key',
    description: 'Press a key on the keyboard',
    inputSchema: z.object({
      key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();
    response.addCode(`// Press ${params.key}`);
    response.addCode(`await page.keyboard.press('${params.key}');`);

    await tab.waitForCompletion(async () => {
      await tab.page.keyboard.press(params.key);
    });
  },
});

const typeSchema = elementSchema.extend({
  text: z.string().describe('Text to type into the element'),
  submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
  slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'),
});

const type = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_type',
    title: 'Type text',
    description: 'Type text into editable element',
    inputSchema: typeSchema,
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    const locator = await tab.refLocator(params);

    await tab.waitForCompletion(async () => {
      if (params.slowly) {
        response.setIncludeSnapshot();
        response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
        await locator.pressSequentially(params.text);
      } else {
        response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
        await locator.fill(params.text);
      }

      if (params.submit) {
        response.setIncludeSnapshot();
        response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
        await locator.press('Enter');
      }
    });
  },
});

export default [
  pressKey,
  type,
];

```

--------------------------------------------------------------------------------
/src/mcp/inProcessTransport.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js';

export class InProcessTransport implements Transport {
  private _server: Server;
  private _serverTransport: InProcessServerTransport;
  private _connected: boolean = false;

  constructor(server: Server) {
    this._server = server;
    this._serverTransport = new InProcessServerTransport(this);
  }

  async start(): Promise<void> {
    if (this._connected)
      throw new Error('InprocessTransport already started!');

    await this._server.connect(this._serverTransport);
    this._connected = true;
  }

  async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
    if (!this._connected)
      throw new Error('Transport not connected');


    this._serverTransport._receiveFromClient(message);
  }

  async close(): Promise<void> {
    if (this._connected) {
      this._connected = false;
      this.onclose?.();
      this._serverTransport.onclose?.();
    }
  }

  onclose?: (() => void) | undefined;
  onerror?: ((error: Error) => void) | undefined;
  onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined;
  sessionId?: string | undefined;
  setProtocolVersion?: ((version: string) => void) | undefined;

  _receiveFromServer(message: JSONRPCMessage, extra?: MessageExtraInfo): void {
    this.onmessage?.(message, extra);
  }
}

class InProcessServerTransport implements Transport {
  private _clientTransport: InProcessTransport;

  constructor(clientTransport: InProcessTransport) {
    this._clientTransport = clientTransport;
  }

  async start(): Promise<void> {
  }

  async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> {
    this._clientTransport._receiveFromServer(message);
  }

  async close(): Promise<void> {
    this.onclose?.();
  }

  onclose?: (() => void) | undefined;
  onerror?: ((error: Error) => void) | undefined;
  onmessage?: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined;
  sessionId?: string | undefined;
  setProtocolVersion?: ((version: string) => void) | undefined;
  _receiveFromClient(message: JSONRPCMessage): void {
    this.onmessage?.(message);
  }
}

```

--------------------------------------------------------------------------------
/tests/tools-console.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import consoleTools from '../src/tools/console.js';
import { Response } from '../src/response.js';
import type { Context } from '../src/context.js';
import type { Tab } from '../src/tab.js';

describe('Console Tools', () => {
  let mockContext: Context;
  let mockTab: Tab;
  let response: Response;

  beforeEach(() => {
    mockTab = {
      consoleMessages: vi.fn().mockReturnValue([
        {
          type: 'log',
          text: 'Info message',
          toString: () => '[LOG] Info message @ test.js:10',
        },
        {
          type: 'error',
          text: 'Error occurred',
          toString: () => '[ERROR] Error occurred @ app.js:25',
        },
        {
          type: 'warning',
          text: 'Warning message',
          toString: () => '[WARNING] Warning message @ util.js:5',
        },
      ]),
      modalStates: vi.fn().mockReturnValue([]),
    } as any;

    mockContext = {
      currentTabOrDie: () => mockTab,
      config: {},
    } as any;

    response = new Response(mockContext, 'test_tool', {});
  });

  describe('browser_console_messages tool', () => {
    const consoleTool = consoleTools.find(t => t.schema.name === 'browser_console_messages')!;

    it('should exist', () => {
      expect(consoleTool).toBeDefined();
      expect(consoleTool.schema.name).toBe('browser_console_messages');
    });

    it('should have correct schema', () => {
      expect(consoleTool.schema.title).toBe('Get console messages');
      expect(consoleTool.schema.type).toBe('readOnly');
    });

    it('should retrieve all console messages', async () => {
      await consoleTool.handle(mockContext, {}, response);

      expect(mockTab.consoleMessages).toHaveBeenCalled();
      expect(response.result()).toContain('Info message');
      expect(response.result()).toContain('Error occurred');
      expect(response.result()).toContain('Warning message');
    });

    it('should format messages correctly', async () => {
      await consoleTool.handle(mockContext, {}, response);

      const result = response.result();
      expect(result).toContain('[LOG]');
      expect(result).toContain('[ERROR]');
      expect(result).toContain('[WARNING]');
    });

    it('should handle empty console messages', async () => {
      mockTab.consoleMessages = vi.fn().mockReturnValue([]);

      await consoleTool.handle(mockContext, {}, response);

      expect(response.result()).toBe('');
    });
  });

  describe('Tool capabilities', () => {
    it('should all have core capability', () => {
      consoleTools.forEach(tool => {
        expect(tool.capability).toBe('core');
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/browserServerBackend.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { fileURLToPath } from 'url';
import { FullConfig } from './config.js';
import { Context } from './context.js';
import { logUnhandledError } from './utils/log.js';
import { Response } from './response.js';
import { SessionLog } from './sessionLog.js';
import { filteredTools } from './tools.js';
import { toMcpTool } from './mcp/tool.js';

import type { Tool } from './tools/tool.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type * as mcpServer from './mcp/server.js';
import type { ServerBackend } from './mcp/server.js';

export class BrowserServerBackend implements ServerBackend {
  private _tools: Tool[];
  private _context: Context | undefined;
  private _sessionLog: SessionLog | undefined;
  private _config: FullConfig;
  private _browserContextFactory: BrowserContextFactory;

  constructor(config: FullConfig, factory: BrowserContextFactory) {
    this._config = config;
    this._browserContextFactory = factory;
    this._tools = filteredTools(config);
  }

  async initialize(server: mcpServer.Server, clientVersion: mcpServer.ClientVersion, roots: mcpServer.Root[]): Promise<void> {
    let rootPath: string | undefined;
    if (roots.length > 0) {
      const firstRootUri = roots[0]?.uri;
      const url = firstRootUri ? new URL(firstRootUri) : undefined;
      rootPath = url ? fileURLToPath(url) : undefined;
    }
    this._sessionLog = this._config.saveSession ? await SessionLog.create(this._config, rootPath) : undefined;
    this._context = new Context({
      tools: this._tools,
      config: this._config,
      browserContextFactory: this._browserContextFactory,
      sessionLog: this._sessionLog,
      clientInfo: { ...clientVersion, rootPath },
    });
  }

  async listTools(): Promise<mcpServer.Tool[]> {
    return this._tools.map(tool => toMcpTool(tool.schema));
  }

  async callTool(name: string, rawArguments: mcpServer.CallToolRequest['params']['arguments']) {
    const tool = this._tools.find(tool => tool.schema.name === name)!;
    if (!tool)
      throw new Error(`Tool "${name}" not found`);
    const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
    const context = this._context!;
    const response = new Response(context, name, parsedArguments);
    context.setRunningTool(name);
    try {
      await tool.handle(context, parsedArguments, response);
      await response.finish();
      this._sessionLog?.logResponse(response);
    } catch (error: any) {
      response.addError(String(error));
    } finally {
      context.setRunningTool(undefined);
    }
    return response.serialize();
  }

  serverClosed() {
    void this._context?.dispose().catch(logUnhandledError);
  }
}

```

--------------------------------------------------------------------------------
/src/tools/tabs.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTool } from './tool.js';

const SECOND = 1000;
const MINUTE = 60 * SECOND;

const browserTabs = defineTool({
  capability: 'core-tabs',
  schema: {
    name: 'browser_tabs',
    title: 'Manage tabs',
    description: 'List, create, close, or select a browser tab.',
    inputSchema: z.object({
      action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'),
      index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'),
    }),
    type: 'destructive',
  },
  handle: async (context, params, response) => {
    switch (params.action) {
      case 'list': {
        await context.ensureTab();
        response.setIncludeTabs();
        return;
      }
      case 'new': {
        await context.newTab();
        response.setIncludeTabs();
        return;
      }
      case 'close': {
        await context.closeTab(params.index);
        response.setIncludeSnapshot();
        return;
      }
      case 'select': {
        if (params.index === undefined)
          throw new Error('Tab index is required');
        await context.selectTab(params.index);
        response.setIncludeSnapshot();
        return;
      }
    }
  },
});

const navigationTimeout = defineTool({
  capability: 'core-tabs',
  schema: {
    name: 'browser_navigation_timeout',
    title: 'Navigation timeout',
    description: 'Sets the timeout for navigation and page load actions. Only affects the current tab and does not persist across browser context recreation.',
    inputSchema: z.object({
      timeout: z.number().min(SECOND * 30).max(MINUTE * 20).describe('Timeout in milliseconds for navigation (0-300000ms)'),
    }),
    type: 'destructive',
  },
  handle: async (context, params, response) => {
    const tabs = context.tabs();
    for (const tab of tabs)
      tab.page.setDefaultNavigationTimeout(params.timeout);

    response.addResult(`Navigation timeout set to ${params.timeout}ms for all tabs.`);
    response.setIncludeTabs();
  },
});

const defaultTimeout = defineTool({
  capability: 'core-tabs',
  schema: {
    name: 'browser_default_timeout',
    title: 'Default timeout',
    description: 'Sets the default timeout for all Playwright operations (clicks, fills, etc). Only affects existing tabs and does not persist across browser context recreation.',
    inputSchema: z.object({
      timeout: z.number().min(SECOND * 30).max(MINUTE * 20).describe('Timeout in milliseconds for default operations (0-300000ms)'),
    }),
    type: 'destructive',
  },
  handle: async (context, params, response) => {
    const tabs = context.tabs();
    for (const tab of tabs)
      tab.page.setDefaultTimeout(params.timeout);


    response.addResult(`Default timeout set to ${params.timeout}ms for all tabs.`);
    response.setIncludeTabs();
  },
});

export default [
  browserTabs,
  navigationTimeout,
  defaultTimeout
];

```

--------------------------------------------------------------------------------
/src/mcp/manualPromise.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

export class ManualPromise<T = void> extends Promise<T> {
  private _resolve!: (t: T) => void;
  private _reject!: (e: Error) => void;
  private _isDone: boolean;

  constructor() {
    let resolve: (t: T) => void;
    let reject: (e: Error) => void;
    super((f, r) => {
      resolve = f;
      reject = r;
    });
    this._isDone = false;
    this._resolve = resolve!;
    this._reject = reject!;
  }

  isDone() {
    return this._isDone;
  }

  resolve(t: T) {
    this._isDone = true;
    this._resolve(t);
  }

  reject(e: Error) {
    this._isDone = true;
    this._reject(e);
  }

  static override get [Symbol.species]() {
    return Promise;
  }

  override get [Symbol.toStringTag]() {
    return 'ManualPromise';
  }
}

export class LongStandingScope {
  private _terminateError: Error | undefined;
  private _closeError: Error | undefined;
  private _terminatePromises = new Map<ManualPromise<Error>, string[]>();
  private _isClosed = false;

  reject(error: Error) {
    this._isClosed = true;
    this._terminateError = error;
    for (const p of this._terminatePromises.keys())
      p.resolve(error);
  }

  close(error: Error) {
    this._isClosed = true;
    this._closeError = error;
    for (const [p, frames] of this._terminatePromises)
      p.resolve(cloneError(error, frames));
  }

  isClosed() {
    return this._isClosed;
  }

  static async raceMultiple<T>(scopes: LongStandingScope[], promise: Promise<T>): Promise<T> {
    return Promise.race(scopes.map(s => s.race(promise)));
  }

  async race<T>(promise: Promise<T> | Promise<T>[]): Promise<T> {
    return this._race(Array.isArray(promise) ? promise : [promise], false) as Promise<T>;
  }

  async safeRace<T>(promise: Promise<T>, defaultValue?: T): Promise<T> {
    return this._race([promise], true, defaultValue);
  }

  private async _race(promises: Promise<any>[], safe: boolean, defaultValue?: any): Promise<any> {
    const terminatePromise = new ManualPromise<Error>();
    const frames = captureRawStack();
    if (this._terminateError)
      terminatePromise.resolve(this._terminateError);
    if (this._closeError)
      terminatePromise.resolve(cloneError(this._closeError, frames));
    this._terminatePromises.set(terminatePromise, frames);
    try {
      return await Promise.race([
        terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
        ...promises
      ]);
    } finally {
      this._terminatePromises.delete(terminatePromise);
    }
  }
}

function cloneError(error: Error, frames: string[]) {
  const clone = new Error();
  clone.name = error.name;
  clone.message = error.message;
  clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
  return clone;
}

function captureRawStack(): string[] {
  const stackTraceLimit = Error.stackTraceLimit;
  Error.stackTraceLimit = 50;
  const error = new Error();
  const stack = error.stack || '';
  Error.stackTraceLimit = stackTraceLimit;
  return stack.split('\n');
}

```

--------------------------------------------------------------------------------
/src/tools/mouse.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool } from './tool.js';

const elementSchema = z.object({
  element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
});

const mouseMove = defineTabTool({
  capability: 'vision',
  schema: {
    name: 'browser_mouse_move_xy',
    title: 'Move mouse',
    description: 'Move mouse to a given position',
    inputSchema: elementSchema.extend({
      x: z.number().describe('X coordinate'),
      y: z.number().describe('Y coordinate'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
    response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);

    await tab.waitForCompletion(async () => {
      await tab.page.mouse.move(params.x, params.y);
    });
  },
});

const mouseClick = defineTabTool({
  capability: 'vision',
  schema: {
    name: 'browser_mouse_click_xy',
    title: 'Click',
    description: 'Click left mouse button at a given position',
    inputSchema: elementSchema.extend({
      x: z.number().describe('X coordinate'),
      y: z.number().describe('Y coordinate'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    response.addCode(`// Click mouse at coordinates (${params.x}, ${params.y})`);
    response.addCode(`await page.mouse.move(${params.x}, ${params.y});`);
    response.addCode(`await page.mouse.down();`);
    response.addCode(`await page.mouse.up();`);

    await tab.waitForCompletion(async () => {
      await tab.page.mouse.move(params.x, params.y);
      await tab.page.mouse.down();
      await tab.page.mouse.up();
    });
  },
});

const mouseDrag = defineTabTool({
  capability: 'vision',
  schema: {
    name: 'browser_mouse_drag_xy',
    title: 'Drag mouse',
    description: 'Drag left mouse button to a given position',
    inputSchema: elementSchema.extend({
      startX: z.number().describe('Start X coordinate'),
      startY: z.number().describe('Start Y coordinate'),
      endX: z.number().describe('End X coordinate'),
      endY: z.number().describe('End Y coordinate'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    response.addCode(`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`);
    response.addCode(`await page.mouse.move(${params.startX}, ${params.startY});`);
    response.addCode(`await page.mouse.down();`);
    response.addCode(`await page.mouse.move(${params.endX}, ${params.endY});`);
    response.addCode(`await page.mouse.up();`);

    await tab.waitForCompletion(async () => {
      await tab.page.mouse.move(params.startX, params.startY);
      await tab.page.mouse.down();
      await tab.page.mouse.move(params.endX, params.endY);
      await tab.page.mouse.up();
    });
  },
});

export default [
  mouseMove,
  mouseClick,
  mouseDrag,
];

```

--------------------------------------------------------------------------------
/tests/tools-common.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import commonTools from '../src/tools/common.js';
import { Response } from '../src/response.js';
import type { Context } from '../src/context.js';
import type { Tab } from '../src/tab.js';

describe('Common Tools', () => {
  let mockContext: Context;
  let mockTab: Tab;
  let mockPage: any;
  let response: Response;

  beforeEach(() => {
    mockPage = {
      url: () => 'https://example.com',
      setViewportSize: vi.fn().mockResolvedValue(undefined),
      close: vi.fn().mockResolvedValue(undefined),
    };

    mockTab = {
      page: mockPage,
      modalStates: vi.fn().mockReturnValue([]),
      waitForCompletion: vi.fn().mockImplementation(async cb => await cb()),
    } as any;

    mockContext = {
      currentTabOrDie: () => mockTab,
      currentTab: () => mockTab,
      closeBrowserContext: vi.fn().mockResolvedValue(undefined),
      config: {},
    } as any;

    response = new Response(mockContext, 'test_tool', {});
  });

  describe('browser_close tool', () => {
    const closeTool = commonTools.find(t => t.schema.name === 'browser_close')!;

    it('should exist', () => {
      expect(closeTool).toBeDefined();
      expect(closeTool.schema.name).toBe('browser_close');
    });

    it('should have correct schema', () => {
      expect(closeTool.schema.title).toBe('Close browser');
      expect(closeTool.schema.type).toBe('readOnly');
    });

    it('should close browser context', async () => {
      await closeTool.handle(mockContext, {}, response);

      expect(mockContext.closeBrowserContext).toHaveBeenCalled();
    });

    it('should generate close code', async () => {
      await closeTool.handle(mockContext, {}, response);

      expect(response.code()).toContain('page.close');
    });
  });

  describe('browser_resize tool', () => {
    const resizeTool = commonTools.find(t => t.schema.name === 'browser_resize')!;

    it('should exist', () => {
      expect(resizeTool).toBeDefined();
      expect(resizeTool.schema.name).toBe('browser_resize');
    });

    it('should have correct schema', () => {
      expect(resizeTool.schema.title).toBe('Resize browser window');
      expect(resizeTool.schema.type).toBe('readOnly');
    });

    it('should resize viewport', async () => {
      await resizeTool.handle(mockContext, { width: 1920, height: 1080 }, response);

      expect(mockTab.waitForCompletion).toHaveBeenCalled();
      expect(mockPage.setViewportSize).toHaveBeenCalledWith({ width: 1920, height: 1080 });
    });

    it('should generate resize code', async () => {
      await resizeTool.handle(mockContext, { width: 800, height: 600 }, response);

      expect(response.code()).toContain('setViewportSize');
      expect(response.code()).toContain('800');
      expect(response.code()).toContain('600');
    });
  });

  describe('Tool capabilities', () => {
    it('should all have core capability', () => {
      commonTools.forEach(tool => {
        expect(tool.capability).toBe('core');
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/tests/tools-network.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import networkTools from '../src/tools/network.js';
import { Response } from '../src/response.js';
import type { Context } from '../src/context.js';
import type { Tab } from '../src/tab.js';

describe('Network Tools', () => {
  let mockContext: Context;
  let mockTab: Tab;
  let response: Response;

  beforeEach(() => {
    const mockRequests = new Map();
    const req1 = { url: () => 'https://api.example.com/data', method: () => 'GET' };
    const res1 = { status: () => 200, statusText: () => 'OK' };
    mockRequests.set(req1, res1);

    const req2 = { url: () => 'https://api.example.com/user', method: () => 'POST' };
    const res2 = { status: () => 201, statusText: () => 'Created' };
    mockRequests.set(req2, res2);

    const req3 = { url: () => 'https://api.example.com/missing', method: () => 'GET' };
    mockRequests.set(req3, null);

    mockTab = {
      requests: vi.fn().mockReturnValue(mockRequests),
      modalStates: vi.fn().mockReturnValue([]),
    } as any;

    mockContext = {
      currentTabOrDie: () => mockTab,
      config: {},
    } as any;

    response = new Response(mockContext, 'test_tool', {});
  });

  describe('browser_network_requests tool', () => {
    const networkTool = networkTools.find(t => t.schema.name === 'browser_network_requests')!;

    it('should exist', () => {
      expect(networkTool).toBeDefined();
      expect(networkTool.schema.name).toBe('browser_network_requests');
    });

    it('should have correct schema', () => {
      expect(networkTool.schema.title).toBe('List network requests');
      expect(networkTool.schema.type).toBe('readOnly');
    });

    it('should retrieve all network requests', async () => {
      await networkTool.handle(mockContext, {}, response);

      expect(mockTab.requests).toHaveBeenCalled();
      expect(response.result()).toContain('https://api.example.com/data');
      expect(response.result()).toContain('https://api.example.com/user');
    });

    it('should show request methods', async () => {
      await networkTool.handle(mockContext, {}, response);

      const result = response.result();
      expect(result).toContain('GET');
      expect(result).toContain('POST');
    });

    it('should show response status', async () => {
      await networkTool.handle(mockContext, {}, response);

      const result = response.result();
      expect(result).toContain('200');
      expect(result).toContain('201');
    });

    it('should handle requests without responses', async () => {
      await networkTool.handle(mockContext, {}, response);

      const result = response.result();
      // Request without response just shows the request line
      expect(result).toContain('https://api.example.com/missing');
    });

    it('should handle empty requests', async () => {
      mockTab.requests = vi.fn().mockReturnValue(new Map());

      await networkTool.handle(mockContext, {}, response);

      expect(response.result()).toBe('');
    });
  });

  describe('Tool capabilities', () => {
    it('should all have core capability', () => {
      networkTools.forEach(tool => {
        expect(tool.capability).toBe('core');
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/actions.d.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

type Point = { x: number, y: number };

export type ActionName =
  'check' |
  'click' |
  'closePage' |
  'fill' |
  'navigate' |
  'openPage' |
  'press' |
  'select' |
  'uncheck' |
  'setInputFiles' |
  'assertText' |
  'assertValue' |
  'assertChecked' |
  'assertVisible' |
  'assertSnapshot';

export type ActionBase = {
  name: ActionName,
  signals: Signal[],
  ariaSnapshot?: string,
};

export type ActionWithSelector = ActionBase & {
  selector: string,
  ref?: string,
};

export type ClickAction = ActionWithSelector & {
  name: 'click',
  button: 'left' | 'middle' | 'right',
  modifiers: number,
  clickCount: number,
  position?: Point,
};

export type CheckAction = ActionWithSelector & {
  name: 'check',
};

export type UncheckAction = ActionWithSelector & {
  name: 'uncheck',
};

export type FillAction = ActionWithSelector & {
  name: 'fill',
  text: string,
};

export type NavigateAction = ActionBase & {
  name: 'navigate',
  url: string,
};

export type OpenPageAction = ActionBase & {
  name: 'openPage',
  url: string,
};

export type ClosesPageAction = ActionBase & {
  name: 'closePage',
};

export type PressAction = ActionWithSelector & {
  name: 'press',
  key: string,
  modifiers: number,
};

export type SelectAction = ActionWithSelector & {
  name: 'select',
  options: string[],
};

export type SetInputFilesAction = ActionWithSelector & {
  name: 'setInputFiles',
  files: string[],
};

export type AssertTextAction = ActionWithSelector & {
  name: 'assertText',
  text: string,
  substring: boolean,
};

export type AssertValueAction = ActionWithSelector & {
  name: 'assertValue',
  value: string,
};

export type AssertCheckedAction = ActionWithSelector & {
  name: 'assertChecked',
  checked: boolean,
};

export type AssertVisibleAction = ActionWithSelector & {
  name: 'assertVisible',
};

export type AssertSnapshotAction = ActionWithSelector & {
  name: 'assertSnapshot',
  ariaSnapshot: string,
};

export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction;
export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;

// Signals.

export type BaseSignal = {
};

export type NavigationSignal = BaseSignal & {
  name: 'navigation',
  url: string,
};

export type PopupSignal = BaseSignal & {
  name: 'popup',
  popupAlias: string,
};

export type DownloadSignal = BaseSignal & {
  name: 'download',
  downloadAlias: string,
};

export type DialogSignal = BaseSignal & {
  name: 'dialog',
  dialogAlias: string,
};

export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;

export type FrameDescription = {
  pageGuid: string;
  pageAlias: string;
  framePath: string[];
};

export type ActionInContext = {
  frame: FrameDescription;
  description?: string;
  action: Action;
  startTime: number;
  endTime?: number;
};

export type SignalInContext = {
  frame: FrameDescription;
  signal: Signal;
  timestamp: number;
};

```

--------------------------------------------------------------------------------
/tests/config.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect } from 'vitest';
import { resolveConfig, outputFile } from '../src/config.js';
import type { Config } from '../config.js';

describe('Config', () => {
  describe('resolveConfig', () => {
    it('should resolve default config when empty config provided', async () => {
      const config = await resolveConfig({});

      expect(config.browser.browserName).toBe('chromium');
      expect(config.timeouts.navigationTimeout).toBe(60000);
      expect(config.timeouts.defaultTimeout).toBe(5000);
      expect(config.saveTrace).toBe(false);
    });

    it('should merge custom browser config', async () => {
      const customConfig: Config = {
        browser: {
          browserName: 'firefox',
          launchOptions: {
            headless: true,
          },
        },
      };

      const config = await resolveConfig(customConfig);

      expect(config.browser.browserName).toBe('firefox');
      expect(config.browser.launchOptions.headless).toBe(true);
    });

    it('should merge custom timeout config', async () => {
      const customConfig: Config = {
        timeouts: {
          navigationTimeout: 30000,
          defaultTimeout: 10000,
        },
      };

      const config = await resolveConfig(customConfig);

      expect(config.timeouts.navigationTimeout).toBe(30000);
      expect(config.timeouts.defaultTimeout).toBe(10000);
    });

    it('should merge network config', async () => {
      const customConfig: Config = {
        network: {
          allowedOrigins: ['example.com'],
          blockedOrigins: ['ads.example.com'],
        },
      };

      const config = await resolveConfig(customConfig);

      expect(config.network.allowedOrigins).toEqual(['example.com']);
      expect(config.network.blockedOrigins).toEqual(['ads.example.com']);
    });

    it('should handle save trace option', async () => {
      const customConfig: Config = {
        saveTrace: true,
      };

      const config = await resolveConfig(customConfig);

      expect(config.saveTrace).toBe(true);
    });

    it('should preserve default values when not overridden', async () => {
      const customConfig: Config = {
        browser: {
          browserName: 'webkit',
        },
      };

      const config = await resolveConfig(customConfig);

      expect(config.browser.browserName).toBe('webkit');
      expect(config.timeouts.navigationTimeout).toBe(60000);
      expect(config.saveTrace).toBe(false);
    });
  });

  describe('outputFile', () => {
    it('should generate output file path with filename', async () => {
      const config = await resolveConfig({});
      const result = await outputFile(config, '/tmp', 'test.txt');

      expect(result).toContain('test.txt');
    });

    it('should use outputDir when specified', async () => {
      const config = await resolveConfig({
        outputDir: '/tmp/custom/output',
      });

      const result = await outputFile(config, '/tmp', 'test.txt');

      expect(result).toContain('/tmp/custom/output');
      expect(result).toContain('test.txt');
    });

    it('should sanitize file paths', async () => {
      const config = await resolveConfig({});

      const result = await outputFile(config, '/tmp', 'test/../../../etc/passwd');

      // Should sanitize to prevent directory traversal
      expect(result).not.toContain('../');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/external-modules.d.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
// Minimal type declarations for optional runtime dependencies.
declare module 'dotenv' {
  export interface DotenvConfigOptions {
    path?: string;
    encoding?: string;
    debug?: boolean;
    override?: boolean;
  }

  export interface DotenvConfigOutput {
    parsed?: Record<string, string>;
    error?: Error;
  }

  export function config(options?: DotenvConfigOptions): DotenvConfigOutput;

  const dotenv: {
    config: typeof config;
  };

  export default dotenv;
}

declare module 'openai' {
  namespace OpenAI {
    namespace Chat {
      namespace Completions {
        type Role = 'user' | 'assistant' | 'tool';

        interface ChatCompletionMessageToolCall {
          id: string;
          type: 'function';
          function: {
            name: string;
            arguments: string;
          };
        }

        interface ChatCompletionMessageParam {
          role: Role;
          content?: string | null;
          tool_calls?: ChatCompletionMessageToolCall[];
          tool_call_id?: string;
        }

        interface ChatCompletionAssistantMessageParam extends ChatCompletionMessageParam {
          role: 'assistant';
        }

        interface ChatCompletionTool {
          type: 'function';
          function: {
            name: string;
            description?: string;
            parameters?: any;
          };
        }
      }
    }
  }

  interface ChatCompletionsApi {
    create(request: {
      model: string;
      messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
      tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
      tool_choice?: 'auto' | 'none';
    }): Promise<{
      choices: Array<{
        message: {
          content?: string | null;
          tool_calls?: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[];
        };
      }>;
    }>;
  }

  class OpenAI {
    constructor(config?: Record<string, unknown>);
    chat: {
      completions: ChatCompletionsApi;
    };
  }

  export { OpenAI };
  export default OpenAI;
}

declare module '@anthropic-ai/sdk' {
  namespace Anthropic {
    namespace Messages {
      type Role = 'user' | 'assistant';

      interface BaseBlock {
        type: string;
      }

      interface TextBlock extends BaseBlock {
        type: 'text';
        text: string;
        citations?: any[];
      }

      interface ToolUseBlock extends BaseBlock {
        type: 'tool_use';
        id: string;
        name: string;
        input: unknown;
      }

      interface ToolResultBlock extends BaseBlock {
        type: 'tool_result';
        tool_use_id: string;
        content: string;
        is_error?: boolean;
      }

      type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
      type ToolResultBlockParam = ToolResultBlock;

      interface MessageParam {
        role: Role;
        content: string | ContentBlock[];
      }

      interface Tool {
        name: string;
        description?: string;
        input_schema: any;
      }
    }
  }

  class Anthropic {
    constructor(config?: Record<string, unknown>);
    messages: {
      create(request: {
        model: string;
        max_tokens: number;
        messages: Anthropic.Messages.MessageParam[];
        tools?: Anthropic.Messages.Tool[];
      }): Promise<{
        content: Anthropic.Messages.ContentBlock[];
      }>;
    };
  }

  export { Anthropic };
  export default Anthropic;
}

```

--------------------------------------------------------------------------------
/src/tools/screenshot.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';

import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

import type * as playwright from 'playwright';

const screenshotSchema = z.object({
  type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
  filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
  element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
  ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
  fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'),
}).refine(data => {
  return !!data.element === !!data.ref;
}, {
  message: 'Both element and ref must be provided or neither.',
  path: ['ref', 'element']
}).refine(data => {
  return !(data.fullPage && (data.element || data.ref));
}, {
  message: 'fullPage cannot be used with element screenshots.',
  path: ['fullPage']
});

const screenshot = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_take_screenshot',
    title: 'Take a screenshot',
    description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
    inputSchema: screenshotSchema,
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const fileType = params.type || 'png';
    const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
    const options: playwright.PageScreenshotOptions = {
      type: fileType,
      quality: fileType === 'png' ? undefined : 90,
      scale: 'css',
      path: fileName,
      ...(params.fullPage !== undefined && { fullPage: params.fullPage })
    };
    const isElementScreenshot = params.element && params.ref;

    const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
    response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);

    // Only get snapshot when element screenshot is needed
    const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;

    if (locator)
      response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
    else
      response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);

    const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
    response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);

    // https://github.com/microsoft/playwright-mcp/issues/817
    // Never return large images to LLM, saving them to the file system is enough.
    if (!params.fullPage) {
      response.addImage({
        contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
        data: buffer
      });
    }
  }
});

export default [
  screenshot,
];

```

--------------------------------------------------------------------------------
/tests/tools-navigate.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import navigateTools from '../src/tools/navigate.js';
import { Response } from '../src/response.js';
import type { Context } from '../src/context.js';
import type { Tab } from '../src/tab.js';

describe('Navigate Tools', () => {
  let mockContext: Context;
  let mockTab: Tab;
  let mockPage: any;
  let response: Response;

  beforeEach(() => {
    mockPage = {
      url: () => 'https://example.com',
      goBack: vi.fn().mockResolvedValue(null),
    };

    mockTab = {
      page: mockPage,
      navigate: vi.fn().mockResolvedValue(undefined),
      modalStates: vi.fn().mockReturnValue([]),
    } as any;

    mockContext = {
      currentTabOrDie: () => mockTab,
      ensureTab: vi.fn().mockResolvedValue(mockTab),
      config: {},
    } as any;

    response = new Response(mockContext, 'test_tool', {});
  });

  describe('browser_navigate tool', () => {
    const navigateTool = navigateTools.find(t => t.schema.name === 'browser_navigate')!;

    it('should exist', () => {
      expect(navigateTool).toBeDefined();
      expect(navigateTool.schema.name).toBe('browser_navigate');
    });

    it('should have correct schema', () => {
      expect(navigateTool.schema.title).toBe('Navigate to a URL');
      expect(navigateTool.schema.type).toBe('destructive');
    });

    it('should navigate to URL', async () => {
      await navigateTool.handle(mockContext, { url: 'https://example.com' }, response);

      expect(mockContext.ensureTab).toHaveBeenCalled();
      expect(mockTab.navigate).toHaveBeenCalledWith('https://example.com');
    });

    it('should include snapshot after navigation', async () => {
      const setIncludeSnapshotSpy = vi.spyOn(response, 'setIncludeSnapshot');

      await navigateTool.handle(mockContext, { url: 'https://example.com' }, response);

      expect(setIncludeSnapshotSpy).toHaveBeenCalled();
    });

    it('should generate navigation code', async () => {
      await navigateTool.handle(mockContext, { url: 'https://example.com' }, response);

      expect(response.code()).toContain('page.goto');
      expect(response.code()).toContain('https://example.com');
    });
  });

  describe('browser_navigate_back tool', () => {
    const backTool = navigateTools.find(t => t.schema.name === 'browser_navigate_back')!;

    it('should exist', () => {
      expect(backTool).toBeDefined();
      expect(backTool.schema.name).toBe('browser_navigate_back');
    });

    it('should have correct schema', () => {
      expect(backTool.schema.title).toBe('Go back');
      expect(backTool.schema.type).toBe('readOnly');
    });

    it('should navigate back', async () => {
      await backTool.handle(mockContext, {}, response);

      expect(mockPage.goBack).toHaveBeenCalled();
    });

    it('should include snapshot after navigation', async () => {
      const setIncludeSnapshotSpy = vi.spyOn(response, 'setIncludeSnapshot');

      await backTool.handle(mockContext, {}, response);

      expect(setIncludeSnapshotSpy).toHaveBeenCalled();
    });

    it('should generate back navigation code', async () => {
      await backTool.handle(mockContext, {}, response);

      expect(response.code()).toContain('page.goBack');
    });
  });

  describe('Tool capabilities', () => {
    it('should all have core capability', () => {
      navigateTools.forEach(tool => {
        expect(tool.capability).toBe('core');
      });
    });

    it('should export expected number of tools', () => {
      expect(navigateTools).toHaveLength(2);
    });
  });
});

```

--------------------------------------------------------------------------------
/config.d.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type * as playwright from 'playwright';

export type ToolCapability =
  | 'core'
  | 'tabs'
  | 'pdf'
  | 'history'
  | 'wait'
  | 'files'
  | 'install'
  | 'testing'
  | 'core-install'
  | 'core-tabs'
  | 'vision'
  | 'verify';

export type Config = {
  /**
   * The browser to use.
   */
  browser?: {
    /**
     * Use browser agent (experimental).
     */
    browserAgent?: string;

    /**
     * The type of browser to use.
     */
    browserName?: 'chromium' | 'firefox' | 'webkit';

    /**
     * Keep the browser profile in memory, do not save it to disk.
     */
    isolated?: boolean;

    /**
     * Path to a user data directory for browser profile persistence.
     * Temporary directory is created by default.
     */
    userDataDir?: string;

    /**
     * Launch options passed to
     * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context
     *
     * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.
     */
    launchOptions?: playwright.LaunchOptions;

    /**
     * Context options for the browser context.
     *
     * This is useful for settings options like `viewport`.
     */
    contextOptions?: playwright.BrowserContextOptions;

    /**
     * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.
     */
    cdpEndpoint?: string;

    /**
     * Remote endpoint to connect to an existing Playwright server.
     */
    remoteEndpoint?: string;
  },

  server?: {
    /**
     * The port to listen on for SSE or MCP transport.
     */
    port?: number;

    /**
     * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
     */
    host?: string;
  },

  /**
   * List of enabled tool capabilities. Possible values:
   *   - 'core': Core browser automation features.
   *   - 'tabs': Tab management features.
   *   - 'pdf': PDF generation and manipulation.
   *   - 'history': Browser history access.
   *   - 'wait': Wait and timing utilities.
   *   - 'files': File upload/download support.
   *   - 'install': Browser installation utilities.
   */
  capabilities?: ToolCapability[];

  /**
   * Run server that uses screenshots (Aria snapshots are used by default).
   */
  vision?: boolean;

  /**
   * Whether to save the Playwright trace of the session into the output directory.
   */
  saveTrace?: boolean;

  /**
   * Whether to persist session logs for the current run.
   */
  saveSession?: boolean;

  /**
   * The directory to save output files.
   */
  outputDir?: string;

  network?: {
    /**
     * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
     */
    allowedOrigins?: string[];

    /**
     * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.
     */
    blockedOrigins?: string[];
  };

  /**
   * Whether to send image responses to the client. Can be "allow", "omit", or "auto". Defaults to "auto", which sends images if the client can display them.
   */
  imageResponses?: 'allow' | 'omit' | 'auto';

  /**
   * Timeout settings for Playwright operations.
   */
  timeouts?: {
    /**
     * Maximum time in milliseconds for page navigation. Defaults to 60000ms (60 seconds).
     */
    navigationTimeout?: number;

    /**
     * Default timeout for all Playwright operations (clicks, fills, etc). Defaults to 5000ms (5 seconds).
     */
    defaultTimeout?: number;
  };
};

```

--------------------------------------------------------------------------------
/src/mcp/proxyBackend.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debug from 'debug';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';

import type { ServerBackend, ClientVersion, Root, Server } from './server.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';

export type MCPProvider = {
  name: string;
  description: string;
  connect(): Promise<Transport>;
};

const errorsDebug = debug('pw:mcp:errors');

export class ProxyBackend implements ServerBackend {
  private _mcpProviders: MCPProvider[];
  private _currentClient: Client | undefined;
  private _contextSwitchTool: Tool;
  private _roots: Root[] = [];

  constructor(mcpProviders: MCPProvider[]) {
    this._mcpProviders = mcpProviders;
    this._contextSwitchTool = this._defineContextSwitchTool();
  }

  async initialize(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
    this._roots = roots;
    await this._setCurrentClient(this._mcpProviders[0]);
  }

  async listTools(): Promise<Tool[]> {
    const response = await this._currentClient!.listTools();
    if (this._mcpProviders.length === 1)
      return response.tools;
    return [
      ...response.tools,
      this._contextSwitchTool,
    ];
  }

  async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
    if (name === this._contextSwitchTool.name)
      return this._callContextSwitchTool(args);
    return await this._currentClient!.callTool({
      name,
      arguments: args,
    }) as CallToolResult;
  }

  serverClosed?(): void {
    void this._currentClient?.close().catch(errorsDebug);
  }

  private async _callContextSwitchTool(params: any): Promise<CallToolResult> {
    try {
      const factory = this._mcpProviders.find(factory => factory.name === params.name);
      if (!factory)
        throw new Error('Unknown connection method: ' + params.name);

      await this._setCurrentClient(factory);
      return {
        content: [{ type: 'text', text: '### Result\nSuccessfully changed connection method.\n' }],
      };
    } catch (error) {
      return {
        content: [{ type: 'text', text: `### Result\nError: ${error}\n` }],
        isError: true,
      };
    }
  }

  private _defineContextSwitchTool(): Tool {
    return {
      name: 'browser_connect',
      description: [
        'Connect to a browser using one of the available methods:',
        ...this._mcpProviders.map(factory => `- "${factory.name}": ${factory.description}`),
      ].join('\n'),
      inputSchema: zodToJsonSchema(z.object({
        name: z.enum(this._mcpProviders.map(factory => factory.name) as [string, ...string[]]).default(this._mcpProviders[0].name).describe('The method to use to connect to the browser'),
      }), { strictUnions: true }) as Tool['inputSchema'],
      annotations: {
        title: 'Connect to a browser context',
        readOnlyHint: true,
        openWorldHint: false,
      },
    };
  }

  private async _setCurrentClient(factory: MCPProvider) {
    await this._currentClient?.close();
    this._currentClient = undefined;

    const client = new Client({ name: 'Playwright MCP Proxy', version: '0.0.0' });
    client.registerCapabilities({
      roots: {
        listChanged: true,
      },
    });
    client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
    client.setRequestHandler(PingRequestSchema, () => ({}));

    const transport = await factory.connect();
    await client.connect(transport);
    this._currentClient = client;
  }
}

```

--------------------------------------------------------------------------------
/tests/context.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Context } from '../src/context.js';
import type { BrowserContextFactory } from '../src/browserContextFactory.js';
import { EventEmitter } from 'events';

describe('Context', () => {
  let mockBrowserContextFactory: BrowserContextFactory;
  let mockBrowserContext: any;

  beforeEach(() => {
    mockBrowserContext = new EventEmitter();
    mockBrowserContext.newPage = vi.fn().mockResolvedValue({});
    mockBrowserContext.pages = vi.fn().mockReturnValue([]);
    mockBrowserContext.route = vi.fn().mockResolvedValue(undefined);
    mockBrowserContext.tracing = {
      start: vi.fn().mockResolvedValue(undefined),
      stop: vi.fn().mockResolvedValue(undefined),
    };

    mockBrowserContextFactory = {
      createContext: vi.fn().mockResolvedValue({
        browserContext: mockBrowserContext,
        close: vi.fn().mockResolvedValue(undefined),
      }),
    } as any;
  });

  afterEach(async () => {
    await Context.disposeAll();
  });

  describe('constructor', () => {
    it('should create context with options', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      expect(context.tools).toEqual([]);
      expect(context.config).toBeDefined();
    });
  });

  describe('tabs', () => {
    it('should return empty array initially', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      expect(context.tabs()).toEqual([]);
    });
  });

  describe('currentTab', () => {
    it('should return undefined when no tabs exist', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      expect(context.currentTab()).toBeUndefined();
    });
  });

  describe('currentTabOrDie', () => {
    it('should throw error when no tabs exist', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      expect(() => context.currentTabOrDie()).toThrow('No open pages available');
    });
  });

  describe('isRunningTool', () => {
    it('should return false initially', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      expect(context.isRunningTool()).toBe(false);
    });

    it('should return true when tool is running', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      context.setRunningTool('test_tool');
      expect(context.isRunningTool()).toBe(true);
    });

    it('should return false after tool completes', () => {
      const context = new Context({
        tools: [],
        config: {} as any,
        browserContextFactory: mockBrowserContextFactory,
        sessionLog: undefined,
        clientInfo: { rootPath: '/tmp' } as any,
      });

      context.setRunningTool('test_tool');
      context.setRunningTool(undefined);
      expect(context.isRunningTool()).toBe(false);
    });
  });
});

```

--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------

```
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import notice from "eslint-plugin-notice";
import path from "path";
import { fileURLToPath } from "url";
import stylistic from "@stylistic/eslint-plugin";
import importRules from "eslint-plugin-import";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const plugins = {
  "@stylistic": stylistic,
  "@typescript-eslint": typescriptEslint,
  notice,
  import: importRules,
};

export const baseRules = {
  "import/extensions": ["error", "ignorePackages", {ts: "always"}],
  "@typescript-eslint/no-floating-promises": "error",
  "@typescript-eslint/no-unused-vars": [
    2,
    { args: "none", caughtErrors: "none" },
  ],

  /**
   * Enforced rules
   */
  // syntax preferences
  "object-curly-spacing": ["error", "always"],
  quotes: [
    2,
    "single",
    {
      avoidEscape: true,
      allowTemplateLiterals: true,
    },
  ],
  "jsx-quotes": [2, "prefer-single"],
  "no-extra-semi": 2,
  "@stylistic/semi": [2],
  "comma-style": [2, "last"],
  "wrap-iife": [2, "inside"],
  "spaced-comment": [
    2,
    "always",
    {
      markers: ["*"],
    },
  ],
  eqeqeq: [2],
  "accessor-pairs": [
    2,
    {
      getWithoutSet: false,
      setWithoutGet: false,
    },
  ],
  "brace-style": [2, "1tbs", { allowSingleLine: true }],
  curly: [2, "multi-or-nest", "consistent"],
  "new-parens": 2,
  "arrow-parens": [2, "as-needed"],
  "prefer-const": 2,
  "quote-props": [2, "consistent"],
  "nonblock-statement-body-position": [2, "below"],

  // anti-patterns
  "no-var": 2,
  "no-with": 2,
  "no-multi-str": 2,
  "no-caller": 2,
  "no-implied-eval": 2,
  "no-labels": 2,
  "no-new-object": 2,
  "no-octal-escape": 2,
  "no-self-compare": 2,
  "no-shadow-restricted-names": 2,
  "no-cond-assign": 2,
  "no-debugger": 2,
  "no-dupe-keys": 2,
  "no-duplicate-case": 2,
  "no-empty-character-class": 2,
  "no-unreachable": 2,
  "no-unsafe-negation": 2,
  radix: 2,
  "valid-typeof": 2,
  "no-implicit-globals": [2],
  "no-unused-expressions": [
    2,
    { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true },
  ],
  "no-proto": 2,

  // es2015 features
  "require-yield": 2,
  "template-curly-spacing": [2, "never"],

  // spacing details
  "space-infix-ops": 2,
  "space-in-parens": [2, "never"],
  "array-bracket-spacing": [2, "never"],
  "comma-spacing": [2, { before: false, after: true }],
  "space-before-function-paren": [
    2,
    {
      anonymous: "never",
      named: "never",
      asyncArrow: "always",
    },
  ],
  "no-whitespace-before-property": 2,
  "keyword-spacing": [
    2,
    {
      overrides: {
        if: { after: true },
        else: { after: true },
        for: { after: true },
        while: { after: true },
        do: { after: true },
        switch: { after: true },
        return: { after: true },
      },
    },
  ],
  "arrow-spacing": [
    2,
    {
      after: true,
      before: true,
    },
  ],
  "@stylistic/type-annotation-spacing": 2,

  // file whitespace
  "no-multiple-empty-lines": [2, { max: 2, maxEOF: 0 }],
  "no-mixed-spaces-and-tabs": 2,
  "no-trailing-spaces": 2,
  "linebreak-style": [process.platform === "win32" ? 0 : 2, "unix"],
  indent: [
    2,
    2,
    { SwitchCase: 1, CallExpression: { arguments: 2 }, MemberExpression: 2 },
  ],
  "key-spacing": [
    2,
    {
      beforeColon: false,
    },
  ],
  "eol-last": 2,

  // copyright
  "notice/notice": [
    2,
    {
      mustMatch: "Copyright",
      templateFile: path.join(__dirname, "utils", "copyright.js"),
    },
  ],

  // react
  "react/react-in-jsx-scope": 0,
  "no-console": 2,
};

const languageOptions = {
  parser: tsParser,
  ecmaVersion: 9,
  sourceType: "module",
  parserOptions: {
    project: path.join(fileURLToPath(import.meta.url), "..", "tsconfig.all.json"),
  }
};

export default [
  {
    ignores: ["**/*.js"],
  },
  {
    files: ["**/*.ts", "**/*.tsx"],
    plugins,
    languageOptions,
    rules: baseRules,
  },
];

```

--------------------------------------------------------------------------------
/tests/tools-utils.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { waitForCompletion, generateLocator, callOnPageNoTrace } from '../src/tools/utils.js';
import type { Tab } from '../src/tab.js';
import { EventEmitter } from 'events';

describe('Tool Utils', () => {
  let mockTab: Tab;
  let mockPage: any;

  beforeEach(() => {
    mockPage = new EventEmitter();
    mockPage.url = () => 'https://example.com';
    mockPage.waitForLoadState = vi.fn().mockResolvedValue(undefined);
    mockPage.evaluate = vi.fn().mockResolvedValue(undefined);
    mockPage._wrapApiCall = vi.fn().mockImplementation(cb => cb());

    mockTab = {
      page: mockPage,
      waitForLoadState: vi.fn().mockResolvedValue(undefined),
      waitForTimeout: vi.fn().mockResolvedValue(undefined),
    } as any;
  });

  describe('waitForCompletion', () => {
    it('should wait for callback to complete', async () => {
      const callback = vi.fn().mockResolvedValue('result');

      const result = await waitForCompletion(mockTab, callback);

      expect(callback).toHaveBeenCalled();
      expect(result).toBe('result');
    });

    it('should wait for pending requests to finish', async () => {
      const callback = vi.fn().mockImplementation(() => {
        const request = { url: 'https://api.example.com' };
        mockPage.emit('request', request);
        setTimeout(() => mockPage.emit('requestfinished', request), 10);
        return Promise.resolve('result');
      });

      const result = await waitForCompletion(mockTab, callback);

      expect(result).toBe('result');
    });

    it('should ignore sub-frame navigation', async () => {
      const callback = vi.fn().mockImplementation(() => {
        const subFrame = { parentFrame: () => ({}) };
        mockPage.emit('framenavigated', subFrame);
        return Promise.resolve('result');
      });

      await waitForCompletion(mockTab, callback);

      // Should not wait for load state for sub-frames
      expect(mockTab.waitForTimeout).toHaveBeenCalled();
    });

    it('should timeout after 10 seconds', async () => {
      vi.useFakeTimers();

      const callback = vi.fn().mockImplementation(() => {
        const request = { url: 'https://slow-api.example.com' };
        mockPage.emit('request', request);
        // Never finish the request
        return Promise.resolve('result');
      });

      const promise = waitForCompletion(mockTab, callback);

      vi.advanceTimersByTime(10000);

      await promise;

      expect(callback).toHaveBeenCalled();

      vi.useRealTimers();
    });

    it('should wait 1 second after completion', async () => {
      const callback = vi.fn().mockResolvedValue('result');

      await waitForCompletion(mockTab, callback);

      expect(mockTab.waitForTimeout).toHaveBeenCalledWith(1000);
    });
  });

  describe('generateLocator', () => {
    it('should generate locator string', async () => {
      const mockLocator = {
        _resolveSelector: vi.fn().mockResolvedValue({
          resolvedSelector: 'button[name="submit"]',
        }),
      } as any;

      // Mock the asLocator function
      vi.mock('playwright-core/lib/utils', () => ({
        asLocator: (lang: string, selector: string) => `locator('${selector}')`,
      }));

      const result = await generateLocator(mockLocator);

      expect(mockLocator._resolveSelector).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should throw error for invalid locator', async () => {
      const mockLocator = {
        _resolveSelector: vi.fn().mockRejectedValue(new Error('Selector not found')),
      } as any;

      await expect(generateLocator(mockLocator)).rejects.toThrow('Ref not found');
    });
  });

  describe('callOnPageNoTrace', () => {
    it('should call function on page without tracing', async () => {
      const callback = vi.fn().mockResolvedValue('result');

      const result = await callOnPageNoTrace(mockPage, callback);

      expect(callback).toHaveBeenCalledWith(mockPage);
      expect(result).toBe('result');
      expect(mockPage._wrapApiCall).toHaveBeenCalled();
    });

    it('should pass through errors', async () => {
      const callback = vi.fn().mockRejectedValue(new Error('Test error'));

      await expect(callOnPageNoTrace(mockPage, callback)).rejects.toThrow('Test error');
    });

    it('should mark call as internal', async () => {
      const callback = vi.fn().mockResolvedValue('result');

      await callOnPageNoTrace(mockPage, callback);

      expect(mockPage._wrapApiCall).toHaveBeenCalledWith(
          expect.any(Function),
          { internal: true }
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/src/sessionLog.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import fs from 'fs';
import path from 'path';

import { Response } from './response.js';
import { logUnhandledError } from './utils/log.js';
import { outputFile  } from './config.js';

import type { FullConfig } from './config.js';
import type * as actions from './actions.js';
import type { Tab, TabSnapshot } from './tab.js';

type LogEntry = {
  timestamp: number;
  toolCall?: {
    toolName: string;
    toolArgs: Record<string, any>;
    result: string;
    isError?: boolean;
  };
  userAction?: actions.Action;
  code: string;
  tabSnapshot?: TabSnapshot;
};

export class SessionLog {
  private _folder: string;
  private _file: string;
  private _ordinal = 0;
  private _pendingEntries: LogEntry[] = [];
  private _sessionFileQueue = Promise.resolve();
  private _flushEntriesTimeout: NodeJS.Timeout | undefined;

  constructor(sessionFolder: string) {
    this._folder = sessionFolder;
    this._file = path.join(this._folder, 'session.md');
  }

  static async create(config: FullConfig, rootPath: string | undefined): Promise<SessionLog> {
    const sessionFolder = await outputFile(config, rootPath, `session-${Date.now()}`);
    await fs.promises.mkdir(sessionFolder, { recursive: true });
    // eslint-disable-next-line no-console
    console.error(`Session: ${sessionFolder}`);
    return new SessionLog(sessionFolder);
  }

  logResponse(response: Response) {
    const entry: LogEntry = {
      timestamp: performance.now(),
      toolCall: {
        toolName: response.toolName,
        toolArgs: response.toolArgs,
        result: response.result(),
        isError: response.isError(),
      },
      code: response.code(),
      tabSnapshot: response.tabSnapshot(),
    };
    this._appendEntry(entry);
  }

  logUserAction(action: actions.Action, tab: Tab, code: string, isUpdate: boolean) {
    code = code.trim();
    if (isUpdate) {
      const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
      if (lastEntry.userAction?.name === action.name) {
        lastEntry.userAction = action;
        lastEntry.code = code;
        return;
      }
    }
    if (action.name === 'navigate') {
      // Already logged at this location.
      const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
      if (lastEntry?.tabSnapshot?.url === action.url)
        return;
    }
    const entry: LogEntry = {
      timestamp: performance.now(),
      userAction: action,
      code,
      tabSnapshot: {
        url: tab.page.url(),
        title: '',
        ariaSnapshot: action.ariaSnapshot || '',
        modalStates: [],
        consoleMessages: [],
        downloads: [],
      },
    };
    this._appendEntry(entry);
  }

  private _appendEntry(entry: LogEntry) {
    this._pendingEntries.push(entry);
    if (this._flushEntriesTimeout)
      clearTimeout(this._flushEntriesTimeout);
    this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1000);
  }

  private async _flushEntries() {
    clearTimeout(this._flushEntriesTimeout);
    const entries = this._pendingEntries;
    this._pendingEntries = [];
    const lines: string[] = [''];

    for (const entry of entries) {
      const ordinal = (++this._ordinal).toString().padStart(3, '0');
      if (entry.toolCall) {
        lines.push(
            `### Tool call: ${entry.toolCall.toolName}`,
            `- Args`,
            '```json',
            JSON.stringify(entry.toolCall.toolArgs, null, 2),
            '```',
        );
        if (entry.toolCall.result) {
          lines.push(
              entry.toolCall.isError ? `- Error` : `- Result`,
              '```',
              entry.toolCall.result,
              '```',
          );
        }
      }

      if (entry.userAction) {
        const actionData = { ...entry.userAction } as any;
        delete actionData.ariaSnapshot;
        delete actionData.selector;
        delete actionData.signals;

        lines.push(
            `### User action: ${entry.userAction.name}`,
            `- Args`,
            '```json',
            JSON.stringify(actionData, null, 2),
            '```',
        );
      }

      if (entry.code) {
        lines.push(
            `- Code`,
            '```js',
            entry.code,
            '```');
      }

      if (entry.tabSnapshot) {
        const fileName = `${ordinal}.snapshot.yml`;
        fs.promises.writeFile(path.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(logUnhandledError);
        lines.push(`- Snapshot: ${fileName}`);
      }

      lines.push('', '');
    }

    this._sessionFileQueue = this._sessionFileQueue.then(() => fs.promises.appendFile(this._file, lines.join('\n')));
  }
}

```

--------------------------------------------------------------------------------
/src/mcp/http.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import assert from 'assert';
import net from 'net';
import http from 'http';
import crypto from 'crypto';

import debug from 'debug';

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as mcpServer from './server.js';

import type { ServerBackendFactory } from './server.js';

const testDebug = debug('pw:mcp:test');

export async function startHttpServer(config: { host?: string, port?: number }, abortSignal?: AbortSignal): Promise<http.Server> {
  const { host, port } = config;
  const httpServer = http.createServer();
  decorateServer(httpServer);
  await new Promise<void>((resolve, reject) => {
    httpServer.on('error', reject);
    abortSignal?.addEventListener('abort', () => {
      httpServer.close();
      reject(new Error('Aborted'));
    });
    httpServer.listen(port, host, () => {
      resolve();
      httpServer.removeListener('error', reject);
    });
  });
  return httpServer;
}

export function httpAddressToString(address: string | net.AddressInfo | null): string {
  assert(address, 'Could not bind server socket');
  if (typeof address === 'string')
    return address;
  const resolvedPort = address.port;
  let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
  if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
    resolvedHost = 'localhost';
  return `http://${resolvedHost}:${resolvedPort}`;
}

export async function installHttpTransport(httpServer: http.Server, serverBackendFactory: ServerBackendFactory) {
  const sseSessions = new Map();
  const streamableSessions = new Map();
  httpServer.on('request', async (req, res) => {
    const url = new URL(`http://localhost${req.url}`);
    if (url.pathname.startsWith('/sse'))
      await handleSSE(serverBackendFactory, req, res, url, sseSessions);
    else
      await handleStreamable(serverBackendFactory, req, res, streamableSessions);
  });
}

async function handleSSE(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, url: URL, sessions: Map<string, SSEServerTransport>) {
  if (req.method === 'POST') {
    const sessionId = url.searchParams.get('sessionId');
    if (!sessionId) {
      res.statusCode = 400;
      return res.end('Missing sessionId');
    }

    const transport = sessions.get(sessionId);
    if (!transport) {
      res.statusCode = 404;
      return res.end('Session not found');
    }

    return await transport.handlePostMessage(req, res);
  } else if (req.method === 'GET') {
    const transport = new SSEServerTransport('/sse', res);
    sessions.set(transport.sessionId, transport);
    testDebug(`create SSE session: ${transport.sessionId}`);
    await mcpServer.connect(serverBackendFactory, transport, false);
    res.on('close', () => {
      testDebug(`delete SSE session: ${transport.sessionId}`);
      sessions.delete(transport.sessionId);
    });
    return;
  }

  res.statusCode = 405;
  res.end('Method not allowed');
}

async function handleStreamable(serverBackendFactory: ServerBackendFactory, req: http.IncomingMessage, res: http.ServerResponse, sessions: Map<string, StreamableHTTPServerTransport>) {
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
  if (sessionId) {
    const transport = sessions.get(sessionId);
    if (!transport) {
      res.statusCode = 404;
      res.end('Session not found');
      return;
    }
    return await transport.handleRequest(req, res);
  }

  if (req.method === 'POST') {
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => crypto.randomUUID(),
      onsessioninitialized: async sessionId => {
        testDebug(`create http session: ${transport.sessionId}`);
        await mcpServer.connect(serverBackendFactory, transport, true);
        sessions.set(sessionId, transport);
      }
    });

    transport.onclose = () => {
      if (!transport.sessionId)
        return;
      sessions.delete(transport.sessionId);
      testDebug(`delete http session: ${transport.sessionId}`);
    };

    await transport.handleRequest(req, res);
    return;
  }

  res.statusCode = 400;
  res.end('Invalid request');
}

function decorateServer(server: net.Server) {
  const sockets = new Set<net.Socket>();
  server.on('connection', socket => {
    sockets.add(socket);
    socket.once('close', () => sockets.delete(socket));
  });

  const close = server.close;
  server.close = (callback?: (err?: Error) => void) => {
    for (const socket of sockets)
      socket.destroy();
    sockets.clear();
    return close.call(server, callback);
  };
}

```

--------------------------------------------------------------------------------
/src/vscode/host.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { fileURLToPath } from 'url';
import path from 'path';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';


import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as mcpServer from '../mcp/server.js';
import { logUnhandledError } from '../utils/log.js';
import { packageJSON } from '../utils/package.js';

import { FullConfig } from '../config.js';
import { BrowserServerBackend } from '../browserServerBackend.js';
import { contextFactory } from '../browserContextFactory.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { ClientVersion, ServerBackend } from '../mcp/server.js';
import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js';

const contextSwitchOptions = z.object({
  connectionString: z.string().optional().describe('The connection string to use to connect to the browser'),
  lib: z.string().optional().describe('The library to use for the connection'),
});

class VSCodeProxyBackend implements ServerBackend {
  name = 'Playwright MCP Client Switcher';
  version = packageJSON.version;

  private _currentClient: Client | undefined;
  private _contextSwitchTool: Tool;
  private _roots: Root[] = [];
  private _clientVersion?: ClientVersion;

  constructor(private readonly _config: FullConfig, private readonly _defaultTransportFactory: () => Promise<Transport>) {
    this._contextSwitchTool = this._defineContextSwitchTool();
  }

  async initialize(server: mcpServer.Server, clientVersion: ClientVersion, roots: Root[]): Promise<void> {
    this._clientVersion = clientVersion;
    this._roots = roots;
    const transport = await this._defaultTransportFactory();
    await this._setCurrentClient(transport);
  }

  async listTools(): Promise<Tool[]> {
    const response = await this._currentClient!.listTools();
    return [
      ...response.tools,
      this._contextSwitchTool,
    ];
  }

  async callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult> {
    if (name === this._contextSwitchTool.name)
      return this._callContextSwitchTool(args as any);
    return await this._currentClient!.callTool({
      name,
      arguments: args,
    }) as CallToolResult;
  }

  serverClosed?(server: mcpServer.Server): void {
    void this._currentClient?.close().catch(logUnhandledError);
  }

  private async _callContextSwitchTool(params: z.infer<typeof contextSwitchOptions>): Promise<CallToolResult> {
    if (!params.connectionString || !params.lib) {
      const transport = await this._defaultTransportFactory();
      await this._setCurrentClient(transport);
      return {
        content: [{ type: 'text', text: '### Result\nSuccessfully disconnected.\n' }],
      };
    }

    await this._setCurrentClient(
        new StdioClientTransport({
          command: process.execPath,
          cwd: process.cwd(),
          args: [
            path.join(fileURLToPath(import.meta.url), '..', 'main.js'),
            JSON.stringify(this._config),
            params.connectionString,
            params.lib,
          ],
        })
    );
    return {
      content: [{ type: 'text', text: '### Result\nSuccessfully connected.\n' }],
    };
  }

  private _defineContextSwitchTool(): Tool {
    return {
      name: 'browser_connect',
      description: 'Do not call, this tool is used in the integration with the Playwright VS Code Extension and meant for programmatic usage only.',
      inputSchema: zodToJsonSchema(contextSwitchOptions, { strictUnions: true }) as Tool['inputSchema'],
      annotations: {
        title: 'Connect to a browser running in VS Code.',
        readOnlyHint: true,
        openWorldHint: false,
      },
    };
  }

  private async _setCurrentClient(transport: Transport) {
    await this._currentClient?.close();
    this._currentClient = undefined;

    const client = new Client(this._clientVersion!);
    client.registerCapabilities({
      roots: {
        listChanged: true,
      },
    });
    client.setRequestHandler(ListRootsRequestSchema, () => ({ roots: this._roots }));
    client.setRequestHandler(PingRequestSchema, () => ({}));

    await client.connect(transport);
    this._currentClient = client;
  }
}

export async function runVSCodeTools(config: FullConfig) {
  const serverBackendFactory: mcpServer.ServerBackendFactory = {
    name: 'Playwright w/ vscode',
    nameInConfig: 'playwright-vscode',
    version: packageJSON.version,
    create: () => new VSCodeProxyBackend(config, () => mcpServer.wrapInProcess(new BrowserServerBackend(config, contextFactory(config))))
  };
  await mcpServer.start(serverBackendFactory, config.server);
  return;
}

```

--------------------------------------------------------------------------------
/tests/tools-tabs.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import tabsTools from '../src/tools/tabs.js';
import { Response } from '../src/response.js';
import type { Context } from '../src/context.js';
import type { Tab } from '../src/tab.js';

describe('Tabs Tools', () => {
  let mockContext: Context;
  let mockTab1: Tab;
  let mockTab2: Tab;
  let response: Response;

  beforeEach(() => {
    mockTab1 = {
      page: {
        url: () => 'https://example.com',
        setDefaultNavigationTimeout: vi.fn(),
        setDefaultTimeout: vi.fn(),
      },
      lastTitle: () => 'Example Page',
      isCurrentTab: () => true,
    } as any;

    mockTab2 = {
      page: {
        url: () => 'https://other.com',
        setDefaultNavigationTimeout: vi.fn(),
        setDefaultTimeout: vi.fn(),
      },
      lastTitle: () => 'Other Page',
      isCurrentTab: () => false,
    } as any;

    mockContext = {
      currentTabOrDie: () => mockTab1,
      tabs: () => [mockTab1, mockTab2],
      newTab: vi.fn().mockResolvedValue(mockTab2),
      selectTab: vi.fn().mockResolvedValue(mockTab2),
      closeTab: vi.fn().mockResolvedValue('https://closed.com'),
      ensureTab: vi.fn().mockResolvedValue(mockTab1),
      config: {
        imageResponses: 'include',
      },
    } as any;

    response = new Response(mockContext, 'test_tool', {});
  });

  describe('browser_tabs tool', () => {
    const tabsTool = tabsTools.find(t => t.schema.name === 'browser_tabs')!;

    it('should exist', () => {
      expect(tabsTool).toBeDefined();
      expect(tabsTool.schema.name).toBe('browser_tabs');
    });

    it('should have correct schema', () => {
      expect(tabsTool.schema.title).toBe('Manage tabs');
      expect(tabsTool.schema.type).toBe('destructive');
    });

    it('should have core-tabs capability', () => {
      expect(tabsTool.capability).toBe('core-tabs');
    });

    it('should list tabs', async () => {
      await tabsTool.handle(mockContext, { action: 'list' }, response);

      expect(mockContext.ensureTab).toHaveBeenCalled();
      const serialized = response.serialize();
      expect(serialized.content[0].text).toContain('Open tabs');
    });

    it('should create new tab', async () => {
      await tabsTool.handle(mockContext, { action: 'new' }, response);

      expect(mockContext.newTab).toHaveBeenCalled();
    });

    it('should select tab by index', async () => {
      await tabsTool.handle(mockContext, { action: 'select', index: 1 }, response);

      expect(mockContext.selectTab).toHaveBeenCalledWith(1);
    });

    it('should throw error when selecting without index', async () => {
      await expect(
          tabsTool.handle(mockContext, { action: 'select' }, response)
      ).rejects.toThrow('Tab index is required');
    });

    it('should close tab by index', async () => {
      await tabsTool.handle(mockContext, { action: 'close', index: 1 }, response);

      expect(mockContext.closeTab).toHaveBeenCalledWith(1);
    });

    it('should close current tab when no index provided', async () => {
      await tabsTool.handle(mockContext, { action: 'close' }, response);

      expect(mockContext.closeTab).toHaveBeenCalledWith(undefined);
    });
  });

  describe('browser_navigation_timeout tool', () => {
    const navTimeoutTool = tabsTools.find(t => t.schema.name === 'browser_navigation_timeout')!;

    it('should exist', () => {
      expect(navTimeoutTool).toBeDefined();
      expect(navTimeoutTool.schema.name).toBe('browser_navigation_timeout');
    });

    it('should set navigation timeout for all tabs', async () => {
      await navTimeoutTool.handle(mockContext, { timeout: 60000 }, response);

      expect(mockTab1.page.setDefaultNavigationTimeout).toHaveBeenCalledWith(60000);
      expect(mockTab2.page.setDefaultNavigationTimeout).toHaveBeenCalledWith(60000);
      expect(response.result()).toContain('60000ms');
    });
  });

  describe('browser_default_timeout tool', () => {
    const defaultTimeoutTool = tabsTools.find(t => t.schema.name === 'browser_default_timeout')!;

    it('should exist', () => {
      expect(defaultTimeoutTool).toBeDefined();
      expect(defaultTimeoutTool.schema.name).toBe('browser_default_timeout');
    });

    it('should set default timeout for all tabs', async () => {
      await defaultTimeoutTool.handle(mockContext, { timeout: 30000 }, response);

      expect(mockTab1.page.setDefaultTimeout).toHaveBeenCalledWith(30000);
      expect(mockTab2.page.setDefaultTimeout).toHaveBeenCalledWith(30000);
      expect(response.result()).toContain('30000ms');
    });
  });

  describe('Tool capabilities', () => {
    it('should all have core-tabs capability', () => {
      tabsTools.forEach(tool => {
        expect(tool.capability).toBe('core-tabs');
      });
    });

    it('should export 3 tools', () => {
      expect(tabsTools).toHaveLength(3);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/response.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { renderModalStates } from './tab.js';

import type { Tab, TabSnapshot } from './tab.js';
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
import type { Context } from './context.js';

export class Response {
  private _result: string[] = [];
  private _code: string[] = [];
  private _images: { contentType: string, data: Buffer }[] = [];
  private _context: Context;
  private _includeSnapshot = false;
  private _includeTabs = false;
  private _tabSnapshot: TabSnapshot | undefined;

  readonly toolName: string;
  readonly toolArgs: Record<string, any>;
  private _isError: boolean | undefined;

  constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
    this._context = context;
    this.toolName = toolName;
    this.toolArgs = toolArgs;
  }

  addResult(result: string) {
    this._result.push(result);
  }

  addError(error: string) {
    this._result.push(error);
    this._isError = true;
  }

  isError() {
    return this._isError;
  }

  result() {
    return this._result.join('\n');
  }

  addCode(code: string) {
    this._code.push(code);
  }

  code() {
    return this._code.join('\n');
  }

  addImage(image: { contentType: string, data: Buffer }) {
    this._images.push(image);
  }

  images() {
    return this._images;
  }

  setIncludeSnapshot() {
    this._includeSnapshot = true;
  }

  setIncludeTabs() {
    this._includeTabs = true;
  }

  async finish() {
    // All the async snapshotting post-action is happening here.
    // Everything below should race against modal states.
    if (this._includeSnapshot && this._context.currentTab())
      this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
    for (const tab of this._context.tabs())
      await tab.updateTitle();
  }

  tabSnapshot(): TabSnapshot | undefined {
    return this._tabSnapshot;
  }

  serialize(): { content: (TextContent | ImageContent)[], isError?: boolean } {
    const response: string[] = [];

    // Start with command result.
    if (this._result.length) {
      response.push('### Result');
      response.push(this._result.join('\n'));
      response.push('');
    }

    // Add code if it exists.
    if (this._code.length) {
      response.push(`### Ran Playwright code
\`\`\`js
${this._code.join('\n')}
\`\`\``);
      response.push('');
    }

    // List browser tabs.
    if (this._includeSnapshot || this._includeTabs)
      response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));

    // Add snapshot if provided.
    if (this._tabSnapshot?.modalStates.length) {
      response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates));
      response.push('');
    } else if (this._tabSnapshot) {
      response.push(renderTabSnapshot(this._tabSnapshot));
      response.push('');
    }

    // Main response part
    const content: (TextContent | ImageContent)[] = [
      { type: 'text', text: response.join('\n') },
    ];

    // Image attachments.
    if (this._context.config.imageResponses !== 'omit') {
      for (const image of this._images)
        content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType });
    }

    return { content, isError: this._isError };
  }
}

function renderTabSnapshot(tabSnapshot: TabSnapshot): string {
  const lines: string[] = [];

  if (tabSnapshot.consoleMessages.length) {
    lines.push(`### New console messages`);
    for (const message of tabSnapshot.consoleMessages)
      lines.push(`- ${trim(message.toString(), 100)}`);
    lines.push('');
  }

  if (tabSnapshot.downloads.length) {
    lines.push(`### Downloads`);
    for (const entry of tabSnapshot.downloads) {
      if (entry.finished)
        lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
      else
        lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
    }
    lines.push('');
  }

  lines.push(`### Page state`);
  lines.push(`- Page URL: ${tabSnapshot.url}`);
  lines.push(`- Page Title: ${tabSnapshot.title}`);
  lines.push(`- Page Snapshot:`);
  lines.push('```yaml');
  lines.push(tabSnapshot.ariaSnapshot);
  lines.push('```');

  return lines.join('\n');
}

function renderTabsMarkdown(tabs: Tab[], force: boolean = false): string[] {
  if (tabs.length === 1 && !force)
    return [];

  if (!tabs.length) {
    return [
      '### Open tabs',
      'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
      '',
    ];
  }

  const lines: string[] = ['### Open tabs'];
  for (let i = 0; i < tabs.length; i++) {
    const tab = tabs[i];
    const current = tab.isCurrentTab() ? ' (current)' : '';
    lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
  }
  lines.push('');
  return lines;
}

function trim(text: string, maxLength: number) {
  if (text.length <= maxLength)
    return text;
  return text.slice(0, maxLength) + '...';
}

```

--------------------------------------------------------------------------------
/src/mcp/server.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debug from 'debug';

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { httpAddressToString, installHttpTransport, startHttpServer } from './http.js';
import { InProcessTransport } from './inProcessTransport.js';

import type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export type { Server } from '@modelcontextprotocol/sdk/server/index.js';
export type { Tool, CallToolResult, CallToolRequest, Root } from '@modelcontextprotocol/sdk/types.js';

const serverDebug = debug('pw:mcp:server');
const errorsDebug = debug('pw:mcp:errors');

export type ClientVersion = { name: string, version: string };

export interface ServerBackend {
  initialize?(server: Server, clientVersion: ClientVersion, roots: Root[]): Promise<void>;
  listTools(): Promise<Tool[]>;
  callTool(name: string, args: CallToolRequest['params']['arguments']): Promise<CallToolResult>;
  serverClosed?(server: Server): void;
}

export type ServerBackendFactory = {
  name: string;
  nameInConfig: string;
  version: string;
  create: () => ServerBackend;
};

export async function connect(factory: ServerBackendFactory, transport: Transport, runHeartbeat: boolean) {
  const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat);
  await server.connect(transport);
}

export async function wrapInProcess(backend: ServerBackend): Promise<Transport> {
  const server = createServer('Internal', '0.0.0', backend, false);
  return new InProcessTransport(server);
}

export function createServer(name: string, version: string, backend: ServerBackend, runHeartbeat: boolean): Server {
  let initializedPromiseResolve = () => {};
  const initializedPromise = new Promise<void>(resolve => initializedPromiseResolve = resolve);
  const server = new Server({ name, version }, {
    capabilities: {
      tools: {},
    }
  });

  server.setRequestHandler(ListToolsRequestSchema, async () => {
    serverDebug('listTools');
    await initializedPromise;
    const tools = await backend.listTools();
    return { tools };
  });

  let heartbeatRunning = false;
  server.setRequestHandler(CallToolRequestSchema, async request => {
    serverDebug('callTool', request);
    await initializedPromise;

    if (runHeartbeat && !heartbeatRunning) {
      heartbeatRunning = true;
      startHeartbeat(server);
    }

    try {
      return await backend.callTool(request.params.name, request.params.arguments || {});
    } catch (error) {
      return {
        content: [{ type: 'text', text: '### Result\n' + String(error) }],
        isError: true,
      };
    }
  });
  addServerListener(server, 'initialized', async () => {
    try {
      const capabilities = server.getClientCapabilities();
      let clientRoots: Root[] = [];
      if (capabilities?.roots) {
        const { roots } = await server.listRoots(undefined, { timeout: 2_000 }).catch(() => ({ roots: [] }));
        clientRoots = roots;
      }
      const clientVersion = server.getClientVersion() ?? { name: 'unknown', version: 'unknown' };
      await backend.initialize?.(server, clientVersion, clientRoots);
      initializedPromiseResolve();
    } catch (e) {
      errorsDebug(e);
    }
  });
  addServerListener(server, 'close', () => backend.serverClosed?.(server));
  return server;
}

const startHeartbeat = (server: Server) => {
  const beat = () => {
    Promise.race([
      server.ping(),
      new Promise((_, reject) => setTimeout(() => reject(new Error('ping timeout')), 5000)),
    ]).then(() => {
      setTimeout(beat, 3000);
    }).catch(() => {
      void server.close();
    });
  };

  beat();
};

function addServerListener(server: Server, event: 'close' | 'initialized', listener: () => void) {
  const oldListener = server[`on${event}`];
  server[`on${event}`] = () => {
    oldListener?.();
    listener();
  };
}

export async function start(serverBackendFactory: ServerBackendFactory, options: { host?: string; port?: number }) {
  if (options.port === undefined) {
    await connect(serverBackendFactory, new StdioServerTransport(), false);
    return;
  }

  const httpServer = await startHttpServer(options);
  await installHttpTransport(httpServer, serverBackendFactory);
  const url = httpAddressToString(httpServer.address());

  const mcpConfig: any = { mcpServers: { } };
  mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = {
    url: `${url}/mcp`
  };
  const message = [
    `Listening on ${url}`,
    'Put this in your client config:',
    JSON.stringify(mcpConfig, undefined, 2),
    'For legacy SSE transport support, you can use the /sse endpoint instead.',
  ].join('\n');
    // eslint-disable-next-line no-console
  console.error(message);
}

```

--------------------------------------------------------------------------------
/src/tools/verify.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';

import { defineTabTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';

const verifyElement = defineTabTool({
  capability: 'verify',
  schema: {
    name: 'browser_verify_element_visible',
    title: 'Verify element visible',
    description: 'Verify element is visible on the page',
    inputSchema: z.object({
      role: z.string().describe('ROLE of the element. Can be found in the snapshot like this: \`- {ROLE} "Accessible Name":\`'),
      accessibleName: z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: \`- role "{ACCESSIBLE_NAME}"\`'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const locator = tab.page.getByRole(params.role as any, { name: params.accessibleName });
    if (await locator.count() === 0) {
      response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`);
      return;
    }

    response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`);
    response.addResult('Done');
  },
});

const verifyText = defineTabTool({
  capability: 'verify',
  schema: {
    name: 'browser_verify_text_visible',
    title: 'Verify text visible',
    description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`,
    inputSchema: z.object({
      text: z.string().describe('TEXT to verify. Can be found in the snapshot like this: \`- role "Accessible Name": {TEXT}\` or like this: \`- text: {TEXT}\`'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const locator = tab.page.getByText(params.text).filter({ visible: true });
    if (await locator.count() === 0) {
      response.addError('Text not found');
      return;
    }

    response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`);
    response.addResult('Done');
  },
});

const verifyList = defineTabTool({
  capability: 'verify',
  schema: {
    name: 'browser_verify_list_visible',
    title: 'Verify list visible',
    description: 'Verify list is visible on the page',
    inputSchema: z.object({
      element: z.string().describe('Human-readable list description'),
      ref: z.string().describe('Exact target element reference that points to the list'),
      items: z.array(z.string()).describe('Items to verify'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const locator = await tab.refLocator({ ref: params.ref, element: params.element });
    const itemTexts: string[] = [];
    for (const item of params.items) {
      const itemLocator = locator.getByText(item);
      if (await itemLocator.count() === 0) {
        response.addError(`Item "${item}" not found`);
        return;
      }
      itemTexts.push((await itemLocator.textContent())!);
    }
    const ariaSnapshot = `\`
- list:
${itemTexts.map(t => `  - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join('\n')}
\``;
    response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`);
    response.addResult('Done');
  },
});

const verifyValue = defineTabTool({
  capability: 'verify',
  schema: {
    name: 'browser_verify_value',
    title: 'Verify value',
    description: 'Verify element value',
    inputSchema: z.object({
      type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the element'),
      element: z.string().describe('Human-readable element description'),
      ref: z.string().describe('Exact target element reference that points to the element'),
      value: z.string().describe('Value to verify. For checkbox, use "true" or "false".'),
    }),
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    const locator = await tab.refLocator({ ref: params.ref, element: params.element });
    const locatorSource = `page.${await generateLocator(locator)}`;
    if (params.type === 'textbox' || params.type === 'slider' || params.type === 'combobox') {
      const value = await locator.inputValue();
      if (value !== params.value) {
        response.addError(`Expected value "${params.value}", but got "${value}"`);
        return;
      }
      response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`);
    } else if (params.type === 'checkbox' || params.type === 'radio') {
      const value = await locator.isChecked();
      if (value !== (params.value === 'true')) {
        response.addError(`Expected value "${params.value}", but got "${value}"`);
        return;
      }
      const matcher = value ? 'toBeChecked' : 'not.toBeChecked';
      response.addCode(`await expect(${locatorSource}).${matcher}();`);
    }
    response.addResult('Done');
  },
});

export default [
  verifyElement,
  verifyText,
  verifyList,
  verifyValue,
];

```

--------------------------------------------------------------------------------
/tests/tool-definitions.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { describe, it, expect, vi } from 'vitest';
import { defineTool, defineTabTool } from '../src/tools/tool.js';
import { z } from 'zod';
import type { Response } from '../src/response.js';

describe('Tool Definitions', () => {
  describe('defineTool', () => {
    it('should define a tool with schema and handler', () => {
      const schema = z.object({
        param: z.string(),
      });

      const handler = vi.fn();

      const tool = defineTool({
        capability: 'core',
        schema: {
          name: 'test_tool',
          title: 'Test Tool',
          description: 'A test tool',
          inputSchema: schema,
          type: 'readOnly',
        },
        handle: handler,
      });

      expect(tool.capability).toBe('core');
      expect(tool.schema.name).toBe('test_tool');
      expect(tool.handle).toBe(handler);
    });

    it('should preserve tool properties', () => {
      const schema = z.object({});

      const tool = defineTool({
        capability: 'core',
        schema: {
          name: 'my_tool',
          title: 'My Tool',
          description: 'Description',
          inputSchema: schema,
          type: 'destructive',
        },
        handle: async () => {},
      });

      expect(tool.schema.title).toBe('My Tool');
      expect(tool.schema.description).toBe('Description');
      expect(tool.schema.type).toBe('destructive');
    });
  });

  describe('defineTabTool', () => {
    it('should define a tab tool that wraps handler', () => {
      const schema = z.object({
        param: z.string(),
      });

      const tabHandler = vi.fn();

      const tool = defineTabTool({
        capability: 'core',
        schema: {
          name: 'tab_tool',
          title: 'Tab Tool',
          description: 'A tab tool',
          inputSchema: schema,
          type: 'readOnly',
        },
        handle: tabHandler,
      });

      expect(tool.capability).toBe('core');
      expect(tool.schema.name).toBe('tab_tool');
      expect(typeof tool.handle).toBe('function');
    });

    it('should call tab handler with current tab', async () => {
      const mockTab = {
        modalStates: vi.fn().mockReturnValue([]),
        modalStatesMarkdown: vi.fn().mockReturnValue([]),
      };

      const mockContext = {
        currentTabOrDie: vi.fn().mockReturnValue(mockTab),
      };

      const mockResponse = {} as Response;

      const tabHandler = vi.fn();

      const tool = defineTabTool({
        capability: 'core',
        schema: {
          name: 'tab_tool',
          title: 'Tab Tool',
          description: 'A tab tool',
          inputSchema: z.object({}),
          type: 'readOnly',
        },
        handle: tabHandler,
      });

      await tool.handle(mockContext as any, {}, mockResponse);

      expect(mockContext.currentTabOrDie).toHaveBeenCalled();
      expect(tabHandler).toHaveBeenCalledWith(mockTab, {}, mockResponse);
    });

    it('should add error when modal state present and tool does not handle it', async () => {
      const mockTab = {
        modalStates: vi.fn().mockReturnValue([{ type: 'dialog' }]),
        modalStatesMarkdown: vi.fn().mockReturnValue(['Dialog present']),
      };

      const mockContext = {
        currentTabOrDie: vi.fn().mockReturnValue(mockTab),
      };

      const mockResponse = {
        addError: vi.fn(),
      } as any;

      const tabHandler = vi.fn();

      const tool = defineTabTool({
        capability: 'core',
        schema: {
          name: 'tab_tool',
          title: 'Tab Tool',
          description: 'A tab tool',
          inputSchema: z.object({}),
          type: 'readOnly',
        },
        handle: tabHandler,
      });

      await tool.handle(mockContext as any, {}, mockResponse);

      expect(mockResponse.addError).toHaveBeenCalled();
      expect(tabHandler).not.toHaveBeenCalled();
    });

    it('should add error when modal state required but not present', async () => {
      const mockTab = {
        modalStates: vi.fn().mockReturnValue([]),
        modalStatesMarkdown: vi.fn().mockReturnValue([]),
      };

      const mockContext = {
        currentTabOrDie: vi.fn().mockReturnValue(mockTab),
      };

      const mockResponse = {
        addError: vi.fn(),
      } as any;

      const tabHandler = vi.fn();

      const tool = defineTabTool({
        capability: 'core',
        schema: {
          name: 'tab_tool',
          title: 'Tab Tool',
          description: 'A tab tool',
          inputSchema: z.object({}),
          type: 'readOnly',
        },
        clearsModalState: 'dialog',
        handle: tabHandler,
      });

      await tool.handle(mockContext as any, {}, mockResponse);

      expect(mockResponse.addError).toHaveBeenCalled();
      expect(tabHandler).not.toHaveBeenCalled();
    });

    it('should call handler when modal state matches', async () => {
      const mockTab = {
        modalStates: vi.fn().mockReturnValue([{ type: 'dialog' }]),
        modalStatesMarkdown: vi.fn().mockReturnValue([]),
      };

      const mockContext = {
        currentTabOrDie: vi.fn().mockReturnValue(mockTab),
      };

      const mockResponse = {
        addError: vi.fn(),
      } as any;

      const tabHandler = vi.fn();

      const tool = defineTabTool({
        capability: 'core',
        schema: {
          name: 'tab_tool',
          title: 'Tab Tool',
          description: 'A tab tool',
          inputSchema: z.object({}),
          type: 'readOnly',
        },
        clearsModalState: 'dialog',
        handle: tabHandler,
      });

      await tool.handle(mockContext as any, {}, mockResponse);

      expect(mockResponse.addError).not.toHaveBeenCalled();
      expect(tabHandler).toHaveBeenCalledWith(mockTab, {}, mockResponse);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/program.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { program, Option } from 'commander';
import * as mcpServer from './mcp/server.js';
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
import { packageJSON } from './utils/package.js';
import { Context } from './context.js';
import { contextFactory } from './browserContextFactory.js';
import { ProxyBackend } from './mcp/proxyBackend.js';
import { BrowserServerBackend } from './browserServerBackend.js';
import { ExtensionContextFactory } from './extension/extensionContextFactory.js';

import { runVSCodeTools } from './vscode/host.js';
import type { MCPProvider } from './mcp/proxyBackend.js';

program
    .version('Version ' + packageJSON.version)
    .name(packageJSON.name)
    .option('--allowed-origins <origins>', 'semicolon-separated list of origins to allow the browser to request. Default is to allow all.', semicolonSeparatedList)
    .option('--blocked-origins <origins>', 'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.', semicolonSeparatedList)
    .option('--block-service-workers', 'block service workers')
    .option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
    .option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
    .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
    .option('--config <path>', 'path to the configuration file.')
    .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
    .option('--executable-path <path>', 'path to the browser executable.')
    .option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.')
    .option('--headless', 'run browser in headless mode, headed by default')
    .option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
    .option('--ignore-https-errors', 'ignore https errors')
    .option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
    .option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
    .option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
    .option('--output-dir <path>', 'path to the directory for output files.')
    .option('--port <port>', 'port to listen on for SSE transport.')
    .option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
    .option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
    .option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
    .option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
    .option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
    .option('--user-agent <ua string>', 'specify user agent string')
    .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
    .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
    .option('--navigation-timeout <ms>', 'maximum time in milliseconds for page navigation. Defaults to 60000ms (60 seconds).', parseInt)
    .option('--default-timeout <ms>', 'default timeout for all Playwright operations (clicks, fills, etc). Defaults to 5000ms (5 seconds).', parseInt)
    .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp())
    .addOption(new Option('--vscode', 'VS Code tools.').hideHelp())
    .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
    .action(async options => {
      setupExitWatchdog();

      if (options.vision) {
        // eslint-disable-next-line no-console
        console.error('The --vision option is deprecated, use --caps=vision instead');
        options.caps = 'vision';
      }

      const config = await resolveCLIConfig(options);
      const browserContextFactory = contextFactory(config);
      const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);

      if (options.extension) {
        const serverBackendFactory: mcpServer.ServerBackendFactory = {
          name: 'Playwright w/ extension',
          nameInConfig: 'playwright-extension',
          version: packageJSON.version,
          create: () => new BrowserServerBackend(config, extensionContextFactory)
        };
        await mcpServer.start(serverBackendFactory, config.server);
        return;
      }

      if (options.vscode) {
        await runVSCodeTools(config);
        return;
      }

      if (options.connectTool) {
        const providers: MCPProvider[] = [
          {
            name: 'default',
            description: 'Starts standalone browser',
            connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, browserContextFactory)),
          },
          {
            name: 'extension',
            description: 'Connect to a browser using the Playwright MCP extension',
            connect: () => mcpServer.wrapInProcess(new BrowserServerBackend(config, extensionContextFactory)),
          },
        ];
        const factory: mcpServer.ServerBackendFactory = {
          name: 'Playwright w/ switch',
          nameInConfig: 'playwright-switch',
          version: packageJSON.version,
          create: () => new ProxyBackend(providers),
        };
        await mcpServer.start(factory, config.server);
        return;
      }

      const factory: mcpServer.ServerBackendFactory = {
        name: 'Playwright',
        nameInConfig: 'playwright',
        version: packageJSON.version,
        create: () => new BrowserServerBackend(config, browserContextFactory)
      };
      await mcpServer.start(factory, config.server);
    });

function setupExitWatchdog() {
  let isExiting = false;
  const handleExit = async () => {
    if (isExiting)
      return;
    isExiting = true;
    setTimeout(() => process.exit(0), 15000);
    await Context.disposeAll();
    process.exit(0);
  };

  process.stdin.on('close', handleExit);
  process.on('SIGINT', handleExit);
  process.on('SIGTERM', handleExit);
}

void program.parseAsync(process.argv);

```

--------------------------------------------------------------------------------
/src/tools/snapshot.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { z } from 'zod';
import { defineTabTool, defineTool } from './tool.js';
import * as javascript from '../utils/codegen.js';
import { generateLocator } from './utils.js';
import AxeBuilder from '@axe-core/playwright';


const tagValues = [
  'wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa',
  'wcag22a', 'wcag22aa', 'wcag22aaa', 'section508', 'cat.aria', 'cat.color',
  'cat.forms', 'cat.keyboard', 'cat.language', 'cat.name-role-value',
  'cat.parsing', 'cat.semantics', 'cat.sensory-and-visual-cues',
  'cat.structure', 'cat.tables', 'cat.text-alternatives', 'cat.time-and-media',
] as const;


const scanPageSchema = z.object({
  violationsTag: z
      .array(z.enum(tagValues))
      .min(1)
      .default([...tagValues])
      .describe('Array of tags to filter violations by. If not specified, all violations are returned.')
});

type AxeScanResult = Awaited<ReturnType<InstanceType<typeof AxeBuilder>['analyze']>>;
type AxeViolation = AxeScanResult['violations'][number];
type AxeNode = AxeViolation['nodes'][number];

const dedupeViolationNodes = (nodes: AxeNode[]): AxeNode[] => {
  const seen = new Set<string>();
  return nodes.filter(node => {
    const key = JSON.stringify({ target: node.target ?? [], html: node.html ?? '' });
    if (seen.has(key))
      return false;

    seen.add(key);
    return true;
  });
};

const scanPage = defineTool({
  capability: 'core',
  schema: {
    name: 'scan_page',
    title: 'Scan page for accessibility violations',
    description: 'Scan the current page for accessibility violations using Axe',
    inputSchema: scanPageSchema,
    type: 'destructive',
  },

  handle: async (context, params, response) => {
    const tab = context.currentTabOrDie();
    const axe = new AxeBuilder({ page: tab.page }).withTags(params.violationsTag);

    const results = await axe.analyze();

    response.addResult([
      `URL: ${results.url}`,
      '',
      `Violations: ${results.violations.length}, Incomplete: ${results.incomplete.length}, Passes: ${results.passes.length}, Inapplicable: ${results.inapplicable.length}`,
    ].join('\n'));


    results.violations.forEach(violation => {
      const uniqueNodes = dedupeViolationNodes(violation.nodes);

      response.addResult([
        '',
        `Tags : ${violation.tags}`,
        `Violations: ${JSON.stringify(uniqueNodes, null, 2)}`,
      ].join('\n'));
    });
  },
});

const snapshot = defineTool({
  capability: 'core',
  schema: {
    name: 'browser_snapshot',
    title: 'Page snapshot',
    description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
    inputSchema: z.object({}),
    type: 'readOnly',
  },

  handle: async (context, params, response) => {
    await context.ensureTab();
    response.setIncludeSnapshot();
  },
});

export const elementSchema = z.object({
  element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
  ref: z.string().describe('Exact target element reference from the page snapshot'),
});

const clickSchema = elementSchema.extend({
  doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
  button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'),
});

const click = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_click',
    title: 'Click',
    description: 'Perform click on a web page',
    inputSchema: clickSchema,
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const locator = await tab.refLocator(params);
    const button = params.button;
    const buttonAttr = button ? `{ button: '${button}' }` : '';

    if (params.doubleClick)
      response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
    else
      response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);


    await tab.waitForCompletion(async () => {
      if (params.doubleClick)
        await locator.dblclick({ button });
      else
        await locator.click({ button });
    });
  },
});

const drag = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_drag',
    title: 'Drag mouse',
    description: 'Perform drag and drop between two elements',
    inputSchema: z.object({
      startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
      startRef: z.string().describe('Exact source element reference from the page snapshot'),
      endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
      endRef: z.string().describe('Exact target element reference from the page snapshot'),
    }),
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const [startLocator, endLocator] = await tab.refLocators([
      { ref: params.startRef, element: params.startElement },
      { ref: params.endRef, element: params.endElement },
    ]);

    await tab.waitForCompletion(async () => {
      await startLocator.dragTo(endLocator);
    });

    response.addCode(`await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`);
  },
});

const hover = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_hover',
    title: 'Hover mouse',
    description: 'Hover over element on page',
    inputSchema: elementSchema,
    type: 'readOnly',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const locator = await tab.refLocator(params);
    response.addCode(`await page.${await generateLocator(locator)}.hover();`);

    await tab.waitForCompletion(async () => {
      await locator.hover();
    });
  },
});

const selectOptionSchema = elementSchema.extend({
  values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
});

const selectOption = defineTabTool({
  capability: 'core',
  schema: {
    name: 'browser_select_option',
    title: 'Select option',
    description: 'Select an option in a dropdown',
    inputSchema: selectOptionSchema,
    type: 'destructive',
  },

  handle: async (tab, params, response) => {
    response.setIncludeSnapshot();

    const locator = await tab.refLocator(params);
    response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);

    await tab.waitForCompletion(async () => {
      await locator.selectOption(params.values);
    });
  },
});

export default [
  snapshot,
  click,
  drag,
  hover,
  selectOption,
  scanPage
];

```
Page 1/2FirstPrevNextLast