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
|
```bash
|
||||||
npx playwriter serve --token <secret>
|
npx playwriter serve --token <secret>
|
||||||
|
# or use environment variable:
|
||||||
|
PLAYWRITER_TOKEN=<secret> npx playwriter serve
|
||||||
```
|
```
|
||||||
|
|
||||||
**In container/VM (where agent runs):**
|
**In container/VM (where agent runs):**
|
||||||
|
|
||||||
|
Using environment variables:
|
||||||
|
|
||||||
```bash
|
```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.
|
Use `host.docker.internal` for devcontainers, or your host's IP for VMs/SSH.
|
||||||
|
|
||||||
## Known Issues
|
## 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.
|
- 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).
|
- 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
|
cli
|
||||||
.command('', 'Start the MCP server (default)')
|
.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')
|
const { startMcp } = await import('./mcp.js')
|
||||||
await startMcp()
|
await startMcp({
|
||||||
|
host: options.host,
|
||||||
|
token: options.token,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
cli
|
cli
|
||||||
.command('serve', 'Start the CDP relay server for remote MCP connections')
|
.command('serve', 'Start the CDP relay server for remote MCP connections')
|
||||||
.option('--host <host>', 'Host to bind to', { default: '0.0.0.0' })
|
.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 }) => {
|
.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()
|
const logger = createFileLogger()
|
||||||
|
|
||||||
process.title = 'playwriter-serve'
|
process.title = 'playwriter-serve'
|
||||||
@@ -38,19 +50,19 @@ cli
|
|||||||
const server = await startPlayWriterCDPRelayServer({
|
const server = await startPlayWriterCDPRelayServer({
|
||||||
port: RELAY_PORT,
|
port: RELAY_PORT,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
token: options.token,
|
token,
|
||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Playwriter CDP relay server started')
|
console.log('Playwriter CDP relay server started')
|
||||||
console.log(` Host: ${options.host}`)
|
console.log(` Host: ${options.host}`)
|
||||||
console.log(` Port: ${RELAY_PORT}`)
|
console.log(` Port: ${RELAY_PORT}`)
|
||||||
console.log(` Token: ${options.token ? '(configured)' : '(none)'}`)
|
console.log(` Token: (configured)`)
|
||||||
console.log(` Logs: ${logger.logFilePath}`)
|
console.log(` Logs: ${logger.logFilePath}`)
|
||||||
console.log('')
|
console.log('')
|
||||||
console.log('Endpoints:')
|
console.log('Endpoints:')
|
||||||
console.log(` Extension: ws://${options.host}:${RELAY_PORT}/extension`)
|
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('')
|
||||||
console.log('Press Ctrl+C to stop.')
|
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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
|
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
@@ -104,18 +105,23 @@ const lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
|||||||
const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
||||||
|
|
||||||
const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988
|
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`
|
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() {
|
interface RemoteConfig {
|
||||||
if (!REMOTE_URL) {
|
host: string
|
||||||
|
port: number
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRemoteConfig(): RemoteConfig | null {
|
||||||
|
const host = process.env.PLAYWRITER_HOST
|
||||||
|
if (!host) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const url = new URL(REMOTE_URL)
|
|
||||||
return {
|
return {
|
||||||
host: url.hostname,
|
host,
|
||||||
port: Number(url.port) || 19988,
|
port: RELAY_PORT,
|
||||||
query: url.search.slice(1),
|
token: process.env.PLAYWRITER_TOKEN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,9 +164,9 @@ function clearConnectionState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLogServerUrl(): string {
|
function getLogServerUrl(): string {
|
||||||
if (REMOTE_URL) {
|
const remote = getRemoteConfig()
|
||||||
const url = new URL(REMOTE_URL)
|
if (remote) {
|
||||||
return `http://${url.host}/mcp-log`
|
return `http://${remote.host}:${remote.port}/mcp-log`
|
||||||
}
|
}
|
||||||
return `http://127.0.0.1:${RELAY_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 }
|
return { browser: state.browser, page: state.page }
|
||||||
}
|
}
|
||||||
|
|
||||||
const remote = parseRemoteUrl()
|
const remote = getRemoteConfig()
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
await ensureRelayServer()
|
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
|
// DO NOT clear browser logs on reset - logs should persist across reconnections
|
||||||
// browserLogs.clear()
|
// browserLogs.clear()
|
||||||
|
|
||||||
const remote = parseRemoteUrl()
|
const remote = getRemoteConfig()
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
await ensureRelayServer()
|
await ensureRelayServer()
|
||||||
}
|
}
|
||||||
@@ -681,7 +687,7 @@ server.tool(
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return 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 })
|
const session = await getCDPSessionForPage({ page: options.page, wsUrl })
|
||||||
cdpSessionCache.set(options.page, session)
|
cdpSessionCache.set(options.page, session)
|
||||||
return session
|
return session
|
||||||
@@ -846,9 +852,8 @@ server.tool(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async function checkRemoteServer(url: string): Promise<void> {
|
async function checkRemoteServer({ host, port }: { host: string; port: number }): Promise<void> {
|
||||||
const parsed = new URL(url)
|
const versionUrl = `http://${host}:${port}/version`
|
||||||
const versionUrl = `http://${parsed.host}/version`
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(versionUrl, { signal: AbortSignal.timeout(3000) })
|
const response = await fetch(versionUrl, { signal: AbortSignal.timeout(3000) })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -858,7 +863,7 @@ async function checkRemoteServer(url: string): Promise<void> {
|
|||||||
const isConnectionError = error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError'
|
const isConnectionError = error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError'
|
||||||
if (isConnectionError) {
|
if (isConnectionError) {
|
||||||
throw new Error(
|
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.`,
|
`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() {
|
export async function startMcp(options: { host?: string; token?: string } = {}) {
|
||||||
if (!REMOTE_URL) {
|
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()
|
await ensureRelayServer()
|
||||||
} else {
|
} else {
|
||||||
console.error(`Using remote CDP relay server: ${REMOTE_URL}`)
|
console.error(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
|
||||||
await checkRemoteServer(REMOTE_URL)
|
await checkRemoteServer(remote)
|
||||||
}
|
}
|
||||||
const transport = new StdioServerTransport()
|
const transport = new StdioServerTransport()
|
||||||
await server.connect(transport)
|
await server.connect(transport)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import os from 'node:os'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
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 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}`
|
return `ws://${host}:${port}/cdp/${id}${queryString}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user