diff --git a/README.md b/README.md index ed6092f..f177c28 100644 --- a/README.md +++ b/README.md @@ -93,27 +93,19 @@ This mode is useful for background or batch operations. ### Running headed browser on Linux w/o DISPLAY When running headed browser on system w/o display or from worker processes of the IDEs, -you can run Playwright in a client-server manner. You'll run the Playwright server -from environment with the DISPLAY +run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable SSE transport. -```sh -npx playwright run-server +```bash +npx @playwright/mcp@latest --port 8931 ``` -And then in MCP config, add following to the `env`: +And then in MCP client config, set the `url` to the SSE endpoint: ```js { "mcpServers": { "playwright": { - "command": "npx", - "args": [ - "@playwright/mcp@latest" - ], - "env": { - // Use the endpoint from the output of the server above. - "PLAYWRIGHT_WS_ENDPOINT": "ws://localhost:/" - } + "url": "http://localhost:8931/sse" } } } diff --git a/src/program.ts b/src/program.ts index f2cfbc3..ed9cf50 100644 --- a/src/program.ts +++ b/src/program.ts @@ -14,17 +14,21 @@ * limitations under the License. */ +import http from 'http'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { program } from 'commander'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; + import { createServer } from './index'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import type { LaunchOptions } from 'playwright'; +import assert from 'assert'; const packageJSON = require('../package.json'); @@ -34,6 +38,7 @@ program .option('--headless', 'Run browser in headless mode, headed by default') .option('--user-data-dir ', 'Path to the user data directory') .option('--vision', 'Run server that uses screenshots (Aria snapshots are used by default)') + .option('--port ', 'Port to listen on for SSE transport.') .action(async options => { const launchOptions: LaunchOptions = { headless: !!options.headless, @@ -46,8 +51,66 @@ program }); setupExitWatchdog(server); - const transport = new StdioServerTransport(); - await server.connect(transport); + if (options.port) { + const sessions = new Map(); + const httpServer = http.createServer(async (req, res) => { + if (req.method === 'POST') { + const host = req.headers.host ?? 'http://unknown'; + const sessionId = new URL(host + req.url!).searchParams.get('sessionId'); + if (!sessionId) { + res.statusCode = 400; + res.end('Missing sessionId'); + return; + } + const transport = sessions.get(sessionId); + if (!transport) { + res.statusCode = 404; + res.end('Session not found'); + return; + } + + await transport.handlePostMessage(req, res); + return; + } else if (req.method === 'GET') { + const transport = new SSEServerTransport('/sse', res); + sessions.set(transport.sessionId, transport); + res.on('close', () => { + sessions.delete(transport.sessionId); + }); + await server.connect(transport); + return; + } else { + res.statusCode = 405; + res.end('Method not allowed'); + } + }); + httpServer.listen(+options.port, () => { + const address = httpServer.address(); + assert(address, 'Could not bind server socket'); + let urlPrefixHumanReadable: string; + if (typeof address === 'string') { + urlPrefixHumanReadable = address; + } else { + const port = address.port; + let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`; + if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]') + resolvedHost = 'localhost'; + urlPrefixHumanReadable = `http://${resolvedHost}:${port}`; + } + console.log(`Listening on ${urlPrefixHumanReadable}`); + console.log('Put this in your client config:'); + console.log(JSON.stringify({ + 'mcpServers': { + 'playwright': { + 'url': `${urlPrefixHumanReadable}/sse` + } + } + }, undefined, 2)); + }); + } else { + const transport = new StdioServerTransport(); + await server.connect(transport); + } }); function setupExitWatchdog(server: Server) {