firefox-devtools-mcp 0.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.
@@ -0,0 +1 @@
1
+ "use strict";var __SnapshotInjected=(()=>{var h=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var q=(t,r)=>{for(var e in r)h(t,e,{get:r[e],enumerable:!0})},z=(t,r,e,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of V(r))!U.call(t,o)&&o!==e&&h(t,o,{get:()=>r[o],enumerable:!(i=W(r,o))||i.enumerable});return t};var B=t=>z(h({},"__esModule",{value:!0}),t);var rt={};q(rt,{createSnapshot:()=>P});var v=["a","button","input","select","textarea","img","video","audio","iframe"],J=["nav","main","section","article","header","footer"],K=["div","span","p","li","ul","ol"];function L(t){if(!t||t.nodeType!==Node.ELEMENT_NODE)return!1;try{let e=window.getComputedStyle(t);if(e.display==="none"||e.visibility==="hidden"||e.opacity==="0")return!1}catch{return!1}let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1||t.hasAttribute("role")||t.hasAttribute("aria-label")||/^h[1-6]$/.test(r)||J.indexOf(r)!==-1)return!0;if(K.indexOf(r)!==-1){let e=(t.textContent||"").trim();if(e.length>0&&e.length<500||t.id||t.className)return!0}return!1}function M(t){if(t.tabIndex>=0)return!0;let e=t.tagName.toLowerCase();return["a","button","input","select","textarea"].indexOf(e)!==-1}function O(t){let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1)return!0;let e=t.getAttribute("role");return!!(e&&["button","link","menuitem","tab"].indexOf(e)!==-1||t.hasAttribute("onclick"))}var Q=100;function k(t){if(t.hasAttribute("aria-label"))return t.getAttribute("aria-label")||void 0;let e=t.id;if(e){let o=document.querySelector(`label[for="${e}"]`);if(o?.textContent)return o.textContent.trim()}if(t.hasAttribute("placeholder"))return t.getAttribute("placeholder")||void 0;if(t.hasAttribute("title"))return t.getAttribute("title")||void 0;if(t.hasAttribute("alt"))return t.getAttribute("alt")||void 0;let i=t.tagName.toLowerCase();if(["button","a","h1","h2","h3","h4","h5","h6"].indexOf(i)!==-1)return g(t)}function g(t){let r="";for(let i=0;i<t.childNodes.length;i++){let o=t.childNodes[i];o&&o.nodeType===Node.TEXT_NODE&&(r+=o.textContent||"")}let e=r.trim();if(e)return e.substring(0,Q)}function $(t){let r={},e=!1,i=["disabled","hidden","selected","expanded"];for(let a of i){let n=t.getAttribute(`aria-${a}`);n!==null&&(r[a]=n==="true",e=!0)}let o=["checked","pressed"];for(let a of o){let n=t.getAttribute(`aria-${a}`);n!==null&&(n==="mixed"?r[a]="mixed":r[a]=n==="true",e=!0)}let l=["autocomplete","haspopup","invalid","label","labelledby","describedby","controls"];for(let a of l){let n=t.getAttribute(`aria-${a}`);n&&(r[a]=n,e=!0)}let s=t.getAttribute("aria-level");if(s){let a=parseInt(s,10);isNaN(a)||(r.level=a,e=!0)}return e?r:void 0}function I(t){let r={};try{let e=window.getComputedStyle(t);r.visible=e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}catch{r.visible=!1}return r.accessible=r.visible&&!t.getAttribute("aria-hidden"),r.focusable=M(t),r.interactive=O(t),r}var Y=["id","data-testid","data-test-id"];function R(t){let r=[],e=t;for(;e&&e.nodeType===Node.ELEMENT_NODE;){let i=e.nodeName.toLowerCase(),o=!1;for(let n of Y){let u=e.getAttribute(n);if(u){n==="id"?i+="#"+CSS.escape(u):i+=`[${n}="${X(u)}"]`,r.unshift(i),o=!0;break}}if(o)break;let l=e.getAttribute("aria-label"),s=e.getAttribute("role");if(l&&s){i+=`[role="${s}"][aria-label="${X(l)}"]`,r.unshift(i),e=e.parentElement;continue}let a=e.parentElement?.children;if(a&&a.length>1){let n=1;for(let u=0;u<a.length;u++){let d=a[u];if(d){if(d===e)break;d.nodeName===e.nodeName&&n++}}(n>1||a.length>1&&a[0]!==e)&&(i+=`:nth-of-type(${n})`)}if(r.unshift(tt(i)),e=e.parentElement,e&&e.nodeName.toLowerCase()==="body"){r.unshift("body");break}}return r.join(" > ")}function G(t){let r=t.id;if(r)return`//*[@id="${Z(r)}"]`;let e=[],i=t;for(;i&&i.nodeType===Node.ELEMENT_NODE;){let o=i.nodeName.toLowerCase(),l=1,s=i.previousElementSibling;for(;s;)s.nodeName.toLowerCase()===o&&l++,s=s.previousElementSibling;let a=i.parentElement,n=!1;a&&(n=Array.from(a.children).filter(E=>E.nodeName.toLowerCase()===o).length>1);let u=n?`${o}[${l}]`:o;if(e.unshift(u),i=i.parentElement,i&&i.nodeName.toLowerCase()==="html"){e.unshift("html");break}}return"/"+e.join("/")}function X(t){return t.replace(/"/g,'\\"').substring(0,64)}function Z(t){return t.indexOf('"')===-1||t.indexOf("'")===-1?t:`concat(${t.split('"').map((e,i,o)=>i===o.length-1?e?`"${e}"`:"":e?`"${e}",'"'`:`"'"`).filter(e=>e).join(",")})`}function tt(t){return t.length<=64?t:t.substring(0,64)}var et=10,H=1e3;function D(t,r,e=!0){let i=0,o=[],l=!1;function s(n,u){if(u>et||i>=H)return l=!0,null;let d=n.tagName.toLowerCase();if(!(d==="body"||d==="html")&&!L(n))return null;let A=`${r}_${i++}`,j=R(n),F=G(n);o.push({uid:A,css:j,xpath:F});let b=n,N=n.getAttribute("role"),T=k(n),y=g(n),x=b.value,C=b.href,S=b.src,w=$(n),_=I(n),c={uid:A,tag:d,...N&&{role:N},...T&&{name:T},...x&&{value:x},...C&&{href:C},...S&&{src:S},...y&&{text:y},...w&&{aria:w},..._&&{computed:_},children:[]};if(d==="iframe"&&e){try{let f=n,p=f.contentDocument||f.contentWindow?.document;if(p?.body){let m=s(p.body,u+1);m&&(m.isIframe=!0,m.frameSrc=f.src,c.children.push(m))}else c.isIframe=!0,c.frameSrc=f.src,c.crossOrigin=!0}catch{c.isIframe=!0,c.frameSrc=n.src,c.crossOrigin=!0}return c}for(let f=0;f<n.children.length;f++){if(i>=H){l=!0;break}let p=n.children[f];if(!p)continue;let m=s(p,u+1);m&&c.children.push(m)}return c}return{tree:s(t,0),uidMap:o,truncated:l}}function P(t){try{let r=D(document.body,t,!0);if(!r.tree)throw new Error("Failed to generate tree");return r}catch{return{tree:null,uidMap:[],truncated:!1}}}typeof window<"u"&&(window.__createSnapshot=P);return B(rt);})();
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "firefox-devtools-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
+ "author": "freema",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "firefox-devtools-mcp": "./dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "dev": "tsx watch src/index.ts",
15
+ "build": "tsup",
16
+ "start": "node dist/index.js",
17
+ "setup": "node scripts/setup-mcp-config.js",
18
+ "clean": "rm -rf dist",
19
+ "typecheck": "tsc --noEmit",
20
+ "lint": "eslint src --ext .ts",
21
+ "lint:fix": "eslint src --ext .ts --fix",
22
+ "format": "prettier --write \"src/**/*.ts\"",
23
+ "format:check": "prettier --check \"src/**/*.ts\"",
24
+ "check": "npm run lint:fix && npm run typecheck",
25
+ "check:all": "npm run check && npm run test:run && npm run build",
26
+ "prepublishOnly": "npm run clean && npm run build",
27
+ "inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
28
+ "inspector:dev": "NODE_ENV=development npx @modelcontextprotocol/inspector npx tsx src/index.ts",
29
+ "test": "vitest",
30
+ "test:run": "vitest run",
31
+ "test:coverage": "vitest run --coverage",
32
+ "test:watch": "vitest watch",
33
+ "test:ui": "vitest --ui",
34
+ "test:tools": "node scripts/test-bidi-devtools.js",
35
+ "test:input": "node scripts/test-input-tools.js",
36
+ "test:screenshot": "node scripts/test-screenshot.js",
37
+ "test:dialog": "node scripts/test-dialog.js"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "mcp-server",
42
+ "model-context-protocol",
43
+ "firefox",
44
+ "firefox-devtools",
45
+ "browser-automation",
46
+ "webdriver-bidi",
47
+ "selenium",
48
+ "devtools",
49
+ "browser-testing",
50
+ "web-automation",
51
+ "claude",
52
+ "claude-ai",
53
+ "ai-agent",
54
+ "llm"
55
+ ],
56
+ "engines": {
57
+ "node": ">=20.19.0"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.17.1",
61
+ "ws": "^8.18.3",
62
+ "yargs": "^17.7.2"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^24.1.0",
66
+ "@types/selenium-webdriver": "^4.35.1",
67
+ "@types/ws": "^8.18.1",
68
+ "@types/yargs": "^17.0.32",
69
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
70
+ "@typescript-eslint/parser": "^6.21.0",
71
+ "@vitest/coverage-v8": "^3.1.4",
72
+ "@vitest/ui": "^3.1.4",
73
+ "dotenv": "^17.2.1",
74
+ "eslint": "^8.57.1",
75
+ "eslint-config-prettier": "^10.1.5",
76
+ "eslint-plugin-prettier": "^5.4.0",
77
+ "geckodriver": "^6.0.2",
78
+ "prettier": "^3.5.3",
79
+ "selenium-webdriver": "^4.36.0",
80
+ "tsup": "^8.0.0",
81
+ "tsx": "^4.7.0",
82
+ "typescript": "^5.3.3",
83
+ "vitest": "^3.1.4"
84
+ },
85
+ "files": [
86
+ "dist",
87
+ "README.md",
88
+ "LICENSE",
89
+ "scripts"
90
+ ],
91
+ "repository": {
92
+ "type": "git",
93
+ "url": "git+https://github.com/freema/firefox-devtools-mcp.git"
94
+ },
95
+ "bugs": {
96
+ "url": "https://github.com/freema/firefox-devtools-mcp/issues"
97
+ },
98
+ "homepage": "https://github.com/freema/firefox-devtools-mcp#readme",
99
+ "publishConfig": {
100
+ "access": "public"
101
+ }
102
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Offline test helpers for loading HTML without external dependencies
3
+ */
4
+
5
+ /**
6
+ * Load HTML into Firefox using about:blank + innerHTML injection
7
+ * This avoids data: URL parsing issues and works offline
8
+ *
9
+ * @param {FirefoxDevTools} firefox - Firefox client instance
10
+ * @param {string} htmlWithScript - HTML content (can include <script> tags)
11
+ * @returns {Promise<void>}
12
+ */
13
+ export async function loadHTML(firefox, htmlWithScript) {
14
+ // Extract all <script> tags and their content
15
+ const scriptMatches = [...htmlWithScript.matchAll(/<script>([\s\S]*?)<\/script>/g)];
16
+ const htmlWithoutScript = htmlWithScript.replace(/<script>[\s\S]*?<\/script>/g, '');
17
+
18
+ await firefox.navigate('about:blank');
19
+ await waitShort();
20
+
21
+ // Set HTML (without script tags)
22
+ await firefox.evaluate(`document.documentElement.innerHTML = \`${htmlWithoutScript}\`;`);
23
+
24
+ // Execute all scripts in order
25
+ for (const match of scriptMatches) {
26
+ if (match[1]) {
27
+ await firefox.evaluate(match[1]);
28
+ }
29
+ }
30
+
31
+ await waitShort();
32
+ }
33
+
34
+ /**
35
+ * Short deterministic wait (setTimeout for Node.js test context)
36
+ * More reliable than arbitrary long waits
37
+ *
38
+ * @param {number} ms - Milliseconds to wait (default: 300)
39
+ * @returns {Promise<void>}
40
+ */
41
+ export async function waitShort(ms = 300) {
42
+ await new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ /**
46
+ * Wait for browser frame update (uses requestAnimationFrame in browser)
47
+ *
48
+ * @param {FirefoxDevTools} firefox - Firefox client instance
49
+ * @returns {Promise<void>}
50
+ */
51
+ export async function waitForFrame(firefox) {
52
+ await firefox.evaluate(() => {
53
+ return new Promise((resolve) => {
54
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
55
+ });
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Wait for element to appear in DOM
61
+ *
62
+ * @param {FirefoxDevTools} firefox - Firefox client instance
63
+ * @param {string} selector - CSS selector
64
+ * @param {number} timeout - Max wait time in ms (default: 5000)
65
+ * @returns {Promise<boolean>} - true if element found, false on timeout
66
+ */
67
+ export async function waitForElement(firefox, selector, timeout = 5000) {
68
+ const startTime = Date.now();
69
+
70
+ while (Date.now() - startTime < timeout) {
71
+ // Pass selector as argument instead of template literal to avoid injection
72
+ const exists = await firefox.evaluate(
73
+ (sel) => !!document.querySelector(sel),
74
+ selector
75
+ );
76
+ if (exists) {
77
+ return true;
78
+ }
79
+ await waitShort(100);
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Wait for condition to be true
87
+ *
88
+ * @param {FirefoxDevTools} firefox - Firefox client instance
89
+ * @param {string} condition - JavaScript expression to evaluate
90
+ * @param {number} timeout - Max wait time in ms (default: 5000)
91
+ * @returns {Promise<boolean>} - true if condition met, false on timeout
92
+ */
93
+ export async function waitForCondition(firefox, condition, timeout = 5000) {
94
+ const startTime = Date.now();
95
+
96
+ while (Date.now() - startTime < timeout) {
97
+ const result = await firefox.evaluate(`return (${condition})`);
98
+ if (result) {
99
+ return true;
100
+ }
101
+ await waitShort(100);
102
+ }
103
+
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * Check if online tests should run
109
+ * @returns {boolean}
110
+ */
111
+ export function shouldRunOnlineTests() {
112
+ return process.env.TEST_ONLINE === '1';
113
+ }
114
+
115
+ /**
116
+ * Skip message for online tests
117
+ * @param {string} testName - Name of the test being skipped
118
+ */
119
+ export function skipOnlineTest(testName) {
120
+ console.log(`ā­ļø Skipping ${testName} (offline mode - set TEST_ONLINE=1 to enable)\n`);
121
+ }
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Simple demo server for testing MCP tools
5
+ * Serves a page with console logs, dialogs, and interactive elements
6
+ */
7
+
8
+ import http from 'http';
9
+
10
+ const PORT = 3456;
11
+
12
+ const HTML_PAGE = `
13
+ <!DOCTYPE html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18
+ <title>MCP - Demo Page</title>
19
+ <style>
20
+ body {
21
+ font-family: system-ui, -apple-system, sans-serif;
22
+ max-width: 800px;
23
+ margin: 50px auto;
24
+ padding: 20px;
25
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
26
+ color: white;
27
+ }
28
+ .container {
29
+ background: rgba(255, 255, 255, 0.1);
30
+ backdrop-filter: blur(10px);
31
+ border-radius: 16px;
32
+ padding: 30px;
33
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
34
+ }
35
+ h1 {
36
+ margin-top: 0;
37
+ font-size: 2.5rem;
38
+ }
39
+ .section {
40
+ margin: 30px 0;
41
+ padding: 20px;
42
+ background: rgba(255, 255, 255, 0.1);
43
+ border-radius: 12px;
44
+ }
45
+ button {
46
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
47
+ border: none;
48
+ color: white;
49
+ padding: 12px 24px;
50
+ font-size: 16px;
51
+ border-radius: 8px;
52
+ cursor: pointer;
53
+ margin: 5px;
54
+ transition: transform 0.2s;
55
+ font-weight: 600;
56
+ }
57
+ button:hover {
58
+ transform: scale(1.05);
59
+ }
60
+ button:active {
61
+ transform: scale(0.95);
62
+ }
63
+ input {
64
+ padding: 10px;
65
+ border-radius: 6px;
66
+ border: 2px solid rgba(255, 255, 255, 0.3);
67
+ background: rgba(255, 255, 255, 0.2);
68
+ color: white;
69
+ font-size: 16px;
70
+ margin: 5px;
71
+ }
72
+ input::placeholder {
73
+ color: rgba(255, 255, 255, 0.6);
74
+ }
75
+ .console-output {
76
+ background: rgba(0, 0, 0, 0.3);
77
+ padding: 15px;
78
+ border-radius: 8px;
79
+ font-family: 'Monaco', 'Consolas', monospace;
80
+ font-size: 14px;
81
+ margin-top: 10px;
82
+ max-height: 200px;
83
+ overflow-y: auto;
84
+ }
85
+ .log { color: #4ade80; }
86
+ .info { color: #60a5fa; }
87
+ .warn { color: #fbbf24; }
88
+ .error { color: #f87171; }
89
+ .debug { color: #a78bfa; }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <div class="container">
94
+ <h1>🦊 Firefox DevTools MCP</h1>
95
+ <p><strong>Demo Page for Testing MCP Tools</strong></p>
96
+ <p>Use this page with the MCP Inspector to test various tools!</p>
97
+
98
+ <div class="section">
99
+ <h2>šŸ“ Console Logs</h2>
100
+ <button onclick="generateLogs()">Generate All Log Types</button>
101
+ <button onclick="generateManyLogs()">Generate 50 Logs</button>
102
+ <button onclick="logObject()">Log Object</button>
103
+ <button onclick="logArray()">Log Array</button>
104
+ <button onclick="console.clear()">Clear Console</button>
105
+ </div>
106
+
107
+ <div class="section">
108
+ <h2>šŸ’¬ Dialogs</h2>
109
+ <button onclick="testAlert()">Show Alert</button>
110
+ <button onclick="testConfirm()">Show Confirm</button>
111
+ <button onclick="testPrompt()">Show Prompt</button>
112
+ </div>
113
+
114
+ <div class="section">
115
+ <h2>āŒØļø Input Elements</h2>
116
+ <input type="text" id="testInput" placeholder="Type something here...">
117
+ <button onclick="showInputValue()">Show Input Value</button>
118
+ <button onclick="clearInput()">Clear Input</button>
119
+ </div>
120
+
121
+ <div class="section">
122
+ <h2>šŸ”„ Page Actions</h2>
123
+ <button onclick="location.reload()">Reload Page</button>
124
+ <button onclick="window.open('about:blank', '_blank')">Open New Tab</button>
125
+ <button onclick="navigateToExample()">Navigate to Example.com</button>
126
+ </div>
127
+
128
+ <div class="section">
129
+ <h2>šŸŽÆ Clickable Elements</h2>
130
+ <button id="clickCounter" onclick="incrementCounter()">Click Counter: 0</button>
131
+ <button onclick="changeBackground()">Change Background</button>
132
+ </div>
133
+
134
+ <div class="section">
135
+ <h2>šŸ“Š Live Console Output</h2>
136
+ <div class="console-output" id="consoleOutput">
137
+ Console logs will appear here...
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <script>
143
+ let clickCount = 0;
144
+ const colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4ade80', '#60a5fa'];
145
+ let colorIndex = 0;
146
+
147
+ // Intercept console methods to show in page
148
+ const originalLog = console.log;
149
+ const originalInfo = console.info;
150
+ const originalWarn = console.warn;
151
+ const originalError = console.error;
152
+ const originalDebug = console.debug;
153
+
154
+ function addToOutput(message, type) {
155
+ const output = document.getElementById('consoleOutput');
156
+ const line = document.createElement('div');
157
+ line.className = type;
158
+ line.textContent = '[' + type.toUpperCase() + '] ' + message;
159
+ output.appendChild(line);
160
+ output.scrollTop = output.scrollHeight;
161
+ }
162
+
163
+ console.log = function(...args) {
164
+ originalLog.apply(console, args);
165
+ addToOutput(args.join(' '), 'log');
166
+ };
167
+
168
+ console.info = function(...args) {
169
+ originalInfo.apply(console, args);
170
+ addToOutput(args.join(' '), 'info');
171
+ };
172
+
173
+ console.warn = function(...args) {
174
+ originalWarn.apply(console, args);
175
+ addToOutput(args.join(' '), 'warn');
176
+ };
177
+
178
+ console.error = function(...args) {
179
+ originalError.apply(console, args);
180
+ addToOutput(args.join(' '), 'error');
181
+ };
182
+
183
+ console.debug = function(...args) {
184
+ originalDebug.apply(console, args);
185
+ addToOutput(args.join(' '), 'debug');
186
+ };
187
+
188
+ // Console log functions
189
+ function generateLogs() {
190
+ console.log('This is a log message');
191
+ console.info('This is an info message');
192
+ console.warn('This is a warning message');
193
+ console.error('This is an error message');
194
+ console.debug('This is a debug message');
195
+ }
196
+
197
+ function generateManyLogs() {
198
+ for (let i = 0; i < 50; i++) {
199
+ const types = ['log', 'info', 'warn', 'error', 'debug'];
200
+ const type = types[i % types.length];
201
+ console[type]('Message #' + (i + 1) + ' - ' + type);
202
+ }
203
+ }
204
+
205
+ function logObject() {
206
+ console.log('Object:', {
207
+ name: 'Firefox DevTools MCP',
208
+ version: '0.1.0',
209
+ features: ['console', 'dialog', 'input', 'screenshot'],
210
+ config: { headless: false, timeout: 5000 }
211
+ });
212
+ }
213
+
214
+ function logArray() {
215
+ console.log('Array:', [1, 2, 3, 'four', { five: 5 }, [6, 7, 8]]);
216
+ }
217
+
218
+ // Dialog functions
219
+ function testAlert() {
220
+ alert('This is an alert dialog! 🚨');
221
+ console.info('Alert dialog was shown');
222
+ }
223
+
224
+ function testConfirm() {
225
+ const result = confirm('Do you want to continue? šŸ¤”');
226
+ console.log('Confirm result:', result);
227
+ }
228
+
229
+ function testPrompt() {
230
+ const result = prompt('What is your favorite color? šŸŽØ');
231
+ console.log('Prompt result:', result);
232
+ }
233
+
234
+ // Input functions
235
+ function showInputValue() {
236
+ const value = document.getElementById('testInput').value;
237
+ console.log('Input value:', value);
238
+ alert('Input value: ' + value);
239
+ }
240
+
241
+ function clearInput() {
242
+ document.getElementById('testInput').value = '';
243
+ console.info('Input cleared');
244
+ }
245
+
246
+ // Page actions
247
+ function navigateToExample() {
248
+ console.info('Navigating to example.com...');
249
+ window.location.href = 'https://example.com';
250
+ }
251
+
252
+ // Clickable elements
253
+ function incrementCounter() {
254
+ clickCount++;
255
+ document.getElementById('clickCounter').textContent = 'Click Counter: ' + clickCount;
256
+ console.log('Button clicked! Count:', clickCount);
257
+ }
258
+
259
+ function changeBackground() {
260
+ colorIndex = (colorIndex + 1) % colors.length;
261
+ document.body.style.background = 'linear-gradient(135deg, ' + colors[colorIndex] + ' 0%, ' + colors[(colorIndex + 1) % colors.length] + ' 100%)';
262
+ console.log('Background changed to:', colors[colorIndex]);
263
+ }
264
+
265
+ // Initial log
266
+ console.log('🦊 Firefox DevTools MCP Demo Page Loaded!');
267
+ console.info('Server running on http://localhost:3456');
268
+ console.info('Use MCP Inspector to test tools: list_console_messages, list_pages, etc.');
269
+ </script>
270
+ </body>
271
+ </html>
272
+ `;
273
+
274
+ const server = http.createServer((req, res) => {
275
+ if (req.url === '/') {
276
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
277
+ res.end(HTML_PAGE);
278
+ } else if (req.url === '/health') {
279
+ res.writeHead(200, { 'Content-Type': 'application/json' });
280
+ res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
281
+ } else {
282
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
283
+ res.end('Not Found');
284
+ }
285
+ });
286
+
287
+ server.listen(PORT, () => {
288
+ console.log('');
289
+ console.log('🦊 Firefox DevTools MCP - Demo Server');
290
+ console.log('=====================================');
291
+ console.log('');
292
+ console.log(`🌐 Server running at: http://localhost:${PORT}`);
293
+ console.log('');
294
+ console.log('šŸ“‹ Available endpoints:');
295
+ console.log(` http://localhost:${PORT}/ - Demo page`);
296
+ console.log(` http://localhost:${PORT}/health - Health check`);
297
+ console.log('');
298
+ console.log('šŸ’” Usage with MCP Inspector:');
299
+ console.log(' 1. Start this server (already running)');
300
+ console.log(` 2. Open MCP Inspector: npm run inspector`);
301
+ console.log(` 3. Use tool: new_page with url "http://localhost:${PORT}"`);
302
+ console.log(' 4. Test tools: list_console_messages, list_pages, etc.');
303
+ console.log('');
304
+ console.log('Press Ctrl+C to stop the server');
305
+ console.log('');
306
+ });
307
+
308
+ server.on('error', (error) => {
309
+ if (error.code === 'EADDRINUSE') {
310
+ console.error(`āŒ Port ${PORT} is already in use!\n`);
311
+ console.error(' Try stopping other servers or change the PORT in demo-server.js\n');
312
+ } else {
313
+ console.error('āŒ Server error:', error.message);
314
+ }
315
+ process.exit(1);
316
+ });
317
+
318
+ // Graceful shutdown
319
+ process.on('SIGINT', () => {
320
+ console.log('\n\nšŸ›‘ Shutting down demo server...');
321
+ server.close(() => {
322
+ console.log('āœ… Server closed');
323
+ process.exit(0);
324
+ });
325
+ });