@wordbricks/playwright-mcp 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.d.ts +5 -0
- package/lib/browserContextFactory.js +16 -3
- package/lib/config.js +2 -0
- package/lib/context.js +9 -0
- package/lib/hooks/schema.js +2 -0
- package/lib/mcp/tool.js +13 -4
- package/lib/program.js +1 -5
- package/lib/response.js +3 -3
- package/lib/tab.js +1 -1
- package/lib/tools/dialogs.js +2 -2
- package/lib/tools/evaluate.js +1 -1
- package/lib/tools/files.js +5 -4
- package/lib/tools/form.js +57 -0
- package/lib/tools/install.js +2 -4
- package/lib/tools/keyboard.js +2 -2
- package/lib/tools/mouse.js +2 -2
- package/lib/tools/navigate.js +1 -1
- package/lib/tools/repl.js +1 -1
- package/lib/tools/scroll.js +1 -1
- package/lib/tools/snapshot.js +15 -10
- package/lib/tools/tabs.js +32 -60
- package/lib/tools/tool.js +1 -1
- package/lib/utils/codegen.js +4 -2
- package/package.json +5 -4
- package/lib/loop/loop.js +0 -69
- package/lib/loop/loopClaude.js +0 -152
- package/lib/loop/loopOpenAI.js +0 -141
- package/lib/loop/main.js +0 -60
- package/lib/loopTools/context.js +0 -66
- package/lib/loopTools/main.js +0 -51
- package/lib/loopTools/perform.js +0 -32
- package/lib/loopTools/snapshot.js +0 -29
- package/lib/loopTools/tool.js +0 -18
package/config.d.ts
CHANGED
|
@@ -63,6 +63,11 @@ export type Config = {
|
|
|
63
63
|
* Remote endpoint to connect to an existing Playwright server.
|
|
64
64
|
*/
|
|
65
65
|
remoteEndpoint?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Path to a JavaScript file to inject into all pages using addInitScript.
|
|
69
|
+
*/
|
|
70
|
+
initScript?: string;
|
|
66
71
|
},
|
|
67
72
|
|
|
68
73
|
server?: {
|
|
@@ -27,6 +27,12 @@ import { createHash } from './utils/guid.js';
|
|
|
27
27
|
import { outputFile } from './config.js';
|
|
28
28
|
import { extensionPath } from './utils/extensionPath.js';
|
|
29
29
|
const TIMEOUT_STR = '30m';
|
|
30
|
+
async function applyInitScript(browserContext, config) {
|
|
31
|
+
if (config.browser.initScript) {
|
|
32
|
+
const scriptContent = await fs.promises.readFile(config.browser.initScript, 'utf8');
|
|
33
|
+
await browserContext.addInitScript(scriptContent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
30
36
|
export function contextFactory(config) {
|
|
31
37
|
if (config.browser.remoteEndpoint)
|
|
32
38
|
return new RemoteContextFactory(config);
|
|
@@ -138,7 +144,9 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
138
144
|
});
|
|
139
145
|
}
|
|
140
146
|
async _doCreateContext(browser) {
|
|
141
|
-
|
|
147
|
+
const browserContext = await browser.newContext(this.config.browser.contextOptions);
|
|
148
|
+
await applyInitScript(browserContext, this.config);
|
|
149
|
+
return browserContext;
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
class CdpContextFactory extends BaseContextFactory {
|
|
@@ -149,7 +157,9 @@ class CdpContextFactory extends BaseContextFactory {
|
|
|
149
157
|
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint);
|
|
150
158
|
}
|
|
151
159
|
async _doCreateContext(browser) {
|
|
152
|
-
|
|
160
|
+
const browserContext = this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
|
|
161
|
+
await applyInitScript(browserContext, this.config);
|
|
162
|
+
return browserContext;
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
class RemoteContextFactory extends BaseContextFactory {
|
|
@@ -164,7 +174,9 @@ class RemoteContextFactory extends BaseContextFactory {
|
|
|
164
174
|
return playwright[this.config.browser.browserName].connect(String(url));
|
|
165
175
|
}
|
|
166
176
|
async _doCreateContext(browser) {
|
|
167
|
-
|
|
177
|
+
const browserContext = await browser.newContext();
|
|
178
|
+
await applyInitScript(browserContext, this.config);
|
|
179
|
+
return browserContext;
|
|
168
180
|
}
|
|
169
181
|
}
|
|
170
182
|
class PersistentContextFactory {
|
|
@@ -199,6 +211,7 @@ class PersistentContextFactory {
|
|
|
199
211
|
handleSIGTERM: false,
|
|
200
212
|
args,
|
|
201
213
|
});
|
|
214
|
+
await applyInitScript(browserContext, this.config);
|
|
202
215
|
// Start auto-close timer
|
|
203
216
|
this._startAutoCloseTimer(browserContext);
|
|
204
217
|
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
package/lib/config.js
CHANGED
|
@@ -120,6 +120,7 @@ export function configFromCLIOptions(cliOptions) {
|
|
|
120
120
|
launchOptions,
|
|
121
121
|
contextOptions,
|
|
122
122
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
|
123
|
+
initScript: cliOptions.initScript,
|
|
123
124
|
},
|
|
124
125
|
server: {
|
|
125
126
|
port: cliOptions.port,
|
|
@@ -151,6 +152,7 @@ function configFromEnv() {
|
|
|
151
152
|
options.headless = envToBoolean(process.env.PLAYWRIGHT_MCP_HEADLESS);
|
|
152
153
|
options.host = envToString(process.env.PLAYWRIGHT_MCP_HOST);
|
|
153
154
|
options.ignoreHttpsErrors = envToBoolean(process.env.PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS);
|
|
155
|
+
options.initScript = envToString(process.env.PLAYWRIGHT_MCP_INIT_SCRIPT);
|
|
154
156
|
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
|
|
155
157
|
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === 'omit')
|
|
156
158
|
options.imageResponses = 'omit';
|
package/lib/context.js
CHANGED
|
@@ -21,6 +21,7 @@ import { buildHookRegistry } from './hooks/registry.js';
|
|
|
21
21
|
import { setEventStore, createEventStore } from './hooks/events.js';
|
|
22
22
|
import { setupNetworkTracking } from './hooks/networkSetup.js';
|
|
23
23
|
import { outputFile } from './config.js';
|
|
24
|
+
import * as codegen from './utils/codegen.js';
|
|
24
25
|
const testDebug = debug('pw:mcp:test');
|
|
25
26
|
export class Context {
|
|
26
27
|
tools;
|
|
@@ -184,6 +185,14 @@ export class Context {
|
|
|
184
185
|
}
|
|
185
186
|
return result;
|
|
186
187
|
}
|
|
188
|
+
lookupSecret(secretName) {
|
|
189
|
+
// if (!this.config.secrets?.[secretName])
|
|
190
|
+
return { value: secretName, code: codegen.quote(secretName) };
|
|
191
|
+
// return {
|
|
192
|
+
// value: this.config.secrets[secretName]!,
|
|
193
|
+
// code: `process.env['${secretName}']`,
|
|
194
|
+
// };
|
|
195
|
+
}
|
|
187
196
|
}
|
|
188
197
|
export class InputRecorder {
|
|
189
198
|
_context;
|
package/lib/hooks/schema.js
CHANGED
|
@@ -15,6 +15,7 @@ export const hookNameSchema = z.enum([
|
|
|
15
15
|
export const toolNameSchema = z.enum([
|
|
16
16
|
'browser_click',
|
|
17
17
|
'browser_extract_framework_state',
|
|
18
|
+
'browser_fill_form',
|
|
18
19
|
'browser_get_snapshot',
|
|
19
20
|
// 'browser_get_visible_html',
|
|
20
21
|
'browser_navigate',
|
|
@@ -26,6 +27,7 @@ export const toolNameSchema = z.enum([
|
|
|
26
27
|
'browser_reload',
|
|
27
28
|
'browser_repl',
|
|
28
29
|
'browser_scroll',
|
|
30
|
+
'browser_type',
|
|
29
31
|
'browser_wait',
|
|
30
32
|
]);
|
|
31
33
|
export const EventTypeSchema = z.enum([
|
package/lib/mcp/tool.js
CHANGED
|
@@ -14,16 +14,25 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
17
|
-
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
const typesWithIntent = ['action', 'assertion', 'input'];
|
|
19
|
+
export function toMcpTool(tool, options) {
|
|
20
|
+
const inputSchema = options?.addIntent && typesWithIntent.includes(tool.type) ? tool.inputSchema.extend({
|
|
21
|
+
intent: z.string().describe('The intent of the call, for example the test step description plan idea')
|
|
22
|
+
}) : tool.inputSchema;
|
|
23
|
+
const readOnly = tool.type === 'readOnly' || tool.type === 'assertion';
|
|
18
24
|
return {
|
|
19
25
|
name: tool.name,
|
|
20
26
|
description: tool.description,
|
|
21
|
-
inputSchema: zodToJsonSchema(
|
|
27
|
+
inputSchema: zodToJsonSchema(inputSchema, { strictUnions: true }),
|
|
22
28
|
annotations: {
|
|
23
29
|
title: tool.title,
|
|
24
|
-
readOnlyHint:
|
|
25
|
-
destructiveHint:
|
|
30
|
+
readOnlyHint: readOnly,
|
|
31
|
+
destructiveHint: !readOnly,
|
|
26
32
|
openWorldHint: true,
|
|
27
33
|
},
|
|
28
34
|
};
|
|
29
35
|
}
|
|
36
|
+
export function defineToolSchema(tool) {
|
|
37
|
+
return tool;
|
|
38
|
+
}
|
package/lib/program.js
CHANGED
|
@@ -20,7 +20,6 @@ import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './
|
|
|
20
20
|
import { packageJSON } from './utils/package.js';
|
|
21
21
|
import { Context } from './context.js';
|
|
22
22
|
import { contextFactory } from './browserContextFactory.js';
|
|
23
|
-
import { runLoopTools } from './loopTools/main.js';
|
|
24
23
|
import { ProxyBackend } from './mcp/proxyBackend.js';
|
|
25
24
|
import { BrowserServerBackend } from './browserServerBackend.js';
|
|
26
25
|
import { ExtensionContextFactory } from './extension/extensionContextFactory.js';
|
|
@@ -40,6 +39,7 @@ program
|
|
|
40
39
|
.option('--headless', 'run browser in headless mode, headed by default')
|
|
41
40
|
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
|
|
42
41
|
.option('--ignore-https-errors', 'ignore https errors')
|
|
42
|
+
.option('--init-script <path>', 'path to a JavaScript file to inject into all pages using addInitScript.')
|
|
43
43
|
.option('--isolated', 'keep the browser profile in memory, do not save it to disk.')
|
|
44
44
|
.option('--image-responses <mode>', 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
|
|
45
45
|
.option('--no-sandbox', 'disable the sandbox for all process types that are normally sandboxed.')
|
|
@@ -71,10 +71,6 @@ program
|
|
|
71
71
|
await mcpTransport.start(serverBackendFactory, config.server);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
if (options.loopTools) {
|
|
75
|
-
await runLoopTools(config);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
74
|
const browserContextFactory = contextFactory(config);
|
|
79
75
|
const providers = [mcpProviderForBrowserContextFactory(config, browserContextFactory)];
|
|
80
76
|
if (options.connectTool)
|
package/lib/response.js
CHANGED
|
@@ -62,9 +62,9 @@ export class Response {
|
|
|
62
62
|
images() {
|
|
63
63
|
return this._images;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// this._includeSnapshot =
|
|
65
|
+
// NOTE Wordbricks Disabled: Page state logging not needed
|
|
66
|
+
setIncludeSnapshot(full) {
|
|
67
|
+
// this._includeSnapshot = full ?? 'incremental';
|
|
68
68
|
}
|
|
69
69
|
setIncludeTabs() {
|
|
70
70
|
this._includeTabs = true;
|
package/lib/tab.js
CHANGED
|
@@ -203,7 +203,7 @@ export class Tab extends EventEmitter {
|
|
|
203
203
|
// This avoids invalidating refs obtained from browser_get_snapshot while still
|
|
204
204
|
// initializing the mapping when needed (e.g., after navigation).
|
|
205
205
|
await this.page._snapshotForAI();
|
|
206
|
-
return params.map(param => this.page.locator(`aria-ref=${param.ref}`)
|
|
206
|
+
return params.map(param => this.page.locator(`aria-ref=${param.ref}`));
|
|
207
207
|
}
|
|
208
208
|
async waitForTimeout(time) {
|
|
209
209
|
if (this._javaScriptBlocked()) {
|
package/lib/tools/dialogs.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
|
-
const handleDialog = defineTabTool({
|
|
18
|
+
export const handleDialog = defineTabTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_handle_dialog',
|
|
@@ -25,7 +25,7 @@ const handleDialog = defineTabTool({
|
|
|
25
25
|
accept: z.boolean().describe('Whether to accept the dialog.'),
|
|
26
26
|
promptText: z.string().optional().describe('The text of the prompt in case of a prompt dialog.'),
|
|
27
27
|
}),
|
|
28
|
-
type: '
|
|
28
|
+
type: 'action',
|
|
29
29
|
},
|
|
30
30
|
handle: async (tab, params, response) => {
|
|
31
31
|
response.setIncludeSnapshot();
|
package/lib/tools/evaluate.js
CHANGED
|
@@ -29,7 +29,7 @@ const evaluate = defineTabTool({
|
|
|
29
29
|
title: 'Evaluate JavaScript',
|
|
30
30
|
description: 'Evaluate JavaScript expression on page or element',
|
|
31
31
|
inputSchema: evaluateSchema,
|
|
32
|
-
type: '
|
|
32
|
+
type: 'action',
|
|
33
33
|
},
|
|
34
34
|
handle: async (tab, params, response) => {
|
|
35
35
|
response.setIncludeSnapshot();
|
package/lib/tools/files.js
CHANGED
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTabTool } from './tool.js';
|
|
18
|
-
const uploadFile = defineTabTool({
|
|
18
|
+
export const uploadFile = defineTabTool({
|
|
19
19
|
capability: 'core',
|
|
20
20
|
schema: {
|
|
21
21
|
name: 'browser_file_upload',
|
|
22
22
|
title: 'Upload files',
|
|
23
23
|
description: 'Upload one or multiple files',
|
|
24
24
|
inputSchema: z.object({
|
|
25
|
-
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be
|
|
25
|
+
paths: z.array(z.string()).optional().describe('The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.'),
|
|
26
26
|
}),
|
|
27
|
-
type: '
|
|
27
|
+
type: 'action',
|
|
28
28
|
},
|
|
29
29
|
handle: async (tab, params, response) => {
|
|
30
30
|
response.setIncludeSnapshot();
|
|
@@ -34,7 +34,8 @@ const uploadFile = defineTabTool({
|
|
|
34
34
|
response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
|
|
35
35
|
tab.clearModalState(modalState);
|
|
36
36
|
await tab.waitForCompletion(async () => {
|
|
37
|
-
|
|
37
|
+
if (params.paths)
|
|
38
|
+
await modalState.fileChooser.setFiles(params.paths);
|
|
38
39
|
});
|
|
39
40
|
},
|
|
40
41
|
clearsModalState: 'fileChooser',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { defineTabTool } from './tool.js';
|
|
18
|
+
import * as codegen from '../utils/codegen.js';
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
|
+
const fillForm = defineTabTool({
|
|
21
|
+
capability: 'core',
|
|
22
|
+
schema: {
|
|
23
|
+
name: 'browser_fill_form',
|
|
24
|
+
title: 'Fill form',
|
|
25
|
+
description: 'Fill multiple form fields',
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
fields: z.array(z.object({
|
|
28
|
+
name: z.string().describe('Human-readable field name'),
|
|
29
|
+
type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
|
|
30
|
+
ref: z.string().describe('Exact target field reference from the page snapshot'),
|
|
31
|
+
value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
|
|
32
|
+
})).describe('Fields to fill in'),
|
|
33
|
+
}),
|
|
34
|
+
type: 'input',
|
|
35
|
+
},
|
|
36
|
+
handle: async (tab, params, response) => {
|
|
37
|
+
for (const field of params.fields) {
|
|
38
|
+
const locator = await tab.refLocator({ element: field.name, ref: field.ref });
|
|
39
|
+
if (field.type === 'textbox' || field.type === 'slider') {
|
|
40
|
+
const secret = tab.context.lookupSecret(field.value);
|
|
41
|
+
await locator.fill(secret.value);
|
|
42
|
+
response.addCode(`await page.${await generateLocator(locator)}.fill(${secret.code});`);
|
|
43
|
+
}
|
|
44
|
+
else if (field.type === 'checkbox' || field.type === 'radio') {
|
|
45
|
+
await locator.setChecked(field.value === 'true');
|
|
46
|
+
response.addCode(`await page.${await generateLocator(locator)}.setChecked(${field.value});`);
|
|
47
|
+
}
|
|
48
|
+
else if (field.type === 'combobox') {
|
|
49
|
+
await locator.selectOption({ label: field.value });
|
|
50
|
+
response.addCode(`await page.${await generateLocator(locator)}.selectOption(${codegen.quote(field.value)});`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export default [
|
|
56
|
+
fillForm,
|
|
57
|
+
];
|
package/lib/tools/install.js
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { fork } from 'child_process';
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import { fileURLToPath } from 'url';
|
|
19
18
|
import { z } from 'zod';
|
|
20
19
|
import { defineTool } from './tool.js';
|
|
21
20
|
const install = defineTool({
|
|
@@ -25,12 +24,11 @@ const install = defineTool({
|
|
|
25
24
|
title: 'Install the browser specified in the config',
|
|
26
25
|
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
|
27
26
|
inputSchema: z.object({}),
|
|
28
|
-
type: '
|
|
27
|
+
type: 'action',
|
|
29
28
|
},
|
|
30
29
|
handle: async (context, params, response) => {
|
|
31
30
|
const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
|
|
32
|
-
const
|
|
33
|
-
const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
|
|
31
|
+
const cliPath = path.join(require.resolve('playwright/package.json'), '../cli.js');
|
|
34
32
|
const child = fork(cliPath, ['install', channel], {
|
|
35
33
|
stdio: 'pipe',
|
|
36
34
|
});
|
package/lib/tools/keyboard.js
CHANGED
|
@@ -27,7 +27,7 @@ const pressKey = defineTabTool({
|
|
|
27
27
|
inputSchema: z.object({
|
|
28
28
|
key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
|
|
29
29
|
}),
|
|
30
|
-
type: '
|
|
30
|
+
type: 'input',
|
|
31
31
|
},
|
|
32
32
|
handle: async (tab, params, response) => {
|
|
33
33
|
response.setIncludeSnapshot();
|
|
@@ -50,7 +50,7 @@ const type = defineTabTool({
|
|
|
50
50
|
title: 'Type text',
|
|
51
51
|
description: 'Type text into editable element',
|
|
52
52
|
inputSchema: typeSchema,
|
|
53
|
-
type: '
|
|
53
|
+
type: 'input',
|
|
54
54
|
},
|
|
55
55
|
handle: async (tab, params, response) => {
|
|
56
56
|
const locator = await tab.refLocator(params);
|
package/lib/tools/mouse.js
CHANGED
|
@@ -48,7 +48,7 @@ const mouseClick = defineTabTool({
|
|
|
48
48
|
x: z.number().describe('X coordinate'),
|
|
49
49
|
y: z.number().describe('Y coordinate'),
|
|
50
50
|
}),
|
|
51
|
-
type: '
|
|
51
|
+
type: 'input',
|
|
52
52
|
},
|
|
53
53
|
handle: async (tab, params, response) => {
|
|
54
54
|
response.setIncludeSnapshot();
|
|
@@ -75,7 +75,7 @@ const mouseDrag = defineTabTool({
|
|
|
75
75
|
endX: z.number().describe('End X coordinate'),
|
|
76
76
|
endY: z.number().describe('End Y coordinate'),
|
|
77
77
|
}),
|
|
78
|
-
type: '
|
|
78
|
+
type: 'input',
|
|
79
79
|
},
|
|
80
80
|
handle: async (tab, params, response) => {
|
|
81
81
|
response.setIncludeSnapshot();
|
package/lib/tools/navigate.js
CHANGED
package/lib/tools/repl.js
CHANGED
|
@@ -220,7 +220,7 @@ const repl = defineTabTool({
|
|
|
220
220
|
title: 'Browser REPL',
|
|
221
221
|
description: 'DevTools-like browser REPL. Per-tab state persists across calls (const/let/functions); supports top-level await. Survives SPA nav; resets on full reload or when switching tabs. Helpers available via window.mcp: JSON5, JSONPath, _, GraphQLClient, gql, graphqlRequest. No need for wrapping in IIFE.',
|
|
222
222
|
inputSchema: replSchema,
|
|
223
|
-
type: '
|
|
223
|
+
type: 'action',
|
|
224
224
|
},
|
|
225
225
|
handle: async (tab, params, response) => {
|
|
226
226
|
const page = tab.page;
|
package/lib/tools/scroll.js
CHANGED
|
@@ -56,7 +56,7 @@ const scrollWheel = defineTabTool({
|
|
|
56
56
|
title: 'Scroll page',
|
|
57
57
|
description: 'Scroll the page using mouse wheel with human-like behavior',
|
|
58
58
|
inputSchema: scrollSchema,
|
|
59
|
-
type: '
|
|
59
|
+
type: 'readOnly',
|
|
60
60
|
},
|
|
61
61
|
handle: async (tab, params, response) => {
|
|
62
62
|
const requestedDeltaY = params.amount || 0;
|
package/lib/tools/snapshot.js
CHANGED
|
@@ -28,7 +28,7 @@ const snapshot = defineTool({
|
|
|
28
28
|
},
|
|
29
29
|
handle: async (context, params, response) => {
|
|
30
30
|
await context.ensureTab();
|
|
31
|
-
response.setIncludeSnapshot();
|
|
31
|
+
response.setIncludeSnapshot('full');
|
|
32
32
|
},
|
|
33
33
|
});
|
|
34
34
|
export const elementSchema = z.object({
|
|
@@ -38,6 +38,7 @@ export const elementSchema = z.object({
|
|
|
38
38
|
const clickSchema = elementSchema.extend({
|
|
39
39
|
doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
|
|
40
40
|
button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'),
|
|
41
|
+
modifiers: z.array(z.enum(['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift'])).optional().describe('Modifier keys to press'),
|
|
41
42
|
});
|
|
42
43
|
const click = defineTabTool({
|
|
43
44
|
capability: 'core',
|
|
@@ -46,22 +47,26 @@ const click = defineTabTool({
|
|
|
46
47
|
title: 'Click',
|
|
47
48
|
description: 'Perform click on a web page',
|
|
48
49
|
inputSchema: clickSchema,
|
|
49
|
-
type: '
|
|
50
|
+
type: 'input',
|
|
50
51
|
},
|
|
51
52
|
handle: async (tab, params, response) => {
|
|
52
53
|
response.setIncludeSnapshot();
|
|
53
54
|
const locator = await tab.refLocator(params);
|
|
54
|
-
const
|
|
55
|
-
|
|
55
|
+
const options = {
|
|
56
|
+
button: params.button,
|
|
57
|
+
modifiers: params.modifiers,
|
|
58
|
+
};
|
|
59
|
+
const formatted = javascript.formatObject(options, ' ', 'oneline');
|
|
60
|
+
const optionsAttr = formatted !== '{}' ? formatted : '';
|
|
56
61
|
if (params.doubleClick)
|
|
57
|
-
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${
|
|
62
|
+
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${optionsAttr});`);
|
|
58
63
|
else
|
|
59
|
-
response.addCode(`await page.${await generateLocator(locator)}.click(${
|
|
64
|
+
response.addCode(`await page.${await generateLocator(locator)}.click(${optionsAttr});`);
|
|
60
65
|
await tab.waitForCompletion(async () => {
|
|
61
66
|
if (params.doubleClick)
|
|
62
|
-
await locator.dblclick(
|
|
67
|
+
await locator.dblclick(options);
|
|
63
68
|
else
|
|
64
|
-
await locator.click(
|
|
69
|
+
await locator.click(options);
|
|
65
70
|
});
|
|
66
71
|
},
|
|
67
72
|
});
|
|
@@ -77,7 +82,7 @@ const drag = defineTabTool({
|
|
|
77
82
|
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
|
|
78
83
|
endRef: z.string().describe('Exact target element reference from the page snapshot'),
|
|
79
84
|
}),
|
|
80
|
-
type: '
|
|
85
|
+
type: 'input',
|
|
81
86
|
},
|
|
82
87
|
handle: async (tab, params, response) => {
|
|
83
88
|
response.setIncludeSnapshot();
|
|
@@ -119,7 +124,7 @@ const selectOption = defineTabTool({
|
|
|
119
124
|
title: 'Select option',
|
|
120
125
|
description: 'Select an option in a dropdown',
|
|
121
126
|
inputSchema: selectOptionSchema,
|
|
122
|
-
type: '
|
|
127
|
+
type: 'input',
|
|
123
128
|
},
|
|
124
129
|
handle: async (tab, params, response) => {
|
|
125
130
|
response.setIncludeSnapshot();
|
package/lib/tools/tabs.js
CHANGED
|
@@ -15,73 +15,45 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { defineTool } from './tool.js';
|
|
18
|
-
const
|
|
18
|
+
const browserTabs = defineTool({
|
|
19
19
|
capability: 'core-tabs',
|
|
20
20
|
schema: {
|
|
21
|
-
name: '
|
|
22
|
-
title: '
|
|
23
|
-
description: 'List browser
|
|
24
|
-
inputSchema: z.object({}),
|
|
25
|
-
type: 'readOnly',
|
|
26
|
-
},
|
|
27
|
-
handle: async (context, params, response) => {
|
|
28
|
-
await context.ensureTab();
|
|
29
|
-
response.setIncludeTabs();
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
const selectTab = defineTool({
|
|
33
|
-
capability: 'core-tabs',
|
|
34
|
-
schema: {
|
|
35
|
-
name: 'browser_tab_select',
|
|
36
|
-
title: 'Select a tab',
|
|
37
|
-
description: 'Select a tab by index',
|
|
38
|
-
inputSchema: z.object({
|
|
39
|
-
index: z.number().describe('The index of the tab to select'),
|
|
40
|
-
}),
|
|
41
|
-
type: 'readOnly',
|
|
42
|
-
},
|
|
43
|
-
handle: async (context, params, response) => {
|
|
44
|
-
await context.selectTab(params.index);
|
|
45
|
-
response.setIncludeSnapshot();
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
const newTab = defineTool({
|
|
49
|
-
capability: 'core-tabs',
|
|
50
|
-
schema: {
|
|
51
|
-
name: 'browser_tab_new',
|
|
52
|
-
title: 'Open a new tab',
|
|
53
|
-
description: 'Open a new tab',
|
|
54
|
-
inputSchema: z.object({
|
|
55
|
-
url: z.string().optional().describe('The URL to navigate to in the new tab. If not provided, the new tab will be blank.'),
|
|
56
|
-
}),
|
|
57
|
-
type: 'readOnly',
|
|
58
|
-
},
|
|
59
|
-
handle: async (context, params, response) => {
|
|
60
|
-
const tab = await context.newTab();
|
|
61
|
-
if (params.url)
|
|
62
|
-
await tab.navigate(params.url);
|
|
63
|
-
response.setIncludeSnapshot();
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const closeTab = defineTool({
|
|
67
|
-
capability: 'core-tabs',
|
|
68
|
-
schema: {
|
|
69
|
-
name: 'browser_tab_close',
|
|
70
|
-
title: 'Close a tab',
|
|
71
|
-
description: 'Close a tab',
|
|
21
|
+
name: 'browser_tabs',
|
|
22
|
+
title: 'Manage tabs',
|
|
23
|
+
description: 'List, create, close, or select a browser tab.',
|
|
72
24
|
inputSchema: z.object({
|
|
73
|
-
|
|
25
|
+
action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'),
|
|
26
|
+
index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'),
|
|
74
27
|
}),
|
|
75
|
-
type: '
|
|
28
|
+
type: 'action',
|
|
76
29
|
},
|
|
77
30
|
handle: async (context, params, response) => {
|
|
78
|
-
|
|
79
|
-
|
|
31
|
+
switch (params.action) {
|
|
32
|
+
case 'list': {
|
|
33
|
+
await context.ensureTab();
|
|
34
|
+
response.setIncludeTabs();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
case 'new': {
|
|
38
|
+
await context.newTab();
|
|
39
|
+
response.setIncludeTabs();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
case 'close': {
|
|
43
|
+
await context.closeTab(params.index);
|
|
44
|
+
response.setIncludeSnapshot('full');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
case 'select': {
|
|
48
|
+
if (params.index === undefined)
|
|
49
|
+
throw new Error('Tab index is required');
|
|
50
|
+
await context.selectTab(params.index);
|
|
51
|
+
response.setIncludeSnapshot('full');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
80
55
|
},
|
|
81
56
|
});
|
|
82
57
|
export default [
|
|
83
|
-
|
|
84
|
-
newTab,
|
|
85
|
-
selectTab,
|
|
86
|
-
closeTab,
|
|
58
|
+
browserTabs,
|
|
87
59
|
];
|
package/lib/tools/tool.js
CHANGED
|
@@ -20,7 +20,7 @@ export function defineTabTool(tool) {
|
|
|
20
20
|
return {
|
|
21
21
|
...tool,
|
|
22
22
|
handle: async (context, params, response) => {
|
|
23
|
-
const tab = context.
|
|
23
|
+
const tab = await context.ensureTab();
|
|
24
24
|
const modalStates = tab.modalStates().map(state => state.type);
|
|
25
25
|
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
|
26
26
|
response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
|
package/lib/utils/codegen.js
CHANGED
|
@@ -31,7 +31,7 @@ export function escapeWithQuotes(text, char = '\'') {
|
|
|
31
31
|
export function quote(text) {
|
|
32
32
|
return escapeWithQuotes(text, '\'');
|
|
33
33
|
}
|
|
34
|
-
export function formatObject(value, indent = ' ') {
|
|
34
|
+
export function formatObject(value, indent = ' ', mode = 'multiline') {
|
|
35
35
|
if (typeof value === 'string')
|
|
36
36
|
return quote(value);
|
|
37
37
|
if (Array.isArray(value))
|
|
@@ -43,7 +43,9 @@ export function formatObject(value, indent = ' ') {
|
|
|
43
43
|
const tokens = [];
|
|
44
44
|
for (const key of keys)
|
|
45
45
|
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
46
|
-
|
|
46
|
+
if (mode === 'multiline')
|
|
47
|
+
return `{\n${tokens.join(`,\n${indent}`)}\n}`;
|
|
48
|
+
return `{ ${tokens.join(', ')} }`;
|
|
47
49
|
}
|
|
48
50
|
return String(value);
|
|
49
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordbricks/playwright-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Playwright Tools for MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"lodash": "^4.17.21",
|
|
61
61
|
"mime": "^4.0.7",
|
|
62
62
|
"ms": "^2.1.3",
|
|
63
|
-
"playwright": "1.
|
|
64
|
-
"playwright-core": "1.
|
|
63
|
+
"playwright": "npm:rebrowser-playwright@1.52.0",
|
|
64
|
+
"playwright-core": "npm:rebrowser-playwright@1.52.0",
|
|
65
65
|
"raw-body": "^3.0.0",
|
|
66
66
|
"typescript-parsec": "0.3.4",
|
|
67
67
|
"ws": "^8.18.1",
|
|
@@ -70,9 +70,10 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@anthropic-ai/sdk": "^0.57.0",
|
|
73
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
73
74
|
"@eslint/eslintrc": "^3.2.0",
|
|
74
75
|
"@eslint/js": "^9.19.0",
|
|
75
|
-
"@playwright/test": "1.
|
|
76
|
+
"@playwright/test": "1.52.0",
|
|
76
77
|
"@stylistic/eslint-plugin": "^3.0.1",
|
|
77
78
|
"@tomjs/unzip-crx": "1.1.3",
|
|
78
79
|
"@types/chrome": "^0.0.315",
|
package/lib/loop/loop.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import debug from 'debug';
|
|
17
|
-
export async function runTask(delegate, client, task, oneShot = false) {
|
|
18
|
-
const { tools } = await client.listTools();
|
|
19
|
-
const taskContent = oneShot ? `Perform following task: ${task}.` : `Perform following task: ${task}. Once the task is complete, call the "done" tool.`;
|
|
20
|
-
const conversation = delegate.createConversation(taskContent, tools, oneShot);
|
|
21
|
-
for (let iteration = 0; iteration < 5; ++iteration) {
|
|
22
|
-
debug('history')('Making API call for iteration', iteration);
|
|
23
|
-
const toolCalls = await delegate.makeApiCall(conversation);
|
|
24
|
-
if (toolCalls.length === 0)
|
|
25
|
-
throw new Error('Call the "done" tool when the task is complete.');
|
|
26
|
-
const toolResults = [];
|
|
27
|
-
for (const toolCall of toolCalls) {
|
|
28
|
-
const doneResult = delegate.checkDoneToolCall(toolCall);
|
|
29
|
-
if (doneResult !== null)
|
|
30
|
-
return conversation.messages;
|
|
31
|
-
const { name, arguments: args, id } = toolCall;
|
|
32
|
-
try {
|
|
33
|
-
debug('tool')(name, args);
|
|
34
|
-
const response = await client.callTool({
|
|
35
|
-
name,
|
|
36
|
-
arguments: args,
|
|
37
|
-
});
|
|
38
|
-
const responseContent = (response.content || []);
|
|
39
|
-
debug('tool')(responseContent);
|
|
40
|
-
const text = responseContent.filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
41
|
-
toolResults.push({
|
|
42
|
-
toolCallId: id,
|
|
43
|
-
content: text,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
debug('tool')(error);
|
|
48
|
-
toolResults.push({
|
|
49
|
-
toolCallId: id,
|
|
50
|
-
content: `Error while executing tool "${name}": ${error instanceof Error ? error.message : String(error)}\n\nPlease try to recover and complete the task.`,
|
|
51
|
-
isError: true,
|
|
52
|
-
});
|
|
53
|
-
// Skip remaining tool calls for this iteration
|
|
54
|
-
for (const remainingToolCall of toolCalls.slice(toolCalls.indexOf(toolCall) + 1)) {
|
|
55
|
-
toolResults.push({
|
|
56
|
-
toolCallId: remainingToolCall.id,
|
|
57
|
-
content: `This tool call is skipped due to previous error.`,
|
|
58
|
-
isError: true,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
delegate.addToolResults(conversation, toolResults);
|
|
65
|
-
if (oneShot)
|
|
66
|
-
return conversation.messages;
|
|
67
|
-
}
|
|
68
|
-
throw new Error('Failed to perform step, max attempts reached');
|
|
69
|
-
}
|
package/lib/loop/loopClaude.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
const model = 'claude-sonnet-4-20250514';
|
|
17
|
-
export class ClaudeDelegate {
|
|
18
|
-
_anthropic;
|
|
19
|
-
async anthropic() {
|
|
20
|
-
if (!this._anthropic) {
|
|
21
|
-
const anthropic = await import('@anthropic-ai/sdk');
|
|
22
|
-
this._anthropic = new anthropic.Anthropic();
|
|
23
|
-
}
|
|
24
|
-
return this._anthropic;
|
|
25
|
-
}
|
|
26
|
-
createConversation(task, tools, oneShot) {
|
|
27
|
-
const llmTools = tools.map(tool => ({
|
|
28
|
-
name: tool.name,
|
|
29
|
-
description: tool.description || '',
|
|
30
|
-
inputSchema: tool.inputSchema,
|
|
31
|
-
}));
|
|
32
|
-
if (!oneShot) {
|
|
33
|
-
llmTools.push({
|
|
34
|
-
name: 'done',
|
|
35
|
-
description: 'Call this tool when the task is complete.',
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
properties: {},
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
messages: [{
|
|
44
|
-
role: 'user',
|
|
45
|
-
content: task
|
|
46
|
-
}],
|
|
47
|
-
tools: llmTools,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async makeApiCall(conversation) {
|
|
51
|
-
// Convert generic messages to Claude format
|
|
52
|
-
const claudeMessages = [];
|
|
53
|
-
for (const message of conversation.messages) {
|
|
54
|
-
if (message.role === 'user') {
|
|
55
|
-
claudeMessages.push({
|
|
56
|
-
role: 'user',
|
|
57
|
-
content: message.content
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else if (message.role === 'assistant') {
|
|
61
|
-
const content = [];
|
|
62
|
-
// Add text content
|
|
63
|
-
if (message.content) {
|
|
64
|
-
content.push({
|
|
65
|
-
type: 'text',
|
|
66
|
-
text: message.content,
|
|
67
|
-
citations: []
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
// Add tool calls
|
|
71
|
-
if (message.toolCalls) {
|
|
72
|
-
for (const toolCall of message.toolCalls) {
|
|
73
|
-
content.push({
|
|
74
|
-
type: 'tool_use',
|
|
75
|
-
id: toolCall.id,
|
|
76
|
-
name: toolCall.name,
|
|
77
|
-
input: toolCall.arguments
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
claudeMessages.push({
|
|
82
|
-
role: 'assistant',
|
|
83
|
-
content
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else if (message.role === 'tool') {
|
|
87
|
-
// Tool results are added differently - we need to find if there's already a user message with tool results
|
|
88
|
-
const lastMessage = claudeMessages[claudeMessages.length - 1];
|
|
89
|
-
const toolResult = {
|
|
90
|
-
type: 'tool_result',
|
|
91
|
-
tool_use_id: message.toolCallId,
|
|
92
|
-
content: message.content,
|
|
93
|
-
is_error: message.isError,
|
|
94
|
-
};
|
|
95
|
-
if (lastMessage && lastMessage.role === 'user' && Array.isArray(lastMessage.content)) {
|
|
96
|
-
// Add to existing tool results message
|
|
97
|
-
lastMessage.content.push(toolResult);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// Create new tool results message
|
|
101
|
-
claudeMessages.push({
|
|
102
|
-
role: 'user',
|
|
103
|
-
content: [toolResult]
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Convert generic tools to Claude format
|
|
109
|
-
const claudeTools = conversation.tools.map(tool => ({
|
|
110
|
-
name: tool.name,
|
|
111
|
-
description: tool.description,
|
|
112
|
-
input_schema: tool.inputSchema,
|
|
113
|
-
}));
|
|
114
|
-
const anthropic = await this.anthropic();
|
|
115
|
-
const response = await anthropic.messages.create({
|
|
116
|
-
model,
|
|
117
|
-
max_tokens: 10000,
|
|
118
|
-
messages: claudeMessages,
|
|
119
|
-
tools: claudeTools,
|
|
120
|
-
});
|
|
121
|
-
// Extract tool calls and add assistant message to generic conversation
|
|
122
|
-
const toolCalls = response.content.filter(block => block.type === 'tool_use');
|
|
123
|
-
const textContent = response.content.filter(block => block.type === 'text').map(block => block.text).join('');
|
|
124
|
-
const llmToolCalls = toolCalls.map(toolCall => ({
|
|
125
|
-
name: toolCall.name,
|
|
126
|
-
arguments: toolCall.input,
|
|
127
|
-
id: toolCall.id,
|
|
128
|
-
}));
|
|
129
|
-
// Add assistant message to generic conversation
|
|
130
|
-
conversation.messages.push({
|
|
131
|
-
role: 'assistant',
|
|
132
|
-
content: textContent,
|
|
133
|
-
toolCalls: llmToolCalls.length > 0 ? llmToolCalls : undefined
|
|
134
|
-
});
|
|
135
|
-
return llmToolCalls;
|
|
136
|
-
}
|
|
137
|
-
addToolResults(conversation, results) {
|
|
138
|
-
for (const result of results) {
|
|
139
|
-
conversation.messages.push({
|
|
140
|
-
role: 'tool',
|
|
141
|
-
toolCallId: result.toolCallId,
|
|
142
|
-
content: result.content,
|
|
143
|
-
isError: result.isError,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
checkDoneToolCall(toolCall) {
|
|
148
|
-
if (toolCall.name === 'done')
|
|
149
|
-
return toolCall.arguments.result;
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
package/lib/loop/loopOpenAI.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
const model = 'gpt-4.1';
|
|
17
|
-
export class OpenAIDelegate {
|
|
18
|
-
_openai;
|
|
19
|
-
async openai() {
|
|
20
|
-
if (!this._openai) {
|
|
21
|
-
const oai = await import('openai');
|
|
22
|
-
this._openai = new oai.OpenAI();
|
|
23
|
-
}
|
|
24
|
-
return this._openai;
|
|
25
|
-
}
|
|
26
|
-
createConversation(task, tools, oneShot) {
|
|
27
|
-
const genericTools = tools.map(tool => ({
|
|
28
|
-
name: tool.name,
|
|
29
|
-
description: tool.description || '',
|
|
30
|
-
inputSchema: tool.inputSchema,
|
|
31
|
-
}));
|
|
32
|
-
if (!oneShot) {
|
|
33
|
-
genericTools.push({
|
|
34
|
-
name: 'done',
|
|
35
|
-
description: 'Call this tool when the task is complete.',
|
|
36
|
-
inputSchema: {
|
|
37
|
-
type: 'object',
|
|
38
|
-
properties: {},
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
messages: [{
|
|
44
|
-
role: 'user',
|
|
45
|
-
content: task
|
|
46
|
-
}],
|
|
47
|
-
tools: genericTools,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
async makeApiCall(conversation) {
|
|
51
|
-
// Convert generic messages to OpenAI format
|
|
52
|
-
const openaiMessages = [];
|
|
53
|
-
for (const message of conversation.messages) {
|
|
54
|
-
if (message.role === 'user') {
|
|
55
|
-
openaiMessages.push({
|
|
56
|
-
role: 'user',
|
|
57
|
-
content: message.content
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else if (message.role === 'assistant') {
|
|
61
|
-
const toolCalls = [];
|
|
62
|
-
if (message.toolCalls) {
|
|
63
|
-
for (const toolCall of message.toolCalls) {
|
|
64
|
-
toolCalls.push({
|
|
65
|
-
id: toolCall.id,
|
|
66
|
-
type: 'function',
|
|
67
|
-
function: {
|
|
68
|
-
name: toolCall.name,
|
|
69
|
-
arguments: JSON.stringify(toolCall.arguments)
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const assistantMessage = {
|
|
75
|
-
role: 'assistant'
|
|
76
|
-
};
|
|
77
|
-
if (message.content)
|
|
78
|
-
assistantMessage.content = message.content;
|
|
79
|
-
if (toolCalls.length > 0)
|
|
80
|
-
assistantMessage.tool_calls = toolCalls;
|
|
81
|
-
openaiMessages.push(assistantMessage);
|
|
82
|
-
}
|
|
83
|
-
else if (message.role === 'tool') {
|
|
84
|
-
openaiMessages.push({
|
|
85
|
-
role: 'tool',
|
|
86
|
-
tool_call_id: message.toolCallId,
|
|
87
|
-
content: message.content,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Convert generic tools to OpenAI format
|
|
92
|
-
const openaiTools = conversation.tools.map(tool => ({
|
|
93
|
-
type: 'function',
|
|
94
|
-
function: {
|
|
95
|
-
name: tool.name,
|
|
96
|
-
description: tool.description,
|
|
97
|
-
parameters: tool.inputSchema,
|
|
98
|
-
},
|
|
99
|
-
}));
|
|
100
|
-
const openai = await this.openai();
|
|
101
|
-
const response = await openai.chat.completions.create({
|
|
102
|
-
model,
|
|
103
|
-
messages: openaiMessages,
|
|
104
|
-
tools: openaiTools,
|
|
105
|
-
tool_choice: 'auto'
|
|
106
|
-
});
|
|
107
|
-
const message = response.choices[0].message;
|
|
108
|
-
// Extract tool calls and add assistant message to generic conversation
|
|
109
|
-
const toolCalls = message.tool_calls || [];
|
|
110
|
-
const genericToolCalls = toolCalls.map(toolCall => {
|
|
111
|
-
const functionCall = toolCall.function;
|
|
112
|
-
return {
|
|
113
|
-
name: functionCall.name,
|
|
114
|
-
arguments: JSON.parse(functionCall.arguments),
|
|
115
|
-
id: toolCall.id,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
// Add assistant message to generic conversation
|
|
119
|
-
conversation.messages.push({
|
|
120
|
-
role: 'assistant',
|
|
121
|
-
content: message.content || '',
|
|
122
|
-
toolCalls: genericToolCalls.length > 0 ? genericToolCalls : undefined
|
|
123
|
-
});
|
|
124
|
-
return genericToolCalls;
|
|
125
|
-
}
|
|
126
|
-
addToolResults(conversation, results) {
|
|
127
|
-
for (const result of results) {
|
|
128
|
-
conversation.messages.push({
|
|
129
|
-
role: 'tool',
|
|
130
|
-
toolCallId: result.toolCallId,
|
|
131
|
-
content: result.content,
|
|
132
|
-
isError: result.isError,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
checkDoneToolCall(toolCall) {
|
|
137
|
-
if (toolCall.name === 'done')
|
|
138
|
-
return toolCall.arguments.result;
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
package/lib/loop/main.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
/* eslint-disable no-console */
|
|
17
|
-
import path from 'path';
|
|
18
|
-
import url from 'url';
|
|
19
|
-
import dotenv from 'dotenv';
|
|
20
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
21
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
22
|
-
import { program } from 'commander';
|
|
23
|
-
import { OpenAIDelegate } from './loopOpenAI.js';
|
|
24
|
-
import { ClaudeDelegate } from './loopClaude.js';
|
|
25
|
-
import { runTask } from './loop.js';
|
|
26
|
-
dotenv.config();
|
|
27
|
-
const __filename = url.fileURLToPath(import.meta.url);
|
|
28
|
-
async function run(delegate) {
|
|
29
|
-
const transport = new StdioClientTransport({
|
|
30
|
-
command: 'node',
|
|
31
|
-
args: [
|
|
32
|
-
path.resolve(__filename, '../../../cli.js'),
|
|
33
|
-
'--save-session',
|
|
34
|
-
'--output-dir', path.resolve(__filename, '../../../sessions')
|
|
35
|
-
],
|
|
36
|
-
stderr: 'inherit',
|
|
37
|
-
env: process.env,
|
|
38
|
-
});
|
|
39
|
-
const client = new Client({ name: 'test', version: '1.0.0' });
|
|
40
|
-
await client.connect(transport);
|
|
41
|
-
await client.ping();
|
|
42
|
-
for (const task of tasks) {
|
|
43
|
-
const messages = await runTask(delegate, client, task);
|
|
44
|
-
for (const message of messages)
|
|
45
|
-
console.log(`${message.role}: ${message.content}`);
|
|
46
|
-
}
|
|
47
|
-
await client.close();
|
|
48
|
-
}
|
|
49
|
-
const tasks = [
|
|
50
|
-
'Open https://playwright.dev/',
|
|
51
|
-
];
|
|
52
|
-
program
|
|
53
|
-
.option('--model <model>', 'model to use')
|
|
54
|
-
.action(async (options) => {
|
|
55
|
-
if (options.model === 'claude')
|
|
56
|
-
await run(new ClaudeDelegate());
|
|
57
|
-
else
|
|
58
|
-
await run(new OpenAIDelegate());
|
|
59
|
-
});
|
|
60
|
-
void program.parseAsync(process.argv);
|
package/lib/loopTools/context.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
17
|
-
import { contextFactory } from '../browserContextFactory.js';
|
|
18
|
-
import { BrowserServerBackend } from '../browserServerBackend.js';
|
|
19
|
-
import { Context as BrowserContext } from '../context.js';
|
|
20
|
-
import { runTask } from '../loop/loop.js';
|
|
21
|
-
import { OpenAIDelegate } from '../loop/loopOpenAI.js';
|
|
22
|
-
import { ClaudeDelegate } from '../loop/loopClaude.js';
|
|
23
|
-
import { InProcessTransport } from '../mcp/inProcessTransport.js';
|
|
24
|
-
import * as mcpServer from '../mcp/server.js';
|
|
25
|
-
export class Context {
|
|
26
|
-
config;
|
|
27
|
-
_client;
|
|
28
|
-
_delegate;
|
|
29
|
-
constructor(config, client) {
|
|
30
|
-
this.config = config;
|
|
31
|
-
this._client = client;
|
|
32
|
-
if (process.env.OPENAI_API_KEY)
|
|
33
|
-
this._delegate = new OpenAIDelegate();
|
|
34
|
-
else if (process.env.ANTHROPIC_API_KEY)
|
|
35
|
-
this._delegate = new ClaudeDelegate();
|
|
36
|
-
else
|
|
37
|
-
throw new Error('No LLM API key found. Please set OPENAI_API_KEY or ANTHROPIC_API_KEY environment variable.');
|
|
38
|
-
}
|
|
39
|
-
static async create(config) {
|
|
40
|
-
const client = new Client({ name: 'Playwright Proxy', version: '1.0.0' });
|
|
41
|
-
const browserContextFactory = contextFactory(config);
|
|
42
|
-
const server = mcpServer.createServer(new BrowserServerBackend(config, browserContextFactory), false);
|
|
43
|
-
await client.connect(new InProcessTransport(server));
|
|
44
|
-
await client.ping();
|
|
45
|
-
return new Context(config, client);
|
|
46
|
-
}
|
|
47
|
-
async runTask(task, oneShot = false) {
|
|
48
|
-
const messages = await runTask(this._delegate, this._client, task, oneShot);
|
|
49
|
-
const lines = [];
|
|
50
|
-
// Skip the first message, which is the user's task.
|
|
51
|
-
for (const message of messages.slice(1)) {
|
|
52
|
-
// Trim out all page snapshots.
|
|
53
|
-
if (!message.content.trim())
|
|
54
|
-
continue;
|
|
55
|
-
const index = oneShot ? -1 : message.content.indexOf('### Page state');
|
|
56
|
-
const trimmedContent = index === -1 ? message.content : message.content.substring(0, index);
|
|
57
|
-
lines.push(`[${message.role}]:`, trimmedContent);
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
async close() {
|
|
64
|
-
await BrowserContext.disposeAll();
|
|
65
|
-
}
|
|
66
|
-
}
|
package/lib/loopTools/main.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import dotenv from 'dotenv';
|
|
17
|
-
import * as mcpTransport from '../mcp/transport.js';
|
|
18
|
-
import { packageJSON } from '../utils/package.js';
|
|
19
|
-
import { Context } from './context.js';
|
|
20
|
-
import { perform } from './perform.js';
|
|
21
|
-
import { snapshot } from './snapshot.js';
|
|
22
|
-
import { toMcpTool } from '../mcp/tool.js';
|
|
23
|
-
export async function runLoopTools(config) {
|
|
24
|
-
dotenv.config();
|
|
25
|
-
const serverBackendFactory = () => new LoopToolsServerBackend(config);
|
|
26
|
-
await mcpTransport.start(serverBackendFactory, config.server);
|
|
27
|
-
}
|
|
28
|
-
class LoopToolsServerBackend {
|
|
29
|
-
name = 'Playwright';
|
|
30
|
-
version = packageJSON.version;
|
|
31
|
-
_config;
|
|
32
|
-
_context;
|
|
33
|
-
_tools = [perform, snapshot];
|
|
34
|
-
constructor(config) {
|
|
35
|
-
this._config = config;
|
|
36
|
-
}
|
|
37
|
-
async initialize() {
|
|
38
|
-
this._context = await Context.create(this._config);
|
|
39
|
-
}
|
|
40
|
-
async listTools() {
|
|
41
|
-
return this._tools.map(tool => toMcpTool(tool.schema));
|
|
42
|
-
}
|
|
43
|
-
async callTool(name, args) {
|
|
44
|
-
const tool = this._tools.find(tool => tool.schema.name === name);
|
|
45
|
-
const parsedArguments = tool.schema.inputSchema.parse(args || {});
|
|
46
|
-
return await tool.handle(this._context, parsedArguments);
|
|
47
|
-
}
|
|
48
|
-
serverClosed() {
|
|
49
|
-
void this._context.close();
|
|
50
|
-
}
|
|
51
|
-
}
|
package/lib/loopTools/perform.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { z } from 'zod';
|
|
17
|
-
import { defineTool } from './tool.js';
|
|
18
|
-
const performSchema = z.object({
|
|
19
|
-
task: z.string().describe('The task to perform with the browser'),
|
|
20
|
-
});
|
|
21
|
-
export const perform = defineTool({
|
|
22
|
-
schema: {
|
|
23
|
-
name: 'browser_perform',
|
|
24
|
-
title: 'Perform a task with the browser',
|
|
25
|
-
description: 'Perform a task with the browser. It can click, type, export, capture screenshot, drag, hover, select options, etc.',
|
|
26
|
-
inputSchema: performSchema,
|
|
27
|
-
type: 'destructive',
|
|
28
|
-
},
|
|
29
|
-
handle: async (context, params) => {
|
|
30
|
-
return await context.runTask(params.task);
|
|
31
|
-
},
|
|
32
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { z } from 'zod';
|
|
17
|
-
import { defineTool } from './tool.js';
|
|
18
|
-
export const snapshot = defineTool({
|
|
19
|
-
schema: {
|
|
20
|
-
name: 'browser_snapshot',
|
|
21
|
-
title: 'Take a snapshot of the browser',
|
|
22
|
-
description: 'Take a snapshot of the browser to read what is on the page.',
|
|
23
|
-
inputSchema: z.object({}),
|
|
24
|
-
type: 'readOnly',
|
|
25
|
-
},
|
|
26
|
-
handle: async (context, params) => {
|
|
27
|
-
return await context.runTask('Capture browser snapshot', true);
|
|
28
|
-
},
|
|
29
|
-
});
|
package/lib/loopTools/tool.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
export function defineTool(tool) {
|
|
17
|
-
return tool;
|
|
18
|
-
}
|