diff --git a/src/connection.ts b/src/connection.ts index d318bba..1f5ade1 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -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 { - const tools = allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability)); +export async function createMCPServer(config: FullConfig, tools: Tool[], browserContextFactory: BrowserContextFactory): Promise { const context = new Context(tools, config, browserContextFactory); const server = new Server({ name: 'Playwright', version: packageJSON.version }, { capabilities: { diff --git a/src/extension/main.ts b/src/extension/main.ts index 9d629bb..1e20cbd 100644 --- a/src/extension/main.ts +++ b/src/extension/main.ts @@ -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); diff --git a/src/index.ts b/src/index.ts index 1751657..2434437 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -25,7 +26,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { 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 { diff --git a/src/eval/loop.ts b/src/loop/loop.ts similarity index 98% rename from src/eval/loop.ts rename to src/loop/loop.ts index 48425e4..f925bc6 100644 --- a/src/eval/loop.ts +++ b/src/loop/loop.ts @@ -47,7 +47,7 @@ export interface LLMDelegate { checkDoneToolCall(toolCall: LLMToolCall): string | null; } -export async function runTask(delegate: LLMDelegate, client: Client, task: string): Promise { +export async function runTask(delegate: LLMDelegate, client: Client, task: string): Promise { const { tools } = await client.listTools(); const conversation = delegate.createConversation(task, tools); diff --git a/src/eval/loopClaude.ts b/src/loop/loopClaude.ts similarity index 100% rename from src/eval/loopClaude.ts rename to src/loop/loopClaude.ts diff --git a/src/eval/loopOpenAI.ts b/src/loop/loopOpenAI.ts similarity index 100% rename from src/eval/loopOpenAI.ts rename to src/loop/loopOpenAI.ts diff --git a/src/eval/main.ts b/src/loop/main.ts similarity index 100% rename from src/eval/main.ts rename to src/loop/main.ts diff --git a/src/loop/onetool.ts b/src/loop/onetool.ts new file mode 100644 index 0000000..933b180 --- /dev/null +++ b/src/loop/onetool.ts @@ -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 { + const transport = new StdioClientTransport({ + command: 'node', + args: [ + path.resolve(__filename, '../../../cli.js'), + ], + stderr: 'inherit', + env: process.env as Record, + }); + + const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' }); + await client.connect(transport); + await client.ping(); + return client; +} diff --git a/src/program.ts b/src/program.ts index 97ce84b..4573960 100644 --- a/src/program.ts +++ b/src/program.ts @@ -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) @@ -55,11 +56,6 @@ 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'); @@ -67,7 +63,12 @@ program } 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) { diff --git a/src/server.ts b/src/server.ts index e1927a4..e34928f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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[]; - constructor(config: FullConfig, contextFactory?: BrowserContextFactory) { + constructor(config: FullConfig, tools: Tool[], contextFactory?: BrowserContextFactory) { this.config = config; + this.tools = tools; this._browserConfig = config.browser; this._contextFactory = contextFactory ?? defaultContextFactory(this._browserConfig); } async createConnection(transport: Transport): Promise { - const server = await createMCPServer(this.config, this._contextFactory); + const server = await createMCPServer(this.config, this.tools, this._contextFactory); await server.connect(transport); } diff --git a/src/tools.ts b/src/tools.ts index 9b7c2a3..a1b1531 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -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[] = [ ...common, @@ -49,3 +50,7 @@ export const allTools: Tool[] = [ ...tabs, ...wait, ]; + +export function filteredTools(config: FullConfig) { + return allTools.filter(tool => tool.capability.startsWith('core') || config.capabilities?.includes(tool.capability)); +}