firefox-css-theme-mcp 0.1.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 +76 -0
- package/dist/firefox.js +155 -0
- package/dist/index.js +161 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# firefox-css-theme-mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server for inspecting, querying, and live-debugging Firefox UI DOM and CSS themes (`userChrome.css`).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js >= 18
|
|
8
|
+
- Firefox Browser
|
|
9
|
+
- `geckodriver` available on your `PATH` or managed via Selenium
|
|
10
|
+
|
|
11
|
+
## Installation & Usage
|
|
12
|
+
|
|
13
|
+
### Option A — CLI (Claude Code / Codex)
|
|
14
|
+
|
|
15
|
+
#### Claude Code
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add firefox-css-theme -- npx -y firefox-css-theme-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
#### Codex
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
codex mcp add firefox-css-theme -- npx -y firefox-css-theme-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Option B — IDE Configuration
|
|
28
|
+
|
|
29
|
+
Add the server configuration to your MCP settings file:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"firefox-css-theme": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "firefox-css-theme-mcp"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Option C — From Source
|
|
43
|
+
|
|
44
|
+
1. Clone the repository and build the project:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/easonwong-de/Firefox-CSS-Theme-MCP.git
|
|
48
|
+
cd Firefox-CSS-Theme-MCP
|
|
49
|
+
npm install
|
|
50
|
+
npm run build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. Add the local build to your MCP configuration:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"firefox-css-theme": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["/path/to/Firefox-CSS-Theme-MCP/dist/index.js"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Available MCP Tools
|
|
67
|
+
|
|
68
|
+
- `launch_browser`: Launches Firefox with chrome debugging capabilities enabled.
|
|
69
|
+
- `close_browser`: Terminates the browser instance.
|
|
70
|
+
- `get_ui_tree`: Dumps the hierarchical DOM tree of the chrome window.
|
|
71
|
+
- `query_ui_elements`: Queries elements matching a CSS selector in the browser chrome.
|
|
72
|
+
- `get_computed_styles`: Extracts computed CSS property values of a specific UI element.
|
|
73
|
+
- `inject_theme_css`: Dynamically injects or updates stylesheets in the live UI for instant feedback.
|
|
74
|
+
- `remove_theme_css`: Removes an injected stylesheet.
|
|
75
|
+
- `take_ui_screenshot`: Captures a screenshot of the browser window or a specific UI component.
|
|
76
|
+
- `execute_chrome_javascript`: Runs privileged JavaScript in the browser chrome window context.
|
package/dist/firefox.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Builder, By } from "selenium-webdriver";
|
|
2
|
+
import * as firefox from "selenium-webdriver/firefox.js";
|
|
3
|
+
export class FirefoxBrowserManager {
|
|
4
|
+
driverInstance = null;
|
|
5
|
+
async initialiseBrowser(binaryPath, profileDirectory) {
|
|
6
|
+
if (this.driverInstance)
|
|
7
|
+
return;
|
|
8
|
+
const firefoxOptions = new firefox.Options();
|
|
9
|
+
firefoxOptions.addArguments("-remote-allow-system-access");
|
|
10
|
+
firefoxOptions.setPreference("devtools.chrome.enabled", true);
|
|
11
|
+
firefoxOptions.setPreference("devtools.debugger.remote-enabled", true);
|
|
12
|
+
firefoxOptions.setPreference("toolkit.legacyUserProfileCustomizations.stylesheets", true);
|
|
13
|
+
if (binaryPath)
|
|
14
|
+
firefoxOptions.setBinary(binaryPath);
|
|
15
|
+
if (profileDirectory)
|
|
16
|
+
firefoxOptions.setProfile(profileDirectory);
|
|
17
|
+
this.driverInstance = await new Builder()
|
|
18
|
+
.forBrowser("firefox")
|
|
19
|
+
.setFirefoxOptions(firefoxOptions)
|
|
20
|
+
.build();
|
|
21
|
+
await this.ensureChromeContext();
|
|
22
|
+
}
|
|
23
|
+
async terminateBrowser() {
|
|
24
|
+
if (!this.driverInstance)
|
|
25
|
+
return;
|
|
26
|
+
await this.driverInstance.quit();
|
|
27
|
+
this.driverInstance = null;
|
|
28
|
+
}
|
|
29
|
+
async ensureChromeContext() {
|
|
30
|
+
if (!this.driverInstance) {
|
|
31
|
+
throw new Error("Firefox driver instance is not initialised.");
|
|
32
|
+
}
|
|
33
|
+
await this.driverInstance.setContext("chrome");
|
|
34
|
+
}
|
|
35
|
+
async executeChromeScript(script, ...argumentsList) {
|
|
36
|
+
await this.ensureChromeContext();
|
|
37
|
+
return (await this.driverInstance.executeScript(script, ...argumentsList));
|
|
38
|
+
}
|
|
39
|
+
async queryElements(selector) {
|
|
40
|
+
await this.ensureChromeContext();
|
|
41
|
+
return await this.executeChromeScript((querySelector) => {
|
|
42
|
+
const matchedElements = Array.from(document.querySelectorAll(querySelector));
|
|
43
|
+
return matchedElements.map((element) => {
|
|
44
|
+
const attributeMap = {};
|
|
45
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
46
|
+
attributeMap[attribute.name] = attribute.value;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
tagName: element.tagName.toLowerCase(),
|
|
50
|
+
id: element.id || "",
|
|
51
|
+
className: element.className || "",
|
|
52
|
+
childCount: element.children.length,
|
|
53
|
+
textContent: (element.textContent || "")
|
|
54
|
+
.trim()
|
|
55
|
+
.slice(0, 100),
|
|
56
|
+
attributes: attributeMap,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}, selector);
|
|
60
|
+
}
|
|
61
|
+
async getComputedStyles(selector, stylePropertyNames) {
|
|
62
|
+
await this.ensureChromeContext();
|
|
63
|
+
return await this.executeChromeScript((targetSelector, requestedProperties) => {
|
|
64
|
+
const targetElement = document.querySelector(targetSelector);
|
|
65
|
+
if (!targetElement) {
|
|
66
|
+
throw new Error("Element not found for selector: " + targetSelector);
|
|
67
|
+
}
|
|
68
|
+
const computedStyleDeclaration = window.getComputedStyle(targetElement);
|
|
69
|
+
const styleResult = {};
|
|
70
|
+
if (Array.isArray(requestedProperties) &&
|
|
71
|
+
requestedProperties.length > 0) {
|
|
72
|
+
for (const propertyName of requestedProperties) {
|
|
73
|
+
styleResult[propertyName] =
|
|
74
|
+
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
for (let index = 0; index < computedStyleDeclaration.length; index++) {
|
|
79
|
+
const propertyName = computedStyleDeclaration[index];
|
|
80
|
+
styleResult[propertyName] =
|
|
81
|
+
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return styleResult;
|
|
85
|
+
}, selector, stylePropertyNames);
|
|
86
|
+
}
|
|
87
|
+
async getUserInterfaceTree(rootSelector = "window", maximumDepth = 3) {
|
|
88
|
+
await this.ensureChromeContext();
|
|
89
|
+
return await this.executeChromeScript((targetSelector, maximumTraversalDepth) => {
|
|
90
|
+
const rootTarget = targetSelector === "window"
|
|
91
|
+
? document.documentElement
|
|
92
|
+
: document.querySelector(targetSelector);
|
|
93
|
+
if (!rootTarget) {
|
|
94
|
+
throw new Error("Root element not found for selector: " +
|
|
95
|
+
targetSelector);
|
|
96
|
+
}
|
|
97
|
+
function serializeNode(node, currentDepth) {
|
|
98
|
+
const attributeMap = {};
|
|
99
|
+
for (const attribute of Array.from(node.attributes)) {
|
|
100
|
+
attributeMap[attribute.name] = attribute.value;
|
|
101
|
+
}
|
|
102
|
+
const serializedChildren = [];
|
|
103
|
+
if (currentDepth < maximumTraversalDepth) {
|
|
104
|
+
for (const child of Array.from(node.children)) {
|
|
105
|
+
serializedChildren.push(serializeNode(child, currentDepth + 1));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
tagName: node.tagName.toLowerCase(),
|
|
110
|
+
id: node.id || "",
|
|
111
|
+
className: node.className || "",
|
|
112
|
+
attributes: attributeMap,
|
|
113
|
+
children: serializedChildren,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return serializeNode(rootTarget, 0);
|
|
117
|
+
}, rootSelector, maximumDepth);
|
|
118
|
+
}
|
|
119
|
+
async injectUserInterfaceStyle(cascadingStyleSheetContent, styleIdentifier = "mcp-injected-style") {
|
|
120
|
+
await this.ensureChromeContext();
|
|
121
|
+
return await this.executeChromeScript((identifier, cssSource) => {
|
|
122
|
+
let existingStyleElement = document.getElementById(identifier);
|
|
123
|
+
if (!existingStyleElement) {
|
|
124
|
+
existingStyleElement = document.createElement("style");
|
|
125
|
+
existingStyleElement.id = identifier;
|
|
126
|
+
existingStyleElement.setAttribute("type", "text/css");
|
|
127
|
+
document.documentElement.appendChild(existingStyleElement);
|
|
128
|
+
}
|
|
129
|
+
existingStyleElement.textContent = cssSource;
|
|
130
|
+
return { success: true, identifier: identifier };
|
|
131
|
+
}, styleIdentifier, cascadingStyleSheetContent);
|
|
132
|
+
}
|
|
133
|
+
async removeUserInterfaceStyle(styleIdentifier) {
|
|
134
|
+
await this.ensureChromeContext();
|
|
135
|
+
return await this.executeChromeScript((identifier) => {
|
|
136
|
+
const targetStyleElement = document.getElementById(identifier);
|
|
137
|
+
if (targetStyleElement) {
|
|
138
|
+
targetStyleElement.remove();
|
|
139
|
+
return { removed: true, identifier: identifier };
|
|
140
|
+
}
|
|
141
|
+
return { removed: false, identifier: identifier };
|
|
142
|
+
}, styleIdentifier);
|
|
143
|
+
}
|
|
144
|
+
async captureScreenshot(targetSelector) {
|
|
145
|
+
await this.ensureChromeContext();
|
|
146
|
+
if (targetSelector) {
|
|
147
|
+
const targetWebElement = await this.driverInstance.findElement(By.css(targetSelector));
|
|
148
|
+
const elementScreenshotBase64 = await targetWebElement.takeScreenshot();
|
|
149
|
+
return { base64Image: elementScreenshotBase64, format: "png" };
|
|
150
|
+
}
|
|
151
|
+
const fullWindowScreenshotBase64 = await this.driverInstance.takeScreenshot();
|
|
152
|
+
return { base64Image: fullWindowScreenshotBase64, format: "png" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export const globalFirefoxManager = new FirefoxBrowserManager();
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { globalFirefoxManager } from "./firefox.js";
|
|
6
|
+
const serverInstance = new McpServer({
|
|
7
|
+
name: "firefox-css-theme-mcp",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
});
|
|
10
|
+
serverInstance.registerTool("launch_browser", {
|
|
11
|
+
description: "Launch a Firefox instance with chrome debugging capabilities enabled.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
binaryPath: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Optional path to Firefox executable binary."),
|
|
17
|
+
profileDirectory: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Optional path to custom Firefox profile directory."),
|
|
21
|
+
},
|
|
22
|
+
}, async (parameters) => {
|
|
23
|
+
await globalFirefoxManager.initialiseBrowser(parameters.binaryPath, parameters.profileDirectory);
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: "Firefox launched successfully in chrome context.",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
serverInstance.registerTool("close_browser", { description: "Close the running Firefox instance." }, async () => {
|
|
34
|
+
await globalFirefoxManager.terminateBrowser();
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: "Firefox closed successfully." }],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
serverInstance.registerTool("get_ui_tree", {
|
|
40
|
+
description: "Retrieve the hierarchical DOM tree of the Firefox chrome user interface window.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
rootSelector: z
|
|
43
|
+
.string()
|
|
44
|
+
.default("window")
|
|
45
|
+
.describe("CSS selector for root element."),
|
|
46
|
+
maximumDepth: z
|
|
47
|
+
.number()
|
|
48
|
+
.default(3)
|
|
49
|
+
.describe("Maximum recursive depth to traverse."),
|
|
50
|
+
},
|
|
51
|
+
}, async (parameters) => {
|
|
52
|
+
const treeData = await globalFirefoxManager.getUserInterfaceTree(parameters.rootSelector, parameters.maximumDepth);
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{ type: "text", text: JSON.stringify(treeData, null, 2) },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
serverInstance.registerTool("query_ui_elements", {
|
|
60
|
+
description: "Find elements in the Firefox UI chrome document matching a CSS selector.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
selector: z
|
|
63
|
+
.string()
|
|
64
|
+
.describe("CSS selector to query in chrome document (e.g. '#nav-bar', '#TabsToolbar')."),
|
|
65
|
+
},
|
|
66
|
+
}, async (parameters) => {
|
|
67
|
+
const elements = await globalFirefoxManager.queryElements(parameters.selector);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{ type: "text", text: JSON.stringify(elements, null, 2) },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
serverInstance.registerTool("get_computed_styles", {
|
|
75
|
+
description: "Extract computed CSS properties for a Firefox UI chrome element.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
selector: z
|
|
78
|
+
.string()
|
|
79
|
+
.describe("CSS selector of the UI element (e.g. '#urlbar-background')."),
|
|
80
|
+
properties: z
|
|
81
|
+
.array(z.string())
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Optional list of specific CSS property names."),
|
|
84
|
+
},
|
|
85
|
+
}, async (parameters) => {
|
|
86
|
+
const styles = await globalFirefoxManager.getComputedStyles(parameters.selector, parameters.properties);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: JSON.stringify(styles, null, 2) }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
serverInstance.registerTool("inject_theme_css", {
|
|
92
|
+
description: "Inject or replace a live stylesheet in the Firefox chrome window for immediate visual testing.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
css: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe("CSS rules to inject into chrome document."),
|
|
97
|
+
styleId: z
|
|
98
|
+
.string()
|
|
99
|
+
.default("mcp-injected-style")
|
|
100
|
+
.describe("Identifier for the style tag."),
|
|
101
|
+
},
|
|
102
|
+
}, async (parameters) => {
|
|
103
|
+
const result = await globalFirefoxManager.injectUserInterfaceStyle(parameters.css, parameters.styleId);
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
serverInstance.registerTool("remove_theme_css", {
|
|
109
|
+
description: "Remove an injected stylesheet from the Firefox chrome window.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
styleId: z
|
|
112
|
+
.string()
|
|
113
|
+
.describe("Identifier of the style tag to remove."),
|
|
114
|
+
},
|
|
115
|
+
}, async (parameters) => {
|
|
116
|
+
const result = await globalFirefoxManager.removeUserInterfaceStyle(parameters.styleId);
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
serverInstance.registerTool("take_ui_screenshot", {
|
|
122
|
+
description: "Capture a PNG screenshot of the Firefox browser chrome window or a specific UI component.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
selector: z
|
|
125
|
+
.string()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("Optional CSS selector to screenshot a specific element."),
|
|
128
|
+
},
|
|
129
|
+
}, async (parameters) => {
|
|
130
|
+
const screenshot = await globalFirefoxManager.captureScreenshot(parameters.selector);
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "image",
|
|
135
|
+
data: screenshot.base64Image,
|
|
136
|
+
mimeType: "image/png",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
serverInstance.registerTool("execute_chrome_javascript", {
|
|
142
|
+
description: "Execute privileged JavaScript code in the Firefox chrome window context.",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
script: z
|
|
145
|
+
.string()
|
|
146
|
+
.describe("JavaScript code string to evaluate in chrome context."),
|
|
147
|
+
},
|
|
148
|
+
}, async (parameters) => {
|
|
149
|
+
const result = await globalFirefoxManager.executeChromeScript(parameters.script);
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
async function main() {
|
|
155
|
+
const transportInstance = new StdioServerTransport();
|
|
156
|
+
await serverInstance.connect(transportInstance);
|
|
157
|
+
}
|
|
158
|
+
main().catch((error) => {
|
|
159
|
+
console.error("Server initialisation failure:", error);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "firefox-css-theme-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model Context Protocol server for inspecting and live-debugging Firefox UI DOM and CSS themes.",
|
|
5
|
+
"homepage": "https://github.com/easonwong-de/Firefox-CSS-Theme-MCP#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/easonwong-de/Firefox-CSS-Theme-MCP/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/easonwong-de/Firefox-CSS-Theme-MCP.git"
|
|
12
|
+
},
|
|
13
|
+
"author": "Eason Wong <me@easonwong.de> (https://easonwong.de/)",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"bin": {
|
|
17
|
+
"firefox-css-theme-mcp": "dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"format": "prettier --write --list-different .",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"start": "node dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
30
|
+
"selenium-webdriver": "^4.47.0",
|
|
31
|
+
"zod": "^4.4.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^26.2.0",
|
|
35
|
+
"@types/selenium-webdriver": "^4.35.6",
|
|
36
|
+
"prettier": "^3.9.6",
|
|
37
|
+
"typescript": "^7.0.2"
|
|
38
|
+
}
|
|
39
|
+
}
|