mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
chore: restart browser if page closed manually (#19)
Fixes https://github.com/microsoft/playwright-mcp/issues/18
This commit is contained in:
parent
f98d5d2e31
commit
a392ba2f41
@ -18,6 +18,7 @@ import * as playwright from 'playwright';
|
||||
|
||||
export class Context {
|
||||
private _launchOptions: playwright.LaunchOptions;
|
||||
private _browser: playwright.Browser | undefined;
|
||||
private _page: playwright.Page | undefined;
|
||||
private _console: playwright.ConsoleMessage[] = [];
|
||||
private _initializePromise: Promise<void> | undefined;
|
||||
@ -39,30 +40,39 @@ export class Context {
|
||||
async close() {
|
||||
const page = await this.ensurePage();
|
||||
await page.close();
|
||||
this._initializePromise = undefined;
|
||||
}
|
||||
|
||||
private async _initialize() {
|
||||
if (this._initializePromise)
|
||||
return this._initializePromise;
|
||||
this._initializePromise = (async () => {
|
||||
const browser = await this._createBrowser();
|
||||
this._page = await browser.newPage();
|
||||
this._browser = await createBrowser(this._launchOptions);
|
||||
this._page = await this._browser.newPage();
|
||||
this._page.on('console', event => this._console.push(event));
|
||||
this._page.on('framenavigated', frame => {
|
||||
if (!frame.parentFrame())
|
||||
this._console.length = 0;
|
||||
});
|
||||
this._page.on('close', () => this._reset());
|
||||
})();
|
||||
return this._initializePromise;
|
||||
}
|
||||
|
||||
private async _createBrowser(): Promise<playwright.Browser> {
|
||||
if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
|
||||
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
|
||||
url.searchParams.set('launch-options', JSON.stringify(this._launchOptions));
|
||||
return await playwright.chromium.connect(String(url));
|
||||
}
|
||||
return await playwright.chromium.launch({ channel: 'chrome', ...this._launchOptions });
|
||||
private _reset() {
|
||||
const browser = this._browser;
|
||||
this._initializePromise = undefined;
|
||||
this._browser = undefined;
|
||||
this._page = undefined;
|
||||
this._console.length = 0;
|
||||
void browser?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function createBrowser(launchOptions: playwright.LaunchOptions): Promise<playwright.Browser> {
|
||||
if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
|
||||
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
|
||||
url.searchParams.set('launch-options', JSON.stringify(launchOptions));
|
||||
return await playwright.chromium.connect(String(url));
|
||||
}
|
||||
return await playwright.chromium.launch({ channel: 'chrome', ...launchOptions });
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
import * as playwright from 'playwright';
|
||||
|
||||
import { Context } from './context';
|
||||
|
||||
@ -31,7 +30,6 @@ export type LaunchOptions = {
|
||||
export class Server {
|
||||
private _server: MCPServer;
|
||||
private _tools: Tool[];
|
||||
private _page: playwright.Page | undefined;
|
||||
private _context: Context;
|
||||
|
||||
constructor(options: { name: string, version: string, tools: Tool[], resources: Resource[] }, launchOptions: LaunchOptions) {
|
||||
@ -90,6 +88,6 @@ export class Server {
|
||||
|
||||
async stop() {
|
||||
await this._server.close();
|
||||
await this._page?.context()?.browser()?.close();
|
||||
await this._context.close();
|
||||
}
|
||||
}
|
||||
|
@ -161,3 +161,69 @@ test('test browser_click', async ({ server }) => {
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
test('test reopen browser', async ({ server }) => {
|
||||
const response2 = await server.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_navigate',
|
||||
arguments: {
|
||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2).toEqual(expect.objectContaining({
|
||||
id: 2,
|
||||
}));
|
||||
|
||||
const response3 = await server.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_close',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response3).toEqual(expect.objectContaining({
|
||||
id: 3,
|
||||
result: {
|
||||
content: [{
|
||||
text: 'Page closed',
|
||||
type: 'text',
|
||||
}],
|
||||
},
|
||||
}));
|
||||
|
||||
const response4 = await server.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_navigate',
|
||||
arguments: {
|
||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response4).toEqual(expect.objectContaining({
|
||||
id: 4,
|
||||
result: {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `
|
||||
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||
- Page Title: Title
|
||||
- Page Snapshot
|
||||
\`\`\`yaml
|
||||
- document [ref=s1e2]: Hello, world!
|
||||
\`\`\`
|
||||
`,
|
||||
}],
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
@ -17,6 +17,7 @@
|
||||
import path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import EventEmitter from 'events';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
import { test as baseTest, expect } from '@playwright/test';
|
||||
|
||||
@ -30,10 +31,11 @@ class MCPServer extends EventEmitter {
|
||||
private _messageResolvers: ((value: any) => void)[] = [];
|
||||
private _buffer: string = '';
|
||||
|
||||
constructor(command: string, args: string[]) {
|
||||
constructor(command: string, args: string[], options?: { env?: NodeJS.ProcessEnv }) {
|
||||
super();
|
||||
this._child = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, ...options?.env },
|
||||
});
|
||||
|
||||
this._child.stdout?.on('data', data => {
|
||||
@ -108,44 +110,64 @@ class MCPServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<{ server: MCPServer }>({
|
||||
server: async ({}, use) => {
|
||||
const server = new MCPServer('node', [path.join(__dirname, '../cli.js'), '--headless']);
|
||||
const initialize = await server.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'Playwright Test',
|
||||
version: '0.0.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
type Fixtures = {
|
||||
server: MCPServer;
|
||||
startServer: (options?: { env?: NodeJS.ProcessEnv }) => Promise<MCPServer>;
|
||||
wsEndpoint: string;
|
||||
};
|
||||
|
||||
expect(initialize).toEqual(expect.objectContaining({
|
||||
id: 0,
|
||||
result: expect.objectContaining({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
export const test = baseTest.extend<Fixtures>({
|
||||
server: async ({ startServer }, use) => {
|
||||
await use(await startServer());
|
||||
},
|
||||
|
||||
startServer: async ({ }, use) => {
|
||||
let server: MCPServer | undefined;
|
||||
|
||||
use(async options => {
|
||||
server = new MCPServer('node', [path.join(__dirname, '../cli.js'), '--headless'], options);
|
||||
const initialize = await server.send({
|
||||
jsonrpc: '2.0',
|
||||
id: 0,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'Playwright Test',
|
||||
version: '0.0.0',
|
||||
},
|
||||
},
|
||||
serverInfo: expect.objectContaining({
|
||||
name: 'Playwright',
|
||||
version: expect.any(String),
|
||||
});
|
||||
|
||||
expect(initialize).toEqual(expect.objectContaining({
|
||||
id: 0,
|
||||
result: expect.objectContaining({
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
serverInfo: expect.objectContaining({
|
||||
name: 'Playwright',
|
||||
version: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
}));
|
||||
|
||||
await server.sendNoReply({
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
await server.sendNoReply({
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
});
|
||||
return server;
|
||||
});
|
||||
|
||||
await use(server);
|
||||
await server.close();
|
||||
await server?.close();
|
||||
},
|
||||
|
||||
wsEndpoint: async ({ }, use) => {
|
||||
const browserServer = await chromium.launchServer();
|
||||
await use(browserServer.wsEndpoint());
|
||||
await browserServer.close();
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user