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
!lib/**/*.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
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
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"
},
"exports": {
"./servers/server": "./lib/servers/server.js",
"./servers/screenshot": "./lib/servers/screenshot.js",
"./servers/snapshot": "./lib/servers/snapshot.js",
"./tools/common": "./lib/tools/common.js",
"./tools/screenshot": "./lib/tools/screenshot.js",
"./tools/snapshot": "./lib/tools/snapshot.js",
"./package.json": "./package.json"
"./package.json": "./package.json",
".": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",

View File

@ -17,13 +17,13 @@
import * as playwright from 'playwright';
export class Context {
private _launchOptions: playwright.LaunchOptions;
private _launchOptions: playwright.LaunchOptions | undefined;
private _browser: playwright.Browser | undefined;
private _page: playwright.Page | undefined;
private _console: playwright.ConsoleMessage[] = [];
private _initializePromise: Promise<void> | undefined;
constructor(launchOptions: playwright.LaunchOptions) {
constructor(launchOptions?: playwright.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) {
const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT);
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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Server } 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 { createServer } from './index';
import type { LaunchOptions } from './server';
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 packageJSON = require('../package.json');
@ -37,57 +33,19 @@ program
const launchOptions: LaunchOptions = {
headless: !!options.headless,
};
const tools = options.vision ? screenshotTools : snapshotTools;
const server = new Server({
name: 'Playwright',
version: packageJSON.version,
tools,
resources,
}, launchOptions);
const server = createServer({ launchOptions });
setupExitWatchdog(server);
await server.start();
const transport = new StdioServerTransport();
await server.connect(transport);
});
function setupExitWatchdog(server: Server) {
process.stdin.on('close', async () => {
setTimeout(() => process.exit(0), 15000);
await server?.stop();
await server.close();
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);

View File

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