diff --git a/extension/playwright.config.ts b/extension/playwright.config.ts index 4af9827..7bf4f0e 100644 --- a/extension/playwright.config.ts +++ b/extension/playwright.config.ts @@ -16,7 +16,7 @@ import { defineConfig } from '@playwright/test'; -import type { TestOptions } from '../tests/fixtures.js'; +import type { TestOptions } from '../tests/fixtures'; export default defineConfig({ testDir: './tests', diff --git a/extension/src/ui/connect.tsx b/extension/src/ui/connect.tsx index 0277562..153017b 100644 --- a/extension/src/ui/connect.tsx +++ b/extension/src/ui/connect.tsx @@ -16,8 +16,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem.js'; -import type { TabInfo } from './tabItem.js'; +import { Button, TabItem } from './tabItem'; +import type { TabInfo } from './tabItem'; type Status = | { type: 'connecting'; message: string } diff --git a/extension/src/ui/status.tsx b/extension/src/ui/status.tsx index 9742bc1..f8de788 100644 --- a/extension/src/ui/status.tsx +++ b/extension/src/ui/status.tsx @@ -16,9 +16,9 @@ import React, { useState, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; -import { Button, TabItem } from './tabItem.js'; +import { Button, TabItem } from './tabItem'; -import type { TabInfo } from './tabItem.js'; +import type { TabInfo } from './tabItem'; interface ConnectionStatus { isConnected: boolean; diff --git a/extension/tests/extension.spec.ts b/extension/tests/extension.spec.ts index 0c69927..91dbed1 100644 --- a/extension/tests/extension.spec.ts +++ b/extension/tests/extension.spec.ts @@ -17,11 +17,11 @@ import fs from 'fs'; import path from 'path'; import { chromium } from 'playwright'; -import { test as base, expect } from '../../tests/fixtures.js'; +import { test as base, expect } from '../../tests/fixtures'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { BrowserContext } from 'playwright'; -import type { StartClient } from '../../tests/fixtures.js'; +import type { StartClient } from '../../tests/fixtures'; type BrowserWithExtension = { userDataDir: string; diff --git a/src/DEPS.list b/src/DEPS.list index c1e18f2..ea6f40c 100644 --- a/src/DEPS.list +++ b/src/DEPS.list @@ -1,7 +1,5 @@ -[*] -./tools/ -./mcp/ -./utils/ - [program.ts] *** + +[index.ts] +*** \ No newline at end of file diff --git a/src/browser/DEPS.list b/src/browser/DEPS.list new file mode 100644 index 0000000..936611f --- /dev/null +++ b/src/browser/DEPS.list @@ -0,0 +1,5 @@ +[*] +./tools/ +../sdk/ +../log.ts +../package.ts diff --git a/src/actions.d.ts b/src/browser/actions.d.ts similarity index 100% rename from src/actions.d.ts rename to src/browser/actions.d.ts diff --git a/src/browserContextFactory.ts b/src/browser/browserContextFactory.ts similarity index 97% rename from src/browserContextFactory.ts rename to src/browser/browserContextFactory.ts index a0f8e92..097b77c 100644 --- a/src/browserContextFactory.ts +++ b/src/browser/browserContextFactory.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import crypto from 'crypto'; import fs from 'fs'; import net from 'net'; import path from 'path'; @@ -23,8 +24,7 @@ import * as playwright from 'playwright'; import { registryDirectory } from 'playwright-core/lib/server/registry/index'; // @ts-ignore import { startTraceViewerServer } from 'playwright-core/lib/server'; -import { logUnhandledError, testDebug } from './utils/log'; -import { createHash } from './utils/guid'; +import { logUnhandledError, testDebug } from '../log'; import { outputFile } from './config'; import type { FullConfig } from './config'; @@ -247,3 +247,7 @@ async function startTraceServer(config: FullConfig, rootPath: string | undefined console.error('\nTrace viewer listening on ' + url); return tracesDir; } + +function createHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); +} diff --git a/src/browserServerBackend.ts b/src/browser/browserServerBackend.ts similarity index 94% rename from src/browserServerBackend.ts rename to src/browser/browserServerBackend.ts index 137fcb8..c773786 100644 --- a/src/browserServerBackend.ts +++ b/src/browser/browserServerBackend.ts @@ -17,16 +17,16 @@ import { fileURLToPath } from 'url'; import { FullConfig } from './config'; import { Context } from './context'; -import { logUnhandledError } from './utils/log'; +import { logUnhandledError } from '../log'; import { Response } from './response'; import { SessionLog } from './sessionLog'; import { filteredTools } from './tools'; -import { toMcpTool } from './mcp/tool'; +import { toMcpTool } from '../sdk/tool'; import type { Tool } from './tools/tool'; import type { BrowserContextFactory } from './browserContextFactory'; -import type * as mcpServer from './mcp/server'; -import type { ServerBackend } from './mcp/server'; +import type * as mcpServer from '../sdk/server'; +import type { ServerBackend } from '../sdk/server'; export class BrowserServerBackend implements ServerBackend { private _tools: Tool[]; diff --git a/src/utils/codegen.ts b/src/browser/codegen.ts similarity index 100% rename from src/utils/codegen.ts rename to src/browser/codegen.ts diff --git a/src/config.ts b/src/browser/config.ts similarity index 96% rename from src/config.ts rename to src/browser/config.ts index f6b410b..64c8e5b 100644 --- a/src/config.ts +++ b/src/browser/config.ts @@ -18,9 +18,8 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { devices } from 'playwright'; -import { sanitizeForFilePath } from './utils/fileUtils'; -import type { Config, ToolCapability } from '../config.js'; +import type { Config, ToolCapability } from '../../config'; import type { BrowserContextOptions, LaunchOptions } from 'playwright'; export type CLIOptions = { @@ -318,3 +317,11 @@ function envToBoolean(value: string | undefined): boolean | undefined { function envToString(value: string | undefined): string | undefined { return value ? value.trim() : undefined; } + +function sanitizeForFilePath(s: string) { + const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); + const separator = s.lastIndexOf('.'); + if (separator === -1) + return sanitize(s); + return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); +} diff --git a/src/context.ts b/src/browser/context.ts similarity index 99% rename from src/context.ts rename to src/browser/context.ts index 0a65212..b209849 100644 --- a/src/context.ts +++ b/src/browser/context.ts @@ -17,7 +17,7 @@ import debug from 'debug'; import * as playwright from 'playwright'; -import { logUnhandledError } from './utils/log'; +import { logUnhandledError } from '../log'; import { Tab } from './tab'; import { outputFile } from './config'; diff --git a/src/response.ts b/src/browser/response.ts similarity index 100% rename from src/response.ts rename to src/browser/response.ts diff --git a/src/sessionLog.ts b/src/browser/sessionLog.ts similarity index 99% rename from src/sessionLog.ts rename to src/browser/sessionLog.ts index c8922f7..7e80087 100644 --- a/src/sessionLog.ts +++ b/src/browser/sessionLog.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { Response } from './response'; -import { logUnhandledError } from './utils/log'; +import { logUnhandledError } from '../log'; import { outputFile } from './config'; import type { FullConfig } from './config'; diff --git a/src/tab.ts b/src/browser/tab.ts similarity index 98% rename from src/tab.ts rename to src/browser/tab.ts index c57b9d7..8d9ea5c 100644 --- a/src/tab.ts +++ b/src/browser/tab.ts @@ -17,8 +17,8 @@ import { EventEmitter } from 'events'; import * as playwright from 'playwright'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils'; -import { logUnhandledError } from './utils/log'; -import { ManualPromise } from './mcp/manualPromise'; +import { logUnhandledError } from '../log'; +import { ManualPromise } from '../sdk/manualPromise'; import { ModalState } from './tools/tool'; import type { Context } from './context'; diff --git a/src/tools.ts b/src/browser/tools.ts similarity index 100% rename from src/tools.ts rename to src/browser/tools.ts diff --git a/src/browser/tools/DEPS.list b/src/browser/tools/DEPS.list new file mode 100644 index 0000000..11683db --- /dev/null +++ b/src/browser/tools/DEPS.list @@ -0,0 +1,3 @@ +[*] +../ +../../sdk/ \ No newline at end of file diff --git a/src/tools/common.ts b/src/browser/tools/common.ts similarity index 97% rename from src/tools/common.ts rename to src/browser/tools/common.ts index 8f8aca7..62f175e 100644 --- a/src/tools/common.ts +++ b/src/browser/tools/common.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool, defineTool } from './tool'; const close = defineTool({ diff --git a/src/tools/console.ts b/src/browser/tools/console.ts similarity index 96% rename from src/tools/console.ts rename to src/browser/tools/console.ts index a2fb6b7..3ad0ba4 100644 --- a/src/tools/console.ts +++ b/src/browser/tools/console.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; const console = defineTabTool({ diff --git a/src/tools/dialogs.ts b/src/browser/tools/dialogs.ts similarity index 97% rename from src/tools/dialogs.ts rename to src/browser/tools/dialogs.ts index 637e4a7..f6954b7 100644 --- a/src/tools/dialogs.ts +++ b/src/browser/tools/dialogs.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; const handleDialog = defineTabTool({ diff --git a/src/tools/evaluate.ts b/src/browser/tools/evaluate.ts similarity index 96% rename from src/tools/evaluate.ts rename to src/browser/tools/evaluate.ts index e1f188c..b3e20ee 100644 --- a/src/tools/evaluate.ts +++ b/src/browser/tools/evaluate.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; import { generateLocator } from './utils'; import type * as playwright from 'playwright'; diff --git a/src/tools/files.ts b/src/browser/tools/files.ts similarity index 97% rename from src/tools/files.ts rename to src/browser/tools/files.ts index a3b2e5a..c1cdf66 100644 --- a/src/tools/files.ts +++ b/src/browser/tools/files.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; const uploadFile = defineTabTool({ diff --git a/src/tools/form.ts b/src/browser/tools/form.ts similarity index 96% rename from src/tools/form.ts rename to src/browser/tools/form.ts index 501f067..aad12ad 100644 --- a/src/tools/form.ts +++ b/src/browser/tools/form.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import { generateLocator } from './utils'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; const fillForm = defineTabTool({ capability: 'core', diff --git a/src/tools/install.ts b/src/browser/tools/install.ts similarity index 98% rename from src/tools/install.ts rename to src/browser/tools/install.ts index 4152ec7..54d67a7 100644 --- a/src/tools/install.ts +++ b/src/browser/tools/install.ts @@ -17,7 +17,7 @@ import { fork } from 'child_process'; import path from 'path'; -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTool } from './tool'; const install = defineTool({ diff --git a/src/tools/keyboard.ts b/src/browser/tools/keyboard.ts similarity index 96% rename from src/tools/keyboard.ts rename to src/browser/tools/keyboard.ts index 70ed94e..e567bcb 100644 --- a/src/tools/keyboard.ts +++ b/src/browser/tools/keyboard.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import { elementSchema } from './snapshot'; import { generateLocator } from './utils'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; const pressKey = defineTabTool({ capability: 'core', diff --git a/src/tools/mouse.ts b/src/browser/tools/mouse.ts similarity index 98% rename from src/tools/mouse.ts rename to src/browser/tools/mouse.ts index 4063f05..0e0fc88 100644 --- a/src/tools/mouse.ts +++ b/src/browser/tools/mouse.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; const elementSchema = z.object({ diff --git a/src/tools/navigate.ts b/src/browser/tools/navigate.ts similarity index 97% rename from src/tools/navigate.ts rename to src/browser/tools/navigate.ts index ca68a5a..9998756 100644 --- a/src/tools/navigate.ts +++ b/src/browser/tools/navigate.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTool, defineTabTool } from './tool'; const navigate = defineTool({ diff --git a/src/tools/network.ts b/src/browser/tools/network.ts similarity index 97% rename from src/tools/network.ts rename to src/browser/tools/network.ts index 6b1b3b7..6860fda 100644 --- a/src/tools/network.ts +++ b/src/browser/tools/network.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import type * as playwright from 'playwright'; diff --git a/src/tools/pdf.ts b/src/browser/tools/pdf.ts similarity index 94% rename from src/tools/pdf.ts rename to src/browser/tools/pdf.ts index d7defb5..b4c9b14 100644 --- a/src/tools/pdf.ts +++ b/src/browser/tools/pdf.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; const pdfSchema = z.object({ filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'), diff --git a/src/tools/screenshot.ts b/src/browser/tools/screenshot.ts similarity index 97% rename from src/tools/screenshot.ts rename to src/browser/tools/screenshot.ts index 256e710..e1f3789 100644 --- a/src/tools/screenshot.ts +++ b/src/browser/tools/screenshot.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; import { generateLocator } from './utils'; import type * as playwright from 'playwright'; diff --git a/src/tools/snapshot.ts b/src/browser/tools/snapshot.ts similarity index 98% rename from src/tools/snapshot.ts rename to src/browser/tools/snapshot.ts index a68cf20..03c6852 100644 --- a/src/tools/snapshot.ts +++ b/src/browser/tools/snapshot.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool, defineTool } from './tool'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; import { generateLocator } from './utils'; const snapshot = defineTool({ diff --git a/src/tools/tabs.ts b/src/browser/tools/tabs.ts similarity index 97% rename from src/tools/tabs.ts rename to src/browser/tools/tabs.ts index 6ee44fe..77ad91f 100644 --- a/src/tools/tabs.ts +++ b/src/browser/tools/tabs.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTool } from './tool'; const browserTabs = defineTool({ diff --git a/src/tools/tool.ts b/src/browser/tools/tool.ts similarity index 90% rename from src/tools/tool.ts rename to src/browser/tools/tool.ts index 41995ec..8f4cdd5 100644 --- a/src/tools/tool.ts +++ b/src/browser/tools/tool.ts @@ -15,12 +15,12 @@ */ import type { z } from 'zod'; -import type { Context } from '../context.js'; +import type { Context } from '../context'; import type * as playwright from 'playwright'; -import type { ToolCapability } from '../../config.js'; -import type { Tab } from '../tab.js'; -import type { Response } from '../response.js'; -import type { ToolSchema } from '../mcp/tool.js'; +import type { ToolCapability } from '../../../config'; +import type { Tab } from '../tab'; +import type { Response } from '../response'; +import type { ToolSchema } from '../../sdk/tool'; export type FileUploadModalState = { type: 'fileChooser'; diff --git a/src/tools/utils.ts b/src/browser/tools/utils.ts similarity index 98% rename from src/tools/utils.ts rename to src/browser/tools/utils.ts index e25aec9..eae01cb 100644 --- a/src/tools/utils.ts +++ b/src/browser/tools/utils.ts @@ -18,7 +18,7 @@ import { asLocator } from 'playwright-core/lib/utils'; import type * as playwright from 'playwright'; -import type { Tab } from '../tab.js'; +import type { Tab } from '../tab'; export async function waitForCompletion(tab: Tab, callback: () => Promise): Promise { const requests = new Set(); diff --git a/src/tools/verify.ts b/src/browser/tools/verify.ts similarity index 98% rename from src/tools/verify.ts rename to src/browser/tools/verify.ts index b28da52..809ed2a 100644 --- a/src/tools/verify.ts +++ b/src/browser/tools/verify.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; -import * as javascript from '../utils/codegen.js'; +import * as javascript from '../codegen'; import { generateLocator } from './utils'; const verifyElement = defineTabTool({ diff --git a/src/tools/wait.ts b/src/browser/tools/wait.ts similarity index 98% rename from src/tools/wait.ts rename to src/browser/tools/wait.ts index 9338047..a0f5382 100644 --- a/src/tools/wait.ts +++ b/src/browser/tools/wait.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { z } from '../mcp/bundle.js'; +import { z } from '../../sdk/bundle'; import { defineTool } from './tool'; const wait = defineTool({ diff --git a/src/extension/DEPS.list b/src/extension/DEPS.list index 796b333..65f47af 100644 --- a/src/extension/DEPS.list +++ b/src/extension/DEPS.list @@ -1,3 +1,4 @@ [*] -../mcp/ -../utils/ +../sdk/ +../browser/ +../log.ts diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts index 5602db6..7408b44 100644 --- a/src/extension/cdpRelay.ts +++ b/src/extension/cdpRelay.ts @@ -30,13 +30,13 @@ import { WebSocket, WebSocketServer } from 'ws'; // @ts-ignore import { registry } from 'playwright-core/lib/server/registry/index'; -import { httpAddressToString } from '../mcp/http.js'; -import { logUnhandledError } from '../utils/log.js'; -import { ManualPromise } from '../mcp/manualPromise.js'; +import { httpAddressToString } from '../sdk/http'; +import { logUnhandledError } from '../log'; +import { ManualPromise } from '../sdk/manualPromise'; import * as protocol from './protocol'; import type websocket from 'ws'; -import type { ClientInfo } from '../browserContextFactory.js'; +import type { ClientInfo } from '../browser/browserContextFactory'; import type { ExtensionCommand, ExtensionEvents } from './protocol'; diff --git a/src/extension/extensionContextFactory.ts b/src/extension/extensionContextFactory.ts index 965e27d..68533f6 100644 --- a/src/extension/extensionContextFactory.ts +++ b/src/extension/extensionContextFactory.ts @@ -16,10 +16,10 @@ import debug from 'debug'; import * as playwright from 'playwright'; -import { startHttpServer } from '../mcp/http.js'; +import { startHttpServer } from '../sdk/http'; import { CDPRelayServer } from './cdpRelay'; -import type { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js'; +import type { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; const debugLogger = debug('pw:mcp:relay'); diff --git a/src/index.ts b/src/index.ts index 73eeb30..398e49e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { BrowserServerBackend } from './browserServerBackend'; -import { resolveConfig } from './config'; -import { contextFactory } from './browserContextFactory'; -import * as mcpServer from './mcp/server'; -import { packageJSON } from './utils/package'; +import { BrowserServerBackend } from './browser/browserServerBackend'; +import { resolveConfig } from './browser/config'; +import { contextFactory } from './browser/browserContextFactory'; +import * as mcpServer from './sdk/server'; +import { packageJSON } from './package'; -import type { Config } from '../config.js'; +import type { Config } from '../config'; import type { BrowserContext } from 'playwright'; -import type { BrowserContextFactory } from './browserContextFactory'; +import type { BrowserContextFactory } from './browser/browserContextFactory'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; export async function createConnection(userConfig: Config = {}, contextGetter?: () => Promise): Promise { diff --git a/src/utils/log.ts b/src/log.ts similarity index 100% rename from src/utils/log.ts rename to src/log.ts diff --git a/src/loop/loop.ts b/src/loop/loop.ts deleted file mode 100644 index 43a8e2e..0000000 --- a/src/loop/loop.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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 debug from 'debug'; -import type { Tool, ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; - -export type LLMToolCall = { - name: string; - arguments: any; - id: string; -}; - -export type LLMTool = { - name: string; - description: string; - inputSchema: any; -}; - -export type LLMMessage = - | { role: 'user'; content: string } - | { role: 'assistant'; content: string; toolCalls?: LLMToolCall[] } - | { role: 'tool'; toolCallId: string; content: string; isError?: boolean }; - -export type LLMConversation = { - messages: LLMMessage[]; - tools: LLMTool[]; -}; - -export interface LLMDelegate { - createConversation(task: string, tools: Tool[], oneShot: boolean): LLMConversation; - makeApiCall(conversation: LLMConversation): Promise; - addToolResults(conversation: LLMConversation, results: Array<{ toolCallId: string; content: string; isError?: boolean }>): void; - checkDoneToolCall(toolCall: LLMToolCall): string | null; -} - -export async function runTask(delegate: LLMDelegate, client: Client, task: string, oneShot: boolean = false): Promise { - const { tools } = await client.listTools(); - const taskContent = oneShot ? `Perform following task: ${task}.` : `Perform following task: ${task}. Once the task is complete, call the "done" tool.`; - const conversation = delegate.createConversation(taskContent, tools, oneShot); - - for (let iteration = 0; iteration < 5; ++iteration) { - debug('history')('Making API call for iteration', iteration); - const toolCalls = await delegate.makeApiCall(conversation); - if (toolCalls.length === 0) - throw new Error('Call the "done" tool when the task is complete.'); - - const toolResults: Array<{ toolCallId: string; content: string; isError?: boolean }> = []; - for (const toolCall of toolCalls) { - const doneResult = delegate.checkDoneToolCall(toolCall); - if (doneResult !== null) - return conversation.messages; - - const { name, arguments: args, id } = toolCall; - try { - debug('tool')(name, args); - const response = await client.callTool({ - name, - arguments: args, - }); - const responseContent = (response.content || []) as (TextContent | ImageContent)[]; - debug('tool')(responseContent); - const text = responseContent.filter(part => part.type === 'text').map(part => part.text).join('\n'); - - toolResults.push({ - toolCallId: id, - content: text, - }); - } catch (error) { - debug('tool')(error); - toolResults.push({ - toolCallId: id, - content: `Error while executing tool "${name}": ${error instanceof Error ? error.message : String(error)}\n\nPlease try to recover and complete the task.`, - isError: true, - }); - - // Skip remaining tool calls for this iteration - for (const remainingToolCall of toolCalls.slice(toolCalls.indexOf(toolCall) + 1)) { - toolResults.push({ - toolCallId: remainingToolCall.id, - content: `This tool call is skipped due to previous error.`, - isError: true, - }); - } - break; - } - } - - delegate.addToolResults(conversation, toolResults); - if (oneShot) - return conversation.messages; - } - - throw new Error('Failed to perform step, max attempts reached'); -} diff --git a/src/loop/loopClaude.ts b/src/loop/loopClaude.ts deleted file mode 100644 index b2f915b..0000000 --- a/src/loop/loopClaude.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * 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 Anthropic from '@anthropic-ai/sdk'; -import type { LLMDelegate, LLMConversation, LLMToolCall, LLMTool } from './loop'; -import type { Tool } from '@modelcontextprotocol/sdk/types.js'; - -const model = 'claude-sonnet-4-20250514'; - -export class ClaudeDelegate implements LLMDelegate { - private _anthropic: Anthropic | undefined; - - async anthropic(): Promise { - if (!this._anthropic) { - const anthropic = await import('@anthropic-ai/sdk'); - this._anthropic = new anthropic.Anthropic() as unknown as Anthropic; - } - return this._anthropic; - } - - createConversation(task: string, tools: Tool[], oneShot: boolean): LLMConversation { - const llmTools: LLMTool[] = tools.map(tool => ({ - name: tool.name, - description: tool.description || '', - inputSchema: tool.inputSchema, - })); - - if (!oneShot) { - llmTools.push({ - name: 'done', - description: 'Call this tool when the task is complete.', - inputSchema: { - type: 'object', - properties: {}, - }, - }); - } - - return { - messages: [{ - role: 'user', - content: task - }], - tools: llmTools, - }; - } - - async makeApiCall(conversation: LLMConversation): Promise { - // Convert generic messages to Claude format - const claudeMessages: Anthropic.Messages.MessageParam[] = []; - - for (const message of conversation.messages) { - if (message.role === 'user') { - claudeMessages.push({ - role: 'user', - content: message.content - }); - } else if (message.role === 'assistant') { - const content: Anthropic.Messages.ContentBlock[] = []; - - // Add text content - if (message.content) { - content.push({ - type: 'text', - text: message.content, - citations: [] - }); - } - - // Add tool calls - if (message.toolCalls) { - for (const toolCall of message.toolCalls) { - content.push({ - type: 'tool_use', - id: toolCall.id, - name: toolCall.name, - input: toolCall.arguments - }); - } - } - - claudeMessages.push({ - role: 'assistant', - content - }); - } else if (message.role === 'tool') { - // Tool results are added differently - we need to find if there's already a user message with tool results - const lastMessage = claudeMessages[claudeMessages.length - 1]; - const toolResult: Anthropic.Messages.ToolResultBlockParam = { - type: 'tool_result', - tool_use_id: message.toolCallId, - content: message.content, - is_error: message.isError, - }; - - if (lastMessage && lastMessage.role === 'user' && Array.isArray(lastMessage.content)) { - // Add to existing tool results message - (lastMessage.content as Anthropic.Messages.ToolResultBlockParam[]).push(toolResult); - } else { - // Create new tool results message - claudeMessages.push({ - role: 'user', - content: [toolResult] - }); - } - } - } - - // Convert generic tools to Claude format - const claudeTools: Anthropic.Messages.Tool[] = conversation.tools.map(tool => ({ - name: tool.name, - description: tool.description, - input_schema: tool.inputSchema, - })); - - const anthropic = await this.anthropic(); - const response = await anthropic.messages.create({ - model, - max_tokens: 10000, - messages: claudeMessages, - tools: claudeTools, - }); - - // Extract tool calls and add assistant message to generic conversation - const toolCalls = response.content.filter(block => block.type === 'tool_use') as Anthropic.Messages.ToolUseBlock[]; - const textContent = response.content.filter(block => block.type === 'text').map(block => (block as Anthropic.Messages.TextBlock).text).join(''); - - const llmToolCalls: LLMToolCall[] = toolCalls.map(toolCall => ({ - name: toolCall.name, - arguments: toolCall.input as any, - id: toolCall.id, - })); - - // Add assistant message to generic conversation - conversation.messages.push({ - role: 'assistant', - content: textContent, - toolCalls: llmToolCalls.length > 0 ? llmToolCalls : undefined - }); - - return llmToolCalls; - } - - addToolResults( - conversation: LLMConversation, - results: Array<{ toolCallId: string; content: string; isError?: boolean }> - ): void { - for (const result of results) { - conversation.messages.push({ - role: 'tool', - toolCallId: result.toolCallId, - content: result.content, - isError: result.isError, - }); - } - } - - checkDoneToolCall(toolCall: LLMToolCall): string | null { - if (toolCall.name === 'done') - return (toolCall.arguments as { result: string }).result; - - return null; - } -} diff --git a/src/loop/loopOpenAI.ts b/src/loop/loopOpenAI.ts deleted file mode 100644 index 24ac46d..0000000 --- a/src/loop/loopOpenAI.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * 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 OpenAI from 'openai'; -import type { LLMDelegate, LLMConversation, LLMToolCall, LLMTool } from './loop'; -import type { Tool } from '@modelcontextprotocol/sdk/types.js'; - -const model = 'gpt-4.1'; - -export class OpenAIDelegate implements LLMDelegate { - private _openai: OpenAI | undefined; - - async openai(): Promise { - if (!this._openai) { - const oai = await import('openai'); - this._openai = new oai.OpenAI() as unknown as OpenAI; - } - return this._openai; - } - - createConversation(task: string, tools: Tool[], oneShot: boolean): LLMConversation { - const genericTools: LLMTool[] = tools.map(tool => ({ - name: tool.name, - description: tool.description || '', - inputSchema: tool.inputSchema, - })); - - if (!oneShot) { - genericTools.push({ - name: 'done', - description: 'Call this tool when the task is complete.', - inputSchema: { - type: 'object', - properties: {}, - }, - }); - } - - return { - messages: [{ - role: 'user', - content: task - }], - tools: genericTools, - }; - } - - async makeApiCall(conversation: LLMConversation): Promise { - // Convert generic messages to OpenAI format - const openaiMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = []; - - for (const message of conversation.messages) { - if (message.role === 'user') { - openaiMessages.push({ - role: 'user', - content: message.content - }); - } else if (message.role === 'assistant') { - const toolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] = []; - - if (message.toolCalls) { - for (const toolCall of message.toolCalls) { - toolCalls.push({ - id: toolCall.id, - type: 'function', - function: { - name: toolCall.name, - arguments: JSON.stringify(toolCall.arguments) - } - }); - } - } - - const assistantMessage: OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam = { - role: 'assistant' - }; - - if (message.content) - assistantMessage.content = message.content; - - if (toolCalls.length > 0) - assistantMessage.tool_calls = toolCalls; - - openaiMessages.push(assistantMessage); - } else if (message.role === 'tool') { - openaiMessages.push({ - role: 'tool', - tool_call_id: message.toolCallId, - content: message.content, - }); - } - } - - // Convert generic tools to OpenAI format - const openaiTools: OpenAI.Chat.Completions.ChatCompletionTool[] = conversation.tools.map(tool => ({ - type: 'function', - function: { - name: tool.name, - description: tool.description, - parameters: tool.inputSchema, - }, - })); - - const openai = await this.openai(); - const response = await openai.chat.completions.create({ - model, - messages: openaiMessages, - tools: openaiTools, - tool_choice: 'auto' - }); - - const message = response.choices[0].message; - - // Extract tool calls and add assistant message to generic conversation - const toolCalls = message.tool_calls || []; - const genericToolCalls: LLMToolCall[] = toolCalls.map(toolCall => { - const functionCall = toolCall.function; - return { - name: functionCall.name, - arguments: JSON.parse(functionCall.arguments), - id: toolCall.id, - }; - }); - - // Add assistant message to generic conversation - conversation.messages.push({ - role: 'assistant', - content: message.content || '', - toolCalls: genericToolCalls.length > 0 ? genericToolCalls : undefined - }); - - return genericToolCalls; - } - - addToolResults( - conversation: LLMConversation, - results: Array<{ toolCallId: string; content: string; isError?: boolean }> - ): void { - for (const result of results) { - conversation.messages.push({ - role: 'tool', - toolCallId: result.toolCallId, - content: result.content, - isError: result.isError, - }); - } - } - - checkDoneToolCall(toolCall: LLMToolCall): string | null { - if (toolCall.name === 'done') - return toolCall.arguments.result; - - return null; - } -} diff --git a/src/loop/main.ts b/src/loop/main.ts deleted file mode 100644 index b43310b..0000000 --- a/src/loop/main.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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. - */ - -/* eslint-disable no-console */ - -import path from 'path'; -import dotenv from 'dotenv'; -import { program } from 'commander'; - -import * as mcpBundle from '../mcp/bundle.js'; -import { OpenAIDelegate } from './loopOpenAI'; -import { ClaudeDelegate } from './loopClaude'; -import { runTask } from './loop'; - -import type { LLMDelegate } from './loop'; - -dotenv.config(); - -async function run(delegate: LLMDelegate) { - const transport = new mcpBundle.StdioClientTransport({ - command: 'node', - args: [ - path.resolve(__dirname, '../../cli.js'), - '--save-session', - '--output-dir', path.resolve(__dirname, '../../sessions') - ], - stderr: 'inherit', - env: process.env as Record, - }); - - const client = new mcpBundle.Client({ name: 'test', version: '1.0.0' }); - await client.connect(transport); - await client.ping(); - - for (const task of tasks) { - const messages = await runTask(delegate, client, task); - for (const message of messages) - console.log(`${message.role}: ${message.content}`); - } - await client.close(); -} - -const tasks = [ - 'Open https://playwright.dev/', -]; - -program - .option('--model ', 'model to use') - .action(async options => { - if (options.model === 'claude') - await run(new ClaudeDelegate()); - else - await run(new OpenAIDelegate()); - }); -void program.parseAsync(process.argv); diff --git a/src/loopTools/DEPS.list b/src/loopTools/DEPS.list deleted file mode 100644 index 5cf594d..0000000 --- a/src/loopTools/DEPS.list +++ /dev/null @@ -1,5 +0,0 @@ -[*] -../ -../loop/ -../mcp/ -../utils/ diff --git a/src/loopTools/context.ts b/src/loopTools/context.ts deleted file mode 100644 index c34d290..0000000 --- a/src/loopTools/context.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * 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 { contextFactory } from '../browserContextFactory.js'; -import { BrowserServerBackend } from '../browserServerBackend.js'; -import { Context as BrowserContext } from '../context.js'; -import { runTask } from '../loop/loop.js'; -import { OpenAIDelegate } from '../loop/loopOpenAI.js'; -import { ClaudeDelegate } from '../loop/loopClaude.js'; -import { InProcessTransport } from '../mcp/inProcessTransport.js'; -import * as mcpServer from '../mcp/server.js'; -import { packageJSON } from '../utils/package.js'; -import * as mcpBundle from '../mcp/bundle.js'; - -import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import type { LLMDelegate } from '../loop/loop.js'; -import type { FullConfig } from '../config.js'; - -export class Context { - readonly config: FullConfig; - private _client: Client; - private _delegate: LLMDelegate; - - constructor(config: FullConfig, client: Client) { - this.config = config; - this._client = client; - if (process.env.OPENAI_API_KEY) - this._delegate = new OpenAIDelegate(); - else if (process.env.ANTHROPIC_API_KEY) - this._delegate = new ClaudeDelegate(); - else - throw new Error('No LLM API key found. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.'); - } - - static async create(config: FullConfig) { - const client = new mcpBundle.Client({ name: 'Playwright Proxy', version: packageJSON.version }); - const browserContextFactory = contextFactory(config); - const server = mcpServer.createServer('Playwright Subagent', packageJSON.version, new BrowserServerBackend(config, browserContextFactory), false); - await client.connect(new InProcessTransport(server)); - await client.ping(); - return new Context(config, client); - } - - async runTask(task: string, oneShot: boolean = false): Promise { - const messages = await runTask(this._delegate, this._client!, task, oneShot); - const lines: string[] = []; - - // Skip the first message, which is the user's task. - for (const message of messages.slice(1)) { - // Trim out all page snapshots. - if (!message.content.trim()) - continue; - const index = oneShot ? -1 : message.content.indexOf('### Page state'); - const trimmedContent = index === -1 ? message.content : message.content.substring(0, index); - lines.push(`[${message.role}]:`, trimmedContent); - } - - return { - content: [{ type: 'text', text: lines.join('\n') }], - }; - } - - async close() { - await BrowserContext.disposeAll(); - } -} diff --git a/src/loopTools/main.ts b/src/loopTools/main.ts deleted file mode 100644 index aaaf611..0000000 --- a/src/loopTools/main.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * 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 dotenv from 'dotenv'; - -import * as mcpServer from '../mcp/server.js'; -import { packageJSON } from '../utils/package.js'; -import { Context } from './context'; -import { perform } from './perform'; -import { snapshot } from './snapshot'; -import { toMcpTool } from '../mcp/tool.js'; - -import type { FullConfig } from '../config.js'; -import type { ServerBackend } from '../mcp/server.js'; -import type { Tool } from './tool'; - -export async function runLoopTools(config: FullConfig) { - dotenv.config(); - const serverBackendFactory = { - name: 'Playwright', - nameInConfig: 'playwright-loop', - version: packageJSON.version, - create: () => new LoopToolsServerBackend(config) - }; - await mcpServer.start(serverBackendFactory, config.server); -} - -class LoopToolsServerBackend implements ServerBackend { - private _config: FullConfig; - private _context: Context | undefined; - private _tools: Tool[] = [perform, snapshot]; - - constructor(config: FullConfig) { - this._config = config; - } - - async initialize() { - this._context = await Context.create(this._config); - } - - async listTools(): Promise { - return this._tools.map(tool => toMcpTool(tool.schema)); - } - - async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise { - const tool = this._tools.find(tool => tool.schema.name === name)!; - const parsedArguments = tool.schema.inputSchema.parse(args || {}); - return await tool.handle(this._context!, parsedArguments); - } - - serverClosed() { - void this._context!.close(); - } -} diff --git a/src/loopTools/perform.ts b/src/loopTools/perform.ts deleted file mode 100644 index 7a1fc7f..0000000 --- a/src/loopTools/perform.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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 { z } from '../mcp/bundle.js'; -import { defineTool } from './tool.js'; - -const performSchema = z.object({ - task: z.string().describe('The task to perform with the browser'), -}); - -export const perform = defineTool({ - schema: { - name: 'browser_perform', - 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: performSchema, - type: 'destructive', - }, - - handle: async (context, params) => { - return await context.runTask(params.task); - }, -}); diff --git a/src/loopTools/snapshot.ts b/src/loopTools/snapshot.ts deleted file mode 100644 index 97dea6a..0000000 --- a/src/loopTools/snapshot.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 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 { z } from '../mcp/bundle.js'; -import { defineTool } from './tool.js'; - -export const snapshot = defineTool({ - schema: { - name: 'browser_snapshot', - title: 'Take a snapshot of the browser', - description: 'Take a snapshot of the browser to read what is on the page.', - inputSchema: z.object({}), - type: 'readOnly', - }, - - handle: async (context, params) => { - return await context.runTask('Capture browser snapshot', true); - }, -}); diff --git a/src/loopTools/tool.ts b/src/loopTools/tool.ts deleted file mode 100644 index 9c185cd..0000000 --- a/src/loopTools/tool.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 { z } from 'zod'; -import type * as mcpServer from '../mcp/server.js'; -import type { Context } from './context'; -import type { ToolSchema } from '../mcp/tool.js'; - - -export type Tool = { - schema: ToolSchema; - handle: (context: Context, params: z.output) => Promise; -}; - -export function defineTool(tool: Tool): Tool { - return tool; -} diff --git a/src/utils/package.ts b/src/package.ts similarity index 93% rename from src/utils/package.ts rename to src/package.ts index 0c415d6..9874319 100644 --- a/src/utils/package.ts +++ b/src/package.ts @@ -17,4 +17,4 @@ import fs from 'fs'; import path from 'path'; -export const packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')); +export const packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')); diff --git a/src/program.ts b/src/program.ts index 819295f..35a74d8 100644 --- a/src/program.ts +++ b/src/program.ts @@ -15,18 +15,17 @@ */ import { program, Option } from 'commander'; -import * as mcpServer from './mcp/server'; -import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config'; -import { packageJSON } from './utils/package'; -import { Context } from './context'; -import { contextFactory } from './browserContextFactory'; -import { runLoopTools } from './loopTools/main'; -import { ProxyBackend } from './mcp/proxyBackend'; -import { BrowserServerBackend } from './browserServerBackend'; +import * as mcpServer from './sdk/server'; +import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './browser/config'; +import { packageJSON } from './package'; +import { Context } from './browser/context'; +import { contextFactory } from './browser/browserContextFactory'; +import { ProxyBackend } from './sdk/proxyBackend'; +import { BrowserServerBackend } from './browser/browserServerBackend'; import { ExtensionContextFactory } from './extension/extensionContextFactory'; import { runVSCodeTools } from './vscode/host'; -import type { MCPProvider } from './mcp/proxyBackend'; +import type { MCPProvider } from './sdk/proxyBackend'; program .version('Version ' + packageJSON.version) @@ -59,7 +58,6 @@ program .option('--viewport-size ', 'specify browser viewport size in pixels, for example "1280, 720"') .addOption(new Option('--connect-tool', 'Allow to switch between different browser connection methods.').hideHelp()) .addOption(new Option('--vscode', 'VS Code tools.').hideHelp()) - .addOption(new Option('--loop-tools', 'Run loop tools').hideHelp()) .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp()) .action(async options => { setupExitWatchdog(); @@ -90,11 +88,6 @@ program return; } - if (options.loopTools) { - await runLoopTools(config); - return; - } - if (options.connectTool) { const providers: MCPProvider[] = [ { diff --git a/src/mcp/DEPS.list b/src/sdk/DEPS.list similarity index 100% rename from src/mcp/DEPS.list rename to src/sdk/DEPS.list diff --git a/src/mcp/README.md b/src/sdk/README.md similarity index 100% rename from src/mcp/README.md rename to src/sdk/README.md diff --git a/src/mcp/bundle.ts b/src/sdk/bundle.ts similarity index 100% rename from src/mcp/bundle.ts rename to src/sdk/bundle.ts diff --git a/src/mcp/http.ts b/src/sdk/http.ts similarity index 100% rename from src/mcp/http.ts rename to src/sdk/http.ts diff --git a/src/mcp/inProcessTransport.ts b/src/sdk/inProcessTransport.ts similarity index 100% rename from src/mcp/inProcessTransport.ts rename to src/sdk/inProcessTransport.ts diff --git a/src/mcp/manualPromise.ts b/src/sdk/manualPromise.ts similarity index 100% rename from src/mcp/manualPromise.ts rename to src/sdk/manualPromise.ts diff --git a/src/mcp/mdb.ts b/src/sdk/mdb.ts similarity index 100% rename from src/mcp/mdb.ts rename to src/sdk/mdb.ts diff --git a/src/mcp/proxyBackend.ts b/src/sdk/proxyBackend.ts similarity index 100% rename from src/mcp/proxyBackend.ts rename to src/sdk/proxyBackend.ts diff --git a/src/mcp/server.ts b/src/sdk/server.ts similarity index 100% rename from src/mcp/server.ts rename to src/sdk/server.ts diff --git a/src/mcp/tool.ts b/src/sdk/tool.ts similarity index 96% rename from src/mcp/tool.ts rename to src/sdk/tool.ts index af2594c..bd94547 100644 --- a/src/mcp/tool.ts +++ b/src/sdk/tool.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { zodToJsonSchema } from '../mcp/bundle.js'; +import { zodToJsonSchema } from '../sdk/bundle'; import type { z } from 'zod'; import type * as mcpServer from './server'; diff --git a/src/tools/DEPS.list b/src/tools/DEPS.list deleted file mode 100644 index b179738..0000000 --- a/src/tools/DEPS.list +++ /dev/null @@ -1,3 +0,0 @@ -[*] -../utils/ -../mcp/ \ No newline at end of file diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts deleted file mode 100644 index b30e5ca..0000000 --- a/src/utils/fileUtils.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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 os from 'os'; -import path from 'path'; - -export function cacheDir() { - let cacheDirectory: string; - if (process.platform === 'linux') - cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); - else if (process.platform === 'darwin') - cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); - else if (process.platform === 'win32') - cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); - else - throw new Error('Unsupported platform: ' + process.platform); - return path.join(cacheDirectory, 'ms-playwright'); -} - -export function sanitizeForFilePath(s: string) { - const sanitize = (s: string) => s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); - const separator = s.lastIndexOf('.'); - if (separator === -1) - return sanitize(s); - return sanitize(s.substring(0, separator)) + '.' + sanitize(s.substring(separator + 1)); -} diff --git a/src/utils/guid.ts b/src/utils/guid.ts deleted file mode 100644 index 1cc64e7..0000000 --- a/src/utils/guid.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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 crypto from 'crypto'; - -export function createGuid(): string { - return crypto.randomBytes(16).toString('hex'); -} - -export function createHash(data: string): string { - return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); -} diff --git a/src/vscode/DEPS.list b/src/vscode/DEPS.list index 29b83cd..b75148d 100644 --- a/src/vscode/DEPS.list +++ b/src/vscode/DEPS.list @@ -1,6 +1,7 @@ [*] -../mcp/ -../utils/ -../config.js -../browserServerBackend.js -../browserContextFactory.js +../sdk/ +../browser/config.ts +../browser/browserServerBackend.ts +../browser/browserContextFactory.ts +../log.ts +../package.ts diff --git a/src/vscode/host.ts b/src/vscode/host.ts index e4e729d..4631f59 100644 --- a/src/vscode/host.ts +++ b/src/vscode/host.ts @@ -16,18 +16,18 @@ import path from 'path'; -import * as mcpBundle from '../mcp/bundle.js'; -import * as mcpServer from '../mcp/server.js'; -import { logUnhandledError } from '../utils/log.js'; -import { packageJSON } from '../utils/package.js'; +import * as mcpBundle from '../sdk/bundle'; +import * as mcpServer from '../sdk/server'; +import { logUnhandledError } from '../log'; +import { packageJSON } from '../package'; -import { FullConfig } from '../config.js'; -import { BrowserServerBackend } from '../browserServerBackend.js'; -import { contextFactory } from '../browserContextFactory.js'; +import { FullConfig } from '../browser/config'; +import { BrowserServerBackend } from '../browser/browserServerBackend'; +import { contextFactory } from '../browser/browserContextFactory'; import type { z as zod } from 'zod'; import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; -import type { ClientVersion, ServerBackend } from '../mcp/server.js'; +import type { ClientVersion, ServerBackend } from '../sdk/server'; import type { Root, Tool, CallToolResult, CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; import type { Browser, BrowserContext, BrowserServer } from 'playwright'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; diff --git a/src/vscode/main.ts b/src/vscode/main.ts index e1905f6..5eb4656 100644 --- a/src/vscode/main.ts +++ b/src/vscode/main.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import * as mcpBundle from '../mcp/bundle.js'; -import * as mcpServer from '../mcp/server.js'; -import { BrowserServerBackend } from '../browserServerBackend.js'; -import { BrowserContextFactory, ClientInfo } from '../browserContextFactory.js'; +import * as mcpBundle from '../sdk/bundle'; +import * as mcpServer from '../sdk/server'; +import { BrowserServerBackend } from '../browser/browserServerBackend'; +import { BrowserContextFactory, ClientInfo } from '../browser/browserContextFactory'; -import type { FullConfig } from '../config.js'; +import type { FullConfig } from '../browser/config'; import type { BrowserContext } from 'playwright-core'; class VSCodeBrowserContextFactory implements BrowserContextFactory { diff --git a/tests/config.spec.ts b/tests/config.spec.ts index 246d754..cdd5d8b 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -16,8 +16,9 @@ import fs from 'node:fs'; -import { Config } from '../config.js'; +import { Config } from '../config'; import { test, expect } from './fixtures'; +import { configFromCLIOptions } from '../lib/browser/config'; test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => { server.setContent('/', ` @@ -68,17 +69,12 @@ test.describe(() => { test.describe('sandbox configuration', () => { test('should enable sandbox by default (no --no-sandbox flag)', async () => { - const { configFromCLIOptions } = await import('../lib/config.js'); const config = configFromCLIOptions({ sandbox: undefined }); - // When --no-sandbox is not passed, chromiumSandbox should not be set to false - // This allows the default (true) to be used expect(config.browser?.launchOptions?.chromiumSandbox).toBeUndefined(); }); test('should disable sandbox when --no-sandbox flag is passed', async () => { - const { configFromCLIOptions } = await import('../lib/config.js'); const config = configFromCLIOptions({ sandbox: false }); - // When --no-sandbox is passed, chromiumSandbox should be explicitly set to false expect(config.browser?.launchOptions?.chromiumSandbox).toBe(false); }); }); diff --git a/tests/mdb.spec.ts b/tests/mdb.spec.ts index 8b98414..1d01a3a 100644 --- a/tests/mdb.spec.ts +++ b/tests/mdb.spec.ts @@ -19,12 +19,12 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; -import { runMainBackend, runOnPauseBackendLoop } from '../src/mcp/mdb.js'; +import { runMainBackend, runOnPauseBackendLoop } from '../src/sdk/mdb'; import { test, expect } from './fixtures'; -import type * as mcpServer from '../src/mcp/server.js'; -import type { ServerBackendOnPause } from '../src/mcp/mdb.js'; +import type * as mcpServer from '../src/sdk/server'; +import type { ServerBackendOnPause } from '../src/sdk/mdb'; test('call top level tool', async () => { const { mdbUrl } = await startMDBAndCLI(); diff --git a/tests/roots.spec.ts b/tests/roots.spec.ts index 94c1e9e..904a326 100644 --- a/tests/roots.spec.ts +++ b/tests/roots.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ +import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import { test, expect } from './fixtures'; -import { createHash } from '../src/utils/guid.js'; const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder'; @@ -77,3 +77,7 @@ test('should list all tools when listRoots is slow', async ({ startClient }) => const tools = await client.listTools(); expect(tools.tools.length).toBeGreaterThan(10); }); + +function createHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7); +} diff --git a/utils/update-readme.js b/utils/update-readme.js index f5855bb..fe5b06a 100644 --- a/utils/update-readme.js +++ b/utils/update-readme.js @@ -21,7 +21,7 @@ const path = require('path') const { zodToJsonSchema } = require('zod-to-json-schema') const { execSync } = require('child_process'); -const { allTools } = require('../lib/tools.js'); +const { allTools } = require('../lib/browser/tools.js'); const capabilities = { 'core': 'Core automation', @@ -35,7 +35,7 @@ const capabilities = { const toolsByCapability = Object.fromEntries(Object.entries(capabilities).map(([capability, title]) => [title, allTools.filter(tool => tool.capability === capability).sort((a, b) => a.schema.name.localeCompare(b.schema.name))])); /** - * @param {import('../src/mcp/tool.js').ToolSchema} tool + * @param {import('../src/sdk/tool.js').ToolSchema} tool * @returns {string[]} */ function formatToolForReadme(tool) {