merge: keep sleep(50) for Runtime.disable
This commit is contained in:
@@ -185,18 +185,30 @@ Run agents in isolated environments (devcontainers, VMs, SSH) while controlling
|
||||
|
||||
```bash
|
||||
npx playwriter serve --token <secret>
|
||||
# or use environment variable:
|
||||
PLAYWRITER_TOKEN=<secret> npx playwriter serve
|
||||
```
|
||||
|
||||
**In container/VM (where agent runs):**
|
||||
|
||||
Using environment variables:
|
||||
|
||||
```bash
|
||||
export PLAYWRITER_URL="ws://host.docker.internal:19988?token=<secret>"
|
||||
export PLAYWRITER_HOST="host.docker.internal"
|
||||
export PLAYWRITER_TOKEN="<secret>"
|
||||
```
|
||||
|
||||
Or using CLI options:
|
||||
|
||||
```bash
|
||||
npx playwriter --host host.docker.internal --token <secret>
|
||||
```
|
||||
|
||||
Use `host.docker.internal` for devcontainers, or your host's IP for VMs/SSH.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- **Do not use `bunx` to run the MCP.** Bun has incomplete support for the `ws` library's WebSocket events, which breaks playwright-core's CDP connection. Use `npx` instead. See bun issues: [#5951](https://github.com/oven-sh/bun/issues/5951), [#9911](https://github.com/oven-sh/bun/issues/9911)
|
||||
- If all pages urls return `about:blank` in every MCP session restart your Chrome browser. This seems to be a Chrome bug that sometimes happen. It is some hidden state in `chrome.debugger` Extensions API. Restarting the extension worker does not fix it.
|
||||
- When connecting the MCP to a page, the browser may switch to light mode. This happens because Playwright, via CDP, automatically sends an "emulate media" command on start. If you'd like to see this behavior changed, you can upvote the related issue [here](https://github.com/microsoft/playwright/issues/37627).
|
||||
|
||||
|
||||
+18
-6
@@ -11,16 +11,28 @@ const cli = cac('playwriter')
|
||||
|
||||
cli
|
||||
.command('', 'Start the MCP server (default)')
|
||||
.action(async () => {
|
||||
.option('--host <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
|
||||
.option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
|
||||
.action(async (options: { host?: string; token?: string }) => {
|
||||
const { startMcp } = await import('./mcp.js')
|
||||
await startMcp()
|
||||
await startMcp({
|
||||
host: options.host,
|
||||
token: options.token,
|
||||
})
|
||||
})
|
||||
|
||||
cli
|
||||
.command('serve', 'Start the CDP relay server for remote MCP connections')
|
||||
.option('--host <host>', 'Host to bind to', { default: '0.0.0.0' })
|
||||
.option('--token <token>', 'Authentication token for /cdp/* endpoints')
|
||||
.option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
|
||||
.action(async (options: { host: string; token?: string }) => {
|
||||
const token = options.token || process.env.PLAYWRITER_TOKEN
|
||||
if (!token) {
|
||||
console.error('Error: Authentication token is required.')
|
||||
console.error('Provide --token <token> or set PLAYWRITER_TOKEN environment variable.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const logger = createFileLogger()
|
||||
|
||||
process.title = 'playwriter-serve'
|
||||
@@ -38,19 +50,19 @@ cli
|
||||
const server = await startPlayWriterCDPRelayServer({
|
||||
port: RELAY_PORT,
|
||||
host: options.host,
|
||||
token: options.token,
|
||||
token,
|
||||
logger,
|
||||
})
|
||||
|
||||
console.log('Playwriter CDP relay server started')
|
||||
console.log(` Host: ${options.host}`)
|
||||
console.log(` Port: ${RELAY_PORT}`)
|
||||
console.log(` Token: ${options.token ? '(configured)' : '(none)'}`)
|
||||
console.log(` Token: (configured)`)
|
||||
console.log(` Logs: ${logger.logFilePath}`)
|
||||
console.log('')
|
||||
console.log('Endpoints:')
|
||||
console.log(` Extension: ws://${options.host}:${RELAY_PORT}/extension`)
|
||||
console.log(` CDP: ws://${options.host}:${RELAY_PORT}/cdp/<client-id>${options.token ? '?token=<token>' : ''}`)
|
||||
console.log(` CDP: ws://${options.host}:${RELAY_PORT}/cdp/<client-id>?token=<token>`)
|
||||
console.log('')
|
||||
console.log('Press Ctrl+C to stop.')
|
||||
|
||||
|
||||
+34
-21
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import { z } from 'zod'
|
||||
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
@@ -104,18 +105,23 @@ const lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||
const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
||||
|
||||
const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
|
||||
const REMOTE_URL = process.env.PLAYWRITER_URL
|
||||
const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
|
||||
|
||||
function parseRemoteUrl() {
|
||||
if (!REMOTE_URL) {
|
||||
interface RemoteConfig {
|
||||
host: string
|
||||
port: number
|
||||
token?: string
|
||||
}
|
||||
|
||||
function getRemoteConfig(): RemoteConfig | null {
|
||||
const host = process.env.PLAYWRITER_HOST
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
const url = new URL(REMOTE_URL)
|
||||
return {
|
||||
host: url.hostname,
|
||||
port: Number(url.port) || 19988,
|
||||
query: url.search.slice(1),
|
||||
host,
|
||||
port: RELAY_PORT,
|
||||
token: process.env.PLAYWRITER_TOKEN,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +164,9 @@ function clearConnectionState() {
|
||||
}
|
||||
|
||||
function getLogServerUrl(): string {
|
||||
if (REMOTE_URL) {
|
||||
const url = new URL(REMOTE_URL)
|
||||
return `http://${url.host}/mcp-log`
|
||||
const remote = getRemoteConfig()
|
||||
if (remote) {
|
||||
return `http://${remote.host}:${remote.port}/mcp-log`
|
||||
}
|
||||
return `http://127.0.0.1:${RELAY_PORT}/mcp-log`
|
||||
}
|
||||
@@ -241,7 +247,7 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
||||
return { browser: state.browser, page: state.page }
|
||||
}
|
||||
|
||||
const remote = parseRemoteUrl()
|
||||
const remote = getRemoteConfig()
|
||||
if (!remote) {
|
||||
await ensureRelayServer()
|
||||
}
|
||||
@@ -374,7 +380,7 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
|
||||
// DO NOT clear browser logs on reset - logs should persist across reconnections
|
||||
// browserLogs.clear()
|
||||
|
||||
const remote = parseRemoteUrl()
|
||||
const remote = getRemoteConfig()
|
||||
if (!remote) {
|
||||
await ensureRelayServer()
|
||||
}
|
||||
@@ -681,7 +687,7 @@ server.tool(
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const wsUrl = getCdpUrl(parseRemoteUrl() || { port: RELAY_PORT })
|
||||
const wsUrl = getCdpUrl(getRemoteConfig() || { port: RELAY_PORT })
|
||||
const session = await getCDPSessionForPage({ page: options.page, wsUrl })
|
||||
cdpSessionCache.set(options.page, session)
|
||||
return session
|
||||
@@ -846,9 +852,8 @@ server.tool(
|
||||
},
|
||||
)
|
||||
|
||||
async function checkRemoteServer(url: string): Promise<void> {
|
||||
const parsed = new URL(url)
|
||||
const versionUrl = `http://${parsed.host}/version`
|
||||
async function checkRemoteServer({ host, port }: { host: string; port: number }): Promise<void> {
|
||||
const versionUrl = `http://${host}:${port}/version`
|
||||
try {
|
||||
const response = await fetch(versionUrl, { signal: AbortSignal.timeout(3000) })
|
||||
if (!response.ok) {
|
||||
@@ -858,7 +863,7 @@ async function checkRemoteServer(url: string): Promise<void> {
|
||||
const isConnectionError = error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError'
|
||||
if (isConnectionError) {
|
||||
throw new Error(
|
||||
`Cannot connect to remote relay server at ${parsed.host}. ` +
|
||||
`Cannot connect to remote relay server at ${host}:${port}. ` +
|
||||
`Make sure 'npx playwriter serve' is running on the host machine.`,
|
||||
)
|
||||
}
|
||||
@@ -866,12 +871,20 @@ async function checkRemoteServer(url: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function startMcp() {
|
||||
if (!REMOTE_URL) {
|
||||
export async function startMcp(options: { host?: string; token?: string } = {}) {
|
||||
if (options.host) {
|
||||
process.env.PLAYWRITER_HOST = options.host
|
||||
}
|
||||
if (options.token) {
|
||||
process.env.PLAYWRITER_TOKEN = options.token
|
||||
}
|
||||
|
||||
const remote = getRemoteConfig()
|
||||
if (!remote) {
|
||||
await ensureRelayServer()
|
||||
} else {
|
||||
console.error(`Using remote CDP relay server: ${REMOTE_URL}`)
|
||||
await checkRemoteServer(REMOTE_URL)
|
||||
console.error(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
|
||||
await checkRemoteServer(remote)
|
||||
}
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
|
||||
@@ -3,9 +3,9 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export function getCdpUrl({ port = 19988, host = '127.0.0.1', query }: { port?: number; host?: string; query?: string } = {}) {
|
||||
export function getCdpUrl({ port = 19988, host = '127.0.0.1', token }: { port?: number; host?: string; token?: string } = {}) {
|
||||
const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`
|
||||
const queryString = query ? `?${query}` : ''
|
||||
const queryString = token ? `?token=${token}` : ''
|
||||
return `ws://${host}:${port}/cdp/${id}${queryString}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user