Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/src/getting-started-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ playwright-cli video-chapter <title> # add chapter marker to video
playwright-cli video-stop --filename=f # stop video recording
```

### WebMCP

Pages can register their own tools for agents through the experimental [WebMCP](https://webmachinelearning.github.io/webmcp/) API. When a page has them, the page status after a navigation reports how many, and the tools can be listed and called directly instead of driving the UI:

```bash
playwright-cli webmcp-list # list tools registered by the page
playwright-cli webmcp-call <name> [--params] # call one, passing a JSON object
```

Tool names, descriptions, schemas and results are provided by the page, so treat them as untrusted input.

WebMCP is experimental and only available in Chromium and Firefox behind a browser flag, passed through the [configuration file](#configuration-file):

```json
{
"browser": { "launchOptions": { "args": ["--enable-features=WebMCP"] } }
}
```

## Sessions

The CLI keeps the browser profile in memory by default — cookies and storage state are preserved between calls within a session but lost when the browser closes. Use `--persistent` to save the profile to disk.
Expand Down Expand Up @@ -308,6 +327,7 @@ This requires the [Playwright Extension](https://github.com/microsoft/playwright
| **Run headed** | `playwright-cli open https://example.com --headed` |
| **Use Firefox** | `playwright-cli open --browser=firefox` |
| **Monitor sessions** | `playwright-cli show` |
| **List page WebMCP tools** | `playwright-cli webmcp-list` |

## What's Next

Expand Down
17 changes: 17 additions & 0 deletions docs/src/getting-started-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ Save and restore browser state including cookies and localStorage:
- **Restore state**: Load previously saved state into a new session.
- **Cookie management**: List, get, set, and delete individual cookies.

### WebMCP tools

Pages can register their own tools for agents through the experimental [WebMCP](https://webmachinelearning.github.io/webmcp/) API. When a page has them, the page status after a navigation reports how many, and `browser_webmcp_list` and `browser_webmcp_call` expose them:

- **List tools**: See the tools the page registers, with their input schemas and annotations.
- **Call a tool**: Invoke one by name, letting the page do the work instead of driving its UI.

Tool names, descriptions, schemas and results are provided by the page, so treat them as untrusted input.

WebMCP is experimental and only available in Chromium and Firefox behind a browser flag, passed through the [configuration file](#configuration-file):

```json
{
"browser": { "launchOptions": { "args": ["--enable-features=WebMCP"] } }
}
```

## Configuration

### Headed mode
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/backend/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export class Context {
throw new Error(`Tab ${index} not found`);
await tab.page.bringToFront();
this._currentTab = tab;
await tab.updateWebMCPTools();
return tab;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ export class Response {
// Render tab titles upon changes or when more than one tab.
const snapshotToFile = this._includeSnapshot !== 'explicit' || !!this._includeSnapshotFileName;
const ariaFormat = this._includeSnapshot === 'none' ? 'none' : (this._json && !snapshotToFile ? 'json' : 'text');
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, this._clientWorkspace, ariaFormat) : undefined;
const updateWebMCP = this._includeSnapshot !== 'none'; // Collect the page's WebMCP tools whenever a snapshot is taken anyway.
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, this._clientWorkspace, ariaFormat, updateWebMCP) : undefined;
const tabHeaders = await Promise.all(this._context.tabs().map(tab => tab.headerSnapshot()));
if (this._includeSnapshot !== 'none' || tabHeaders.some(header => header.changed)) {
if (tabHeaders.length !== 1)
Expand Down Expand Up @@ -352,6 +353,8 @@ export function renderTabMarkdown(tab: TabHeader): string[] {
lines.push(`- HTTP status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`);
if (tab.console.errors || tab.console.warnings)
lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`);
if (tab.webmcpToolCount)
lines.push(`- ${tab.webmcpToolCount} webmcp tool${tab.webmcpToolCount === 1 ? '' : 's'} available on the page`);
return lines;
}

Expand Down
32 changes: 29 additions & 3 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import { LogFile } from './logFile';
import { ModalState } from './tool';
import { handleDialog } from './dialogs';
import { uploadFile } from './files';
import { listWebMCPTools } from './webmcp';

import type { AriaSnapshotJSON } from '@isomorphic/ariaSnapshot';
import type { Disposable } from '@isomorphic/disposable';
import type { Context, ContextConfig } from './context';
import type { WebMCPListing } from './webmcp';
import type * as playwright from '../../..';

const TabEvents = {
Expand Down Expand Up @@ -80,6 +82,7 @@ export type TabHeader = {
crashed: boolean;
mainDocumentStatus?: { status: number, statusText: string };
console: { total: number, warnings: number, errors: number };
webmcpToolCount?: number;
};

type TabSnapshot = {
Expand All @@ -102,6 +105,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
private _modalStates: ModalState[] = [];
private _initializedPromise: Promise<void>;
private _recentEventEntries: EventEntry[] = [];
private _webmcpTools: WebMCPListing | undefined;
private _consoleLog: LogFile;
private _disposables: Disposable[];
readonly actionTimeoutOptions: { timeout?: number; };
Expand Down Expand Up @@ -232,6 +236,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
}

private _clearCollectedArtifacts() {
this._webmcpTools = undefined;
this._downloads.length = 0;
this._requests.length = 0;
this._mainDocumentStatus = undefined;
Expand Down Expand Up @@ -304,6 +309,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
crashed: this.crashed,
mainDocumentStatus: this._mainDocumentStatus,
console: consoleCounts,
webmcpToolCount: this._webmcpTools?.tools.length,
};

if (!tabHeaderEquals(this._lastHeader, newHeader)) {
Expand Down Expand Up @@ -413,8 +419,10 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._requests.length = 0;
}

async captureSnapshot(root: playwright.Locator | undefined, depth: number | undefined, boxes: boolean | undefined, relativeTo: string | undefined, ariaFormat: 'none' | 'text' | 'json' = 'text'): Promise<TabSnapshot> {
async captureSnapshot(root: playwright.Locator | undefined, depth: number | undefined, boxes: boolean | undefined, relativeTo: string | undefined, ariaFormat: 'none' | 'text' | 'json' = 'text', updateWebMCP: boolean = false): Promise<TabSnapshot> {
await this._initializedPromise;
// Kick the WebMCP refresh off next to the aria snapshot so its latency hides behind the tree walk.
const webmcpPromise = updateWebMCP ? this.updateWebMCPTools() : undefined;
let tabSnapshot: TabSnapshot | undefined;
let modalStates: ModalState[] = [];
if (ariaFormat !== 'none') {
Expand Down Expand Up @@ -456,11 +464,28 @@ export class Tab extends EventEmitter<TabEventsInterface> {
this._recentEventEntries = [];
}

return tabSnapshot ?? {
const result = tabSnapshot ?? {
ariaSnapshot: '',
modalStates,
events: [],
};
if (webmcpPromise)
await this._raceAgainstModalStates(() => webmcpPromise);
return result;
}

webmcpTools(): WebMCPListing | undefined {
return this._webmcpTools;
}

async updateWebMCPTools(): Promise<void> {
if (this._javaScriptBlocked())
return;
const listing = await listWebMCPTools(this);
// A dialog that opened while probing produces the same empty listing as a page
// with no tools, so keep what we had rather than clobbering the cache with it.
if (!this._javaScriptBlocked())
this._webmcpTools = listing;
}

private _javaScriptBlocked(): boolean {
Expand Down Expand Up @@ -632,5 +657,6 @@ function tabHeaderEquals(a: TabHeader, b: TabHeader): boolean {
a.mainDocumentStatus?.statusText === b.mainDocumentStatus?.statusText &&
a.console.errors === b.console.errors &&
a.console.warnings === b.console.warnings &&
a.console.total === b.console.total;
a.console.total === b.console.total &&
a.webmcpToolCount === b.webmcpToolCount;
}
2 changes: 2 additions & 0 deletions packages/playwright-core/src/tools/backend/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import tracing from './tracing';
import verify from './verify';
import video from './video';
import wait from './wait';
import webmcp from './webmcp';
import webstorage from './webstorage';

import type { Tool } from './tool';
Expand Down Expand Up @@ -73,6 +74,7 @@ export const browserTools: Tool<any>[] = [
...verify,
...video,
...wait,
...webmcp,
...webstorage,
];

Expand Down
Loading
Loading