chrome-extension-tester-mcp 2.0.0 → 2.2.0
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/README.md +151 -1
- package/package.json +1 -1
- package/src/index.js +7 -2
- package/src/state.js +41 -9
- package/src/tools/account-login.js +630 -0
- package/src/tools/assertion.js +1 -1
- package/src/tools/badge.js +2 -2
- package/src/tools/connect-browser.js +335 -0
- package/src/tools/context-menu.js +2 -2
- package/src/tools/dom.js +1 -1
- package/src/tools/index.js +4 -0
- package/src/tools/load-extension.js +2 -0
- package/src/tools/messaging.js +2 -2
- package/src/tools/network.js +1 -1
- package/src/tools/options-page.js +1 -1
- package/src/tools/popup.js +1 -0
- package/src/tools/storage.js +2 -2
- package/src/tools/tabs.js +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ An **MCP (Model Context Protocol) server** that lets Claude interactively test a
|
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
|
+
- [Why](#why)
|
|
10
|
+
- [Architecture](#architecture)
|
|
9
11
|
- [Features](#features)
|
|
10
12
|
- [Requirements](#requirements)
|
|
11
13
|
- [Installation](#installation)
|
|
@@ -13,12 +15,31 @@ An **MCP (Model Context Protocol) server** that lets Claude interactively test a
|
|
|
13
15
|
- [Setup with Claude Code (npx)](#setup-with-claude-code-npx)
|
|
14
16
|
- [Available Tools](#available-tools)
|
|
15
17
|
- [Testing Agent Prompt](#testing-agent-prompt)
|
|
18
|
+
- [Example: testing an extension popup](#example-testing-an-extension-popup)
|
|
16
19
|
- [Example Prompts](#example-prompts)
|
|
17
20
|
- [Project Structure](#project-structure)
|
|
18
21
|
- [Notes](#notes)
|
|
19
22
|
|
|
20
23
|
---
|
|
21
24
|
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
Testing a Chrome extension during development means manually clicking reload, opening the popup, checking storage in DevTools, watching the service worker console, and copy-pasting errors back to the agent on every iteration. This MCP server gives an AI coding agent direct access to all of that through tool calls, so the agent can iterate on its own. It exists because the manual loop made working with Claude Code on extensions too slow.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
graph LR
|
|
35
|
+
A[AI Agent<br/>Claude / Cursor] -->|MCP protocol| B[This server<br/>14 tools]
|
|
36
|
+
B -->|Playwright| C[Chromium<br/>persistent context]
|
|
37
|
+
C -->|loads| D[Extension under test]
|
|
38
|
+
B -.->|reads / writes| E[(state.js<br/>browser, page, extensionId)]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
22
43
|
## Features
|
|
23
44
|
|
|
24
45
|
- Load and reload any unpacked Chrome extension
|
|
@@ -32,6 +53,7 @@ An **MCP (Model Context Protocol) server** that lets Claude interactively test a
|
|
|
32
53
|
- Test context menu registration and handler invocation
|
|
33
54
|
- Run assertions that return structured PASS / FAIL results
|
|
34
55
|
- Take screenshots at any point during testing
|
|
56
|
+
- Create and reuse test accounts on any website using disposable email (via Guerrilla Mail API)
|
|
35
57
|
|
|
36
58
|
---
|
|
37
59
|
|
|
@@ -139,6 +161,77 @@ Add to your project's `.mcp.json` or user-level MCP config:
|
|
|
139
161
|
| `send_message_to_background` | Send `chrome.runtime.sendMessage` from the popup context and return the response |
|
|
140
162
|
| `test_context_menu` | Check `contextMenus` API availability, simulate right-click, or invoke a menu item handler directly |
|
|
141
163
|
| `simulate_tab_events` | Open, close, switch, list, or close all browser tabs |
|
|
164
|
+
| `test_account_login` | Create or reuse a test account on any website using a disposable email; credentials are stored in `test-accounts.json` and reused across sessions |
|
|
165
|
+
|
|
166
|
+
### `load_extension`
|
|
167
|
+
Launch Chromium with an unpacked extension and capture its ID.
|
|
168
|
+
**Inputs:** `extension_path` (string, required) — path to the unpacked extension folder.
|
|
169
|
+
**Returns:** Text confirming the resolved path and the detected extension ID.
|
|
170
|
+
|
|
171
|
+
### `interact_with_popup`
|
|
172
|
+
Open the popup and click, type, or read its DOM.
|
|
173
|
+
**Inputs:** `action` (string, required: `open` | `click` | `type` | `get_text` | `get_html`); `selector` (string); `value` (string, for `type`).
|
|
174
|
+
**Returns:** Text describing the action result, or the requested text/HTML.
|
|
175
|
+
|
|
176
|
+
### `open_options_page`
|
|
177
|
+
Open the options page (or any extension page) and interact with it.
|
|
178
|
+
**Inputs:** `page` (string, default `options.html`); `action` (string: `open` | `click` | `type` | `get_text` | `get_html`); `selector` (string); `value` (string).
|
|
179
|
+
**Returns:** Text describing the action result, or the requested text/HTML.
|
|
180
|
+
|
|
181
|
+
### `inspect_dom`
|
|
182
|
+
Query a selector or evaluate JS in a page, optionally navigating first.
|
|
183
|
+
**Inputs:** `url` (string); `selector` (string); `script` (string, overrides `selector`).
|
|
184
|
+
**Returns:** Text with matched elements' outerHTML, or the JSON-serialized script result.
|
|
185
|
+
|
|
186
|
+
### `get_service_worker_logs`
|
|
187
|
+
Read buffered background service worker console logs.
|
|
188
|
+
**Inputs:** `clear_after` (boolean, default `false`).
|
|
189
|
+
**Returns:** Text listing captured log entries, or a "none captured yet" message.
|
|
190
|
+
|
|
191
|
+
### `take_screenshot`
|
|
192
|
+
Save a screenshot of the current page or popup.
|
|
193
|
+
**Inputs:** `output_path` (string, default `./screenshot.png`); `full_page` (boolean, default `false`).
|
|
194
|
+
**Returns:** Text with the saved file path.
|
|
195
|
+
|
|
196
|
+
### `run_assertion`
|
|
197
|
+
Assert an element exists/has text, or that a JS expression is truthy.
|
|
198
|
+
**Inputs:** `description` (string, required); `selector` (string); `expected_text` (string); `script` (string, overrides `selector`).
|
|
199
|
+
**Returns:** Text beginning with `PASS` or `FAIL`, followed by detail.
|
|
200
|
+
|
|
201
|
+
### `extension_storage`
|
|
202
|
+
Read from or write to `chrome.storage` (local / sync / session).
|
|
203
|
+
**Inputs:** `action` (string, required: `get` | `set` | `remove` | `clear`); `area` (string, default `local`); `keys` (string[]); `data` (object, for `set`).
|
|
204
|
+
**Returns:** Text with storage contents, or a confirmation of the write/removal.
|
|
205
|
+
|
|
206
|
+
### `monitor_network`
|
|
207
|
+
Capture and inspect network requests during navigation.
|
|
208
|
+
**Inputs:** `action` (string, required: `navigate_and_capture` | `get_captured` | `clear`); `url` (string); `filter_pattern` (string); `include_types` (string[]).
|
|
209
|
+
**Returns:** Text listing captured requests as `[method] [type] status url`.
|
|
210
|
+
|
|
211
|
+
### `check_badge`
|
|
212
|
+
Read or assert the action badge text and background color.
|
|
213
|
+
**Inputs:** `action` (string, required: `get` | `assert_text` | `assert_color`); `tab_id` (number); `expected_text` (string); `expected_color` (number[] RGBA).
|
|
214
|
+
**Returns:** Text with the badge value, or a `PASS` / `FAIL` assertion result.
|
|
215
|
+
|
|
216
|
+
### `send_message_to_background`
|
|
217
|
+
Send `chrome.runtime.sendMessage` from the popup and return the response.
|
|
218
|
+
**Inputs:** `message` (object, required); `timeout_ms` (number, default `5000`).
|
|
219
|
+
**Returns:** Text with the sent message and JSON response, or a failure message.
|
|
220
|
+
|
|
221
|
+
### `test_context_menu`
|
|
222
|
+
Check the `contextMenus` API, simulate a right-click, or trigger an item.
|
|
223
|
+
**Inputs:** `action` (string, required: `check_api` | `right_click` | `trigger_item`); `url` (string); `selector` (string); `menu_item_id` (string); `page_url` (string).
|
|
224
|
+
**Returns:** Text with API availability, dispatch confirmation, or trigger result.
|
|
225
|
+
|
|
226
|
+
### `simulate_tab_events`
|
|
227
|
+
Open, close, switch, list, or close all browser tabs.
|
|
228
|
+
**Inputs:** `action` (string, required: `open` | `close` | `switch` | `list` | `close_all`); `url` (string); `tab_index` (number).
|
|
229
|
+
**Returns:** Text describing the affected tab(s), or the list of open tabs.
|
|
230
|
+
|
|
231
|
+
### `test_account_login`
|
|
232
|
+
Create or reuse a test account on a site using a disposable email.
|
|
233
|
+
**Inputs:** `action` (string, required: `auto` | `create` | `login`); `account_key` (string, required); `signup_url` / `login_url` (string); selector overrides (`email_selector`, `password_selector`, `submit_selector`, `pre_click_selector`); multi-step fields (`step2_url`, `step2_password_selector`, `step2_submit_selector`).
|
|
234
|
+
**Returns:** Text reporting account creation/login status plus a screenshot path.
|
|
142
235
|
|
|
143
236
|
---
|
|
144
237
|
|
|
@@ -176,6 +269,52 @@ Claude will write the test plan, execute every test, and return a full report.
|
|
|
176
269
|
|
|
177
270
|
---
|
|
178
271
|
|
|
272
|
+
## Example: testing an extension popup
|
|
273
|
+
|
|
274
|
+
A typical loop the agent can run on its own:
|
|
275
|
+
|
|
276
|
+
**1. Load the extension**
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{ "tool": "load_extension", "arguments": { "extension_path": "/tmp/my-extension" } }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
Extension loaded.
|
|
284
|
+
Path: /tmp/my-extension
|
|
285
|
+
Extension ID: ddnjmkpjnchafihagpljebmkdpejhaoj
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**2. Open the popup and read its HTML**
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{ "tool": "interact_with_popup", "arguments": { "action": "open" } }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<body>
|
|
296
|
+
<h1>Tab Saver</h1>
|
|
297
|
+
<button id="save">Save open tabs</button>
|
|
298
|
+
<span id="count">0 saved</span>
|
|
299
|
+
</body>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**3. Read local storage**
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
{ "tool": "extension_storage", "arguments": { "action": "get", "area": "local" } }
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
storage.local contents:
|
|
310
|
+
{
|
|
311
|
+
"savedTabs": [],
|
|
312
|
+
"enabled": true
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
179
318
|
## Example Prompts
|
|
180
319
|
|
|
181
320
|
```
|
|
@@ -218,6 +357,14 @@ Open a tab to https://news.ycombinator.com, then another to https://github.com,
|
|
|
218
357
|
Right-click on https://example.com and trigger the context menu item with id "my-action"
|
|
219
358
|
```
|
|
220
359
|
|
|
360
|
+
```
|
|
361
|
+
Create a test account on https://example.com/signup and save it as "my_test_account"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
Log in to https://example.com/login using the stored "my_test_account" credentials
|
|
366
|
+
```
|
|
367
|
+
|
|
221
368
|
---
|
|
222
369
|
|
|
223
370
|
## Project Structure
|
|
@@ -244,7 +391,8 @@ chrome-extension-testing-mcp/
|
|
|
244
391
|
│ ├── context-menu.js
|
|
245
392
|
│ ├── badge.js
|
|
246
393
|
│ ├── messaging.js
|
|
247
|
-
│
|
|
394
|
+
│ ├── tabs.js
|
|
395
|
+
│ └── account-login.js
|
|
248
396
|
├── package.json
|
|
249
397
|
└── README.md
|
|
250
398
|
```
|
|
@@ -259,6 +407,8 @@ chrome-extension-testing-mcp/
|
|
|
259
407
|
- Call `load_extension` again at any time to get a fresh browser instance
|
|
260
408
|
- Native Chrome context menus cannot be automated by Playwright — use `test_context_menu` with `trigger_item` to invoke handlers directly
|
|
261
409
|
- Badge and storage tools communicate via the service worker, so the extension must have a background service worker (MV3)
|
|
410
|
+
- `test_account_login` uses the [Guerrilla Mail API](https://www.guerrillamail.com/GuerrillaMailAPI.html) to generate disposable emails — no browser navigation required, no bot-blocking. Credentials are stored in `test-accounts.json` at the project root (add this to `.gitignore`)
|
|
411
|
+
- Use `action: "auto"` for `test_account_login` to automatically reuse stored credentials or create a new account if none exist
|
|
262
412
|
|
|
263
413
|
---
|
|
264
414
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-extension-tester-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "MCP server for interactive Chrome extension testing via Playwright — load, interact, assert, inspect storage, network, badges, messaging, tabs, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/index.js
CHANGED
|
@@ -7,11 +7,16 @@ import {
|
|
|
7
7
|
ListPromptsRequestSchema,
|
|
8
8
|
GetPromptRequestSchema,
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
10
11
|
import { TOOLS, HANDLERS } from "./tools/index.js";
|
|
11
12
|
import { PROMPTS, PROMPT_HANDLERS } from "./prompts/index.js";
|
|
12
13
|
|
|
14
|
+
// Read the version from package.json so it never drifts from the published value.
|
|
15
|
+
const packageJsonUrl = new URL("../package.json", import.meta.url);
|
|
16
|
+
const pkg = JSON.parse(readFileSync(packageJsonUrl, "utf-8"));
|
|
17
|
+
|
|
13
18
|
const server = new Server(
|
|
14
|
-
{ name: "chrome-extension-tester", version:
|
|
19
|
+
{ name: "chrome-extension-tester", version: pkg.version },
|
|
15
20
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
16
21
|
);
|
|
17
22
|
|
|
@@ -64,4 +69,4 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
64
69
|
|
|
65
70
|
const transport = new StdioServerTransport();
|
|
66
71
|
await server.connect(transport);
|
|
67
|
-
console.error(
|
|
72
|
+
console.error(`Chrome Extension Tester MCP server running (v${pkg.version})...`);
|
package/src/state.js
CHANGED
|
@@ -4,6 +4,10 @@ import path from "path";
|
|
|
4
4
|
|
|
5
5
|
export const state = {
|
|
6
6
|
browser: null,
|
|
7
|
+
// BrowserContext when connected via CDP (browser.contexts()[0]); null when using launchPersistentContext
|
|
8
|
+
context: null,
|
|
9
|
+
// "launched" = Playwright owns the browser process; "cdp" = attached to user's real browser
|
|
10
|
+
connectionMode: null,
|
|
7
11
|
page: null,
|
|
8
12
|
extensionId: null,
|
|
9
13
|
swLogs: [],
|
|
@@ -23,28 +27,56 @@ export async function ensureBrowser(extensionPath) {
|
|
|
23
27
|
],
|
|
24
28
|
});
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Prefer an already-registered service worker; otherwise wait for one to register.
|
|
31
|
+
const existingWorkers = state.browser.serviceWorkers();
|
|
32
|
+
let workerUrl = existingWorkers.length > 0 ? existingWorkers[0].url() : null;
|
|
33
|
+
|
|
34
|
+
if (!workerUrl) {
|
|
35
|
+
const worker = await state.browser.waitForEvent("serviceworker", { timeout: 5000 });
|
|
36
|
+
workerUrl = worker.url();
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
const extensionIdMatch = workerUrl.match(/chrome-extension:\/\/([a-z]{32})\//);
|
|
40
|
+
if (extensionIdMatch) state.extensionId = extensionIdMatch[1];
|
|
41
|
+
|
|
42
|
+
state.connectionMode = "launched";
|
|
34
43
|
state.page = await state.browser.newPage();
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export async function ensurePage() {
|
|
38
47
|
if (!state.page || state.page.isClosed()) {
|
|
39
|
-
|
|
48
|
+
const ctx = state.context || state.browser;
|
|
49
|
+
if (!ctx) throw new Error("No browser connected. Call load_extension or connect_browser first.");
|
|
50
|
+
state.page = await ctx.newPage();
|
|
51
|
+
}
|
|
52
|
+
return state.page;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Like ensurePage but launches a plain headed Chromium if no browser is running yet.
|
|
57
|
+
* Used by tools that don't require an extension (e.g. test_account_login).
|
|
58
|
+
*/
|
|
59
|
+
export async function ensurePageStandalone() {
|
|
60
|
+
if (!state.browser) {
|
|
61
|
+
state.browser = await chromium.launchPersistentContext("", {
|
|
62
|
+
headless: false,
|
|
63
|
+
});
|
|
64
|
+
state.connectionMode = "launched";
|
|
40
65
|
state.page = await state.browser.newPage();
|
|
41
66
|
}
|
|
67
|
+
|
|
68
|
+
if (!state.page || state.page.isClosed()) {
|
|
69
|
+
const ctx = state.context || state.browser;
|
|
70
|
+
state.page = await ctx.newPage();
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
return state.page;
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
export async function getServiceWorker() {
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
const ctx = state.context || state.browser;
|
|
78
|
+
if (!ctx) throw new Error("No browser connected. Call load_extension or connect_browser first.");
|
|
79
|
+
const workers = ctx.serviceWorkers();
|
|
48
80
|
if (!workers.length) throw new Error("No service worker found. Extension may not have a background service worker.");
|
|
49
81
|
return workers[0];
|
|
50
82
|
}
|