Skip to main content
Glama
playwrightMcp.ts3.42 kB
// src/playwrightMcp.ts import http, { IncomingMessage, ServerResponse } from 'http'; import { AddressInfo } from 'net'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; /** Handle returned to testRunner so it can shut the server down. */ export interface PlaywrightMcpHandle { baseUrl: string; // host:port (NO path) shutdown(): Promise<void>; closeTransport(): void; } /** * Start an in-process Playwright-MCP server. * • Binds on 127.0.0.1:0 (OS chooses a free port) * • Exposes one route /messages * – GET → opens SSE stream * – POST → delivers JSON-RPC command via same transport */ export async function startPlaywrightMcp(): Promise<PlaywrightMcpHandle> { /* ── 1. Create bare HTTP server on a free port ─────────────────── */ const server = http.createServer(); await new Promise<void>((ok) => server.listen(0, '127.0.0.1', ok)); const { port } = server.address() as AddressInfo; const baseUrl = `http://127.0.0.1:${port}`; /* ── 2. Dynamically import the ESM-only @playwright/mcp ─────────── */ // Done via Function wrapper so ts-node (CommonJS) doesn’t rewrite to require(). const { createConnection } = await (new Function('spec', 'return import(spec)')( '@playwright/mcp' ) as Promise<{ createConnection: any }>); /* ── 3. Spin up the headless browser MCP connection ────────────── */ const mcp = await createConnection({ browser: { isolated: true, // no persistent profile launchOptions: { headless: true }, }, capabilities: ['tabs'], }); /* ── 4. One SSE transport shared for the whole session ─────────── */ let transport: SSEServerTransport | undefined; server.on('request', async (req: IncomingMessage, res: ServerResponse) => { if (!req.url?.startsWith('/messages')) { res.writeHead(404).end('Not found'); return; } if (req.method === 'GET') { // First GET establishes the SSE stream transport = new SSEServerTransport('/messages', res as any); await mcp.connect(transport); // handshake -> “endpoint” event return; // SSE handler keeps res open } if (req.method === 'POST') { if (!transport) { res.writeHead(503).end('SSE not established'); return; } // Collect body (JSON-RPC message) const chunks: Buffer[] = []; req.on('data', (c) => chunks.push(c)); req.on('end', async () => { try { await (transport as any).handlePostMessage( req, res, Buffer.concat(chunks).toString('utf8') ); } catch (e: any) { res.writeHead(500).end(e.message ?? 'internal error'); } }); return; } res.writeHead(405).end('Method not allowed'); }); console.log(`[playwright-mcp] listening on ${baseUrl}/messages`); return { baseUrl, // testRunner adds /messages shutdown: () => new Promise<void>((ok) => server.close(() => ok())), closeTransport: () => { // sdk ≥0.7 provides transport.close(); fallback to res.end() try { (transport as any)?.close?.(); } catch { try { (transport as any)?.res?.end(); } catch { /* ignore */ } } }, }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server