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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bhuvanrj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # Chrome Extension Tester — MCP Server
2
+
3
+ An **MCP (Model Context Protocol) server** that lets Claude interactively test any unpacked Chrome extension using Playwright. Load your extension, interact with its popup and options page, inspect storage, monitor network requests, check badges, test messaging, and more — all through natural language.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Requirements](#requirements)
11
+ - [Installation](#installation)
12
+ - [Setup with Claude Desktop](#setup-with-claude-desktop)
13
+ - [Setup with Claude Code (npx)](#setup-with-claude-code-npx)
14
+ - [Available Tools](#available-tools)
15
+ - [Testing Agent Prompt](#testing-agent-prompt)
16
+ - [Example Prompts](#example-prompts)
17
+ - [Project Structure](#project-structure)
18
+ - [Notes](#notes)
19
+
20
+ ---
21
+
22
+ ## Features
23
+
24
+ - Load and reload any unpacked Chrome extension
25
+ - Interact with popup and options pages (click, type, read content)
26
+ - Inspect and manipulate `chrome.storage` (local / sync / session)
27
+ - Read background service worker console logs
28
+ - Monitor and inspect network requests
29
+ - Check and assert badge text and color
30
+ - Send messages to the background script and validate responses
31
+ - Simulate tab open / close / switch events
32
+ - Test context menu registration and handler invocation
33
+ - Run assertions that return structured PASS / FAIL results
34
+ - Take screenshots at any point during testing
35
+
36
+ ---
37
+
38
+ ## Requirements
39
+
40
+ - **Node.js** 18 or higher
41
+ - **Claude Desktop** or **Claude Code** with MCP support
42
+ - A Chrome extension with a `manifest.json` (Manifest V2 or V3)
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ### Option A — npx (no install needed)
49
+
50
+ ```bash
51
+ npx chrome-extension-tester-mcp
52
+ ```
53
+
54
+ ### Option B — install globally
55
+
56
+ ```bash
57
+ npm install -g chrome-extension-tester-mcp
58
+ ```
59
+
60
+ ### Option C — clone and run locally
61
+
62
+ ```bash
63
+ git clone https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp.git
64
+ cd chrome-extension-testing-mcp
65
+ npm install
66
+ npx playwright install chromium
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Setup with Claude Desktop
72
+
73
+ Add the following to your Claude Desktop MCP config file:
74
+
75
+ **macOS / Linux** — `~/.config/claude/claude_desktop_config.json`
76
+ **Windows** — `%APPDATA%\Claude\claude_desktop_config.json`
77
+
78
+ ### Using npx (recommended)
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "chrome-extension-tester": {
84
+ "command": "npx",
85
+ "args": ["chrome-extension-tester-mcp"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Using a local clone
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "chrome-extension-tester": {
97
+ "command": "node",
98
+ "args": ["/absolute/path/to/chrome-extension-testing-mcp/src/index.js"]
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Restart Claude Desktop after saving the config.
105
+
106
+ ---
107
+
108
+ ## Setup with Claude Code (npx)
109
+
110
+ Add to your project's `.mcp.json` or user-level MCP config:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "chrome-extension-tester": {
116
+ "command": "npx",
117
+ "args": ["chrome-extension-tester-mcp"]
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Available Tools
126
+
127
+ | Tool | What it does |
128
+ |------|-------------|
129
+ | `load_extension` | Launch Chromium with an unpacked extension; captures the extension ID automatically |
130
+ | `interact_with_popup` | Open the popup, then click elements, type text, or read content |
131
+ | `open_options_page` | Open the extension's options / settings page and interact with it |
132
+ | `inspect_dom` | Navigate to a URL, query a DOM selector, or evaluate arbitrary JavaScript |
133
+ | `get_service_worker_logs` | Read buffered background service worker console logs; optionally clear them |
134
+ | `take_screenshot` | Save a screenshot of the current page or popup |
135
+ | `run_assertion` | Assert that an element exists, has specific text, or a JS expression is truthy — returns PASS or FAIL |
136
+ | `extension_storage` | Get, set, remove, or clear keys in `chrome.storage.local`, `.sync`, or `.session` |
137
+ | `monitor_network` | Capture network requests during navigation; retrieve or clear the captured list |
138
+ | `check_badge` | Read or assert the extension action badge text and background color |
139
+ | `send_message_to_background` | Send `chrome.runtime.sendMessage` from the popup context and return the response |
140
+ | `test_context_menu` | Check `contextMenus` API availability, simulate right-click, or invoke a menu item handler directly |
141
+ | `simulate_tab_events` | Open, close, switch, list, or close all browser tabs |
142
+
143
+ ---
144
+
145
+ ## Testing Agent Prompt
146
+
147
+ The server includes a built-in MCP prompt called **`extension-tester-agent`** — a fully automated testing agent that validates all implemented changes and returns a structured report.
148
+
149
+ ### Arguments
150
+
151
+ | Argument | Required | Description |
152
+ |----------|----------|-------------|
153
+ | `extension_path` | yes | Absolute path to the unpacked extension folder |
154
+ | `extension_description` | yes | What the extension does — features, UI, storage, background behaviour |
155
+ | `changes` | yes | Everything implemented or changed in this session |
156
+
157
+ ### What it does
158
+
159
+ 1. **Understands** the extension and derives a set of tests from the changes list
160
+ 2. **Writes a test plan** — every change maps to at least one test and the right MCP tool
161
+ 3. **Executes every test** — never skips, takes screenshots on failure
162
+ 4. **Reports** a structured PASS / FAIL table with details on any failures
163
+
164
+ ### How to invoke
165
+
166
+ After implementing changes, tell Claude:
167
+
168
+ ```
169
+ Use the extension-tester-agent prompt with:
170
+ - extension_path: /path/to/my-extension
171
+ - extension_description: "A tab manager that saves sessions to chrome.storage.local and restores them via a popup"
172
+ - changes: "Added save button; save button writes open tabs to storage.local; badge shows count of saved tabs"
173
+ ```
174
+
175
+ Claude will write the test plan, execute every test, and return a full report.
176
+
177
+ ---
178
+
179
+ ## Example Prompts
180
+
181
+ ```
182
+ Load my extension from /Users/me/my-extension and open the popup
183
+ ```
184
+
185
+ ```
186
+ Click the button with selector #save and take a screenshot
187
+ ```
188
+
189
+ ```
190
+ Navigate to https://example.com and check if my content script injected a .banner element
191
+ ```
192
+
193
+ ```
194
+ Read all keys from chrome.storage.local
195
+ ```
196
+
197
+ ```
198
+ Set { "enabled": true } in chrome.storage.local and verify it was saved
199
+ ```
200
+
201
+ ```
202
+ Navigate to https://example.com, capture all network requests, then show me any that were blocked
203
+ ```
204
+
205
+ ```
206
+ Check the badge text — it should say "ON"
207
+ ```
208
+
209
+ ```
210
+ Send the message { "type": "GET_STATUS" } to the background and show the response
211
+ ```
212
+
213
+ ```
214
+ Open a tab to https://news.ycombinator.com, then another to https://github.com, then list all open tabs
215
+ ```
216
+
217
+ ```
218
+ Right-click on https://example.com and trigger the context menu item with id "my-action"
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Project Structure
224
+
225
+ ```
226
+ chrome-extension-testing-mcp/
227
+ ├── src/
228
+ │ ├── index.js # MCP server entry point
229
+ │ ├── state.js # Shared browser state and helpers
230
+ │ ├── prompts/
231
+ │ │ ├── index.js # Registers MCP prompts
232
+ │ │ └── extension-tester.js # extension-tester-agent prompt definition
233
+ │ └── tools/
234
+ │ ├── index.js # Aggregates all tool definitions and handlers
235
+ │ ├── load-extension.js
236
+ │ ├── popup.js
237
+ │ ├── dom.js
238
+ │ ├── logs.js
239
+ │ ├── screenshot.js
240
+ │ ├── assertion.js
241
+ │ ├── storage.js
242
+ │ ├── network.js
243
+ │ ├── options-page.js
244
+ │ ├── context-menu.js
245
+ │ ├── badge.js
246
+ │ ├── messaging.js
247
+ │ └── tabs.js
248
+ ├── package.json
249
+ └── README.md
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Notes
255
+
256
+ - The browser launches in **headed mode** (visible window) so you can watch tests run in real time
257
+ - Screenshots default to `./screenshot.png` unless a custom path is provided
258
+ - Service worker logs are buffered from the moment `load_extension` is called
259
+ - Call `load_extension` again at any time to get a fresh browser instance
260
+ - Native Chrome context menus cannot be automated by Playwright — use `test_context_menu` with `trigger_item` to invoke handlers directly
261
+ - Badge and storage tools communicate via the service worker, so the extension must have a background service worker (MV3)
262
+
263
+ ---
264
+
265
+ ## License
266
+
267
+ MIT
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "chrome-extension-tester-mcp",
3
+ "version": "2.0.0",
4
+ "description": "MCP server for interactive Chrome extension testing via Playwright — load, interact, assert, inspect storage, network, badges, messaging, tabs, and more.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "chrome-extension-tester": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node --watch src/index.js",
13
+ "postinstall": "playwright install chromium"
14
+ },
15
+ "files": [
16
+ "src/"
17
+ ],
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "chrome-extension",
22
+ "testing",
23
+ "playwright",
24
+ "automation",
25
+ "browser-testing",
26
+ "claude"
27
+ ],
28
+ "author": "bhuvanrj",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp"
33
+ },
34
+ "homepage": "https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp/issues"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.0",
40
+ "playwright": "^1.44.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }
package/src/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ ListPromptsRequestSchema,
8
+ GetPromptRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { TOOLS, HANDLERS } from "./tools/index.js";
11
+ import { PROMPTS, PROMPT_HANDLERS } from "./prompts/index.js";
12
+
13
+ const server = new Server(
14
+ { name: "chrome-extension-tester", version: "2.0.0" },
15
+ { capabilities: { tools: {}, prompts: {} } }
16
+ );
17
+
18
+ // ── Tools ─────────────────────────────────────────────────────────────────────
19
+
20
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
21
+
22
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
23
+ const { name, arguments: args } = request.params;
24
+ const handler = HANDLERS[name];
25
+
26
+ if (!handler) {
27
+ return {
28
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
29
+ isError: true,
30
+ };
31
+ }
32
+
33
+ try {
34
+ return await handler(args || {});
35
+ } catch (err) {
36
+ return {
37
+ content: [{ type: "text", text: `Error in ${name}: ${err.message}` }],
38
+ isError: true,
39
+ };
40
+ }
41
+ });
42
+
43
+ // ── Prompts ───────────────────────────────────────────────────────────────────
44
+
45
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
46
+
47
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
48
+ const { name, arguments: args } = request.params;
49
+ const getMessages = PROMPT_HANDLERS[name];
50
+
51
+ if (!getMessages) {
52
+ throw new Error(`Unknown prompt: ${name}`);
53
+ }
54
+
55
+ const definition = PROMPTS.find((p) => p.name === name);
56
+
57
+ return {
58
+ description: definition.description,
59
+ messages: getMessages(args || {}),
60
+ };
61
+ });
62
+
63
+ // ── Start ─────────────────────────────────────────────────────────────────────
64
+
65
+ const transport = new StdioServerTransport();
66
+ await server.connect(transport);
67
+ console.error("Chrome Extension Tester MCP server running (v2.0.0)...");
@@ -0,0 +1,138 @@
1
+ export const definition = {
2
+ name: "extension-tester-agent",
3
+ description:
4
+ "A testing agent that validates all implemented changes in a Chrome extension. Understands what the extension does, derives tests from the changes list, runs every test using the MCP tools, and returns a structured PASS/FAIL report.",
5
+ arguments: [
6
+ {
7
+ name: "extension_path",
8
+ description: "Absolute path to the unpacked extension folder (must contain manifest.json)",
9
+ required: true,
10
+ },
11
+ {
12
+ name: "extension_description",
13
+ description:
14
+ "What this extension does — its purpose, key features, UI elements, storage it uses, pages it injects into, background behaviour, etc. The more detail the better.",
15
+ required: true,
16
+ },
17
+ {
18
+ name: "changes",
19
+ description:
20
+ "Everything that was implemented or changed in this session. List each change on its own line or separated by semicolons.",
21
+ required: true,
22
+ },
23
+ ],
24
+ };
25
+
26
+ export function getMessages({ extension_path, extension_description, changes }) {
27
+ return [
28
+ {
29
+ role: "user",
30
+ content: {
31
+ type: "text",
32
+ text: `
33
+ You are a Chrome extension testing agent. Your job is to validate that every implemented change works correctly before the result is returned to the user.
34
+
35
+ You have access to the chrome-extension-tester MCP tools:
36
+ - load_extension → launch Chrome with the extension loaded
37
+ - interact_with_popup → open popup, click, type, read text/HTML
38
+ - open_options_page → open and interact with the options/settings page
39
+ - inspect_dom → navigate to a URL, query selectors, or run JS
40
+ - get_service_worker_logs → read background service worker console output
41
+ - take_screenshot → save a screenshot (use on any failure)
42
+ - run_assertion → assert element exists, has text, or a JS expression is true
43
+ - extension_storage → get/set/remove/clear chrome.storage (local/sync/session)
44
+ - monitor_network → capture network requests during navigation
45
+ - check_badge → read or assert the extension icon badge
46
+ - send_message_to_background → send chrome.runtime.sendMessage and capture the response
47
+ - test_context_menu → verify context menu API or simulate right-click events
48
+ - simulate_tab_events → open, close, switch, and list browser tabs
49
+
50
+ ---
51
+
52
+ ## Extension
53
+
54
+ **Path:** ${extension_path}
55
+
56
+ **What it does:**
57
+ ${extension_description}
58
+
59
+ ---
60
+
61
+ ## Changes to validate
62
+
63
+ ${changes}
64
+
65
+ ---
66
+
67
+ ## Your process — follow this exactly
68
+
69
+ ### Phase 1 — Understand
70
+ Read the extension description and the changes list carefully.
71
+ For each change, reason about:
72
+ - What part of the extension is affected (popup UI, background logic, content script, storage, messaging, options page, badge, context menu, network, tabs)
73
+ - What the correct behaviour looks like after this change
74
+ - Which MCP tools are the right ones to verify it
75
+
76
+ ### Phase 2 — Write a test plan
77
+ Before running anything, output a numbered test plan:
78
+ - One line per test
79
+ - Format: [TOOL] Description of what is being verified
80
+ - Cover EVERY change — do not skip anything
81
+
82
+ Example:
83
+ 1. [load_extension] Load extension from path, confirm extension ID is detected
84
+ 2. [interact_with_popup] Open popup, assert #save-button exists
85
+ 3. [run_assertion] Assert #save-button has text "Save"
86
+ 4. [extension_storage] After clicking save, verify storage.local has { saved: true }
87
+ 5. [get_service_worker_logs] Check no errors logged during save flow
88
+
89
+ ### Phase 3 — Execute every test
90
+ - Start by calling load_extension
91
+ - Work through the test plan top to bottom
92
+ - For each test: call the tool, record the result (PASS / FAIL + detail)
93
+ - On any FAIL: call take_screenshot immediately, note the output path
94
+ - Do not stop on failure — run every test and collect all results
95
+
96
+ ### Phase 4 — Report
97
+ Output a final report in this exact format:
98
+
99
+ ---
100
+ ## Test Report
101
+
102
+ **Extension:** ${extension_path}
103
+ **Result:** PASS ← or FAIL
104
+
105
+ ### Results
106
+
107
+ | # | Test | Result | Detail |
108
+ |---|------|--------|--------|
109
+ | 1 | [load_extension] Load extension | PASS | ID: abcdef... |
110
+ | 2 | [interact_with_popup] #save-button exists | FAIL | Element not found |
111
+ | 3 | ... | ... | ... |
112
+
113
+ ### Failures
114
+ (only if FAIL)
115
+ For each failed test:
116
+ - What was expected
117
+ - What was actually observed
118
+ - Screenshot path if taken
119
+ - Likely cause based on the change description
120
+
121
+ ### Summary
122
+ X of Y tests passed.
123
+ (If all pass: "All changes verified. Ready to return to user.")
124
+ (If any fail: "Do not return to user. Fix the following before re-testing: ...")
125
+ ---
126
+
127
+ ## Rules
128
+ - Always load_extension first, even if the browser is already open
129
+ - Never mark the result PASS if any single test failed
130
+ - If a tool call errors, that counts as a FAIL for that test
131
+ - Do not invent tests for things not in the changes list
132
+ - Do not skip tests because they seem obvious
133
+ - Be concise in tool calls — don't narrate, just execute
134
+ `.trim(),
135
+ },
136
+ },
137
+ ];
138
+ }
@@ -0,0 +1,9 @@
1
+ import * as extensionTester from "./extension-tester.js";
2
+
3
+ const allPrompts = [extensionTester];
4
+
5
+ export const PROMPTS = allPrompts.map((p) => p.definition);
6
+
7
+ export const PROMPT_HANDLERS = Object.fromEntries(
8
+ allPrompts.map((p) => [p.definition.name, p.getMessages])
9
+ );
package/src/state.js ADDED
@@ -0,0 +1,50 @@
1
+ import { chromium } from "playwright";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ export const state = {
6
+ browser: null,
7
+ page: null,
8
+ extensionId: null,
9
+ swLogs: [],
10
+ networkCaptures: [],
11
+ };
12
+
13
+ export async function ensureBrowser(extensionPath) {
14
+ if (state.browser) return;
15
+ const absPath = path.resolve(extensionPath);
16
+ if (!fs.existsSync(absPath)) throw new Error(`Extension path not found: ${absPath}`);
17
+
18
+ state.browser = await chromium.launchPersistentContext("", {
19
+ headless: false,
20
+ args: [
21
+ `--disable-extensions-except=${absPath}`,
22
+ `--load-extension=${absPath}`,
23
+ ],
24
+ });
25
+
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];
32
+ }
33
+
34
+ state.page = await state.browser.newPage();
35
+ }
36
+
37
+ export async function ensurePage() {
38
+ if (!state.page || state.page.isClosed()) {
39
+ if (!state.browser) throw new Error("Browser not started. Call load_extension first.");
40
+ state.page = await state.browser.newPage();
41
+ }
42
+ return state.page;
43
+ }
44
+
45
+ export async function getServiceWorker() {
46
+ if (!state.browser) throw new Error("Browser not started. Call load_extension first.");
47
+ const workers = state.browser.serviceWorkers();
48
+ if (!workers.length) throw new Error("No service worker found. Extension may not have a background service worker.");
49
+ return workers[0];
50
+ }
@@ -0,0 +1,66 @@
1
+ import { ensurePage } from "../state.js";
2
+
3
+ export const definition = {
4
+ name: "run_assertion",
5
+ description: "Run a test assertion against the current page. Checks a condition and reports PASS or FAIL.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ description: {
10
+ type: "string",
11
+ description: "Human-readable description of what is being tested",
12
+ },
13
+ selector: {
14
+ type: "string",
15
+ description: "CSS selector to check existence or text content of",
16
+ },
17
+ expected_text: {
18
+ type: "string",
19
+ description: "Expected text content of the element (optional, used with selector)",
20
+ },
21
+ script: {
22
+ type: "string",
23
+ description: "JS expression that must return true to pass (overrides selector)",
24
+ },
25
+ },
26
+ required: ["description"],
27
+ },
28
+ };
29
+
30
+ export async function handler(args) {
31
+ const p = await ensurePage();
32
+ let passed = false;
33
+ let detail = "";
34
+
35
+ try {
36
+ if (args.script) {
37
+ passed = !!(await p.evaluate(args.script));
38
+ detail = `Script: ${args.script}`;
39
+ } else if (args.selector) {
40
+ const el = await p.$(args.selector);
41
+ if (!el) {
42
+ passed = false;
43
+ detail = `Element "${args.selector}" not found`;
44
+ } else if (args.expected_text !== undefined) {
45
+ const actual = await el.textContent();
46
+ passed = actual.trim() === args.expected_text.trim();
47
+ detail = `Expected: "${args.expected_text}" | Got: "${actual.trim()}"`;
48
+ } else {
49
+ passed = true;
50
+ detail = `Element "${args.selector}" exists`;
51
+ }
52
+ } else {
53
+ return { content: [{ type: "text", text: "Provide a selector or script for the assertion." }] };
54
+ }
55
+ } catch (e) {
56
+ passed = false;
57
+ detail = `Error: ${e.message}`;
58
+ }
59
+
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: `${passed ? "PASS" : "FAIL"} — ${args.description}\n${detail}`,
64
+ }],
65
+ };
66
+ }