# MCP Tools and CLI Commands

## Adding MCP Tools

### Step 1: Create the Tool File

Create `packages/playwright-core/src/tools/backend/<your-tool>.ts`.

Import zod from the MCP bundle and use `defineTool` or `defineTabTool`:

```typescript
import { z } from '../../zodBundle';
import { defineTool, defineTabTool } from './tool';
```

**Choose `defineTabTool` vs `defineTool`:**
- `defineTabTool` — most tools use this. Receives a `Tab` object, auto-handles modal state (dialogs/file choosers).
- `defineTool` — receives the full `Context`. Use when you need `context.ensureBrowserContext()` without a specific tab, or need custom tab management.

**Tool definition pattern:**

```typescript
const myTool = defineTabTool({
  capability: 'core',  // ToolCapability — see step 2

  // Optional: only available in skill mode (not exposed via MCP)
  // skillOnly: true,

  // Optional: this tool clears a modal state ('dialog' | 'fileChooser')
  // clearsModalState: 'dialog',

  schema: {
    name: 'browser_my_tool',       // MCP tool name (browser_ prefix)
    title: 'My Tool',              // Human-readable title
    description: 'Does something', // Description shown to LLM
    inputSchema: z.object({
      ref: z.string().describe('Element reference from snapshot'),
      value: z.string().optional().describe('Optional value'),
    }),
    type: 'action',  // 'input' | 'assertion' | 'action' | 'readOnly'
  },

  handle: async (tab, params, response) => {
    // Implementation using tab.page (Playwright Page object)
    await tab.page.click(`[ref="${params.ref}"]`);

    // Add generated Playwright code
    response.addCode(`await page.click('[ref="${params.ref}"]');`);

    // Include page snapshot in response (for navigation/state changes)
    response.setIncludeSnapshot();

    // Or add text result
    response.addTextResult('Done');
  },
});

export default [myTool];
```

**Schema type values:**
- `'action'` — state-changing operations (navigate, click, fill)
- `'input'` — user input (typing, keyboard)
- `'readOnly'` — queries that don't modify state (list cookies, get snapshot)
- `'assertion'` — testing/verification tools

**Response API:**
- `response.addTextResult(text)` — add text to result section
- `response.addError(error)` — add error message
- `response.addCode(code)` — add generated Playwright code snippet
- `response.setIncludeSnapshot()` — include ARIA snapshot in response
- `response.setIncludeFullSnapshot(filename?)` — force full snapshot
- `response.addResult(title, data, fileTemplate)` — add file result
- `response.registerImageResult(data, 'png'|'jpeg')` — add image

**Context tool example** (for browser-context-level operations):

```typescript
const myContextTool = defineTool({
  capability: 'storage',
  schema: { /* ... */ type: 'readOnly' },

  handle: async (context, params, response) => {
    const browserContext = await context.ensureBrowserContext();
    const cookies = await browserContext.cookies();
    response.addTextResult(cookies.map(c => `${c.name}=${c.value}`).join('\n'));
  },
});
```

### Step 2: Add ToolCapability (if needed)

If your tool doesn't fit an existing capability, add a new one to `packages/playwright-core/src/tools/mcp/config.d.ts`:

```typescript
export type ToolCapability =
  'config' |
  'core' |           // Always enabled
  'core-navigation' | // Always enabled
  'core-tabs' |      // Always enabled
  'core-input' |     // Always enabled
  'core-install' |   // Always enabled
  'network' |
  'pdf' |
  'storage' |
  'testing' |
  'vision' |
  'devtools';        // Add yours here
```

**Capability filtering rules:**
- Tools with `core*` capabilities are always enabled
- Other capabilities must be enabled via `--caps` or config `capabilities` array
- `skillOnly: true` tools are only available in skill mode, never via MCP

### Step 3: Register the Tool

In `packages/playwright-core/src/tools/backend/tools.ts`:

```typescript
import myTool from './myTool';

export const browserTools: Tool<any>[] = [
  // ... existing tools ...
  ...myTool,
];
```

### Step 4: Write Tests

Create `tests/mcp/<category>.spec.ts`. Use the fixtures from `./fixtures`:

```typescript
import { test, expect } from './fixtures';

test('browser_my_tool', async ({ client, server }) => {
  // Setup: navigate to a page first
  await client.callTool({
    name: 'browser_navigate',
    arguments: { url: server.PREFIX },
  });

  // Call your tool
  expect(await client.callTool({
    name: 'browser_my_tool',
    arguments: { ref: 'e1' },
  })).toHaveResponse({
    code: `await page.click('[ref="e1"]');`,
    snapshot: expect.stringContaining('some content'),
  });
});

test('browser_my_tool error case', async ({ client }) => {
  expect(await client.callTool({
    name: 'browser_my_tool',
    arguments: { ref: 'invalid' },
  })).toHaveResponse({
    error: expect.stringContaining('Error:'),
    isError: true,
  });
});
```

**Test fixtures:**
- `client` — MCP client, call tools via `client.callTool({ name, arguments })`
- `startClient(options?)` — client factory, for custom config/args/roots
- `server` — HTTP test server (`server.PREFIX`, `server.HELLO_WORLD`, `server.setContent(path, html, contentType)`)
- `httpsServer` — HTTPS test server

**Custom matchers:**
- `toHaveResponse({ code?, snapshot?, page?, error?, isError?, result?, events?, modalState? })` — matches parsed response sections
- `toHaveTextResponse(text)` — matches raw text with normalization

**Parsed response sections:**
- `code` — generated Playwright code (without ```js fences)
- `snapshot` — ARIA page snapshot (with ```yaml fences)
- `page` — page info (URL, title)
- `error` — error message
- `result` — text result
- `events` — console messages, downloads
- `modalState` — active dialog/file chooser info
- `tabs` — tab listing
- `isError` — boolean

### Testing MCP Tools
- Run tests: `npm run ctest-mcp <category>`
- Do not run `test --debug`

---

## Adding CLI Commands

CLI commands are thin wrappers over MCP tools. They live in the daemon and map CLI args to MCP tool calls.

### Step 1: Implement the MCP Tool

Implement the corresponding MCP tool first (see section above). CLI commands call MCP tools via `toolName`/`toolParams`.

### Step 2: Add the Command Declaration

In `packages/playwright-core/src/tools/cli-daemon/commands.ts`, use `declareCommand()`:

```typescript
import { z } from '../../zodBundle';
import { declareCommand } from './command';

const myCommand = declareCommand({
  name: 'my-command',           // CLI command name (kebab-case)
  description: 'Does something', // Shown in help
  category: 'core',             // Category for help grouping

  // Positional arguments (ordered, parsed from CLI positional args)
  args: z.object({
    url: z.string().describe('The URL to navigate to'),
    ref: z.string().optional().describe('Optional element reference'),
  }),

  // Named options (parsed from --flag or --flag=value)
  options: z.object({
    submit: z.boolean().optional().describe('Whether to submit'),
    filename: z.string().optional().describe('Output filename'),
  }),

  // MCP tool name — string or function for dynamic routing
  toolName: 'browser_my_tool',
  // OR dynamic:
  // toolName: ({ submit }) => submit ? 'browser_submit' : 'browser_type',

  // Map CLI args/options to MCP tool params
  toolParams: ({ url, ref, submit, filename }) => ({
    url,
    ref,
    submit,
    filename,
  }),
});
```

Then add to the `commandsArray` at the bottom of the file, in the correct category section:

```typescript
const commandsArray: AnyCommandSchema[] = [
  // core category
  open,
  close,
  // ... existing commands ...
  myCommand,   // <-- add here in the right category
  // ...
];
```

**Categories** (defined in `packages/playwright-core/src/tools/cli-daemon/command.ts`):

```typescript
type Category = 'core' | 'navigation' | 'keyboard' | 'mouse' | 'export' |
                'storage' | 'tabs' | 'network' | 'devtools' | 'browsers' |
                'config' | 'install';
```

To add a new category:
1. Add it to `Category` type in `packages/playwright-core/src/tools/cli-daemon/command.ts`
2. Add it to the `categories` array in `packages/playwright-core/src/tools/cli-daemon/helpGenerator.ts`:
   ```typescript
   const categories: { name: Category, title: string }[] = [
     // ... existing ...
     { name: 'mycat', title: 'My Category' },
   ];
   ```

**Special tool patterns:**
- `toolName: ''` — command handled specially by daemon (e.g., `close`, `list`, `install`)
- Use `numberArg` for numeric CLI args: `x: numberArg.describe('X coordinate')`
- Param renaming: `toolParams: ({ w: width, h: height }) => ({ width, height })`
- Dynamic toolName: `toolName: ({ clear }) => clear ? 'browser_clear' : 'browser_list'`

### Step 3: Update SKILL File

Update `packages/playwright/src/skill/SKILL.md` with the new command documentation.
Add reference docs in `packages/playwright/src/skill/references/` if the feature is complex.

Run `npm run playwright-cli -- --help` to verify the help output includes your new command.

### Step 4: Write CLI Tests

Create `tests/mcp/cli-<category>.spec.ts`. Use fixtures from `./cli-fixtures`:

```typescript
import { test, expect } from './cli-fixtures';

test('my-command', async ({ cli, server }) => {
  // Open a page first
  await cli('open', server.PREFIX);

  // Run your command
  const { output, snapshot } = await cli('my-command', 'arg1', '--option=value');
  expect(output).toContain('expected text');
  expect(snapshot).toContain('expected snapshot content');
});
```

**CLI test fixtures:**
- `cli(...args)` — run CLI command, returns `{ output, error, exitCode, snapshot, attachments }`
  - `output` — stdout text
  - `snapshot` — extracted ARIA snapshot (if present)
  - `attachments` — file attachments `{ name, data }[]`
  - `error` — stderr text
  - `exitCode` — process exit code

### Testing CLI Commands
- Run tests: `npm run ctest-mcp cli-<category>`
- Do not run `test --debug`

---

## Adding Config Options

When you need to add a new config option, update these files in order:

### 1. Type definition: `packages/playwright-core/src/tools/mcp/config.d.ts`

Add the option to the `Config` type with JSDoc:

```typescript
export type Config = {
  // ... existing ...

  /**
   * Description of the new option.
   */
  myOption?: string;
};
```

### 2. CLI options type: `packages/playwright-core/src/tools/mcp/config.ts`

Add to `CLIOptions` type:

```typescript
export type CLIOptions = {
  // ... existing ...
  myOption?: string;
};
```

If the option needs to be in `FullConfig` (with required/resolved values), update `FullConfig` and `defaultConfig`:

```typescript
export type FullConfig = Config & {
  // ... existing ...
  myOption: string;  // required in resolved config
};

export const defaultConfig: FullConfig = {
  // ... existing ...
  myOption: 'default-value',
};
```

### 3. Config from CLI: `configFromCLIOptions()` in `config.ts`

Map CLI option to config:

```typescript
const config: Config = {
  // ... existing ...
  myOption: cliOptions.myOption,
};
```

### 4. Config from env: `configFromEnv()` in `config.ts`

Add environment variable mapping:

```typescript
options.myOption = envToString(process.env.PLAYWRIGHT_MCP_MY_OPTION);
// For booleans: envToBoolean(process.env.PLAYWRIGHT_MCP_MY_OPTION)
// For numbers: numberParser(process.env.PLAYWRIGHT_MCP_MY_OPTION)
// For comma lists: commaSeparatedList(process.env.PLAYWRIGHT_MCP_MY_OPTION)
// For semicolon lists: semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_MY_OPTION)
```

### 5. MCP server CLI: `packages/playwright-core/src/tools/mcp/program.ts`

Add CLI flag:

```typescript
command
  .option('--my-option <value>', 'description of option')
```

### 6. Merge config (if nested)

If the option is nested, update `mergeConfig()` in `config.ts` to deep-merge it.

**Config resolution order:** `defaultConfig` → config file → env vars → CLI args (last wins).

---

## SKILL File

The skill file is located at `packages/playwright/src/skill/SKILL.md`. It contains documentation for all available CLI commands and MCP tools. Update it whenever you add new commands or tools.

Reference docs live in `packages/playwright/src/skill/references/`:
- `request-mocking.md` — network mocking patterns
- `running-code.md` — code execution
- `session-management.md` — session handling
- `storage-state.md` — state persistence
- `test-generation.md` — test creation
- `tracing.md` — trace recording
- `video-recording.md` — video capture

Run `npm run playwright-cli -- --help` to see the latest available commands and use them to update the skill file.

---

## Architecture Reference

### Directory Structure

```
packages/playwright-core/src/tools/
├── backend/                  # All MCP tool implementations
│   ├── tool.ts               # Tool/TabTool types, defineTool(), defineTabTool()
│   ├── tools.ts              # Tool registry (browserTools array, filteredTools)
│   ├── browserBackend.ts     # Browser backend
│   ├── context.ts            # Browser context management
│   ├── tab.ts                # Tab management
│   ├── response.ts           # Response class, parseResponse()
│   ├── common.ts             # close, resize
│   ├── navigate.ts           # navigate, goBack, goForward, reload
│   ├── snapshot.ts           # page snapshot
│   ├── form.ts               # click, type, fill, select, check
│   ├── keyboard.ts           # press, keydown, keyup
│   ├── mouse.ts              # mouse move, click, wheel
│   ├── tabs.ts               # tab management
│   ├── cookies.ts            # cookie CRUD
│   ├── webstorage.ts         # localStorage, sessionStorage
│   ├── storage.ts            # storage state save/load
│   ├── network.ts            # network requests listing
│   ├── route.ts              # request mocking/routing
│   ├── console.ts            # console messages
│   ├── evaluate.ts           # JS evaluation
│   ├── screenshot.ts         # screenshots
│   ├── pdf.ts                # PDF generation
│   ├── files.ts              # file upload
│   ├── dialogs.ts            # dialog handling
│   ├── verify.ts             # assertions
│   ├── wait.ts               # wait operations
│   ├── tracing.ts            # trace recording
│   ├── video.ts              # video recording
│   ├── runCode.ts            # run Playwright code
│   ├── devtools.ts           # DevTools integration
│   ├── config.ts             # config tool
│   └── utils.ts              # shared utilities
├── mcp/                      # MCP server
│   ├── config.d.ts           # Config type, ToolCapability type
│   ├── config.ts             # Config resolution, CLIOptions, FullConfig
│   ├── program.ts            # MCP server CLI setup
│   ├── index.ts              # MCP server entry
│   ├── browserFactory.ts     # Browser factory
│   ├── extensionContextFactory.ts
│   ├── cdpRelay.ts           # CDP relay
│   ├── watchdog.ts           # Watchdog
│   └── log.ts                # Logging
├── cli-client/               # CLI client
│   ├── program.ts            # CLI client entry (argument parsing)
│   ├── session.ts            # Session management
│   └── registry.ts           # Session registry
├── cli-daemon/               # CLI daemon
│   ├── command.ts            # Category type, CommandSchema, declareCommand(), parseCommand()
│   ├── commands.ts           # All CLI command declarations
│   ├── helpGenerator.ts      # Help text generation (generateHelp, generateHelpJSON)
│   ├── daemon.ts             # Daemon server
│   └── program.ts            # Daemon program entry
├── dashboard/                # Dashboard UI
│   ├── dashboardApp.ts       # Dashboard app
│   └── dashboardController.ts
├── utils/
│   ├── socketConnection.ts   # Socket connection utilities
│   └── mcp/                  # MCP SDK utilities
│       ├── server.ts         # MCP server wrapper
│       ├── tool.ts           # ToolSchema type, toMcpTool()
│       └── http.ts           # HTTP utilities
└── exports.ts                # Public exports

packages/playwright/src/
└── skill/
    ├── SKILL.md              # Skill documentation
    └── references/           # Reference docs

tests/mcp/
├── fixtures.ts               # MCP test fixtures (client, startClient, server)
├── cli-fixtures.ts           # CLI test fixtures (cli helper)
├── <category>.spec.ts        # MCP tool tests
└── cli-<category>.spec.ts    # CLI command tests
```

### Execution Flow

```
MCP Server mode:
  LLM → MCP protocol → Server.callTool(name, args)
    → zod validates input → Tool.handle(context|tab, params, response)
    → response.serialize() → MCP protocol → LLM

CLI mode:
  User → `playwright-cli my-command arg1 --opt=val`
    → Client parses with minimist → sends to Daemon via socket
    → parseCommand() maps CLI args to MCP tool params via zod
    → backend.callTool(toolName, toolParams)
    → Response formatted → printed to stdout
```
