mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-10-12 00:25:14 +03:00
chore: prep folders for copying upstream (#987)
This commit is contained in:
@@ -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<TestOptions>({
|
||||
testDir: './tests',
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
[*]
|
||||
./tools/
|
||||
./mcp/
|
||||
./utils/
|
||||
|
||||
[program.ts]
|
||||
***
|
||||
|
||||
[index.ts]
|
||||
***
|
||||
5
src/browser/DEPS.list
Normal file
5
src/browser/DEPS.list
Normal file
@@ -0,0 +1,5 @@
|
||||
[*]
|
||||
./tools/
|
||||
../sdk/
|
||||
../log.ts
|
||||
../package.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);
|
||||
}
|
||||
@@ -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[];
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
3
src/browser/tools/DEPS.list
Normal file
3
src/browser/tools/DEPS.list
Normal file
@@ -0,0 +1,3 @@
|
||||
[*]
|
||||
../
|
||||
../../sdk/
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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';
|
||||
@@ -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({
|
||||
@@ -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',
|
||||
@@ -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({
|
||||
@@ -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',
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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';
|
||||
@@ -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.'),
|
||||
@@ -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';
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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';
|
||||
@@ -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<R>(tab: Tab, callback: () => Promise<R>): Promise<R> {
|
||||
const requests = new Set<playwright.Request>();
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -1,3 +1,4 @@
|
||||
[*]
|
||||
../mcp/
|
||||
../utils/
|
||||
../sdk/
|
||||
../browser/
|
||||
../log.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';
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
14
src/index.ts
14
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<BrowserContext>): Promise<Server> {
|
||||
|
||||
108
src/loop/loop.ts
108
src/loop/loop.ts
@@ -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<LLMToolCall[]>;
|
||||
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<LLMMessage[]> {
|
||||
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');
|
||||
}
|
||||
@@ -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<Anthropic> {
|
||||
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<LLMToolCall[]> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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<OpenAI> {
|
||||
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<LLMToolCall[]> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>,
|
||||
});
|
||||
|
||||
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>', 'model to use')
|
||||
.action(async options => {
|
||||
if (options.model === 'claude')
|
||||
await run(new ClaudeDelegate());
|
||||
else
|
||||
await run(new OpenAIDelegate());
|
||||
});
|
||||
void program.parseAsync(process.argv);
|
||||
@@ -1,5 +0,0 @@
|
||||
[*]
|
||||
../
|
||||
../loop/
|
||||
../mcp/
|
||||
../utils/
|
||||
@@ -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<mcpServer.CallToolResult> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<any>[] = [perform, snapshot];
|
||||
|
||||
constructor(config: FullConfig) {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this._context = await Context.create(this._config);
|
||||
}
|
||||
|
||||
async listTools(): Promise<mcpServer.Tool[]> {
|
||||
return this._tools.map(tool => toMcpTool(tool.schema));
|
||||
}
|
||||
|
||||
async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise<mcpServer.CallToolResult> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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<Input extends z.Schema = z.Schema> = {
|
||||
schema: ToolSchema<Input>;
|
||||
handle: (context: Context, params: z.output<Input>) => Promise<mcpServer.CallToolResult>;
|
||||
};
|
||||
|
||||
export function defineTool<Input extends z.Schema>(tool: Tool<Input>): Tool<Input> {
|
||||
return tool;
|
||||
}
|
||||
@@ -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'));
|
||||
@@ -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 <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[] = [
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
@@ -1,3 +0,0 @@
|
||||
[*]
|
||||
../utils/
|
||||
../mcp/
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<any>} tool
|
||||
* @param {import('../src/sdk/tool.js').ToolSchema<any>} tool
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function formatToolForReadme(tool) {
|
||||
|
||||
Reference in New Issue
Block a user