From 4baf54b84bfa9b327823dcf3d29ce7d50f979a58 Mon Sep 17 00:00:00 2001 From: DmytroNebelskyi Date: Wed, 2 Apr 2025 11:04:03 +0300 Subject: [PATCH] Add API mocking functionality to Playwright MCP --- README.md | 17 +++++++ src/index.ts | 5 ++ src/tools/mock.ts | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 src/tools/mock.ts diff --git a/README.md b/README.md index 18da004f8..ed908cbd3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit - **Fast and lightweight**: Uses Playwright's accessibility tree, not pixel-based input. - **LLM-friendly**: No vision models needed, operates purely on structured data. - **Deterministic tool application**: Avoids ambiguity common with screenshot-based approaches. +- **API Mocking**: Intercept and mock API responses for testing and simulation. ### Use Cases @@ -14,6 +15,7 @@ A Model Context Protocol (MCP) server that provides browser automation capabilit - Data extraction from structured content - Automated testing driven by LLMs - General-purpose browser interaction for agents +- API mocking and response simulation ### Example config @@ -247,6 +249,21 @@ The Playwright MCP provides a set of tools for browser automation. Here are all - Parameters: - `time` (number): The time to wait in seconds (capped at 10 seconds) +- **browser_mock_api** + - Description: Mock API responses by intercepting network requests + - Parameters: + - `url` (string): URL pattern to match for interception (supports glob and regex patterns) + - `method` (string, optional): HTTP method to match (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, or ALL) + - `status` (number, default 200): HTTP status code to return + - `contentType` (string, default "application/json"): Content-Type header for the response + - `body` (string): Response body content (typically JSON formatted as a string) + - `headers` (object, optional): Additional response headers to include + +- **browser_clear_mock** + - Description: Clear previously configured API mocks + - Parameters: + - `url` (string, optional): URL pattern to remove mocking for. If not provided, all mocks will be cleared + - **browser_close** - Description: Close the page - Parameters: None diff --git a/src/index.ts b/src/index.ts index a54a67406..d4a582340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { createServerWithTools } from './server'; import * as snapshot from './tools/snapshot'; import * as common from './tools/common'; import * as screenshot from './tools/screenshot'; +import * as mock from './tools/mock'; import { console } from './resources/console'; import type { Tool } from './tools/tool'; @@ -44,6 +45,8 @@ const snapshotTools: Tool[] = [ snapshot.type, snapshot.selectOption, snapshot.screenshot, + mock.mockApi(true), + mock.clearMock(true), ...commonTools, ]; @@ -57,6 +60,8 @@ const screenshotTools: Tool[] = [ screenshot.click, screenshot.drag, screenshot.type, + mock.mockApi(false), + mock.clearMock(false), ...commonTools, ]; diff --git a/src/tools/mock.ts b/src/tools/mock.ts new file mode 100644 index 000000000..cdbb71687 --- /dev/null +++ b/src/tools/mock.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { captureAriaSnapshot } from './utils'; + +import type { ToolFactory, Tool } from './tool'; +import type * as playwright from 'playwright'; + +const mockApiSchema = z.object({ + url: z.string().describe('URL pattern to match for interception (supports glob and regex patterns)'), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'ALL']).optional() + .describe('HTTP method to match (ALL matches any method)'), + status: z.number().min(100).max(599).default(200) + .describe('HTTP status code to return'), + contentType: z.string().default('application/json') + .describe('Content-Type header for the response'), + body: z.string().describe('Response body content (typically JSON formatted as a string)'), + headers: z.record(z.string()).optional() + .describe('Additional response headers to include'), +}); + +export const mockApi: ToolFactory = snapshot => ({ + schema: { + name: 'browser_mock_api', + description: 'Mock API responses by intercepting network requests', + inputSchema: zodToJsonSchema(mockApiSchema), + }, + handle: async (context, params) => { + const validatedParams = mockApiSchema.parse(params); + const page = context.existingPage(); + + // Generate a unique ID for this route handler + const routeId = `route_${Date.now()}_${Math.floor(Math.random() * 10000)}`; + + // Create the mock response + const mockResponse = { + status: validatedParams.status, + headers: { + 'Content-Type': validatedParams.contentType, + ...(validatedParams.headers || {}) + }, + body: validatedParams.body + }; + + // Set up the route handler with the appropriate method filter if specified + if (validatedParams.method && validatedParams.method !== 'ALL') { + await page.route(validatedParams.url, (route: playwright.Route) => { + if (route.request().method() === validatedParams.method) { + route.fulfill(mockResponse); + } else { + route.continue(); + } + }, { times: 0 }); + } else { + await page.route(validatedParams.url, (route: playwright.Route) => { + route.fulfill(mockResponse); + }, { times: 0 }); + } + + if (snapshot) { + return captureAriaSnapshot(context); + } + + return { + content: [{ + type: 'text', + text: `API mock created for ${validatedParams.url}${validatedParams.method ? ` (${validatedParams.method})` : ''} with status ${validatedParams.status}`, + }], + }; + }, +}); + +const clearMockSchema = z.object({ + url: z.string().optional().describe('URL pattern to remove mocking for. If not provided, all mocks will be cleared'), +}); + +export const clearMock: ToolFactory = snapshot => ({ + schema: { + name: 'browser_clear_mock', + description: 'Clear previously configured API mocks', + inputSchema: zodToJsonSchema(clearMockSchema), + }, + handle: async (context, params) => { + const validatedParams = clearMockSchema.parse(params); + const page = context.existingPage(); + + if (validatedParams.url) { + await page.unroute(validatedParams.url); + } else { + // Clear all routes - this is a bit of a hack as there's no direct "unroute all" method + // We use a pattern that matches all URLs and then unroute it + await page.unroute('**'); + } + + if (snapshot) { + return captureAriaSnapshot(context); + } + + return { + content: [{ + type: 'text', + text: validatedParams.url + ? `Cleared API mock for ${validatedParams.url}` + : 'Cleared all API mocks', + }], + }; + }, +}); \ No newline at end of file