diff --git a/src/context.ts b/src/context.ts index bb4af93..943e290 100644 --- a/src/context.ts +++ b/src/context.ts @@ -16,9 +16,15 @@ import * as playwright from 'playwright'; +export type ContextOptions = { + userDataDir: string; + launchOptions?: playwright.LaunchOptions; + cdpEndpoint?: string; + remoteEndpoint?: string; +}; + export class Context { - private _userDataDir: string; - private _launchOptions: playwright.LaunchOptions | undefined; + private _options: ContextOptions; private _browser: playwright.Browser | undefined; private _page: playwright.Page | undefined; private _console: playwright.ConsoleMessage[] = []; @@ -26,9 +32,8 @@ export class Context { private _fileChooser: playwright.FileChooser | undefined; private _lastSnapshotFrames: playwright.FrameLocator[] = []; - constructor(userDataDir: string, launchOptions?: playwright.LaunchOptions) { - this._userDataDir = userDataDir; - this._launchOptions = launchOptions; + constructor(options: ContextOptions) { + this._options = options; } async createPage(): Promise { @@ -96,16 +101,25 @@ export class Context { } private async _createPage(): Promise<{ browser?: playwright.Browser, page: playwright.Page }> { - if (process.env.PLAYWRIGHT_WS_ENDPOINT) { - const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT); - if (this._launchOptions) - url.searchParams.set('launch-options', JSON.stringify(this._launchOptions)); + if (this._options.remoteEndpoint) { + const url = new URL(this._options.remoteEndpoint); + if (this._options.launchOptions) + url.searchParams.set('launch-options', JSON.stringify(this._options.launchOptions)); const browser = await playwright.chromium.connect(String(url)); const page = await browser.newPage(); return { browser, page }; } - const context = await playwright.chromium.launchPersistentContext(this._userDataDir, this._launchOptions); + if (this._options.cdpEndpoint) { + const browser = await playwright.chromium.connectOverCDP(this._options.cdpEndpoint); + const browserContext = browser.contexts()[0]; + let [page] = browserContext.pages(); + if (!page) + page = await browserContext.newPage(); + return { browser, page }; + } + + const context = await playwright.chromium.launchPersistentContext(this._options.userDataDir, this._options.launchOptions); const [page] = context.pages(); return { page }; } diff --git a/src/index.ts b/src/index.ts index ea71f12..d43a359 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ const resources: Resource[] = [ type Options = { userDataDir?: string; launchOptions?: LaunchOptions; + cdpEndpoint?: string; vision?: boolean; }; @@ -80,5 +81,6 @@ export function createServer(options?: Options): Server { resources, userDataDir: options?.userDataDir ?? '', launchOptions: options?.launchOptions, + cdpEndpoint: options?.cdpEndpoint, }); } diff --git a/src/program.ts b/src/program.ts index 3d05257..c196106 100644 --- a/src/program.ts +++ b/src/program.ts @@ -39,6 +39,7 @@ program .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.') + .option('--cdp-endpoint ', 'CDP endpoint to connect to.') .action(async options => { const launchOptions: LaunchOptions = { headless: !!options.headless, @@ -49,6 +50,7 @@ program userDataDir, launchOptions, vision: !!options.vision, + cdpEndpoint: options.cdpEndpoint, })); setupExitWatchdog(serverList); diff --git a/src/server.ts b/src/server.ts index fb163c8..2b20f98 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,20 +21,18 @@ import { Context } from './context'; import type { Tool } from './tools/tool'; import type { Resource } from './resources/resource'; -import type { LaunchOptions } from 'playwright'; +import type { ContextOptions } from './context'; -type Options = { +type Options = ContextOptions & { name: string; version: string; tools: Tool[]; resources: Resource[], - userDataDir: string; - launchOptions?: LaunchOptions; }; export function createServerWithTools(options: Options): Server { - const { name, version, tools, resources, userDataDir, launchOptions } = options; - const context = new Context(userDataDir, launchOptions); + const { name, version, tools, resources } = options; + const context = new Context(options); const server = new Server({ name, version }, { capabilities: { tools: {}, diff --git a/tests/basic.spec.ts b/tests/basic.spec.ts index b2e198f..ee9ecc7 100644 --- a/tests/basic.spec.ts +++ b/tests/basic.spec.ts @@ -313,3 +313,21 @@ test('sse transport', async () => { cp.kill(); } }); + +test('cdp server', async ({ cdpEndpoint, startClient }) => { + const client = await startClient({ args: [`--cdp-endpoint=${cdpEndpoint}`] }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: 'data:text/html,TitleHello, world!', + }, + })).toHaveTextContent(` +- Page URL: data:text/html,TitleHello, world! +- Page Title: Title +- Page Snapshot +\`\`\`yaml +- document [ref=s1e2]: Hello, world! +\`\`\` +` + ); +}); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index a1d7209..e805f31 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -24,8 +24,9 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; type Fixtures = { client: Client; visionClient: Client; - startClient: (options?: { env?: NodeJS.ProcessEnv, vision?: boolean }) => Promise; + startClient: (options?: { args?: string[], vision?: boolean }) => Promise; wsEndpoint: string; + cdpEndpoint: string; }; export const test = baseTest.extend({ @@ -46,6 +47,8 @@ export const test = baseTest.extend({ const args = ['--headless', '--user-data-dir', userDataDir]; if (options?.vision) args.push('--vision'); + if (options?.args) + args.push(...options.args); const transport = new StdioClientTransport({ command: 'node', args: [path.join(__dirname, '../cli.js'), ...args], @@ -64,6 +67,15 @@ export const test = baseTest.extend({ await use(browserServer.wsEndpoint()); await browserServer.close(); }, + + cdpEndpoint: async ({ }, use, testInfo) => { + const port = 3200 + (+process.env.TEST_PARALLEL_INDEX!); + const browser = await chromium.launchPersistentContext(testInfo.outputPath('user-data-dir'), { + args: [`--remote-debugging-port=${port}`], + }); + await use(`http://localhost:${port}`); + await browser.close(); + }, }); type Response = Awaited>;