513 lines
17 KiB
Markdown
513 lines
17 KiB
Markdown
# 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
|
|
```
|