@ejazullah/browser-mcp 0.0.56
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/LICENSE +202 -0
- package/README.md +860 -0
- package/cli.js +19 -0
- package/index.d.ts +23 -0
- package/index.js +1061 -0
- package/lib/auth.js +82 -0
- package/lib/browserContextFactory.js +205 -0
- package/lib/browserServerBackend.js +125 -0
- package/lib/config.js +266 -0
- package/lib/context.js +232 -0
- package/lib/databaseLogger.js +264 -0
- package/lib/extension/cdpRelay.js +346 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/extension/main.js +26 -0
- package/lib/fileUtils.js +32 -0
- package/lib/httpServer.js +39 -0
- package/lib/index.js +39 -0
- package/lib/javascript.js +49 -0
- package/lib/log.js +21 -0
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +143 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +66 -0
- package/lib/loopTools/main.js +49 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/manualPromise.js +111 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/server.js +93 -0
- package/lib/mcp/transport.js +217 -0
- package/lib/mongoDBLogger.js +252 -0
- package/lib/package.js +20 -0
- package/lib/program.js +113 -0
- package/lib/response.js +172 -0
- package/lib/sessionLog.js +156 -0
- package/lib/tab.js +266 -0
- package/lib/tools/cdp.js +169 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +47 -0
- package/lib/tools/evaluate.js +53 -0
- package/lib/tools/extraction.js +217 -0
- package/lib/tools/files.js +44 -0
- package/lib/tools/forms.js +180 -0
- package/lib/tools/getext.js +99 -0
- package/lib/tools/install.js +53 -0
- package/lib/tools/interactions.js +191 -0
- package/lib/tools/keyboard.js +86 -0
- package/lib/tools/mouse.js +99 -0
- package/lib/tools/navigate.js +70 -0
- package/lib/tools/network.js +41 -0
- package/lib/tools/pdf.js +40 -0
- package/lib/tools/screenshot.js +75 -0
- package/lib/tools/selectors.js +233 -0
- package/lib/tools/snapshot.js +169 -0
- package/lib/tools/states.js +147 -0
- package/lib/tools/tabs.js +87 -0
- package/lib/tools/tool.js +33 -0
- package/lib/tools/utils.js +74 -0
- package/lib/tools/wait.js +56 -0
- package/lib/tools.js +64 -0
- package/lib/utils.js +26 -0
- package/openapi.json +683 -0
- package/package.json +92 -0
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
// Schema for CSS selector based operations
|
|
19
|
+
const cssSelectorSchema = z.object({
|
|
20
|
+
selector: z.string().describe('CSS selector to target the element (e.g., "#id", ".class", "button", "[data-test=\'value\']")'),
|
|
21
|
+
attribute: z.string().optional().describe('Optional attribute to get instead of text content (e.g., "href", "src", "value")'),
|
|
22
|
+
index: z.number().optional().describe('Index of element if multiple elements match (0-based)'),
|
|
23
|
+
});
|
|
24
|
+
// Schema for XPath selector based operations
|
|
25
|
+
const xpathSelectorSchema = z.object({
|
|
26
|
+
xpath: z.string().describe('XPath expression to target the element (e.g., "//button[@id=\'submit\']", "//div[contains(@class, \'content\')]")'),
|
|
27
|
+
attribute: z.string().optional().describe('Optional attribute to get instead of text content (e.g., "href", "src", "value")'),
|
|
28
|
+
});
|
|
29
|
+
// Schema for text-based locators
|
|
30
|
+
const textSelectorSchema = z.object({
|
|
31
|
+
text: z.string().describe('Text content to search for in elements'),
|
|
32
|
+
elementType: z.string().optional().describe('Element type to filter by (e.g., "button", "a", "div")'),
|
|
33
|
+
exact: z.boolean().optional().default(false).describe('Whether to match text exactly or use contains'),
|
|
34
|
+
attribute: z.string().optional().describe('Optional attribute to get instead of text content'),
|
|
35
|
+
});
|
|
36
|
+
// Schema for role-based locators (accessibility)
|
|
37
|
+
const roleSelectorSchema = z.object({
|
|
38
|
+
role: z.string().describe('ARIA role of the element (e.g., "button", "link", "textbox", "heading")'),
|
|
39
|
+
name: z.string().optional().describe('Accessible name of the element'),
|
|
40
|
+
level: z.number().optional().describe('Level for headings (1-6)'),
|
|
41
|
+
attribute: z.string().optional().describe('Optional attribute to get instead of text content'),
|
|
42
|
+
});
|
|
43
|
+
const getTextByCSS = defineTabTool({
|
|
44
|
+
capability: 'core',
|
|
45
|
+
schema: {
|
|
46
|
+
name: 'browser_get_text_by_css',
|
|
47
|
+
title: 'Get text by CSS selector',
|
|
48
|
+
description: 'Get text content or attribute from element using CSS selector',
|
|
49
|
+
inputSchema: cssSelectorSchema,
|
|
50
|
+
type: 'readOnly',
|
|
51
|
+
},
|
|
52
|
+
handle: async (tab, params, response) => {
|
|
53
|
+
try {
|
|
54
|
+
let locator = tab.page.locator(params.selector);
|
|
55
|
+
// If index is specified and multiple elements might match
|
|
56
|
+
if (params.index !== undefined) {
|
|
57
|
+
locator = locator.nth(params.index);
|
|
58
|
+
}
|
|
59
|
+
let result;
|
|
60
|
+
if (params.attribute) {
|
|
61
|
+
result = await locator.getAttribute(params.attribute) || '';
|
|
62
|
+
response.addCode(`const attributeValue = await page.locator('${params.selector}')${params.index !== undefined ? `.nth(${params.index})` : ''}.getAttribute('${params.attribute}');`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
result = await locator.textContent() || '';
|
|
66
|
+
response.addCode(`const textContent = await page.locator('${params.selector}')${params.index !== undefined ? `.nth(${params.index})` : ''}.textContent();`);
|
|
67
|
+
}
|
|
68
|
+
response.addResult(`${params.attribute ? `Attribute "${params.attribute}"` : 'Text content'}: ${result}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
response.addError(`Failed to get ${params.attribute ? `attribute "${params.attribute}"` : 'text content'} using CSS selector "${params.selector}": ${error}`);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const getTextByXPath = defineTabTool({
|
|
76
|
+
capability: 'core',
|
|
77
|
+
schema: {
|
|
78
|
+
name: 'browser_get_text_by_xpath',
|
|
79
|
+
title: 'Get text by XPath',
|
|
80
|
+
description: 'Get text content or attribute from element using XPath expression',
|
|
81
|
+
inputSchema: xpathSelectorSchema,
|
|
82
|
+
type: 'readOnly',
|
|
83
|
+
},
|
|
84
|
+
handle: async (tab, params, response) => {
|
|
85
|
+
try {
|
|
86
|
+
const locator = tab.page.locator(`xpath=${params.xpath}`);
|
|
87
|
+
let result;
|
|
88
|
+
if (params.attribute) {
|
|
89
|
+
result = await locator.getAttribute(params.attribute) || '';
|
|
90
|
+
response.addCode(`const attributeValue = await page.locator('xpath=${params.xpath}').getAttribute('${params.attribute}');`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result = await locator.textContent() || '';
|
|
94
|
+
response.addCode(`const textContent = await page.locator('xpath=${params.xpath}').textContent();`);
|
|
95
|
+
}
|
|
96
|
+
response.addResult(`${params.attribute ? `Attribute "${params.attribute}"` : 'Text content'}: ${result}`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
response.addError(`Failed to get ${params.attribute ? `attribute "${params.attribute}"` : 'text content'} using XPath "${params.xpath}": ${error}`);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const getTextByText = defineTabTool({
|
|
104
|
+
capability: 'core',
|
|
105
|
+
schema: {
|
|
106
|
+
name: 'browser_get_text_by_text',
|
|
107
|
+
title: 'Get text by text content',
|
|
108
|
+
description: 'Find element by its text content and get text or attribute',
|
|
109
|
+
inputSchema: textSelectorSchema,
|
|
110
|
+
type: 'readOnly',
|
|
111
|
+
},
|
|
112
|
+
handle: async (tab, params, response) => {
|
|
113
|
+
try {
|
|
114
|
+
let locator;
|
|
115
|
+
if (params.elementType) {
|
|
116
|
+
// Filter by element type first
|
|
117
|
+
if (params.exact) {
|
|
118
|
+
locator = tab.page.locator(params.elementType).filter({ hasText: new RegExp(`^${params.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
locator = tab.page.locator(params.elementType).filter({ hasText: params.text });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Search in all elements
|
|
126
|
+
if (params.exact) {
|
|
127
|
+
locator = tab.page.getByText(params.text, { exact: true });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
locator = tab.page.getByText(params.text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let result;
|
|
134
|
+
if (params.attribute) {
|
|
135
|
+
result = await locator.getAttribute(params.attribute) || '';
|
|
136
|
+
response.addCode(`const attributeValue = await page.${params.elementType ? `locator('${params.elementType}').filter({ hasText: '${params.text}' })` : `getByText('${params.text}'${params.exact ? ', { exact: true }' : ''})`}.getAttribute('${params.attribute}');`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
result = await locator.textContent() || '';
|
|
140
|
+
response.addCode(`const textContent = await page.${params.elementType ? `locator('${params.elementType}').filter({ hasText: '${params.text}' })` : `getByText('${params.text}'${params.exact ? ', { exact: true }' : ''})`}.textContent();`);
|
|
141
|
+
}
|
|
142
|
+
response.addResult(`${params.attribute ? `Attribute "${params.attribute}"` : 'Text content'}: ${result}`);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
response.addError(`Failed to get ${params.attribute ? `attribute "${params.attribute}"` : 'text content'} for element with text "${params.text}": ${error}`);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const getTextByRole = defineTabTool({
|
|
150
|
+
capability: 'core',
|
|
151
|
+
schema: {
|
|
152
|
+
name: 'browser_get_text_by_role',
|
|
153
|
+
title: 'Get text by ARIA role',
|
|
154
|
+
description: 'Get text content or attribute from element using ARIA role',
|
|
155
|
+
inputSchema: roleSelectorSchema,
|
|
156
|
+
type: 'readOnly',
|
|
157
|
+
},
|
|
158
|
+
handle: async (tab, params, response) => {
|
|
159
|
+
try {
|
|
160
|
+
let locator;
|
|
161
|
+
if (params.role === 'heading' && params.level) {
|
|
162
|
+
locator = tab.page.getByRole('heading', { level: params.level, name: params.name });
|
|
163
|
+
}
|
|
164
|
+
else if (params.name) {
|
|
165
|
+
locator = tab.page.getByRole(params.role, { name: params.name });
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
locator = tab.page.getByRole(params.role);
|
|
169
|
+
}
|
|
170
|
+
let result;
|
|
171
|
+
if (params.attribute) {
|
|
172
|
+
result = await locator.getAttribute(params.attribute) || '';
|
|
173
|
+
response.addCode(`const attributeValue = await page.getByRole('${params.role}'${params.name ? `, { name: '${params.name}' }` : ''}${params.level ? `, { level: ${params.level} }` : ''}).getAttribute('${params.attribute}');`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
result = await locator.textContent() || '';
|
|
177
|
+
response.addCode(`const textContent = await page.getByRole('${params.role}'${params.name ? `, { name: '${params.name}' }` : ''}${params.level ? `, { level: ${params.level} }` : ''}).textContent();`);
|
|
178
|
+
}
|
|
179
|
+
response.addResult(`${params.attribute ? `Attribute "${params.attribute}"` : 'Text content'}: ${result}`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
response.addError(`Failed to get ${params.attribute ? `attribute "${params.attribute}"` : 'text content'} for ${params.role} element: ${error}`);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
// Tool to get all text from multiple elements
|
|
187
|
+
const getAllText = defineTabTool({
|
|
188
|
+
capability: 'core',
|
|
189
|
+
schema: {
|
|
190
|
+
name: 'browser_get_all_text',
|
|
191
|
+
title: 'Get all text from multiple elements',
|
|
192
|
+
description: 'Get text content from all elements matching a CSS selector',
|
|
193
|
+
inputSchema: z.object({
|
|
194
|
+
selector: z.string().describe('CSS selector to target multiple elements'),
|
|
195
|
+
attribute: z.string().optional().describe('Optional attribute to get instead of text content'),
|
|
196
|
+
limit: z.number().optional().default(10).describe('Maximum number of elements to process'),
|
|
197
|
+
}),
|
|
198
|
+
type: 'readOnly',
|
|
199
|
+
},
|
|
200
|
+
handle: async (tab, params, response) => {
|
|
201
|
+
try {
|
|
202
|
+
const locator = tab.page.locator(params.selector);
|
|
203
|
+
const count = await locator.count();
|
|
204
|
+
const limit = Math.min(count, params.limit || 10);
|
|
205
|
+
const results = [];
|
|
206
|
+
for (let i = 0; i < limit; i++) {
|
|
207
|
+
const elementLocator = locator.nth(i);
|
|
208
|
+
let result;
|
|
209
|
+
if (params.attribute) {
|
|
210
|
+
result = await elementLocator.getAttribute(params.attribute) || '';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
result = await elementLocator.textContent() || '';
|
|
214
|
+
}
|
|
215
|
+
if (result.trim()) {
|
|
216
|
+
results.push(`[${i}] ${result}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
response.addCode(`const elements = await page.locator('${params.selector}').all();\nconst results = await Promise.all(elements.slice(0, ${limit}).map(el => el.${params.attribute ? `getAttribute('${params.attribute}')` : 'textContent()'}));`);
|
|
220
|
+
response.addResult(`Found ${count} elements, showing first ${limit}:\n${results.join('\n')}`);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
response.addError(`Failed to get ${params.attribute ? `attribute "${params.attribute}"` : 'text content'} from elements matching "${params.selector}": ${error}`);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
export default [
|
|
228
|
+
getTextByCSS,
|
|
229
|
+
getTextByXPath,
|
|
230
|
+
getTextByText,
|
|
231
|
+
getTextByRole,
|
|
232
|
+
getAllText,
|
|
233
|
+
];
|
|
@@ -0,0 +1,169 @@
|
|
|
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, defineTool } from './tool.js';
|
|
18
|
+
import * as javascript from '../javascript.js';
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
|
+
const snapshot = defineTool({
|
|
21
|
+
capability: 'core',
|
|
22
|
+
schema: {
|
|
23
|
+
name: 'browser_snapshot',
|
|
24
|
+
title: 'Page snapshot',
|
|
25
|
+
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
|
|
26
|
+
inputSchema: z.object({}),
|
|
27
|
+
type: 'readOnly',
|
|
28
|
+
},
|
|
29
|
+
handle: async (context, params, response) => {
|
|
30
|
+
await context.ensureTab();
|
|
31
|
+
response.setIncludeSnapshot();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
export const elementSchema = z.object({
|
|
35
|
+
element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
|
|
36
|
+
ref: z.string().describe('Exact target element reference from the page snapshot'),
|
|
37
|
+
});
|
|
38
|
+
const clickSchema = elementSchema.extend({
|
|
39
|
+
doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
|
|
40
|
+
button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'),
|
|
41
|
+
});
|
|
42
|
+
const click = defineTabTool({
|
|
43
|
+
capability: 'core',
|
|
44
|
+
schema: {
|
|
45
|
+
name: 'browser_click',
|
|
46
|
+
title: 'Click',
|
|
47
|
+
description: 'Perform click on a web page',
|
|
48
|
+
inputSchema: clickSchema,
|
|
49
|
+
type: 'destructive',
|
|
50
|
+
},
|
|
51
|
+
handle: async (tab, params, response) => {
|
|
52
|
+
response.setIncludeSnapshot();
|
|
53
|
+
// Get locator with selector information for database logging
|
|
54
|
+
const { locator, selector, resolvedSelector } = await tab.refLocatorWithSelector(params);
|
|
55
|
+
// Store element interaction data for database logging
|
|
56
|
+
response.setElementInteraction({
|
|
57
|
+
elementRef: params.ref,
|
|
58
|
+
elementDescription: params.element,
|
|
59
|
+
playwrightSelector: selector,
|
|
60
|
+
resolvedSelector: resolvedSelector,
|
|
61
|
+
});
|
|
62
|
+
const button = params.button;
|
|
63
|
+
const buttonAttr = button ? `{ button: '${button}' }` : '';
|
|
64
|
+
if (params.doubleClick)
|
|
65
|
+
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
|
|
66
|
+
else
|
|
67
|
+
response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
|
|
68
|
+
await tab.waitForCompletion(async () => {
|
|
69
|
+
if (params.doubleClick)
|
|
70
|
+
await locator.dblclick({ button });
|
|
71
|
+
else
|
|
72
|
+
await locator.click({ button });
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const drag = defineTabTool({
|
|
77
|
+
capability: 'core',
|
|
78
|
+
schema: {
|
|
79
|
+
name: 'browser_drag',
|
|
80
|
+
title: 'Drag mouse',
|
|
81
|
+
description: 'Perform drag and drop between two elements',
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
|
|
84
|
+
startRef: z.string().describe('Exact source element reference from the page snapshot'),
|
|
85
|
+
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
|
|
86
|
+
endRef: z.string().describe('Exact target element reference from the page snapshot'),
|
|
87
|
+
}),
|
|
88
|
+
type: 'destructive',
|
|
89
|
+
},
|
|
90
|
+
handle: async (tab, params, response) => {
|
|
91
|
+
response.setIncludeSnapshot();
|
|
92
|
+
// Get locators with selector information for database logging
|
|
93
|
+
const startInfo = await tab.refLocatorWithSelector({ ref: params.startRef, element: params.startElement });
|
|
94
|
+
const endInfo = await tab.refLocatorWithSelector({ ref: params.endRef, element: params.endElement });
|
|
95
|
+
// Store element interaction data for database logging (log start element primarily)
|
|
96
|
+
response.setElementInteraction({
|
|
97
|
+
elementRef: params.startRef,
|
|
98
|
+
elementDescription: `${params.startElement} -> ${params.endElement}`,
|
|
99
|
+
playwrightSelector: startInfo.selector,
|
|
100
|
+
resolvedSelector: startInfo.resolvedSelector,
|
|
101
|
+
});
|
|
102
|
+
await tab.waitForCompletion(async () => {
|
|
103
|
+
await startInfo.locator.dragTo(endInfo.locator);
|
|
104
|
+
});
|
|
105
|
+
response.addCode(`await page.${await generateLocator(startInfo.locator)}.dragTo(page.${await generateLocator(endInfo.locator)});`);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const hover = defineTabTool({
|
|
109
|
+
capability: 'core',
|
|
110
|
+
schema: {
|
|
111
|
+
name: 'browser_hover',
|
|
112
|
+
title: 'Hover mouse',
|
|
113
|
+
description: 'Hover over element on page',
|
|
114
|
+
inputSchema: elementSchema,
|
|
115
|
+
type: 'readOnly',
|
|
116
|
+
},
|
|
117
|
+
handle: async (tab, params, response) => {
|
|
118
|
+
response.setIncludeSnapshot();
|
|
119
|
+
// Get locator with selector information for database logging
|
|
120
|
+
const { locator, selector, resolvedSelector } = await tab.refLocatorWithSelector(params);
|
|
121
|
+
// Store element interaction data for database logging
|
|
122
|
+
response.setElementInteraction({
|
|
123
|
+
elementRef: params.ref,
|
|
124
|
+
elementDescription: params.element,
|
|
125
|
+
playwrightSelector: selector,
|
|
126
|
+
resolvedSelector: resolvedSelector,
|
|
127
|
+
});
|
|
128
|
+
response.addCode(`await page.${await generateLocator(locator)}.hover();`);
|
|
129
|
+
await tab.waitForCompletion(async () => {
|
|
130
|
+
await locator.hover();
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const selectOptionSchema = elementSchema.extend({
|
|
135
|
+
values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
|
|
136
|
+
});
|
|
137
|
+
const selectOption = defineTabTool({
|
|
138
|
+
capability: 'core',
|
|
139
|
+
schema: {
|
|
140
|
+
name: 'browser_select_option',
|
|
141
|
+
title: 'Select option',
|
|
142
|
+
description: 'Select an option in a dropdown',
|
|
143
|
+
inputSchema: selectOptionSchema,
|
|
144
|
+
type: 'destructive',
|
|
145
|
+
},
|
|
146
|
+
handle: async (tab, params, response) => {
|
|
147
|
+
response.setIncludeSnapshot();
|
|
148
|
+
// Get locator with selector information for database logging
|
|
149
|
+
const { locator, selector, resolvedSelector } = await tab.refLocatorWithSelector(params);
|
|
150
|
+
// Store element interaction data for database logging
|
|
151
|
+
response.setElementInteraction({
|
|
152
|
+
elementRef: params.ref,
|
|
153
|
+
elementDescription: params.element,
|
|
154
|
+
playwrightSelector: selector,
|
|
155
|
+
resolvedSelector: resolvedSelector,
|
|
156
|
+
});
|
|
157
|
+
response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);
|
|
158
|
+
await tab.waitForCompletion(async () => {
|
|
159
|
+
await locator.selectOption(params.values);
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
export default [
|
|
164
|
+
snapshot,
|
|
165
|
+
click,
|
|
166
|
+
drag,
|
|
167
|
+
hover,
|
|
168
|
+
selectOption,
|
|
169
|
+
];
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { elementSchema } from './snapshot.js';
|
|
19
|
+
import { generateLocator } from './utils.js';
|
|
20
|
+
// Check if element is visible
|
|
21
|
+
const isVisible = defineTabTool({
|
|
22
|
+
capability: 'core',
|
|
23
|
+
schema: {
|
|
24
|
+
name: 'browser_is_visible',
|
|
25
|
+
title: 'Check if element is visible',
|
|
26
|
+
description: 'Check if an element is visible on the page',
|
|
27
|
+
inputSchema: elementSchema,
|
|
28
|
+
type: 'readOnly',
|
|
29
|
+
},
|
|
30
|
+
handle: async (tab, params, response) => {
|
|
31
|
+
try {
|
|
32
|
+
const locator = await tab.refLocator(params);
|
|
33
|
+
const visible = await locator.isVisible();
|
|
34
|
+
response.addCode(`const isVisible = await page.${await generateLocator(locator)}.isVisible();`);
|
|
35
|
+
response.addResult(`Element is ${visible ? 'visible' : 'not visible'}`);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
response.addError(`Failed to check visibility: ${error}`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
// Check if element is enabled
|
|
43
|
+
const isEnabled = defineTabTool({
|
|
44
|
+
capability: 'core',
|
|
45
|
+
schema: {
|
|
46
|
+
name: 'browser_is_enabled',
|
|
47
|
+
title: 'Check if element is enabled',
|
|
48
|
+
description: 'Check if an element is enabled (not disabled)',
|
|
49
|
+
inputSchema: elementSchema,
|
|
50
|
+
type: 'readOnly',
|
|
51
|
+
},
|
|
52
|
+
handle: async (tab, params, response) => {
|
|
53
|
+
try {
|
|
54
|
+
const locator = await tab.refLocator(params);
|
|
55
|
+
const enabled = await locator.isEnabled();
|
|
56
|
+
response.addCode(`const isEnabled = await page.${await generateLocator(locator)}.isEnabled();`);
|
|
57
|
+
response.addResult(`Element is ${enabled ? 'enabled' : 'disabled'}`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
response.addError(`Failed to check if element is enabled: ${error}`);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// Check if checkbox/radio is checked
|
|
65
|
+
const isChecked = defineTabTool({
|
|
66
|
+
capability: 'core',
|
|
67
|
+
schema: {
|
|
68
|
+
name: 'browser_is_checked',
|
|
69
|
+
title: 'Check if checkbox/radio is selected',
|
|
70
|
+
description: 'Check if a checkbox or radio button is checked/selected',
|
|
71
|
+
inputSchema: elementSchema,
|
|
72
|
+
type: 'readOnly',
|
|
73
|
+
},
|
|
74
|
+
handle: async (tab, params, response) => {
|
|
75
|
+
try {
|
|
76
|
+
const locator = await tab.refLocator(params);
|
|
77
|
+
const checked = await locator.isChecked();
|
|
78
|
+
response.addCode(`const isChecked = await page.${await generateLocator(locator)}.isChecked();`);
|
|
79
|
+
response.addResult(`Element is ${checked ? 'checked' : 'not checked'}`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
response.addError(`Failed to check if element is checked: ${error}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
// Wait for element to appear/disappear
|
|
87
|
+
const waitForElement = defineTabTool({
|
|
88
|
+
capability: 'core',
|
|
89
|
+
schema: {
|
|
90
|
+
name: 'browser_wait_for_element',
|
|
91
|
+
title: 'Wait for element state',
|
|
92
|
+
description: 'Wait for an element to appear, disappear, or change state',
|
|
93
|
+
inputSchema: elementSchema.extend({
|
|
94
|
+
state: z.enum(['visible', 'hidden', 'attached', 'detached']).default('visible').describe('State to wait for'),
|
|
95
|
+
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds'),
|
|
96
|
+
}),
|
|
97
|
+
type: 'readOnly',
|
|
98
|
+
},
|
|
99
|
+
handle: async (tab, params, response) => {
|
|
100
|
+
try {
|
|
101
|
+
const locator = await tab.refLocator(params);
|
|
102
|
+
await locator.waitFor({
|
|
103
|
+
state: params.state,
|
|
104
|
+
timeout: params.timeout
|
|
105
|
+
});
|
|
106
|
+
response.addCode(`await page.${await generateLocator(locator)}.waitFor({ state: '${params.state}', timeout: ${params.timeout} });`);
|
|
107
|
+
response.addResult(`Element is now ${params.state}`);
|
|
108
|
+
if (params.state === 'visible' || params.state === 'attached') {
|
|
109
|
+
response.setIncludeSnapshot();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
response.addError(`Failed to wait for element to be ${params.state}: ${error}`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
// Count matching elements
|
|
118
|
+
const getElementCount = defineTabTool({
|
|
119
|
+
capability: 'core',
|
|
120
|
+
schema: {
|
|
121
|
+
name: 'browser_get_element_count',
|
|
122
|
+
title: 'Count elements',
|
|
123
|
+
description: 'Count the number of elements matching a CSS selector',
|
|
124
|
+
inputSchema: z.object({
|
|
125
|
+
selector: z.string().describe('CSS selector to count elements'),
|
|
126
|
+
}),
|
|
127
|
+
type: 'readOnly',
|
|
128
|
+
},
|
|
129
|
+
handle: async (tab, params, response) => {
|
|
130
|
+
try {
|
|
131
|
+
const locator = tab.page.locator(params.selector);
|
|
132
|
+
const count = await locator.count();
|
|
133
|
+
response.addCode(`const count = await page.locator('${params.selector}').count();`);
|
|
134
|
+
response.addResult(`Found ${count} elements matching "${params.selector}"`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
response.addError(`Failed to count elements: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
export default [
|
|
142
|
+
isVisible,
|
|
143
|
+
isEnabled,
|
|
144
|
+
isChecked,
|
|
145
|
+
waitForElement,
|
|
146
|
+
getElementCount,
|
|
147
|
+
];
|
|
@@ -0,0 +1,87 @@
|
|
|
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 listTabs = defineTool({
|
|
19
|
+
capability: 'core-tabs',
|
|
20
|
+
schema: {
|
|
21
|
+
name: 'browser_tab_list',
|
|
22
|
+
title: 'List tabs',
|
|
23
|
+
description: 'List browser tabs',
|
|
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',
|
|
72
|
+
inputSchema: z.object({
|
|
73
|
+
index: z.number().optional().describe('The index of the tab to close. Closes current tab if not provided.'),
|
|
74
|
+
}),
|
|
75
|
+
type: 'destructive',
|
|
76
|
+
},
|
|
77
|
+
handle: async (context, params, response) => {
|
|
78
|
+
await context.closeTab(params.index);
|
|
79
|
+
response.setIncludeSnapshot();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
export default [
|
|
83
|
+
listTabs,
|
|
84
|
+
newTab,
|
|
85
|
+
selectTab,
|
|
86
|
+
closeTab,
|
|
87
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
19
|
+
export function defineTabTool(tool) {
|
|
20
|
+
return {
|
|
21
|
+
...tool,
|
|
22
|
+
handle: async (context, params, response) => {
|
|
23
|
+
const tab = context.currentTabOrDie();
|
|
24
|
+
const modalStates = tab.modalStates().map(state => state.type);
|
|
25
|
+
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
|
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'));
|
|
27
|
+
else if (!tool.clearsModalState && modalStates.length)
|
|
28
|
+
response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
|
|
29
|
+
else
|
|
30
|
+
return tool.handle(tab, params, response);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|