firefox-css-theme-mcp 0.1.6 → 0.1.7
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 +11 -66
- package/index.js +42 -0
- package/package.json +8 -18
- package/dist/firefox.js +0 -255
- package/dist/index.js +0 -177
- package/dist/types.js +0 -1
package/README.md
CHANGED
|
@@ -1,88 +1,33 @@
|
|
|
1
|
-
# Firefox CSS Theme MCP
|
|
1
|
+
# Firefox CSS Theme MCP (Deprecated)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This package (`firefox-css-theme-mcp`) has been renamed and superseded by [`firefox-css-theme`](https://www.npmjs.com/package/firefox-css-theme).
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Please update your configurations to use `firefox-css-theme`.
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## Migration
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- Firefox Browser
|
|
11
|
-
|
|
12
|
-
## Installation & Usage
|
|
13
|
-
|
|
14
|
-
### Option A — CLI
|
|
15
|
-
|
|
16
|
-
#### Claude Code
|
|
10
|
+
### Claude Code
|
|
17
11
|
|
|
18
12
|
```bash
|
|
19
|
-
claude mcp add firefox-css-theme -- npx -y firefox-css-theme
|
|
13
|
+
claude mcp add firefox-css-theme -- npx -y firefox-css-theme mcp
|
|
20
14
|
```
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
### Codex
|
|
23
17
|
|
|
24
18
|
```bash
|
|
25
|
-
codex mcp add firefox-css-theme -- npx -y firefox-css-theme
|
|
19
|
+
codex mcp add firefox-css-theme -- npx -y firefox-css-theme mcp
|
|
26
20
|
```
|
|
27
21
|
|
|
28
|
-
###
|
|
29
|
-
|
|
30
|
-
Add the server configuration to your MCP settings file:
|
|
22
|
+
### MCP Settings File
|
|
31
23
|
|
|
32
24
|
```json
|
|
33
25
|
{
|
|
34
26
|
"mcpServers": {
|
|
35
27
|
"firefox-css-theme": {
|
|
36
28
|
"command": "npx",
|
|
37
|
-
"args": ["-y", "firefox-css-theme
|
|
29
|
+
"args": ["-y", "firefox-css-theme", "mcp"]
|
|
38
30
|
}
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
33
|
```
|
|
42
|
-
|
|
43
|
-
### Option C — From Source
|
|
44
|
-
|
|
45
|
-
1. Clone the repository and build the project:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
git clone https://github.com/easonwong-de/Firefox-CSS-Theme-MCP.git
|
|
49
|
-
cd Firefox-CSS-Theme-MCP
|
|
50
|
-
npm install
|
|
51
|
-
npm run build
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
2. Add the local build to your MCP configuration:
|
|
55
|
-
|
|
56
|
-
```json
|
|
57
|
-
{
|
|
58
|
-
"mcpServers": {
|
|
59
|
-
"firefox-css-theme": {
|
|
60
|
-
"command": "node",
|
|
61
|
-
"args": [
|
|
62
|
-
"/path/to/Firefox-CSS-Theme-MCP/dist/index.js",
|
|
63
|
-
"--nova-ui"
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Command-line Flags
|
|
71
|
-
|
|
72
|
-
| Flag | Type | Description |
|
|
73
|
-
| :----------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
-
| `--nova-ui` | `boolean` | Enables the Firefox Nova UI redesign preferences (`browser.nova.enabled` and `browser.newtabpage.activity-stream.nova.enabled`). |
|
|
75
|
-
| `--headless` | `boolean` | Runs Firefox in headless mode without displaying a graphical user interface window. |
|
|
76
|
-
|
|
77
|
-
## Available MCP Tools
|
|
78
|
-
|
|
79
|
-
- `launch_browser`: Launches Firefox with chrome debugging capabilities enabled.
|
|
80
|
-
- `close_browser`: Terminates the browser instance.
|
|
81
|
-
- `customize_toolbar`: Activates or controls the Firefox "Customize Toolbar" mode.
|
|
82
|
-
- `get_ui_tree`: Dumps the hierarchical DOM tree of the chrome window.
|
|
83
|
-
- `query_ui_elements`: Queries elements matching a CSS selector in the browser chrome.
|
|
84
|
-
- `get_computed_styles`: Extracts computed CSS property values of a specific UI element.
|
|
85
|
-
- `inject_theme_css`: Dynamically injects or updates stylesheets in the live UI for instant feedback.
|
|
86
|
-
- `remove_theme_css`: Removes an injected stylesheet.
|
|
87
|
-
- `take_ui_screenshot`: Captures a screenshot of the browser window or a specific UI component.
|
|
88
|
-
- `execute_chrome_javascript`: Runs privileged JavaScript in the browser chrome window context.
|
package/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
console.warn(
|
|
6
|
+
"\x1b[33m[DEPRECATED]\x1b[0m 'firefox-css-theme-mcp' has been renamed to 'firefox-css-theme'. " +
|
|
7
|
+
"Please update your configuration to use 'npx -y firefox-css-theme mcp'.",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const arguments_ = ["mcp", ...process.argv.slice(2)];
|
|
11
|
+
|
|
12
|
+
const childProcess = spawn("firefox-css-theme", arguments_, {
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
shell: process.platform === "win32",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
childProcess.on("error", (error) => {
|
|
18
|
+
if (error && "code" in error && error.code === "ENOENT") {
|
|
19
|
+
const fallbackProcess = spawn(
|
|
20
|
+
"npx",
|
|
21
|
+
["-y", "firefox-css-theme@latest", ...arguments_],
|
|
22
|
+
{ stdio: "inherit", shell: process.platform === "win32" },
|
|
23
|
+
);
|
|
24
|
+
fallbackProcess.on("exit", (code, signal) => {
|
|
25
|
+
if (signal) {
|
|
26
|
+
process.kill(process.pid, signal);
|
|
27
|
+
} else {
|
|
28
|
+
process.exit(code ?? 0);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
childProcess.on("exit", (code, signal) => {
|
|
37
|
+
if (signal) {
|
|
38
|
+
process.kill(process.pid, signal);
|
|
39
|
+
} else {
|
|
40
|
+
process.exit(code ?? 0);
|
|
41
|
+
}
|
|
42
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firefox-css-theme-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "DEPRECATED: Renamed to firefox-css-theme. Use 'npx -y firefox-css-theme mcp' instead.",
|
|
5
5
|
"homepage": "https://github.com/easonwong-de/Firefox-CSS-Theme-MCP#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/easonwong-de/Firefox-CSS-Theme-MCP/issues"
|
|
@@ -13,30 +13,20 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Eason Wong <me@easonwong.de> (https://easonwong.de/)",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"main": "
|
|
16
|
+
"main": "index.js",
|
|
17
17
|
"bin": {
|
|
18
|
-
"firefox-css-theme-mcp": "
|
|
18
|
+
"firefox-css-theme-mcp": "index.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"
|
|
21
|
+
"index.js"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"
|
|
25
|
-
"format": "prettier --write --list-different .",
|
|
26
|
-
"prepublishOnly": "npm run build",
|
|
27
|
-
"start": "node dist/index.js",
|
|
28
|
-
"test": "tsx tests/run.ts --headless"
|
|
24
|
+
"format": "prettier --write --list-different ."
|
|
29
25
|
},
|
|
30
26
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"selenium-webdriver": "^4.47.0",
|
|
33
|
-
"zod": "^4.4.3"
|
|
27
|
+
"firefox-css-theme": ">=0.1.0"
|
|
34
28
|
},
|
|
35
29
|
"devDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"@types/selenium-webdriver": "^4.35.6",
|
|
38
|
-
"prettier": "^3.9.6",
|
|
39
|
-
"tsx": "^4.23.12",
|
|
40
|
-
"typescript": "^7.0.2"
|
|
30
|
+
"prettier": "^3.9.6"
|
|
41
31
|
}
|
|
42
32
|
}
|
package/dist/firefox.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { Builder, By } from "selenium-webdriver";
|
|
2
|
-
import * as firefox from "selenium-webdriver/firefox.js";
|
|
3
|
-
export class FirefoxManager {
|
|
4
|
-
driver = null;
|
|
5
|
-
async initialiseBrowser(binaryPath, profileDirectory) {
|
|
6
|
-
if (this.driver)
|
|
7
|
-
return;
|
|
8
|
-
const isNovaUiEnabled = process.argv.includes("--nova-ui");
|
|
9
|
-
const isHeadlessEnabled = process.argv.includes("--headless");
|
|
10
|
-
const options = new firefox.Options();
|
|
11
|
-
options.addArguments("-no-remote", "-new-instance");
|
|
12
|
-
if (isHeadlessEnabled)
|
|
13
|
-
options.addArguments("-headless");
|
|
14
|
-
options.setPreference("devtools.chrome.enabled", true);
|
|
15
|
-
options.setPreference("devtools.debugger.remote-enabled", true);
|
|
16
|
-
options.setPreference("devtools.debugger.prompt-connection", false);
|
|
17
|
-
options.setPreference("toolkit.legacyUserProfileCustomizations.stylesheets", true);
|
|
18
|
-
options.setPreference("marionette.allow-system-access", true);
|
|
19
|
-
options.setPreference("remote.allow-system-access", true);
|
|
20
|
-
options.setPreference("browser.nova.enabled", isNovaUiEnabled);
|
|
21
|
-
options.setPreference("browser.newtabpage.activity-stream.nova.enabled", isNovaUiEnabled);
|
|
22
|
-
if (binaryPath)
|
|
23
|
-
options.setBinary(binaryPath);
|
|
24
|
-
if (profileDirectory)
|
|
25
|
-
options.setProfile(profileDirectory);
|
|
26
|
-
const service = new firefox.ServiceBuilder()
|
|
27
|
-
.enableVerboseLogging(true)
|
|
28
|
-
.addArguments("--allow-system-access");
|
|
29
|
-
try {
|
|
30
|
-
this.driver = await new Builder()
|
|
31
|
-
.forBrowser("firefox")
|
|
32
|
-
.setFirefoxOptions(options)
|
|
33
|
-
.setFirefoxService(service)
|
|
34
|
-
.build();
|
|
35
|
-
await this.ensureChromeContext();
|
|
36
|
-
await this.openBrowserToolbox().catch(() => { });
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
if (this.driver) {
|
|
40
|
-
await this.driver.quit().catch(() => { });
|
|
41
|
-
this.driver = null;
|
|
42
|
-
}
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async terminateBrowser() {
|
|
47
|
-
if (!this.driver)
|
|
48
|
-
return;
|
|
49
|
-
await this.driver.quit();
|
|
50
|
-
this.driver = null;
|
|
51
|
-
}
|
|
52
|
-
async ensureChromeContext() {
|
|
53
|
-
if (!this.driver) {
|
|
54
|
-
throw new Error("Firefox driver instance is not initialised.");
|
|
55
|
-
}
|
|
56
|
-
await this.driver.setContext("chrome");
|
|
57
|
-
}
|
|
58
|
-
async executeChromeScript(script, ...argumentsList) {
|
|
59
|
-
await this.ensureChromeContext();
|
|
60
|
-
return (await this.driver.executeScript(script, ...argumentsList));
|
|
61
|
-
}
|
|
62
|
-
async queryElements(selector) {
|
|
63
|
-
await this.ensureChromeContext();
|
|
64
|
-
return await this.executeChromeScript((querySelector) => {
|
|
65
|
-
const matchedElements = Array.from(document.querySelectorAll(querySelector));
|
|
66
|
-
return matchedElements.map((element) => {
|
|
67
|
-
const attributeMap = {};
|
|
68
|
-
for (const attribute of Array.from(element.attributes)) {
|
|
69
|
-
attributeMap[attribute.name] = attribute.value;
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
tagName: element.tagName.toLowerCase(),
|
|
73
|
-
id: element.id || "",
|
|
74
|
-
className: element.className || "",
|
|
75
|
-
childCount: element.children.length,
|
|
76
|
-
textContent: (element.textContent || "")
|
|
77
|
-
.trim()
|
|
78
|
-
.slice(0, 100),
|
|
79
|
-
attributes: attributeMap,
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
}, selector);
|
|
83
|
-
}
|
|
84
|
-
async getComputedStyles(selector, stylePropertyNames) {
|
|
85
|
-
await this.ensureChromeContext();
|
|
86
|
-
return await this.executeChromeScript((targetSelector, requestedProperties) => {
|
|
87
|
-
const targetElement = document.querySelector(targetSelector);
|
|
88
|
-
if (!targetElement) {
|
|
89
|
-
throw new Error("Element not found for selector: " + targetSelector);
|
|
90
|
-
}
|
|
91
|
-
const computedStyleDeclaration = window.getComputedStyle(targetElement);
|
|
92
|
-
const styleResult = {};
|
|
93
|
-
if (Array.isArray(requestedProperties) &&
|
|
94
|
-
requestedProperties.length > 0) {
|
|
95
|
-
for (const propertyName of requestedProperties) {
|
|
96
|
-
styleResult[propertyName] =
|
|
97
|
-
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
for (let index = 0; index < computedStyleDeclaration.length; index++) {
|
|
102
|
-
const propertyName = computedStyleDeclaration[index];
|
|
103
|
-
styleResult[propertyName] =
|
|
104
|
-
computedStyleDeclaration.getPropertyValue(propertyName);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return styleResult;
|
|
108
|
-
}, selector, stylePropertyNames);
|
|
109
|
-
}
|
|
110
|
-
async getUserInterfaceTree(rootSelector = "window", maximumDepth = 3) {
|
|
111
|
-
await this.ensureChromeContext();
|
|
112
|
-
return await this.executeChromeScript((targetSelector, maximumTraversalDepth) => {
|
|
113
|
-
const rootTarget = targetSelector === "window"
|
|
114
|
-
? document.documentElement
|
|
115
|
-
: document.querySelector(targetSelector);
|
|
116
|
-
if (!rootTarget) {
|
|
117
|
-
throw new Error("Root element not found for selector: " +
|
|
118
|
-
targetSelector);
|
|
119
|
-
}
|
|
120
|
-
const rootNodeHierarchy = {
|
|
121
|
-
tagName: rootTarget.tagName.toLowerCase(),
|
|
122
|
-
id: rootTarget.id || "",
|
|
123
|
-
className: rootTarget.className || "",
|
|
124
|
-
attributes: {},
|
|
125
|
-
children: [],
|
|
126
|
-
};
|
|
127
|
-
for (const attribute of Array.from(rootTarget.attributes)) {
|
|
128
|
-
rootNodeHierarchy.attributes[attribute.name] =
|
|
129
|
-
attribute.value;
|
|
130
|
-
}
|
|
131
|
-
const nodeQueue = [
|
|
132
|
-
{
|
|
133
|
-
domNode: rootTarget,
|
|
134
|
-
hierarchyNode: rootNodeHierarchy,
|
|
135
|
-
currentDepth: 0,
|
|
136
|
-
},
|
|
137
|
-
];
|
|
138
|
-
while (nodeQueue.length > 0) {
|
|
139
|
-
const queueItem = nodeQueue.shift();
|
|
140
|
-
if (!queueItem ||
|
|
141
|
-
queueItem.currentDepth >= maximumTraversalDepth) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
for (const childElement of Array.from(queueItem.domNode.children)) {
|
|
145
|
-
const childAttributes = {};
|
|
146
|
-
for (const attribute of Array.from(childElement.attributes)) {
|
|
147
|
-
childAttributes[attribute.name] = attribute.value;
|
|
148
|
-
}
|
|
149
|
-
const childHierarchyNode = {
|
|
150
|
-
tagName: childElement.tagName.toLowerCase(),
|
|
151
|
-
id: childElement.id || "",
|
|
152
|
-
className: childElement.className || "",
|
|
153
|
-
attributes: childAttributes,
|
|
154
|
-
children: [],
|
|
155
|
-
};
|
|
156
|
-
queueItem.hierarchyNode.children.push(childHierarchyNode);
|
|
157
|
-
nodeQueue.push({
|
|
158
|
-
domNode: childElement,
|
|
159
|
-
hierarchyNode: childHierarchyNode,
|
|
160
|
-
currentDepth: queueItem.currentDepth + 1,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return rootNodeHierarchy;
|
|
165
|
-
}, rootSelector, maximumDepth);
|
|
166
|
-
}
|
|
167
|
-
async customizeToolbar(action = "enter") {
|
|
168
|
-
await this.ensureChromeContext();
|
|
169
|
-
return await this.executeChromeScript(async (requestedAction) => {
|
|
170
|
-
const customizeMode = window.gCustomizeMode;
|
|
171
|
-
const documentElement = document.documentElement;
|
|
172
|
-
if (!customizeMode) {
|
|
173
|
-
const customizeCommand = document.getElementById("cmd_CustomizeToolbars");
|
|
174
|
-
if (customizeCommand) {
|
|
175
|
-
customizeCommand.doCommand();
|
|
176
|
-
return {
|
|
177
|
-
isCustomizing: documentElement.hasAttribute("customizing"),
|
|
178
|
-
success: true,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
throw new Error("Customize toolbar mode is not available in the current context.");
|
|
182
|
-
}
|
|
183
|
-
const isCurrentlyCustomizing = documentElement.hasAttribute("customizing") ||
|
|
184
|
-
Boolean(customizeMode.visible);
|
|
185
|
-
const shouldEnter = requestedAction === "enter";
|
|
186
|
-
if (shouldEnter !== isCurrentlyCustomizing) {
|
|
187
|
-
const transitionEventName = shouldEnter
|
|
188
|
-
? "customizationready"
|
|
189
|
-
: "aftercustomization";
|
|
190
|
-
await new Promise((resolve) => {
|
|
191
|
-
const timeoutIdentifier = setTimeout(resolve, 5000);
|
|
192
|
-
window.addEventListener(transitionEventName, () => {
|
|
193
|
-
clearTimeout(timeoutIdentifier);
|
|
194
|
-
resolve();
|
|
195
|
-
}, { once: true });
|
|
196
|
-
customizeMode[requestedAction]();
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
isCustomizing: documentElement.hasAttribute("customizing") ||
|
|
201
|
-
Boolean(customizeMode.visible),
|
|
202
|
-
success: true,
|
|
203
|
-
};
|
|
204
|
-
}, action);
|
|
205
|
-
}
|
|
206
|
-
async injectUserInterfaceStyle(cascadingStyleSheetContent, styleIdentifier = "mcp-injected-style") {
|
|
207
|
-
await this.ensureChromeContext();
|
|
208
|
-
return await this.executeChromeScript((identifier, cssSource) => {
|
|
209
|
-
let existingStyleElement = document.getElementById(identifier);
|
|
210
|
-
if (!existingStyleElement) {
|
|
211
|
-
existingStyleElement = document.createElement("style");
|
|
212
|
-
existingStyleElement.id = identifier;
|
|
213
|
-
existingStyleElement.setAttribute("type", "text/css");
|
|
214
|
-
document.documentElement.appendChild(existingStyleElement);
|
|
215
|
-
}
|
|
216
|
-
existingStyleElement.textContent = cssSource;
|
|
217
|
-
return { success: true, identifier: identifier };
|
|
218
|
-
}, styleIdentifier, cascadingStyleSheetContent);
|
|
219
|
-
}
|
|
220
|
-
async removeUserInterfaceStyle(styleIdentifier) {
|
|
221
|
-
await this.ensureChromeContext();
|
|
222
|
-
return await this.executeChromeScript((identifier) => {
|
|
223
|
-
const targetStyleElement = document.getElementById(identifier);
|
|
224
|
-
if (targetStyleElement) {
|
|
225
|
-
targetStyleElement.remove();
|
|
226
|
-
return { removed: true, identifier: identifier };
|
|
227
|
-
}
|
|
228
|
-
return { removed: false, identifier: identifier };
|
|
229
|
-
}, styleIdentifier);
|
|
230
|
-
}
|
|
231
|
-
async captureScreenshot(targetSelector) {
|
|
232
|
-
await this.ensureChromeContext();
|
|
233
|
-
if (targetSelector) {
|
|
234
|
-
const targetWebElement = await this.driver.findElement(By.css(targetSelector));
|
|
235
|
-
const elementScreenshotBase64 = await targetWebElement.takeScreenshot();
|
|
236
|
-
return { base64Image: elementScreenshotBase64, format: "png" };
|
|
237
|
-
}
|
|
238
|
-
const fullWindowScreenshotBase64 = await this.driver.takeScreenshot();
|
|
239
|
-
return { base64Image: fullWindowScreenshotBase64, format: "png" };
|
|
240
|
-
}
|
|
241
|
-
async openBrowserToolbox() {
|
|
242
|
-
await this.ensureChromeContext();
|
|
243
|
-
return await this.executeChromeScript(() => {
|
|
244
|
-
try {
|
|
245
|
-
const { BrowserToolboxLauncher } = ChromeUtils.importESModule("resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs");
|
|
246
|
-
BrowserToolboxLauncher.init();
|
|
247
|
-
return { success: true };
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
return { success: false };
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
export const firefoxManager = new FirefoxManager();
|
package/dist/index.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { firefoxManager } from "./firefox.js";
|
|
7
|
-
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
8
|
-
const server = new McpServer({
|
|
9
|
-
name: packageJson.name,
|
|
10
|
-
version: packageJson.version,
|
|
11
|
-
});
|
|
12
|
-
server.registerTool("launch_browser", {
|
|
13
|
-
description: "Launch a Firefox instance with chrome debugging capabilities enabled.",
|
|
14
|
-
inputSchema: {
|
|
15
|
-
binaryPath: z
|
|
16
|
-
.string()
|
|
17
|
-
.optional()
|
|
18
|
-
.describe("Optional path to Firefox executable binary."),
|
|
19
|
-
profileDirectory: z
|
|
20
|
-
.string()
|
|
21
|
-
.optional()
|
|
22
|
-
.describe("Optional path to custom Firefox profile directory."),
|
|
23
|
-
},
|
|
24
|
-
}, async (parameters) => {
|
|
25
|
-
await firefoxManager.initialiseBrowser(parameters.binaryPath, parameters.profileDirectory);
|
|
26
|
-
return {
|
|
27
|
-
content: [
|
|
28
|
-
{
|
|
29
|
-
type: "text",
|
|
30
|
-
text: "Firefox launched successfully in chrome context.",
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
server.registerTool("close_browser", { description: "Close the running Firefox instance." }, async () => {
|
|
36
|
-
await firefoxManager.terminateBrowser();
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text", text: "Firefox closed successfully." }],
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
server.registerTool("customize_toolbar", {
|
|
42
|
-
description: "Activate or manage the Firefox Customize Toolbar mode in the browser chrome.",
|
|
43
|
-
inputSchema: {
|
|
44
|
-
action: z
|
|
45
|
-
.enum(["enter", "exit"])
|
|
46
|
-
.default("enter")
|
|
47
|
-
.describe("Action to perform on customize toolbar mode ('enter' or 'exit'). Defaults to 'enter'."),
|
|
48
|
-
},
|
|
49
|
-
}, async (parameters) => {
|
|
50
|
-
const result = await firefoxManager.customizeToolbar(parameters.action);
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
server.registerTool("get_ui_tree", {
|
|
56
|
-
description: "Retrieve the hierarchical DOM tree of the Firefox chrome user interface window.",
|
|
57
|
-
inputSchema: {
|
|
58
|
-
rootSelector: z
|
|
59
|
-
.string()
|
|
60
|
-
.default("window")
|
|
61
|
-
.describe("CSS selector for root element."),
|
|
62
|
-
maximumDepth: z
|
|
63
|
-
.number()
|
|
64
|
-
.default(3)
|
|
65
|
-
.describe("Maximum recursive depth to traverse."),
|
|
66
|
-
},
|
|
67
|
-
}, async (parameters) => {
|
|
68
|
-
const treeData = await firefoxManager.getUserInterfaceTree(parameters.rootSelector, parameters.maximumDepth);
|
|
69
|
-
return {
|
|
70
|
-
content: [
|
|
71
|
-
{ type: "text", text: JSON.stringify(treeData, null, 2) },
|
|
72
|
-
],
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
server.registerTool("query_ui_elements", {
|
|
76
|
-
description: "Find elements in the Firefox UI chrome document matching a CSS selector.",
|
|
77
|
-
inputSchema: {
|
|
78
|
-
selector: z
|
|
79
|
-
.string()
|
|
80
|
-
.describe("CSS selector to query in chrome document (e.g. '#nav-bar', '#TabsToolbar')."),
|
|
81
|
-
},
|
|
82
|
-
}, async (parameters) => {
|
|
83
|
-
const elements = await firefoxManager.queryElements(parameters.selector);
|
|
84
|
-
return {
|
|
85
|
-
content: [
|
|
86
|
-
{ type: "text", text: JSON.stringify(elements, null, 2) },
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
server.registerTool("get_computed_styles", {
|
|
91
|
-
description: "Extract computed CSS properties for a Firefox UI chrome element.",
|
|
92
|
-
inputSchema: {
|
|
93
|
-
selector: z
|
|
94
|
-
.string()
|
|
95
|
-
.describe("CSS selector of the UI element (e.g. '#urlbar-background')."),
|
|
96
|
-
properties: z
|
|
97
|
-
.array(z.string())
|
|
98
|
-
.optional()
|
|
99
|
-
.describe("Optional list of specific CSS property names."),
|
|
100
|
-
},
|
|
101
|
-
}, async (parameters) => {
|
|
102
|
-
const styles = await firefoxManager.getComputedStyles(parameters.selector, parameters.properties);
|
|
103
|
-
return {
|
|
104
|
-
content: [{ type: "text", text: JSON.stringify(styles, null, 2) }],
|
|
105
|
-
};
|
|
106
|
-
});
|
|
107
|
-
server.registerTool("inject_theme_css", {
|
|
108
|
-
description: "Inject or replace a live stylesheet in the Firefox chrome window for immediate visual testing.",
|
|
109
|
-
inputSchema: {
|
|
110
|
-
css: z
|
|
111
|
-
.string()
|
|
112
|
-
.describe("CSS rules to inject into chrome document."),
|
|
113
|
-
styleId: z
|
|
114
|
-
.string()
|
|
115
|
-
.default("mcp-injected-style")
|
|
116
|
-
.describe("Identifier for the style tag."),
|
|
117
|
-
},
|
|
118
|
-
}, async (parameters) => {
|
|
119
|
-
const result = await firefoxManager.injectUserInterfaceStyle(parameters.css, parameters.styleId);
|
|
120
|
-
return {
|
|
121
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
server.registerTool("remove_theme_css", {
|
|
125
|
-
description: "Remove an injected stylesheet from the Firefox chrome window.",
|
|
126
|
-
inputSchema: {
|
|
127
|
-
styleId: z
|
|
128
|
-
.string()
|
|
129
|
-
.describe("Identifier of the style tag to remove."),
|
|
130
|
-
},
|
|
131
|
-
}, async (parameters) => {
|
|
132
|
-
const result = await firefoxManager.removeUserInterfaceStyle(parameters.styleId);
|
|
133
|
-
return {
|
|
134
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
server.registerTool("take_ui_screenshot", {
|
|
138
|
-
description: "Capture a PNG screenshot of the Firefox browser chrome window or a specific UI component.",
|
|
139
|
-
inputSchema: {
|
|
140
|
-
selector: z
|
|
141
|
-
.string()
|
|
142
|
-
.optional()
|
|
143
|
-
.describe("Optional CSS selector to screenshot a specific element."),
|
|
144
|
-
},
|
|
145
|
-
}, async (parameters) => {
|
|
146
|
-
const screenshot = await firefoxManager.captureScreenshot(parameters.selector);
|
|
147
|
-
return {
|
|
148
|
-
content: [
|
|
149
|
-
{
|
|
150
|
-
type: "image",
|
|
151
|
-
data: screenshot.base64Image,
|
|
152
|
-
mimeType: "image/png",
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
server.registerTool("execute_chrome_javascript", {
|
|
158
|
-
description: "Execute privileged JavaScript code in the Firefox chrome window context.",
|
|
159
|
-
inputSchema: {
|
|
160
|
-
script: z
|
|
161
|
-
.string()
|
|
162
|
-
.describe("JavaScript code string to evaluate in chrome context."),
|
|
163
|
-
},
|
|
164
|
-
}, async (parameters) => {
|
|
165
|
-
const result = await firefoxManager.executeChromeScript(parameters.script);
|
|
166
|
-
return {
|
|
167
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
168
|
-
};
|
|
169
|
-
});
|
|
170
|
-
async function main() {
|
|
171
|
-
const transport = new StdioServerTransport();
|
|
172
|
-
await server.connect(transport);
|
|
173
|
-
}
|
|
174
|
-
main().catch((error) => {
|
|
175
|
-
console.error("Server initialisation failure:", error);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
});
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|