chrome-extension-tester-mcp 2.0.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.
@@ -0,0 +1,76 @@
1
+ import { getServiceWorker } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "check_badge",
5
+ description: "Read or assert the extension icon badge text and background color from chrome.action.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ action: {
10
+ type: "string",
11
+ enum: ["get", "assert_text", "assert_color"],
12
+ description: "get: return current badge text and color; assert_text: check badge text equals expected; assert_color: check badge background color matches expected RGBA",
13
+ },
14
+ tab_id: {
15
+ type: "number",
16
+ description: "Tab ID to scope the badge check (optional, defaults to global badge)",
17
+ },
18
+ expected_text: {
19
+ type: "string",
20
+ description: "Expected badge text (for assert_text)",
21
+ },
22
+ expected_color: {
23
+ type: "array",
24
+ items: { type: "number" },
25
+ description: "Expected RGBA color array e.g. [255, 0, 0, 255] (for assert_color)",
26
+ },
27
+ },
28
+ required: ["action"],
29
+ },
30
+ };
31
+
32
+ export async function handler(args) {
33
+ const sw = await getServiceWorker();
34
+ const tabId = args.tab_id ?? undefined;
35
+
36
+ const badge = await sw.evaluate(async (tabId) => {
37
+ const details = tabId !== undefined ? { tabId } : {};
38
+ const [text, color] = await Promise.all([
39
+ new Promise((resolve) => chrome.action.getBadgeText(details, resolve)),
40
+ new Promise((resolve) => chrome.action.getBadgeBackgroundColor(details, resolve)),
41
+ ]);
42
+ return { text, color };
43
+ }, tabId);
44
+
45
+ if (args.action === "get") {
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: `Badge Text: "${badge.text || "(empty)"}\nBadge Color (RGBA): [${badge.color.join(", ")}]`,
50
+ }],
51
+ };
52
+ }
53
+
54
+ if (args.action === "assert_text") {
55
+ const passed = badge.text.trim() === (args.expected_text ?? "").trim();
56
+ return {
57
+ content: [{
58
+ type: "text",
59
+ text: `${passed ? "PASS" : "FAIL"} — Badge text assertion\nExpected: "${args.expected_text}" | Got: "${badge.text}"`,
60
+ }],
61
+ };
62
+ }
63
+
64
+ if (args.action === "assert_color") {
65
+ if (!args.expected_color?.length) {
66
+ return { content: [{ type: "text", text: "Provide an 'expected_color' RGBA array for assert_color." }] };
67
+ }
68
+ const passed = args.expected_color.every((v, i) => v === badge.color[i]);
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: `${passed ? "PASS" : "FAIL"} — Badge color assertion\nExpected: [${args.expected_color.join(", ")}] | Got: [${badge.color.join(", ")}]`,
73
+ }],
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,85 @@
1
+ import { ensurePage, getServiceWorker } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "test_context_menu",
5
+ description: "Test context menus registered by the extension. Can verify the contextMenus API is available and simulate right-click events on page elements.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ action: {
10
+ type: "string",
11
+ enum: ["check_api", "right_click", "trigger_item"],
12
+ description: "check_api: verify contextMenus API is available in service worker; right_click: simulate right-click on a selector (triggers contextmenu DOM event); trigger_item: invoke a context menu item handler by id via service worker",
13
+ },
14
+ url: {
15
+ type: "string",
16
+ description: "URL to navigate to before right-clicking (optional)",
17
+ },
18
+ selector: {
19
+ type: "string",
20
+ description: "CSS selector of the element to right-click (for 'right_click' action)",
21
+ },
22
+ menu_item_id: {
23
+ type: "string",
24
+ description: "The context menu item ID to trigger (for 'trigger_item' action — matches the id passed to chrome.contextMenus.create)",
25
+ },
26
+ page_url: {
27
+ type: "string",
28
+ description: "The pageUrl to pass to the onClicked handler (for 'trigger_item' action)",
29
+ },
30
+ },
31
+ required: ["action"],
32
+ },
33
+ };
34
+
35
+ export async function handler(args) {
36
+ if (args.action === "check_api") {
37
+ const sw = await getServiceWorker();
38
+ const result = await sw.evaluate(() => ({
39
+ available: typeof chrome.contextMenus !== "undefined",
40
+ hasCreate: typeof chrome.contextMenus?.create === "function",
41
+ hasRemove: typeof chrome.contextMenus?.remove === "function",
42
+ hasUpdate: typeof chrome.contextMenus?.update === "function",
43
+ }));
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text: `Context Menus API:\n${JSON.stringify(result, null, 2)}\n\nNote: chrome.contextMenus.getAll() is not available in MV3 service workers. Items are registered imperatively via chrome.contextMenus.create().`,
48
+ }],
49
+ };
50
+ }
51
+
52
+ if (args.action === "right_click") {
53
+ if (!args.selector) return { content: [{ type: "text", text: "Provide a 'selector' for right_click action." }] };
54
+ const p = await ensurePage();
55
+ if (args.url) await p.goto(args.url, { waitUntil: "domcontentloaded" });
56
+ await p.click(args.selector, { button: "right" });
57
+ return {
58
+ content: [{
59
+ type: "text",
60
+ text: `Right-click dispatched on "${args.selector}".\nThis fires the contextmenu DOM event on the element. Note: native Chrome context menus cannot be interacted with via Playwright — use 'trigger_item' to invoke the handler directly.`,
61
+ }],
62
+ };
63
+ }
64
+
65
+ if (args.action === "trigger_item") {
66
+ if (!args.menu_item_id) return { content: [{ type: "text", text: "Provide a 'menu_item_id' to trigger." }] };
67
+ const sw = await getServiceWorker();
68
+ // Directly invoke the onClicked listener by dispatching a synthetic event via the SW
69
+ const result = await sw.evaluate(({ menuItemId, pageUrl }) => {
70
+ return new Promise((resolve) => {
71
+ const info = { menuItemId, editable: false, pageUrl: pageUrl || "about:blank" };
72
+ const tab = { id: 0, index: 0, url: pageUrl || "about:blank", active: true, highlighted: true, pinned: false, incognito: false };
73
+ try {
74
+ chrome.contextMenus.onClicked.dispatch(info, tab);
75
+ resolve({ dispatched: true, menuItemId });
76
+ } catch (e) {
77
+ resolve({ dispatched: false, error: e.message });
78
+ }
79
+ });
80
+ }, { menuItemId: args.menu_item_id, pageUrl: args.page_url });
81
+ return {
82
+ content: [{ type: "text", text: `Context menu trigger result:\n${JSON.stringify(result, null, 2)}` }],
83
+ };
84
+ }
85
+ }
@@ -0,0 +1,47 @@
1
+ import { ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "inspect_dom",
5
+ description: "Inspect DOM elements or run JavaScript in a page context. Optionally navigate to a URL first.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ url: {
10
+ type: "string",
11
+ description: "URL to navigate to before inspecting (optional)",
12
+ },
13
+ selector: {
14
+ type: "string",
15
+ description: "CSS selector to query — returns outerHTML of all matching elements",
16
+ },
17
+ script: {
18
+ type: "string",
19
+ description: "JavaScript expression to evaluate in page context (overrides selector)",
20
+ },
21
+ },
22
+ },
23
+ };
24
+
25
+ export async function handler(args) {
26
+ const p = await ensurePage();
27
+ if (args.url) await p.goto(args.url, { waitUntil: "domcontentloaded" });
28
+
29
+ if (args.script) {
30
+ const result = await p.evaluate(args.script);
31
+ return { content: [{ type: "text", text: `Script result:\n${JSON.stringify(result, null, 2)}` }] };
32
+ }
33
+
34
+ if (args.selector) {
35
+ const elements = await p.$$eval(args.selector, (els) => els.map((el) => el.outerHTML));
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: elements.length
40
+ ? `Found ${elements.length} element(s) matching "${args.selector}":\n\n${elements.join("\n\n")}`
41
+ : `No elements found matching "${args.selector}"`,
42
+ }],
43
+ };
44
+ }
45
+
46
+ return { content: [{ type: "text", text: "Provide either a selector or a script." }] };
47
+ }
@@ -0,0 +1,35 @@
1
+ import * as loadExtension from "./load-extension.js";
2
+ import * as popup from "./popup.js";
3
+ import * as dom from "./dom.js";
4
+ import * as logs from "./logs.js";
5
+ import * as screenshot from "./screenshot.js";
6
+ import * as assertion from "./assertion.js";
7
+ import * as storage from "./storage.js";
8
+ import * as network from "./network.js";
9
+ import * as optionsPage from "./options-page.js";
10
+ import * as contextMenu from "./context-menu.js";
11
+ import * as badge from "./badge.js";
12
+ import * as messaging from "./messaging.js";
13
+ import * as tabs from "./tabs.js";
14
+
15
+ const allTools = [
16
+ loadExtension,
17
+ popup,
18
+ dom,
19
+ logs,
20
+ screenshot,
21
+ assertion,
22
+ storage,
23
+ network,
24
+ optionsPage,
25
+ contextMenu,
26
+ badge,
27
+ messaging,
28
+ tabs,
29
+ ];
30
+
31
+ export const TOOLS = allTools.map((t) => t.definition);
32
+
33
+ export const HANDLERS = Object.fromEntries(
34
+ allTools.map((t) => [t.definition.name, t.handler])
35
+ );
@@ -0,0 +1,48 @@
1
+ import path from "path";
2
+ import { state, ensureBrowser } from "../state.js";
3
+
4
+ export const definition = {
5
+ name: "load_extension",
6
+ description: "Load an unpacked Chrome extension from a local path and launch the browser. Call again to reload/restart.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ extension_path: {
11
+ type: "string",
12
+ description: "Absolute or relative path to the unpacked extension folder (must contain manifest.json)",
13
+ },
14
+ },
15
+ required: ["extension_path"],
16
+ },
17
+ };
18
+
19
+ export async function handler(args) {
20
+ if (state.browser) {
21
+ await state.browser.close();
22
+ state.browser = null;
23
+ state.page = null;
24
+ state.extensionId = null;
25
+ state.swLogs.length = 0;
26
+ state.networkCaptures.length = 0;
27
+ }
28
+
29
+ await ensureBrowser(args.extension_path);
30
+
31
+ state.browser.serviceWorkers().forEach((sw) => {
32
+ sw.on("console", (msg) =>
33
+ state.swLogs.push(`[${new Date().toISOString()}] ${msg.type()}: ${msg.text()}`)
34
+ );
35
+ });
36
+ state.browser.on("serviceworker", (sw) => {
37
+ sw.on("console", (msg) =>
38
+ state.swLogs.push(`[${new Date().toISOString()}] ${msg.type()}: ${msg.text()}`)
39
+ );
40
+ });
41
+
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: `Extension loaded.\nPath: ${path.resolve(args.extension_path)}\nExtension ID: ${state.extensionId || "unknown (no service worker detected)"}`,
46
+ }],
47
+ };
48
+ }
@@ -0,0 +1,28 @@
1
+ import { state } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "get_service_worker_logs",
5
+ description: "Fetch console logs captured from the extension's background service worker.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ clear_after: {
10
+ type: "boolean",
11
+ description: "Clear the log buffer after reading (default: false)",
12
+ },
13
+ },
14
+ },
15
+ };
16
+
17
+ export async function handler(args) {
18
+ const logs = [...state.swLogs];
19
+ if (args.clear_after) state.swLogs.length = 0;
20
+ return {
21
+ content: [{
22
+ type: "text",
23
+ text: logs.length
24
+ ? `Service Worker Logs (${logs.length} entries):\n\n${logs.join("\n")}`
25
+ : "No service worker logs captured yet.",
26
+ }],
27
+ };
28
+ }
@@ -0,0 +1,66 @@
1
+ import { state, ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "send_message_to_background",
5
+ description: "Send a chrome.runtime.sendMessage to the extension's background service worker and return the response. Runs from within the extension's popup context.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ message: {
10
+ type: "object",
11
+ description: "The message object to send (must be JSON-serializable)",
12
+ },
13
+ timeout_ms: {
14
+ type: "number",
15
+ description: "Milliseconds to wait for a response before timing out (default: 5000)",
16
+ },
17
+ },
18
+ required: ["message"],
19
+ },
20
+ };
21
+
22
+ export async function handler(args) {
23
+ if (!state.extensionId) {
24
+ return { content: [{ type: "text", text: "Extension ID not detected. Call load_extension first." }] };
25
+ }
26
+
27
+ const p = await ensurePage();
28
+ const currentUrl = p.url();
29
+
30
+ // Must be in extension context to use chrome.runtime.sendMessage
31
+ if (!currentUrl.startsWith(`chrome-extension://${state.extensionId}`)) {
32
+ await p.goto(`chrome-extension://${state.extensionId}/popup.html`, { waitUntil: "domcontentloaded" });
33
+ }
34
+
35
+ const timeout = args.timeout_ms || 5000;
36
+
37
+ const result = await p.evaluate(
38
+ ({ message, timeout }) =>
39
+ new Promise((resolve) => {
40
+ const timer = setTimeout(
41
+ () => resolve({ error: "Timeout: no response received within " + timeout + "ms" }),
42
+ timeout
43
+ );
44
+ chrome.runtime.sendMessage(message, (response) => {
45
+ clearTimeout(timer);
46
+ if (chrome.runtime.lastError) {
47
+ resolve({ error: chrome.runtime.lastError.message });
48
+ } else {
49
+ resolve({ response });
50
+ }
51
+ });
52
+ }),
53
+ { message: args.message, timeout }
54
+ );
55
+
56
+ if (result.error) {
57
+ return { content: [{ type: "text", text: `Message failed: ${result.error}` }] };
58
+ }
59
+
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: `Message sent: ${JSON.stringify(args.message, null, 2)}\n\nResponse: ${JSON.stringify(result.response, null, 2)}`,
64
+ }],
65
+ };
66
+ }
@@ -0,0 +1,84 @@
1
+ import { state, ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "monitor_network",
5
+ description: "Monitor, capture, and inspect network requests. Useful for testing extensions that block, redirect, or modify requests (e.g. ad blockers, header modifiers).",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ action: {
10
+ type: "string",
11
+ enum: ["navigate_and_capture", "get_captured", "clear"],
12
+ description: "navigate_and_capture: go to a URL and record all requests; get_captured: return buffered results; clear: empty the buffer",
13
+ },
14
+ url: {
15
+ type: "string",
16
+ description: "URL to navigate to (required for navigate_and_capture)",
17
+ },
18
+ filter_pattern: {
19
+ type: "string",
20
+ description: "Only show requests whose URL contains this string (optional)",
21
+ },
22
+ include_types: {
23
+ type: "array",
24
+ items: { type: "string" },
25
+ description: "Filter by resource type: document, script, stylesheet, image, xhr, fetch, etc. (optional)",
26
+ },
27
+ },
28
+ required: ["action"],
29
+ },
30
+ };
31
+
32
+ export async function handler(args) {
33
+ if (args.action === "clear") {
34
+ state.networkCaptures.length = 0;
35
+ return { content: [{ type: "text", text: "Network capture buffer cleared." }] };
36
+ }
37
+
38
+ if (args.action === "get_captured") {
39
+ let entries = [...state.networkCaptures];
40
+ if (args.filter_pattern) entries = entries.filter((r) => r.url.includes(args.filter_pattern));
41
+ if (args.include_types?.length) entries = entries.filter((r) => args.include_types.includes(r.resourceType));
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: entries.length
46
+ ? `${entries.length} captured request(s):\n\n${entries.map((r) => `[${r.method}] [${r.resourceType}] ${r.status} ${r.url}`).join("\n")}`
47
+ : "No captured requests match the filter.",
48
+ }],
49
+ };
50
+ }
51
+
52
+ if (args.action === "navigate_and_capture") {
53
+ if (!args.url) return { content: [{ type: "text", text: "Provide a 'url' to navigate to." }] };
54
+
55
+ const p = await ensurePage();
56
+ const captured = [];
57
+
58
+ const onResponse = (response) => {
59
+ captured.push({
60
+ method: response.request().method(),
61
+ url: response.url(),
62
+ status: response.status(),
63
+ resourceType: response.request().resourceType(),
64
+ });
65
+ };
66
+
67
+ p.on("response", onResponse);
68
+ await p.goto(args.url, { waitUntil: "networkidle" });
69
+ p.off("response", onResponse);
70
+
71
+ captured.forEach((r) => state.networkCaptures.push(r));
72
+
73
+ let displayed = captured;
74
+ if (args.filter_pattern) displayed = displayed.filter((r) => r.url.includes(args.filter_pattern));
75
+ if (args.include_types?.length) displayed = displayed.filter((r) => args.include_types.includes(r.resourceType));
76
+
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: `Captured ${captured.length} total request(s)${args.filter_pattern ? ` (${displayed.length} matching "${args.filter_pattern}")` : ""}:\n\n${displayed.map((r) => `[${r.method}] [${r.resourceType}] ${r.status} ${r.url}`).join("\n")}`,
81
+ }],
82
+ };
83
+ }
84
+ }
@@ -0,0 +1,64 @@
1
+ import { state, ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "open_options_page",
5
+ description: "Open the extension options/settings page (options.html or a custom page). Supports any extension page by filename.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ page: {
10
+ type: "string",
11
+ description: "Extension page filename to open (default: options.html). Change if the extension uses a different filename.",
12
+ },
13
+ action: {
14
+ type: "string",
15
+ enum: ["open", "click", "type", "get_text", "get_html"],
16
+ description: "Action to perform after opening (optional, defaults to just opening)",
17
+ },
18
+ selector: {
19
+ type: "string",
20
+ description: "CSS selector for interaction actions",
21
+ },
22
+ value: {
23
+ type: "string",
24
+ description: "Value to type (for 'type' action)",
25
+ },
26
+ },
27
+ },
28
+ };
29
+
30
+ export async function handler(args) {
31
+ if (!state.extensionId) {
32
+ return { content: [{ type: "text", text: "Extension ID not detected. Call load_extension first." }] };
33
+ }
34
+
35
+ const pageName = args.page || "options.html";
36
+ const optionsUrl = `chrome-extension://${state.extensionId}/${pageName}`;
37
+ const p = await ensurePage();
38
+
39
+ await p.goto(optionsUrl, { waitUntil: "domcontentloaded" });
40
+
41
+ if (!args.action || args.action === "open") {
42
+ return { content: [{ type: "text", text: `Options page opened at ${optionsUrl}` }] };
43
+ }
44
+
45
+ if (args.action === "click") {
46
+ await p.click(args.selector);
47
+ return { content: [{ type: "text", text: `Clicked: ${args.selector}` }] };
48
+ }
49
+
50
+ if (args.action === "type") {
51
+ await p.fill(args.selector, args.value || "");
52
+ return { content: [{ type: "text", text: `Typed "${args.value}" into ${args.selector}` }] };
53
+ }
54
+
55
+ if (args.action === "get_text") {
56
+ const text = await p.textContent(args.selector);
57
+ return { content: [{ type: "text", text: `Text content of "${args.selector}":\n${text}` }] };
58
+ }
59
+
60
+ if (args.action === "get_html") {
61
+ const html = await p.innerHTML(args.selector);
62
+ return { content: [{ type: "text", text: `Inner HTML of "${args.selector}":\n${html}` }] };
63
+ }
64
+ }
@@ -0,0 +1,61 @@
1
+ import { state, ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "interact_with_popup",
5
+ description: "Open the extension popup and interact with UI elements (click, type, read text/HTML).",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ action: {
10
+ type: "string",
11
+ enum: ["open", "click", "type", "get_text", "get_html"],
12
+ description: "Action to perform",
13
+ },
14
+ selector: {
15
+ type: "string",
16
+ description: "CSS selector for the target element (not needed for 'open')",
17
+ },
18
+ value: {
19
+ type: "string",
20
+ description: "Text to type (only for 'type' action)",
21
+ },
22
+ },
23
+ required: ["action"],
24
+ },
25
+ };
26
+
27
+ export async function handler(args) {
28
+ if (!state.extensionId && args.action === "open") {
29
+ return {
30
+ content: [{ type: "text", text: "Extension ID not detected. Make sure the extension has a background service worker." }],
31
+ };
32
+ }
33
+
34
+ const p = await ensurePage();
35
+
36
+ if (args.action === "open") {
37
+ const popupUrl = `chrome-extension://${state.extensionId}/popup.html`;
38
+ await p.goto(popupUrl, { waitUntil: "domcontentloaded" });
39
+ return { content: [{ type: "text", text: `Popup opened at ${popupUrl}` }] };
40
+ }
41
+
42
+ if (args.action === "click") {
43
+ await p.click(args.selector);
44
+ return { content: [{ type: "text", text: `Clicked: ${args.selector}` }] };
45
+ }
46
+
47
+ if (args.action === "type") {
48
+ await p.fill(args.selector, args.value || "");
49
+ return { content: [{ type: "text", text: `Typed "${args.value}" into ${args.selector}` }] };
50
+ }
51
+
52
+ if (args.action === "get_text") {
53
+ const text = await p.textContent(args.selector);
54
+ return { content: [{ type: "text", text: `Text content of "${args.selector}":\n${text}` }] };
55
+ }
56
+
57
+ if (args.action === "get_html") {
58
+ const html = await p.innerHTML(args.selector);
59
+ return { content: [{ type: "text", text: `Inner HTML of "${args.selector}":\n${html}` }] };
60
+ }
61
+ }
@@ -0,0 +1,27 @@
1
+ import path from "path";
2
+ import { ensurePage } from "../state.js";
3
+
4
+ export const definition = {
5
+ name: "take_screenshot",
6
+ description: "Take a screenshot of the current browser page or popup.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ output_path: {
11
+ type: "string",
12
+ description: "File path to save the screenshot (default: ./screenshot.png)",
13
+ },
14
+ full_page: {
15
+ type: "boolean",
16
+ description: "Capture the full scrollable page (default: false)",
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export async function handler(args) {
23
+ const p = await ensurePage();
24
+ const outPath = path.resolve(args.output_path || "./screenshot.png");
25
+ await p.screenshot({ path: outPath, fullPage: args.full_page || false });
26
+ return { content: [{ type: "text", text: `Screenshot saved to: ${outPath}` }] };
27
+ }