# Directory Structure
```
├── .gitignore
├── image
│ ├── AvailableMCPTools.png
│ └── UsingMCPServer.png
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── requestHandler.ts
│ ├── tools.ts
│ └── toolsHandler.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | dist
3 |
4 | .DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Browser Automation
2 |
3 | This is demo project to practice Model Context Protocol based server implemenation for automating browsing with Playwright. It interacts with a Claude Desktop client to accept user prompts and use server to control browser.
4 |
5 | <a href="https://glama.ai/mcp/servers/hokppvk1dy"><img width="380" height="200" src="https://glama.ai/mcp/servers/hokppvk1dy/badge" alt="Browser Automation Server MCP server" /></a>
6 |
7 | ## Pre-requisites
8 |
9 | - [Playwright](https://playwright.dev/)
10 | - [Claude Desktop](https://claude.ai/download)
11 | - [Node.js](https://nodejs.org/en/download/)
12 |
13 | ## Building
14 |
15 | 1. Clone the repository: `git clone https://github.com/hrmeetsingh/mcp-browser-automation.git`
16 | 2. Install dependencies: `npm install`
17 | 3. Verify the output executables are present in `dist` folder
18 |
19 | ## Integration
20 |
21 | 1. Create a configuration file in `~/Application\ Support/Claude/claude_desktop_config.json` (This is for macOS)
22 | 2. Copy the following to the file:
23 | ```json
24 | {
25 | "mcpServers": {
26 | "mcp-browser-automation": {
27 | "command": "node",
28 | "args": ["/path/to/mcp-browser-automation/dist/index.js"]
29 | }
30 | }
31 | }
32 | ```
33 | 3. Start Claude Desktop
34 |
35 | ## Usage
36 |
37 | 1. Open Claude Desktop
38 | 2. Start a new conversation to open a browser and navigate to a URL
39 |
40 | ## Example
41 |
42 | - Added MCP Server options
43 | 
44 |
45 | - Navigating to a URL and doing actions with playwright
46 | 
47 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2022",
5 | "moduleResolution": "bundler",
6 | "outDir": "./dist",
7 | "strict": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "declaration": true
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules", "dist"]
14 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import { createToolDefinitions } from "./tools.js";
6 | import { setupRequestHandlers } from "./requestHandler.js";
7 |
8 | async function runServer() {
9 | const server = new Server(
10 | {
11 | name: "executeautomation/mcp-browser-automation",
12 | version: "0.2.6",
13 | },
14 | {
15 | capabilities: {
16 | resources: {},
17 | tools: {},
18 | },
19 | }
20 | );
21 |
22 | // Create tool definitions
23 | const TOOLS = createToolDefinitions();
24 |
25 | // Setup request handlers
26 | setupRequestHandlers(server, TOOLS);
27 |
28 | // Create transport and connect
29 | const transport = new StdioServerTransport();
30 | await server.connect(transport);
31 | }
32 |
33 | runServer().catch(console.error);
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-browser-automation",
3 | "version": "1.0.0",
4 | "description": "This is demo project to practice Model Context Protocol implemenation for automating browsing",
5 | "author": "Harmeet Singh",
6 | "types": "dist/index.d.ts",
7 | "type": "module",
8 | "bin": {
9 | "mcp-browser-automation": "dist/index.js"
10 | },
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "tsc && shx chmod +x dist/*.js",
16 | "prepare": "npm run build",
17 | "watch": "tsc --watch"
18 | },
19 | "dependencies": {
20 | "@modelcontextprotocol/sdk": "1.0.3",
21 | "playwright": "1.49.1",
22 | "@playwright/browser-chromium": "1.49.1"
23 | },
24 | "keywords": ["playwright", "automation", "AI", "Claude", "Model Context Protocol"],
25 | "devDependencies": {
26 | "@types/node": "^20.10.5",
27 | "shx": "^0.3.4",
28 | "typescript": "^5.6.2"
29 | },
30 | "license": "MIT"
31 | }
32 |
```
--------------------------------------------------------------------------------
/src/requestHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import {
3 | ListResourcesRequestSchema,
4 | ReadResourceRequestSchema,
5 | ListToolsRequestSchema,
6 | CallToolRequestSchema,
7 | Tool
8 | } from "@modelcontextprotocol/sdk/types.js";
9 | import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolsHandler.js";
10 |
11 | export function setupRequestHandlers(server: Server, tools: Tool[]) {
12 | // List resources handler
13 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({
14 | resources: [
15 | {
16 | uri: "console://logs",
17 | mimeType: "text/plain",
18 | name: "Browser console logs",
19 | },
20 | ...Array.from(getScreenshots().keys()).map(name => ({
21 | uri: `screenshot://${name}`,
22 | mimeType: "image/png",
23 | name: `Screenshot: ${name}`,
24 | })),
25 | ],
26 | }));
27 |
28 | // Read resource handler
29 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
30 | const uri = request.params.uri.toString();
31 |
32 | if (uri === "console://logs") {
33 | return {
34 | contents: [{
35 | uri,
36 | mimeType: "text/plain",
37 | text: getConsoleLogs().join("\n"),
38 | }],
39 | };
40 | }
41 |
42 | if (uri.startsWith("screenshot://")) {
43 | const name = uri.split("://")[1];
44 | const screenshot = getScreenshots().get(name);
45 | if (screenshot) {
46 | return {
47 | contents: [{
48 | uri,
49 | mimeType: "image/png",
50 | blob: screenshot,
51 | }],
52 | };
53 | }
54 | }
55 |
56 | throw new Error(`Resource not found: ${uri}`);
57 | });
58 |
59 | // List tools handler
60 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
61 | tools: tools,
62 | }));
63 |
64 | // Call tool handler
65 | server.setRequestHandler(CallToolRequestSchema, async (request) =>
66 | handleToolCall(request.params.name, request.params.arguments ?? {}, server)
67 | );
68 | }
```
--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
2 |
3 | export function createToolDefinitions(): Tool[] {
4 | return [
5 | {
6 | name: "playwright_navigate",
7 | description: "Navigate to a URL",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | url: { type: "string" },
12 | },
13 | required: ["url"],
14 | },
15 | },
16 | {
17 | name: "playwright_screenshot",
18 | description: "Take a screenshot of the current page or a specific element",
19 | inputSchema: {
20 | type: "object",
21 | properties: {
22 | name: { type: "string", description: "Name for the screenshot" },
23 | selector: { type: "string", description: "CSS selector for element to screenshot" },
24 | width: { type: "number", description: "Width in pixels (default: 800)" },
25 | height: { type: "number", description: "Height in pixels (default: 600)" },
26 | storeBase64: { type: "boolean", description: "Store screenshot in base64 format (default: true)" },
27 | savePng: { type: "boolean", description: "Save screenshot as PNG file (default: false)" },
28 | downloadsDir: { type: "string", description: "Custom downloads directory path (default: user's Downloads folder)" },
29 | },
30 | required: ["name"],
31 | },
32 | },
33 | {
34 | name: "playwright_click",
35 | description: "Click an element on the page",
36 | inputSchema: {
37 | type: "object",
38 | properties: {
39 | selector: { type: "string", description: "CSS selector for element to click" },
40 | },
41 | required: ["selector"],
42 | },
43 | },
44 | {
45 | name: "playwright_fill",
46 | description: "fill out an input field",
47 | inputSchema: {
48 | type: "object",
49 | properties: {
50 | selector: { type: "string", description: "CSS selector for input field" },
51 | value: { type: "string", description: "Value to fill" },
52 | },
53 | required: ["selector", "value"],
54 | },
55 | },
56 | {
57 | name: "playwright_select",
58 | description: "Select an element on the page with Select tag",
59 | inputSchema: {
60 | type: "object",
61 | properties: {
62 | selector: { type: "string", description: "CSS selector for element to select" },
63 | value: { type: "string", description: "Value to select" },
64 | },
65 | required: ["selector", "value"],
66 | },
67 | },
68 | {
69 | name: "playwright_hover",
70 | description: "Hover an element on the page",
71 | inputSchema: {
72 | type: "object",
73 | properties: {
74 | selector: { type: "string", description: "CSS selector for element to hover" },
75 | },
76 | required: ["selector"],
77 | },
78 | },
79 | {
80 | name: "playwright_evaluate",
81 | description: "Execute JavaScript in the browser console",
82 | inputSchema: {
83 | type: "object",
84 | properties: {
85 | script: { type: "string", description: "JavaScript code to execute" },
86 | },
87 | required: ["script"],
88 | },
89 | },
90 | {
91 | name: "playwright_get",
92 | description: "Perform an HTTP GET request",
93 | inputSchema: {
94 | type: "object",
95 | properties: {
96 | url: { type: "string", description: "URL to perform GET operation" }
97 | },
98 | required: ["url"],
99 | },
100 | },
101 | {
102 | name: "playwright_post",
103 | description: "Perform an HTTP POST request",
104 | inputSchema: {
105 | type: "object",
106 | properties: {
107 | url: { type: "string", description: "URL to perform POST operation" },
108 | value: { type: "string", description: "Data to post in the body" },
109 | },
110 | required: ["url", "value"],
111 | },
112 | },
113 | {
114 | name: "playwright_put",
115 | description: "Perform an HTTP PUT request",
116 | inputSchema: {
117 | type: "object",
118 | properties: {
119 | url: { type: "string", description: "URL to perform PUT operation" },
120 | value: { type: "string", description: "Data to PUT in the body" },
121 | },
122 | required: ["url", "value"],
123 | },
124 | },
125 | {
126 | name: "playwright_patch",
127 | description: "Perform an HTTP PATCH request",
128 | inputSchema: {
129 | type: "object",
130 | properties: {
131 | url: { type: "string", description: "URL to perform PUT operation" },
132 | value: { type: "string", description: "Data to PATCH in the body" },
133 | },
134 | required: ["url", "value"],
135 | },
136 | },
137 | {
138 | name: "playwright_delete",
139 | description: "Perform an HTTP DELETE request",
140 | inputSchema: {
141 | type: "object",
142 | properties: {
143 | url: { type: "string", description: "URL to perform DELETE operation" }
144 | },
145 | required: ["url"],
146 | },
147 | },
148 | ];
149 | }
150 |
151 | // Browser-requiring tools for conditional browser launch
152 | export const BROWSER_TOOLS = [
153 | "playwright_navigate",
154 | "playwright_screenshot",
155 | "playwright_click",
156 | "playwright_fill",
157 | "playwright_select",
158 | "playwright_hover",
159 | "playwright_evaluate"
160 | ];
161 |
162 |
163 | // API Request tools for conditional launch
164 | export const API_TOOLS = [
165 | "playwright_get",
166 | "playwright_post",
167 | "playwright_put",
168 | "playwright_delete",
169 | "playwright_patch"
170 | ];
```
--------------------------------------------------------------------------------
/src/toolsHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { chromium, Browser, Page, request, APIRequest, APIRequestContext } from "playwright";
2 | import { CallToolResult, TextContent, ImageContent } from "@modelcontextprotocol/sdk/types.js";
3 | import { BROWSER_TOOLS, API_TOOLS } from "./tools.js";
4 | import fs from 'node:fs';
5 | import * as os from 'os';
6 | import * as path from 'path';
7 |
8 | // Global state
9 | let browser: Browser | undefined;
10 | let page: Page | undefined;
11 | const consoleLogs: string[] = [];
12 | const screenshots = new Map<string, string>();
13 | const defaultDownloadsPath = path.join(os.homedir(), 'Downloads');
14 |
15 | async function ensureBrowser() {
16 | if (!browser) {
17 | browser = await chromium.launch({ headless: false });
18 | const context = await browser.newContext({
19 | viewport: { width: 1920, height: 1080 },
20 | deviceScaleFactor: 1,
21 | });
22 |
23 | page = await context.newPage();
24 |
25 | page.on("console", (msg) => {
26 | const logEntry = `[${msg.type()}] ${msg.text()}`;
27 | consoleLogs.push(logEntry);
28 | // Note: server.notification is assumed to be passed in from the main server
29 | });
30 | }
31 | return page!;
32 | }
33 |
34 | async function ensureApiContext(url: string) {
35 | return await request.newContext({
36 | baseURL: url,
37 | });
38 | }
39 |
40 | export async function handleToolCall(
41 | name: string,
42 | args: any,
43 | server: any
44 | ): Promise<{ toolResult: CallToolResult }> {
45 | // Check if the tool requires browser interaction
46 | const requiresBrowser = BROWSER_TOOLS.includes(name);
47 | // Check if the tool requires api interaction
48 | const requiresApi = API_TOOLS.includes(name);
49 | let page: Page | undefined;
50 | let apiContext: APIRequestContext;
51 |
52 | // Only launch browser if the tool requires browser interaction
53 | if (requiresBrowser) {
54 | page = await ensureBrowser();
55 | }
56 |
57 | // Set up API context for API-related operations
58 | if (requiresApi) {
59 | apiContext = await ensureApiContext(args.url);
60 | }
61 |
62 | switch (name) {
63 | case "playwright_navigate":
64 | try {
65 | await page!.goto(args.url, {
66 | timeout: args.timeout || 30000,
67 | waitUntil: args.waitUntil || "load"
68 | });
69 | return {
70 | toolResult: {
71 | content: [{
72 | type: "text",
73 | text: `Navigated to ${args.url} with ${args.waitUntil || "load"} wait`,
74 | }],
75 | isError: false,
76 | },
77 | };
78 | } catch (error) {
79 | return {
80 | toolResult: {
81 | content: [{
82 | type: "text",
83 | text: `Navigation failed: ${(error as Error).message}`,
84 | }],
85 | isError: true,
86 | },
87 | };
88 | }
89 |
90 | case "playwright_screenshot": {
91 | try {
92 | const screenshotOptions: any = {
93 | type: args.type || "png",
94 | fullPage: !!args.fullPage
95 | };
96 |
97 | if (args.selector) {
98 | const element = await page!.$(args.selector);
99 | if (!element) {
100 | return {
101 | toolResult: {
102 | content: [{
103 | type: "text",
104 | text: `Element not found: ${args.selector}`,
105 | }],
106 | isError: true,
107 | },
108 | };
109 | }
110 | screenshotOptions.element = element;
111 | }
112 |
113 | if (args.mask) {
114 | screenshotOptions.mask = await Promise.all(
115 | args.mask.map(async (selector: string) => await page!.$(selector))
116 | );
117 | }
118 |
119 | const screenshot = await page!.screenshot(screenshotOptions);
120 | const base64Screenshot = screenshot.toString('base64');
121 |
122 | const responseContent: (TextContent | ImageContent)[] = [];
123 |
124 | // Handle PNG file saving
125 | if (args.savePng !== false) {
126 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
127 | const filename = `${args.name}-${timestamp}.png`;
128 | const downloadsDir = args.downloadsDir || defaultDownloadsPath;
129 |
130 | // Create downloads directory if it doesn't exist
131 | if (!fs.existsSync(downloadsDir)) {
132 | fs.mkdirSync(downloadsDir, { recursive: true });
133 | }
134 |
135 | const filePath = path.join(downloadsDir, filename);
136 | await fs.promises.writeFile(filePath, screenshot);
137 | responseContent.push({
138 | type: "text",
139 | text: `Screenshot saved to: ${filePath}`,
140 | } as TextContent);
141 | }
142 |
143 | // Handle base64 storage
144 | if (args.storeBase64 !== false) {
145 | screenshots.set(args.name, base64Screenshot);
146 | server.notification({
147 | method: "notifications/resources/list_changed",
148 | });
149 |
150 | responseContent.push({
151 | type: "image",
152 | data: base64Screenshot,
153 | mimeType: "image/png",
154 | } as ImageContent);
155 | }
156 |
157 | return {
158 | toolResult: {
159 | content: responseContent,
160 | isError: false,
161 | },
162 | };
163 | } catch (error) {
164 | return {
165 | toolResult: {
166 | content: [{
167 | type: "text",
168 | text: `Screenshot failed: ${(error as Error).message}`,
169 | }],
170 | isError: true,
171 | },
172 | };
173 | }
174 | }
175 | case "playwright_click":
176 | try {
177 | await page!.click(args.selector);
178 | return {
179 | toolResult: {
180 | content: [{
181 | type: "text",
182 | text: `Clicked: ${args.selector}`,
183 | }],
184 | isError: false,
185 | },
186 | };
187 | } catch (error) {
188 | return {
189 | toolResult: {
190 | content: [{
191 | type: "text",
192 | text: `Failed to click ${args.selector}: ${(error as Error).message}`,
193 | }],
194 | isError: true,
195 | },
196 | };
197 | }
198 |
199 | case "playwright_fill":
200 | try {
201 | await page!.waitForSelector(args.selector);
202 | await page!.fill(args.selector, args.value);
203 | return {
204 | toolResult: {
205 | content: [{
206 | type: "text",
207 | text: `Filled ${args.selector} with: ${args.value}`,
208 | }],
209 | isError: false,
210 | },
211 | };
212 | } catch (error) {
213 | return {
214 | toolResult: {
215 | content: [{
216 | type: "text",
217 | text: `Failed to type ${args.selector}: ${(error as Error).message}`,
218 | }],
219 | isError: true,
220 | },
221 | };
222 | }
223 |
224 | case "playwright_select":
225 | try {
226 | await page!.waitForSelector(args.selector);
227 | await page!.selectOption(args.selector, args.value);
228 | return {
229 | toolResult: {
230 | content: [{
231 | type: "text",
232 | text: `Selected ${args.selector} with: ${args.value}`,
233 | }],
234 | isError: false,
235 | },
236 | };
237 | } catch (error) {
238 | return {
239 | toolResult: {
240 | content: [{
241 | type: "text",
242 | text: `Failed to select ${args.selector}: ${(error as Error).message}`,
243 | }],
244 | isError: true,
245 | },
246 | };
247 | }
248 |
249 | case "playwright_hover":
250 | try {
251 | await page!.waitForSelector(args.selector);
252 | await page!.hover(args.selector);
253 | return {
254 | toolResult: {
255 | content: [{
256 | type: "text",
257 | text: `Hovered ${args.selector}`,
258 | }],
259 | isError: false,
260 | },
261 | };
262 | } catch (error) {
263 | return {
264 | toolResult: {
265 | content: [{
266 | type: "text",
267 | text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
268 | }],
269 | isError: true,
270 | },
271 | };
272 | }
273 |
274 | case "playwright_evaluate":
275 | try {
276 | const result = await page!.evaluate((script) => {
277 | const logs: string[] = [];
278 | const originalConsole = { ...console };
279 |
280 | ['log', 'info', 'warn', 'error'].forEach(method => {
281 | (console as any)[method] = (...args: any[]) => {
282 | logs.push(`[${method}] ${args.join(' ')}`);
283 | (originalConsole as any)[method](...args);
284 | };
285 | });
286 |
287 | try {
288 | const result = eval(script);
289 | Object.assign(console, originalConsole);
290 | return { result, logs };
291 | } catch (error) {
292 | Object.assign(console, originalConsole);
293 | throw error;
294 | }
295 | }, args.script);
296 |
297 | return {
298 | toolResult: {
299 | content: [
300 | {
301 | type: "text",
302 | text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
303 | },
304 | ],
305 | isError: false,
306 | },
307 | };
308 | } catch (error) {
309 | return {
310 | toolResult: {
311 | content: [{
312 | type: "text",
313 | text: `Script execution failed: ${(error as Error).message}`,
314 | }],
315 | isError: true,
316 | },
317 | };
318 | }
319 |
320 | case "playwright_get":
321 | try {
322 | var response = await apiContext!.get(args.url);
323 |
324 | return {
325 | toolResult: {
326 | content: [{
327 | type: "text",
328 | text: `Performed GET Operation ${args.url}`,
329 | },
330 | {
331 | type: "text",
332 | text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
333 | },
334 | {
335 | type: "text",
336 | text: `Response code ${response.status()}`
337 | }
338 | ],
339 | isError: false,
340 | },
341 | };
342 | } catch (error) {
343 | return {
344 | toolResult: {
345 | content: [{
346 | type: "text",
347 | text: `Failed to perform GET operation on ${args.url}: ${(error as Error).message}`,
348 | }],
349 | isError: true,
350 | },
351 | };
352 | }
353 |
354 | case "playwright_post":
355 | try {
356 | var data = {
357 | data: args.value,
358 | headers: {
359 | 'Content-Type': 'application/json'
360 | }
361 | };
362 |
363 | var response = await apiContext!.post(args.url, data);
364 | return {
365 | toolResult: {
366 | content: [{
367 | type: "text",
368 | text: `Performed POST Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
369 | },
370 | {
371 | type: "text",
372 | text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
373 | },
374 | {
375 | type: "text",
376 | text: `Response code ${response.status()}`
377 | }],
378 | isError: false,
379 | },
380 | };
381 | } catch (error) {
382 | return {
383 | toolResult: {
384 | content: [{
385 | type: "text",
386 | text: `Failed to perform POST operation on ${args.url}: ${(error as Error).message}`,
387 | }],
388 | isError: true,
389 | },
390 | };
391 | }
392 |
393 | case "playwright_put":
394 | try {
395 | var data = {
396 | data: args.value,
397 | headers: {
398 | 'Content-Type': 'application/json'
399 | }
400 | };
401 | var response = await apiContext!.put(args.url, data);
402 |
403 | return {
404 | toolResult: {
405 | content: [{
406 | type: "text",
407 | text: `Performed PUT Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
408 | }, {
409 | type: "text",
410 | text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
411 | },
412 | {
413 | type: "text",
414 | text: `Response code ${response.status()}`
415 | }],
416 | isError: false,
417 | },
418 | };
419 | } catch (error) {
420 | return {
421 | toolResult: {
422 | content: [{
423 | type: "text",
424 | text: `Failed to perform PUT operation on ${args.url}: ${(error as Error).message}`,
425 | }],
426 | isError: true,
427 | },
428 | };
429 | }
430 |
431 | case "playwright_delete":
432 | try {
433 | var response = await apiContext!.delete(args.url);
434 |
435 | return {
436 | toolResult: {
437 | content: [{
438 | type: "text",
439 | text: `Performed delete Operation ${args.url}`,
440 | },
441 | {
442 | type: "text",
443 | text: `Response code ${response.status()}`
444 | }],
445 | isError: false,
446 | },
447 | };
448 | } catch (error) {
449 | return {
450 | toolResult: {
451 | content: [{
452 | type: "text",
453 | text: `Failed to perform delete operation on ${args.url}: ${(error as Error).message}`,
454 | }],
455 | isError: true,
456 | },
457 | };
458 | }
459 |
460 | case "playwright_patch":
461 | try {
462 | var data = {
463 | data: args.value,
464 | headers: {
465 | 'Content-Type': 'application/json'
466 | }
467 | };
468 | var response = await apiContext!.patch(args.url, data);
469 |
470 | return {
471 | toolResult: {
472 | content: [{
473 | type: "text",
474 | text: `Performed PATCH Operation ${args.url} with data ${JSON.stringify(args.value, null, 2)}`,
475 | }, {
476 | type: "text",
477 | text: `Response: ${JSON.stringify(await response.json(), null, 2)}`,
478 | }, {
479 | type: "text",
480 | text: `Response code ${response.status()}`
481 | }],
482 | isError: false,
483 | },
484 | };
485 | } catch (error) {
486 | return {
487 | toolResult: {
488 | content: [{
489 | type: "text",
490 | text: `Failed to perform PATCH operation on ${args.url}: ${(error as Error).message}`,
491 | }],
492 | isError: true,
493 | },
494 | };
495 | }
496 |
497 | default:
498 | return {
499 | toolResult: {
500 | content: [{
501 | type: "text",
502 | text: `Unknown tool: ${name}`,
503 | }],
504 | isError: true,
505 | },
506 | };
507 | }
508 | }
509 |
510 | // Expose utility functions for resource management
511 | export function getConsoleLogs(): string[] {
512 | return consoleLogs;
513 | }
514 |
515 | export function getScreenshots(): Map<string, string> {
516 | return screenshots;
517 | }
```