diff --git a/.npmignore b/.npmignore index 88b2d42..f29dce2 100644 --- a/.npmignore +++ b/.npmignore @@ -3,3 +3,4 @@ README.md LICENSE !lib/**/*.js !cli.js +!index.* diff --git a/README.md b/README.md index c88b70d..9383157 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..6a79f62 --- /dev/null +++ b/index.d.ts @@ -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; diff --git a/index.js b/index.js new file mode 100755 index 0000000..faf60b5 --- /dev/null +++ b/index.js @@ -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 }; diff --git a/package.json b/package.json index 198e6eb..604893d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/context.ts b/src/context.ts index abce420..3534cc1 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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 | 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 { +async function createBrowser(launchOptions?: playwright.LaunchOptions): Promise { if (process.env.PLAYWRIGHT_WS_ENDPOINT) { const url = new URL(process.env.PLAYWRIGHT_WS_ENDPOINT); url.searchParams.set('launch-options', JSON.stringify(launchOptions)); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4188317 --- /dev/null +++ b/src/index.ts @@ -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); +} diff --git a/src/program.ts b/src/program.ts index 161f8c1..fb97211 100644 --- a/src/program.ts +++ b/src/program.ts @@ -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); diff --git a/src/server.ts b/src/server.ts index ae05d2e..842ccbb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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; }