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 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
- └── tabs.js
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.0.0",
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: "2.0.0" },
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("Chrome Extension Tester MCP server running (v2.0.0)...");
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
- await new Promise((r) => setTimeout(r, 1000));
27
- const workers = state.browser.serviceWorkers();
28
- if (workers.length > 0) {
29
- const url = workers[0].url();
30
- const match = url.match(/chrome-extension:\/\/([a-z]{32})\//);
31
- if (match) state.extensionId = match[1];
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
- if (!state.browser) throw new Error("Browser not started. Call load_extension first.");
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
- if (!state.browser) throw new Error("Browser not started. Call load_extension first.");
47
- const workers = state.browser.serviceWorkers();
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
  }