Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema, Tool as McpTool } from '
import { zodToJsonSchema } from 'zod-to-json-schema';
import { Context } from './context.js';
import { Response } from './response.js';
import { allTools } from './tools.js';
import { packageJSON } from './package.js';
import { FullConfig } from './config.js';
import { SessionLog } from './sessionLog.js';
import { logUnhandledError } from './log.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { Tool } from './tools/tool.js';

export async function createMCPServer(config: FullConfig, browserContextFactory: BrowserContextFactory): Promise<Server> {
const tools = allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
export async function createMCPServer(config: FullConfig, tools: Tool<any>[], browserContextFactory: BrowserContextFactory): Promise<Server> {
const context = new Context(tools, config, browserContextFactory);
const server = new Server({ name: 'Playwright', version: packageJSON.version }, {
capabilities: {
Expand Down
14 changes: 6 additions & 8 deletions src/extension/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@
* limitations under the License.
*/

import { resolveCLIConfig } from '../config.js';
import { startHttpServer, startHttpTransport, startStdioTransport } from '../transport.js';
import { Server } from '../server.js';
import { startCDPRelayServer } from './cdpRelay.js';
import { filteredTools } from '../tools.js';

import type { CLIOptions } from '../config.js';
import type { FullConfig } from '../config.js';

export async function runWithExtension(options: CLIOptions) {
const config = await resolveCLIConfig(options);
export async function runWithExtension(config: FullConfig) {
const contextFactory = await startCDPRelayServer(9225, config.browser.launchOptions.channel || 'chrome');

const server = new Server(config, contextFactory);
const server = new Server(config, filteredTools(config), contextFactory);
server.setupExitWatchdog();

if (options.port !== undefined) {
const httpServer = await startHttpServer({ port: options.port });
if (config.server.port !== undefined) {
const httpServer = await startHttpServer(config.server);
startHttpTransport(httpServer, server);
} else {
await startStdioTransport(server);
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { createMCPServer } from './connection.js';
import { resolveConfig } from './config.js';
import { contextFactory } from './browserContextFactory.js';
import { filteredTools } from './tools.js';
import type { Config } from '../config.js';
import type { BrowserContext } from 'playwright';
import type { BrowserContextFactory } from './browserContextFactory.js';
Expand All @@ -25,7 +26,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise<BrowserContext>): Promise<Server> {
const config = await resolveConfig(userConfig);
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config.browser);
return createMCPServer(config, factory);
return createMCPServer(config, filteredTools(config), factory);
}

class SimpleBrowserContextFactory implements BrowserContextFactory {
Expand Down
2 changes: 1 addition & 1 deletion src/eval/loop.ts → src/loop/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface LLMDelegate {
checkDoneToolCall(toolCall: LLMToolCall): string | null;
}

export async function runTask(delegate: LLMDelegate, client: Client, task: string): Promise<string | undefined> {
export async function runTask(delegate: LLMDelegate, client: Client, task: string): Promise<string> {
const { tools } = await client.listTools();
const conversation = delegate.createConversation(task, tools);

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
84 changes: 84 additions & 0 deletions src/loop/onetool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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 path from 'path';
import url from 'url';
import dotenv from 'dotenv';
import { z } from 'zod';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

import { FullConfig } from '../config.js';
import { defineTool } from '../tools/tool.js';
import { Server } from '../server.js';
import { startHttpServer, startHttpTransport, startStdioTransport } from '../transport.js';
import { OpenAIDelegate } from './loopOpenAI.js';
import { runTask } from './loop.js';

dotenv.config();

const __filename = url.fileURLToPath(import.meta.url);

let innerClient: Client | undefined;
const delegate = new OpenAIDelegate();

const oneTool = defineTool({
capability: 'core',

schema: {
name: 'browser',
title: 'Perform a task with the browser',
description: 'Perform a task with the browser. It can click, type, export, capture screenshot, drag, hover, select options, etc.',
inputSchema: z.object({
task: z.string().describe('The task to perform with the browser'),
}),
type: 'readOnly',
},

handle: async (context, params, response) => {
const result = await runTask(delegate!, innerClient!, params.task);
response.addResult(result);
},
});

export async function runOneTool(config: FullConfig) {
innerClient = await createInnerClient();
const server = new Server(config, [oneTool]);
server.setupExitWatchdog();

if (config.server.port !== undefined) {
const httpServer = await startHttpServer(config.server);
startHttpTransport(httpServer, server);
} else {
await startStdioTransport(server);
}
}

async function createInnerClient(): Promise<Client> {
const transport = new StdioClientTransport({
command: 'node',
args: [
path.resolve(__filename, '../../../cli.js'),
],
stderr: 'inherit',
env: process.env as Record<string, string>,
});

const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
await client.connect(transport);
await client.ping();
return client;
}
13 changes: 7 additions & 6 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './
import { Server } from './server.js';
import { packageJSON } from './package.js';
import { runWithExtension } from './extension/main.js';
import { filteredTools } from './tools.js';

program
.version('Version ' + packageJSON.version)
Expand Down Expand Up @@ -55,19 +56,19 @@ program
.addOption(new Option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
.addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
.action(async options => {
if (options.extension) {
await runWithExtension(options);
return;
}

if (options.vision) {
// eslint-disable-next-line no-console
console.error('The --vision option is deprecated, use --caps=vision instead');
options.caps = 'vision';
}
const config = await resolveCLIConfig(options);

const server = new Server(config);
if (options.extension) {
await runWithExtension(config);
return;
}

const server = new Server(config, filteredTools(config));
server.setupExitWatchdog();

if (config.server.port !== undefined) {
Expand Down
7 changes: 5 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ import { contextFactory as defaultContextFactory } from './browserContextFactory
import type { FullConfig } from './config.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type { Tool } from './tools/tool.js';

export class Server {
readonly config: FullConfig;
private _browserConfig: FullConfig['browser'];
private _contextFactory: BrowserContextFactory;
readonly tools: Tool<any>[];

constructor(config: FullConfig, contextFactory?: BrowserContextFactory) {
constructor(config: FullConfig, tools: Tool<any>[], contextFactory?: BrowserContextFactory) {
this.config = config;
this.tools = tools;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep it inside createMCPServer, I don't see any usages of the field?

this._browserConfig = config.browser;
this._contextFactory = contextFactory ?? defaultContextFactory(this._browserConfig);
}

async createConnection(transport: Transport): Promise<void> {
const server = await createMCPServer(this.config, this._contextFactory);
const server = await createMCPServer(this.config, this.tools, this._contextFactory);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the usage

await server.connect(transport);
}

Expand Down
5 changes: 5 additions & 0 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import wait from './tools/wait.js';
import mouse from './tools/mouse.js';

import type { Tool } from './tools/tool.js';
import type { FullConfig } from './config.js';

export const allTools: Tool<any>[] = [
...common,
Expand All @@ -49,3 +50,7 @@ export const allTools: Tool<any>[] = [
...tabs,
...wait,
];

export function filteredTools(config: FullConfig) {
return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability));
}
Loading