chore: export server for custom transports (#20)

Fixes https://github.com/microsoft/playwright-mcp/issues/11
This commit is contained in:
Pavel Feldman 2025-03-25 14:46:39 -07:00 committed by GitHub
parent a394c5be52
commit 8f3214a06a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 205 additions and 122 deletions

View File

@ -3,3 +3,4 @@ README.md
LICENSE LICENSE
!lib/**/*.js !lib/**/*.js
!cli.js !cli.js
!index.*

View File

@ -121,6 +121,20 @@ To use Vision Mode, add the `--vision` flag when starting the server:
Vision Mode works best with the computer use models that are able to interact with elements using Vision Mode works best with the computer use models that are able to interact with elements using
X Y coordinate space, based on the provided screenshot. X Y coordinate space, based on the provided screenshot.
### Programmatic usage with custom transports
```js
import { createServer } from '@playwright/mcp';
// ...
const server = createServer({
launchOptions: { headless: true }
});
transport = new SSEServerTransport("/messages", res);
server.connect(transport);
```
### Snapshot Mode ### Snapshot Mode
The Playwright MCP provides a set of tools for browser automation. Here are all available tools: The Playwright MCP provides a set of tools for browser automation. Here are all available tools:

31
index.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LaunchOptions } from 'playwright';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
type Options = {
launchOptions?: LaunchOptions;
/**
* Use screenshots instead of snapshots. Less accurate, reliable and overall
* slower, but contains visual representation of the page.
* @default false
*/
vision?: boolean;
};
export function createServer(options?: Options): Server;

19
index.js Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { createServer } = require('./lib/index');
module.exports = { createServer };

View File

@ -21,13 +21,11 @@
"test": "playwright test" "test": "playwright test"
}, },
"exports": { "exports": {
"./servers/server": "./lib/servers/server.js", "./package.json": "./package.json",
"./servers/screenshot": "./lib/servers/screenshot.js", ".": {
"./servers/snapshot": "./lib/servers/snapshot.js", "types": "./index.d.ts",
"./tools/common": "./lib/tools/common.js", "default": "./index.js"
"./tools/screenshot": "./lib/tools/screenshot.js", }
"./tools/snapshot": "./lib/tools/snapshot.js",
"./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1", "@modelcontextprotocol/sdk": "^1.6.1",

View File

@ -17,13 +17,13 @@
import * as playwright from 'playwright'; import * as playwright from 'playwright';
export class Context { export class Context {
private _launchOptions: playwright.LaunchOptions; private _launchOptions: playwright.LaunchOptions | undefined;
private _browser: playwright.Browser | undefined; private _browser: playwright.Browser | undefined;
private _page: playwright.Page | undefined; private _page: playwright.Page | undefined;
private _console: playwright.ConsoleMessage[] = []; private _console: playwright.ConsoleMessage[] = [];
private _initializePromise: Promise<void> | undefined; private _initializePromise: Promise<void> | undefined;
constructor(launchOptions: playwright.LaunchOptions) { constructor(launchOptions?: playwright.LaunchOptions) {
this._launchOptions = launchOptions; this._launchOptions = launchOptions;
} }
@ -68,7 +68,7 @@ export class Context {
} }
} }
async function createBrowser(launchOptions: playwright.LaunchOptions): Promise<playwright.Browser> { async function createBrowser(launchOptions?: playwright.LaunchOptions): Promise<playwright.Browser> {
if (process.env.PLAYWRIGHT_WS_ENDPOINT) { if (process.env.PLAYWRIGHT_WS_ENDPOINT) {
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT); const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
url.searchParams.set('launch-options', JSON.stringify(launchOptions)); url.searchParams.set('launch-options', JSON.stringify(launchOptions));

77
src/index.ts Normal file
View File

@ -0,0 +1,77 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createServerWithTools } from './server';
import * as snapshot from './tools/snapshot';
import * as common from './tools/common';
import * as screenshot from './tools/screenshot';
import { console } from './resources/console';
import type { Tool } from './tools/tool';
import type { Resource } from './resources/resource';
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { LaunchOptions } from 'playwright';
const commonTools: Tool[] = [
common.pressKey,
common.wait,
common.pdf,
common.close,
];
const snapshotTools: Tool[] = [
common.navigate(true),
common.goBack(true),
common.goForward(true),
snapshot.snapshot,
snapshot.click,
snapshot.hover,
snapshot.type,
...commonTools,
];
const screenshotTools: Tool[] = [
common.navigate(false),
common.goBack(false),
common.goForward(false),
screenshot.screenshot,
screenshot.moveMouse,
screenshot.click,
screenshot.drag,
screenshot.type,
...commonTools,
];
const resources: Resource[] = [
console,
];
type Options = {
vision?: boolean;
launchOptions?: LaunchOptions;
};
const packageJSON = require('../package.json');
export function createServer(options?: Options): Server {
const tools = options?.vision ? screenshotTools : snapshotTools;
return createServerWithTools(
'Playwright',
packageJSON.version,
tools,
resources,
options?.launchOptions);
}

View File

@ -15,16 +15,12 @@
*/ */
import { program } from 'commander'; import { program } from 'commander';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Server } from './server'; import { createServer } from './index';
import * as snapshot from './tools/snapshot';
import * as common from './tools/common';
import * as screenshot from './tools/screenshot';
import { console } from './resources/console';
import type { LaunchOptions } from './server'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import type { Tool } from './tools/tool'; import type { LaunchOptions } from 'playwright';
import type { Resource } from './resources/resource';
const packageJSON = require('../package.json'); const packageJSON = require('../package.json');
@ -37,57 +33,19 @@ program
const launchOptions: LaunchOptions = { const launchOptions: LaunchOptions = {
headless: !!options.headless, headless: !!options.headless,
}; };
const tools = options.vision ? screenshotTools : snapshotTools; const server = createServer({ launchOptions });
const server = new Server({
name: 'Playwright',
version: packageJSON.version,
tools,
resources,
}, launchOptions);
setupExitWatchdog(server); setupExitWatchdog(server);
await server.start();
const transport = new StdioServerTransport();
await server.connect(transport);
}); });
function setupExitWatchdog(server: Server) { function setupExitWatchdog(server: Server) {
process.stdin.on('close', async () => { process.stdin.on('close', async () => {
setTimeout(() => process.exit(0), 15000); setTimeout(() => process.exit(0), 15000);
await server?.stop(); await server.close();
process.exit(0); process.exit(0);
}); });
} }
const commonTools: Tool[] = [
common.pressKey,
common.wait,
common.pdf,
common.close,
];
const snapshotTools: Tool[] = [
common.navigate(true),
common.goBack(true),
common.goForward(true),
snapshot.snapshot,
snapshot.click,
snapshot.hover,
snapshot.type,
...commonTools,
];
const screenshotTools: Tool[] = [
common.navigate(false),
common.goBack(false),
common.goForward(false),
screenshot.screenshot,
screenshot.moveMouse,
screenshot.click,
screenshot.drag,
screenshot.type,
...commonTools,
];
const resources: Resource[] = [
console,
];
program.parse(process.argv); program.parse(process.argv);

View File

@ -14,80 +14,65 @@
* limitations under the License. * limitations under the License.
*/ */
import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js'; import { Server } 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 { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { Context } from './context'; import { Context } from './context';
import type { Tool } from './tools/tool'; import type { Tool } from './tools/tool';
import type { Resource } from './resources/resource'; import type { Resource } from './resources/resource';
import type { LaunchOptions } from 'playwright';
export type LaunchOptions = { export function createServerWithTools(name: string, version: string, tools: Tool[], resources: Resource[], launchOption?: LaunchOptions): Server {
headless?: boolean; const context = new Context(launchOption);
}; const server = new Server({ name, version }, {
capabilities: {
tools: {},
resources: {},
}
});
export class Server { server.setRequestHandler(ListToolsRequestSchema, async () => {
private _server: MCPServer; return { tools: tools.map(tool => tool.schema) };
private _tools: Tool[]; });
private _context: Context;
constructor(options: { name: string, version: string, tools: Tool[], resources: Resource[] }, launchOptions: LaunchOptions) { server.setRequestHandler(ListResourcesRequestSchema, async () => {
const { name, version, tools, resources } = options; return { resources: resources.map(resource => resource.schema) };
this._context = new Context(launchOptions); });
this._server = new MCPServer({ name, version }, {
capabilities: {
tools: {},
resources: {},
}
});
this._tools = tools;
this._server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(CallToolRequestSchema, async request => {
return { tools: tools.map(tool => tool.schema) }; const tool = tools.find(tool => tool.schema.name === request.params.name);
}); if (!tool) {
return {
content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
isError: true,
};
}
this._server.setRequestHandler(ListResourcesRequestSchema, async () => { try {
return { resources: resources.map(resource => resource.schema) }; const result = await tool.handle(context, request.params.arguments);
}); return result;
} catch (error) {
return {
content: [{ type: 'text', text: String(error) }],
isError: true,
};
}
});
this._server.setRequestHandler(CallToolRequestSchema, async request => { server.setRequestHandler(ReadResourceRequestSchema, async request => {
const tool = this._tools.find(tool => tool.schema.name === request.params.name); const resource = resources.find(resource => resource.schema.uri === request.params.uri);
if (!tool) { if (!resource)
return { return { contents: [] };
content: [{ type: 'text', text: `Tool "${request.params.name}" not found` }],
isError: true,
};
}
try { const contents = await resource.read(context, request.params.uri);
const result = await tool.handle(this._context, request.params.arguments); return { contents };
return result; });
} catch (error) {
return {
content: [{ type: 'text', text: String(error) }],
isError: true,
};
}
});
this._server.setRequestHandler(ReadResourceRequestSchema, async request => { server.close = async () => {
const resource = resources.find(resource => resource.schema.uri === request.params.uri); await server.close();
if (!resource) await context.close();
return { contents: [] }; };
const contents = await resource.read(this._context, request.params.uri); return server;
return { contents };
});
}
async start() {
const transport = new StdioServerTransport();
await this._server.connect(transport);
}
async stop() {
await this._server.close();
await this._context.close();
}
} }