firefox-css-theme 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.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/cli.js +101 -0
- package/dist/commands/create.js +34 -0
- package/dist/commands/mcp.js +219 -0
- package/dist/commands/profiles.js +17 -0
- package/dist/commands/save.js +91 -0
- package/dist/commands/start.js +89 -0
- package/dist/commands/styles.js +1 -0
- package/dist/firefox.js +366 -0
- package/dist/index.js +11 -0
- package/dist/processor.js +37 -0
- package/dist/profiles.js +98 -0
- package/dist/registry.js +48 -0
- package/dist/types.js +1 -0
- package/dist/watcher.js +111 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eason Wong
|
|
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,181 @@
|
|
|
1
|
+
# Firefox CSS Theme
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/firefox-css-theme)
|
|
4
|
+
[](https://github.com/easonwong-de/Firefox-CSS-Theme-MCP/actions/workflows/test.yml)
|
|
5
|
+
|
|
6
|
+
A CLI toolkit for scaffolding, compiling, live-debugging, and watching Firefox UI and CSS themes (`userChrome.css` and `userContent.css`). It also provides a Model Context Protocol (MCP) server for AI-assisted inspection and styling.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Node.js >= 20
|
|
11
|
+
- Firefox Browser
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### Installation
|
|
16
|
+
|
|
17
|
+
Install as a development dependency within your theme repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev firefox-css-theme
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You can add workflow scripts to your `package.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"create": "firefox-css-theme create",
|
|
29
|
+
"start": "firefox-css-theme start",
|
|
30
|
+
"profiles": "firefox-css-theme profiles",
|
|
31
|
+
"save": "firefox-css-theme save"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> `userChrome.css` and `userContent.css` must be located at the project root directory by default; otherwise, specify their paths using the `-c, --chrome` and `-u, --content` options.
|
|
38
|
+
|
|
39
|
+
### Scaffold Theme Files
|
|
40
|
+
|
|
41
|
+
Generate boilerplate `userChrome.css` and `userContent.css` files with Firefox namespace headers:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run create
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
|
|
49
|
+
- `-c, --chrome <path>`: Custom output path for `userChrome.css`.
|
|
50
|
+
- `-u, --content <path>`: Custom output path for `userContent.css`.
|
|
51
|
+
- `-f, --force`: Overwrite existing files.
|
|
52
|
+
|
|
53
|
+
### Start Live Development Session
|
|
54
|
+
|
|
55
|
+
Launch Firefox with hot stylesheet reloading:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run start
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
|
|
63
|
+
- `-p, --profile <name>`: Designate a specific existing Firefox profile by name. If omitted, Firefox runs in an isolated temporary profile that is automatically created and deleted on exit.
|
|
64
|
+
- `-c, --chrome <path>`: Custom path to `userChrome.css`.
|
|
65
|
+
- `-u, --content <path>`: Custom path to `userContent.css`.
|
|
66
|
+
- `--no-watch`: Disable file watching and live reloading.
|
|
67
|
+
- `--headless`: Run Firefox in headless mode.
|
|
68
|
+
- `--nova-ui`: Enable Firefox Nova UI redesign preferences.
|
|
69
|
+
- `--binary <path>`: Path to custom Firefox executable binary.
|
|
70
|
+
|
|
71
|
+
### Discover Available Profiles
|
|
72
|
+
|
|
73
|
+
List all detected Firefox profiles:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm run profiles
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Save Theme to Profile
|
|
80
|
+
|
|
81
|
+
Compile and save stylesheets directly into a profile's `chrome/` directory:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run save -- --profile <name>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
|
|
89
|
+
- `-p, --profile <name>`: Target Firefox profile name (mandatory if multiple profiles exist).
|
|
90
|
+
- `-c, --chrome <path>`: Custom path to `userChrome.css`.
|
|
91
|
+
- `-u, --content <path>`: Custom path to `userContent.css`.
|
|
92
|
+
- `-f, --force`: Overwrite without confirmation prompt.
|
|
93
|
+
|
|
94
|
+
> [!WARNING]
|
|
95
|
+
> This operation will overwrite existing CSS themes in the profile. A backup is advised.
|
|
96
|
+
|
|
97
|
+
## CLI Usage
|
|
98
|
+
|
|
99
|
+
You can also run commands on demand without local installation using `npx firefox-css-theme [command] [option]`.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Options:
|
|
103
|
+
-v, --version Show version number
|
|
104
|
+
-h, --help display help for command
|
|
105
|
+
|
|
106
|
+
Commands:
|
|
107
|
+
create [options] Scaffold starter userChrome.css and userContent.css files
|
|
108
|
+
start [options] Launch Firefox with live stylesheet bundling and hot-reloading
|
|
109
|
+
profiles List all detected Firefox profiles
|
|
110
|
+
save [options] Save compiled stylesheets into a Firefox profile's chrome folder
|
|
111
|
+
mcp [options] Start the Model Context Protocol (MCP) server
|
|
112
|
+
help [command] display help for command
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## MCP Server Usage
|
|
116
|
+
|
|
117
|
+
The MCP server enables AI assistants to inspect the Firefox chrome DOM, query computed styles, inject custom CSS, and capture UI screenshots in real time.
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
### Configuration
|
|
122
|
+
|
|
123
|
+
Add the server to your preferred client or development environment:
|
|
124
|
+
|
|
125
|
+
#### Claude Code
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
claude mcp add firefox-css-theme -- npx -y firefox-css-theme mcp --nova-ui
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Codex
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
codex mcp add firefox-css-theme -- npx -y firefox-css-theme mcp --nova-ui
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### IDE MCP Settings (`mcpServers`)
|
|
138
|
+
|
|
139
|
+
Using `npx`:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"mcpServers": {
|
|
144
|
+
"firefox-css-theme": {
|
|
145
|
+
"command": "npx",
|
|
146
|
+
"args": ["-y", "firefox-css-theme", "mcp", "--nova-ui"]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Or using an absolute path to a local build:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"firefox-css-theme": {
|
|
158
|
+
"command": "node",
|
|
159
|
+
"args": [
|
|
160
|
+
"/absolute/path/to/firefox-css-theme-mcp/dist/cli.js",
|
|
161
|
+
"mcp",
|
|
162
|
+
"--nova-ui"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Available MCP Tools
|
|
170
|
+
|
|
171
|
+
- `launch_browser`: Launches Firefox with chrome debugging capabilities enabled.
|
|
172
|
+
- `close_browser`: Terminates the browser instance.
|
|
173
|
+
- `customize_toolbar`: Activates or controls the Firefox "Customise Toolbar" mode.
|
|
174
|
+
- `get_ui_tree`: Dumps the hierarchical DOM tree of the chrome window.
|
|
175
|
+
- `query_ui_elements`: Queries elements matching a CSS selector in the browser chrome.
|
|
176
|
+
- `get_computed_styles`: Extracts computed CSS property values of a specific UI element.
|
|
177
|
+
- `inject_theme_css`: Injects stylesheets in the live UI (`target: "chrome" | "content"`).
|
|
178
|
+
- `remove_theme_css`: Removes an injected stylesheet by id (`target: "chrome" | "content"`).
|
|
179
|
+
- `list_theme_css`: Lists all currently injected custom stylesheets (`target: "chrome" | "content"`).
|
|
180
|
+
- `take_ui_screenshot`: Captures a screenshot of the browser window or a specific UI component.
|
|
181
|
+
- `execute_chrome_javascript`: Runs privileged JavaScript in the browser chrome window context.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createCommand } from "./commands/create.js";
|
|
6
|
+
import { startMcpServer } from "./commands/mcp.js";
|
|
7
|
+
import { profilesCommand } from "./commands/profiles.js";
|
|
8
|
+
import { saveCommand } from "./commands/save.js";
|
|
9
|
+
import { startCommand } from "./commands/start.js";
|
|
10
|
+
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("firefox-css-theme")
|
|
14
|
+
.description(packageJson.description)
|
|
15
|
+
.version(packageJson.version, "-v, --version", "Show version number");
|
|
16
|
+
program
|
|
17
|
+
.command("create")
|
|
18
|
+
.description("Scaffold starter userChrome.css and userContent.css files")
|
|
19
|
+
.option("-c, --chrome <path>", "Path to userChrome.css")
|
|
20
|
+
.option("-u, --content <path>", "Path to userContent.css")
|
|
21
|
+
.option("-f, --force", "Overwrite existing files without confirmation")
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
await createCommand({
|
|
24
|
+
chromePath: options.chrome,
|
|
25
|
+
contentPath: options.content,
|
|
26
|
+
force: options.force,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("start")
|
|
31
|
+
.description("Launch Firefox with live stylesheet bundling and hot-reloading")
|
|
32
|
+
.option("-p, --profile <name>", "Designate Firefox profile name")
|
|
33
|
+
.option("-c, --chrome <path>", "Path to userChrome.css (default: ./userChrome.css)")
|
|
34
|
+
.option("-u, --content <path>", "Path to userContent.css (default: ./userContent.css)")
|
|
35
|
+
.option("--no-watch", "Disable file watching in start mode")
|
|
36
|
+
.option("--headless", "Run Firefox in headless mode")
|
|
37
|
+
.option("--nova-ui", "Enable Firefox Nova UI preferences")
|
|
38
|
+
.option("--binary <path>", "Path to custom Firefox executable")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
await startCommand({
|
|
41
|
+
binaryPath: options.binary,
|
|
42
|
+
chromePath: options.chrome,
|
|
43
|
+
contentPath: options.content,
|
|
44
|
+
headless: options.headless,
|
|
45
|
+
novaUi: options.novaUi,
|
|
46
|
+
profileName: options.profile,
|
|
47
|
+
watch: options.watch,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command("profiles")
|
|
52
|
+
.description("List all detected Firefox profiles")
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await profilesCommand();
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command("save")
|
|
58
|
+
.description("Save compiled stylesheets into a Firefox profile's chrome folder")
|
|
59
|
+
.option("-p, --profile <name>", "Target Firefox profile name")
|
|
60
|
+
.option("-c, --chrome <path>", "Path to userChrome.css")
|
|
61
|
+
.option("-u, --content <path>", "Path to userContent.css")
|
|
62
|
+
.option("-f, --force", "Overwrite existing files without confirmation")
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
await saveCommand({
|
|
65
|
+
chromePath: options.chrome,
|
|
66
|
+
contentPath: options.content,
|
|
67
|
+
force: options.force,
|
|
68
|
+
profileName: options.profile,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
program
|
|
72
|
+
.command("mcp")
|
|
73
|
+
.description("Start the Model Context Protocol (MCP) server")
|
|
74
|
+
.option("--nova-ui", "Enable Firefox Nova UI preferences")
|
|
75
|
+
.option("--headless", "Run Firefox in headless mode")
|
|
76
|
+
.option("--binary <path>", "Path to custom Firefox executable")
|
|
77
|
+
.option("-p, --profile <name>", "Designate Firefox profile name")
|
|
78
|
+
.allowUnknownOption()
|
|
79
|
+
.action(async (options) => {
|
|
80
|
+
await startMcpServer({
|
|
81
|
+
binaryPath: options.binary,
|
|
82
|
+
headless: options.headless,
|
|
83
|
+
novaUi: options.novaUi,
|
|
84
|
+
profileName: options.profile,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
async function main() {
|
|
88
|
+
const scriptBinaryName = path.basename(process.argv[1] || "");
|
|
89
|
+
if (scriptBinaryName === "firefox-css-theme-mcp" ||
|
|
90
|
+
scriptBinaryName === "firefox-css-theme-mcp.js") {
|
|
91
|
+
const isNovaUi = process.argv.includes("--nova-ui");
|
|
92
|
+
const isHeadless = process.argv.includes("--headless");
|
|
93
|
+
await startMcpServer({ headless: isHeadless, novaUi: isNovaUi });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await program.parseAsync(process.argv);
|
|
97
|
+
}
|
|
98
|
+
main().catch((error) => {
|
|
99
|
+
console.error(error instanceof Error ? error.message : error);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_CHROME_CSS = `/* Firefox userChrome.css */
|
|
4
|
+
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
|
5
|
+
@namespace html url("http://www.w3.org/1999/xhtml");
|
|
6
|
+
|
|
7
|
+
`;
|
|
8
|
+
const DEFAULT_CONTENT_CSS = `/* Firefox userContent.css */
|
|
9
|
+
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
|
10
|
+
@namespace html url("http://www.w3.org/1999/xhtml");
|
|
11
|
+
|
|
12
|
+
`;
|
|
13
|
+
/**
|
|
14
|
+
* Scaffolds starter userChrome.css and userContent.css boilerplate files with
|
|
15
|
+
* namespace headers.
|
|
16
|
+
*/
|
|
17
|
+
export async function createCommand(options = {}) {
|
|
18
|
+
const chromePath = path.resolve(process.cwd(), options.chromePath || "userChrome.css");
|
|
19
|
+
const contentPath = path.resolve(process.cwd(), options.contentPath || "userContent.css");
|
|
20
|
+
if (existsSync(chromePath) && !options.force) {
|
|
21
|
+
console.warn(`\x1b[33m[skip]\x1b[0m ${chromePath} already exists. Use --force to overwrite.`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
writeFileSync(chromePath, DEFAULT_CHROME_CSS, "utf8");
|
|
25
|
+
console.log(`\x1b[32m[created]\x1b[0m ${chromePath}`);
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(contentPath) && !options.force) {
|
|
28
|
+
console.warn(`\x1b[33m[skip]\x1b[0m ${contentPath} already exists. Use --force to overwrite.`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
writeFileSync(contentPath, DEFAULT_CONTENT_CSS, "utf8");
|
|
32
|
+
console.log(`\x1b[32m[created]\x1b[0m ${contentPath}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
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 { firefoxManager } from "../firefox.js";
|
|
6
|
+
import { compileCssString } from "../processor.js";
|
|
7
|
+
/** Creates and starts the Model Context Protocol (MCP) server over standard I/O. */
|
|
8
|
+
export async function startMcpServer(options = {}) {
|
|
9
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: packageJson.name,
|
|
12
|
+
version: packageJson.version,
|
|
13
|
+
});
|
|
14
|
+
server.registerTool("launch_browser", {
|
|
15
|
+
description: "Launch a Firefox instance with chrome debugging capabilities enabled.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
binaryPath: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Optional path to Firefox executable binary."),
|
|
21
|
+
profileName: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Optional name of existing Firefox profile. If omitted, uses an isolated temporary profile."),
|
|
25
|
+
headless: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Optional flag to run Firefox in headless mode."),
|
|
29
|
+
novaUi: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Optional flag to enable Firefox Nova UI preferences."),
|
|
33
|
+
},
|
|
34
|
+
}, async (parameters) => {
|
|
35
|
+
const profileName = parameters.profileName ?? options.profileName;
|
|
36
|
+
await firefoxManager.initialiseBrowser(parameters.binaryPath ?? options.binaryPath, profileName, parameters.headless ?? options.headless, parameters.novaUi ?? options.novaUi);
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: "Firefox launched successfully in chrome context.",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
server.registerTool("close_browser", { description: "Close the running Firefox instance." }, async () => {
|
|
47
|
+
await firefoxManager.terminateBrowser();
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{ type: "text", text: "Firefox closed successfully." },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
server.registerTool("customize_toolbar", {
|
|
55
|
+
description: "Activate or manage the Firefox Customise Toolbar mode in the browser chrome.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
action: z
|
|
58
|
+
.enum(["enter", "exit"])
|
|
59
|
+
.default("enter")
|
|
60
|
+
.describe("Action to perform on customise toolbar mode ('enter' or 'exit'). Defaults to 'enter'."),
|
|
61
|
+
},
|
|
62
|
+
}, async (parameters) => {
|
|
63
|
+
const result = await firefoxManager.customiseToolbar(parameters.action);
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
server.registerTool("get_ui_tree", {
|
|
71
|
+
description: "Retrieve the hierarchical DOM tree of the Firefox chrome user interface window.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
rootSelector: z
|
|
74
|
+
.string()
|
|
75
|
+
.default("window")
|
|
76
|
+
.describe("CSS selector for root element."),
|
|
77
|
+
maxDepth: z
|
|
78
|
+
.number()
|
|
79
|
+
.default(3)
|
|
80
|
+
.describe("Maximum recursive depth to traverse."),
|
|
81
|
+
},
|
|
82
|
+
}, async (parameters) => {
|
|
83
|
+
const treeData = await firefoxManager.getUiTree(parameters.rootSelector, parameters.maxDepth);
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{ type: "text", text: JSON.stringify(treeData, null, 2) },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
server.registerTool("query_ui_elements", {
|
|
91
|
+
description: "Find elements in the Firefox UI chrome document matching a CSS selector.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
selector: z
|
|
94
|
+
.string()
|
|
95
|
+
.describe("CSS selector to query in chrome document (e.g. '#nav-bar', '#TabsToolbar')."),
|
|
96
|
+
},
|
|
97
|
+
}, async (parameters) => {
|
|
98
|
+
const elements = await firefoxManager.queryElements(parameters.selector);
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{ type: "text", text: JSON.stringify(elements, null, 2) },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
server.registerTool("get_computed_styles", {
|
|
106
|
+
description: "Extract computed CSS properties for a Firefox UI chrome element.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
selector: z
|
|
109
|
+
.string()
|
|
110
|
+
.describe("CSS selector of the UI element (e.g. '#urlbar-background')."),
|
|
111
|
+
properties: z
|
|
112
|
+
.array(z.string())
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Optional list of specific CSS property names."),
|
|
115
|
+
},
|
|
116
|
+
}, async (parameters) => {
|
|
117
|
+
const styles = await firefoxManager.getComputedStyles(parameters.selector, parameters.properties);
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{ type: "text", text: JSON.stringify(styles, null, 2) },
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
server.registerTool("inject_theme_css", {
|
|
125
|
+
description: "Inject or replace a live stylesheet in the Firefox window.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
css: z.string().describe("CSS rules to inject."),
|
|
128
|
+
id: z
|
|
129
|
+
.string()
|
|
130
|
+
.default("mcp-injected-style")
|
|
131
|
+
.describe("Identifier for the style."),
|
|
132
|
+
target: z
|
|
133
|
+
.enum(["chrome", "content"])
|
|
134
|
+
.describe("Target scope: chrome for browser UI, or content for web pages."),
|
|
135
|
+
},
|
|
136
|
+
}, async (parameters) => {
|
|
137
|
+
const compiledCss = await compileCssString(parameters.css);
|
|
138
|
+
const result = parameters.target === "content"
|
|
139
|
+
? await firefoxManager.injectContentStyle(compiledCss.css, parameters.id)
|
|
140
|
+
: await firefoxManager.injectChromeStyle(compiledCss.css, parameters.id);
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
server.registerTool("remove_theme_css", {
|
|
148
|
+
description: "Remove an injected stylesheet from the Firefox window.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
id: z.string().describe("Identifier of the style to remove."),
|
|
151
|
+
target: z
|
|
152
|
+
.enum(["chrome", "content"])
|
|
153
|
+
.describe("Target scope of the style to remove: chrome or content."),
|
|
154
|
+
},
|
|
155
|
+
}, async (parameters) => {
|
|
156
|
+
const result = parameters.target === "content"
|
|
157
|
+
? await firefoxManager.removeContentStyle(parameters.id)
|
|
158
|
+
: await firefoxManager.removeChromeStyle(parameters.id);
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
server.registerTool("list_theme_css", {
|
|
166
|
+
description: "List all custom stylesheets currently injected in the Firefox window.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
target: z
|
|
169
|
+
.enum(["chrome", "content"])
|
|
170
|
+
.describe("Target scope to list: chrome or content."),
|
|
171
|
+
},
|
|
172
|
+
}, async (parameters) => {
|
|
173
|
+
const result = parameters.target === "content"
|
|
174
|
+
? await firefoxManager.listContentStyles()
|
|
175
|
+
: await firefoxManager.listChromeStyles();
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
server.registerTool("take_ui_screenshot", {
|
|
183
|
+
description: "Capture a PNG screenshot of the Firefox browser chrome window or a specific UI component.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
selector: z
|
|
186
|
+
.string()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("Optional CSS selector to screenshot a specific element."),
|
|
189
|
+
},
|
|
190
|
+
}, async (parameters) => {
|
|
191
|
+
const screenshot = await firefoxManager.getScreenshot(parameters.selector);
|
|
192
|
+
return {
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: "image",
|
|
196
|
+
data: screenshot.base64Image,
|
|
197
|
+
mimeType: "image/png",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
server.registerTool("execute_chrome_javascript", {
|
|
203
|
+
description: "Execute privileged JavaScript code in the Firefox chrome window context.",
|
|
204
|
+
inputSchema: {
|
|
205
|
+
script: z
|
|
206
|
+
.string()
|
|
207
|
+
.describe("JavaScript code string to evaluate in chrome context."),
|
|
208
|
+
},
|
|
209
|
+
}, async (parameters) => {
|
|
210
|
+
const result = await firefoxManager.executeChromeScript(parameters.script);
|
|
211
|
+
return {
|
|
212
|
+
content: [
|
|
213
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
const transport = new StdioServerTransport();
|
|
218
|
+
await server.connect(transport);
|
|
219
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { listFirefoxProfiles } from "../profiles.js";
|
|
2
|
+
/** Executes the 'profiles' CLI command to list all detected Firefox profiles. */
|
|
3
|
+
export async function profilesCommand() {
|
|
4
|
+
const profiles = listFirefoxProfiles();
|
|
5
|
+
if (profiles.length === 0) {
|
|
6
|
+
console.log("No Firefox profiles detected in default configuration directory.");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log("\nDetected Firefox Profiles:\n");
|
|
10
|
+
for (const profile of profiles) {
|
|
11
|
+
const defaultBadge = profile.isDefault
|
|
12
|
+
? " \x1b[32m(default)\x1b[0m"
|
|
13
|
+
: "";
|
|
14
|
+
console.log(` \x1b[1m${profile.name}\x1b[0m${defaultBadge}`);
|
|
15
|
+
console.log(` \x1b[90m${profile.path}\x1b[0m\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
4
|
+
import { compileCssString } from "../processor.js";
|
|
5
|
+
import { listFirefoxProfiles, resolveProfileDirectoryByName, } from "../profiles.js";
|
|
6
|
+
/**
|
|
7
|
+
* Saves and compiles theme stylesheets directly into the target Firefox
|
|
8
|
+
* profile's chrome folder.
|
|
9
|
+
*/
|
|
10
|
+
export async function saveCommand(options = {}) {
|
|
11
|
+
let profileName = options.profileName;
|
|
12
|
+
if (!profileName) {
|
|
13
|
+
const availableProfiles = listFirefoxProfiles();
|
|
14
|
+
if (availableProfiles.length === 0) {
|
|
15
|
+
throw new Error("No Firefox profile detected.");
|
|
16
|
+
}
|
|
17
|
+
else if (availableProfiles.length === 1) {
|
|
18
|
+
profileName = availableProfiles[0].name;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const profileNames = availableProfiles
|
|
22
|
+
.map((profile) => profile.name)
|
|
23
|
+
.join(", ");
|
|
24
|
+
throw new Error(`Multiple Firefox profiles detected (${profileNames}). Specify target profile with -p, --profile <name>.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const profileDir = resolveProfileDirectoryByName(profileName);
|
|
28
|
+
const chromeDir = path.join(profileDir, "chrome");
|
|
29
|
+
if (!existsSync(chromeDir)) {
|
|
30
|
+
mkdirSync(chromeDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
const srcChromePath = options.chromePath
|
|
33
|
+
? path.resolve(process.cwd(), options.chromePath)
|
|
34
|
+
: existsSync(path.resolve(process.cwd(), "userChrome.css"))
|
|
35
|
+
? path.resolve(process.cwd(), "userChrome.css")
|
|
36
|
+
: undefined;
|
|
37
|
+
const srcContentPath = options.contentPath
|
|
38
|
+
? path.resolve(process.cwd(), options.contentPath)
|
|
39
|
+
: existsSync(path.resolve(process.cwd(), "userContent.css"))
|
|
40
|
+
? path.resolve(process.cwd(), "userContent.css")
|
|
41
|
+
: undefined;
|
|
42
|
+
if (!srcChromePath && !srcContentPath) {
|
|
43
|
+
console.warn("No stylesheet files found to save.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const destChromePath = path.join(chromeDir, "userChrome.css");
|
|
47
|
+
const destContentPath = path.join(chromeDir, "userContent.css");
|
|
48
|
+
const requiresConfirmation = !options.force &&
|
|
49
|
+
((srcChromePath && existsSync(destChromePath)) ||
|
|
50
|
+
(srcContentPath && existsSync(destContentPath)));
|
|
51
|
+
if (requiresConfirmation) {
|
|
52
|
+
const readlineInterface = readline.createInterface({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stdout,
|
|
55
|
+
});
|
|
56
|
+
const userResponse = await readlineInterface.question(`\x1b[33m[warning]\x1b[0m Existing theme files will be overwritten in profile "${profileName}". Proceed? (y/N) `);
|
|
57
|
+
readlineInterface.close();
|
|
58
|
+
if (userResponse.trim().toLowerCase() !== "y") {
|
|
59
|
+
console.log("Save operation cancelled.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (srcChromePath) {
|
|
64
|
+
const sourceContent = readFileSync(srcChromePath, "utf8");
|
|
65
|
+
let mergedCss = sourceContent;
|
|
66
|
+
if (existsSync(destChromePath)) {
|
|
67
|
+
const existingDestinationContent = readFileSync(destChromePath, "utf8");
|
|
68
|
+
if (!existingDestinationContent.includes(sourceContent.trim()) &&
|
|
69
|
+
!sourceContent.includes(existingDestinationContent.trim())) {
|
|
70
|
+
mergedCss = `${existingDestinationContent}\n\n${sourceContent}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const compiledBundle = await compileCssString(mergedCss, srcChromePath);
|
|
74
|
+
writeFileSync(destChromePath, compiledBundle.css, "utf8");
|
|
75
|
+
console.log(`\x1b[32m[saved]\x1b[0m userChrome.css -> ${destChromePath}`);
|
|
76
|
+
}
|
|
77
|
+
if (srcContentPath) {
|
|
78
|
+
const sourceContent = readFileSync(srcContentPath, "utf8");
|
|
79
|
+
let mergedCss = sourceContent;
|
|
80
|
+
if (existsSync(destContentPath)) {
|
|
81
|
+
const existingDestinationContent = readFileSync(destContentPath, "utf8");
|
|
82
|
+
if (!existingDestinationContent.includes(sourceContent.trim()) &&
|
|
83
|
+
!sourceContent.includes(existingDestinationContent.trim())) {
|
|
84
|
+
mergedCss = `${existingDestinationContent}\n\n${sourceContent}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const compiledBundle = await compileCssString(mergedCss, srcContentPath);
|
|
88
|
+
writeFileSync(destContentPath, compiledBundle.css, "utf8");
|
|
89
|
+
console.log(`\x1b[32m[saved]\x1b[0m userContent.css -> ${destContentPath}`);
|
|
90
|
+
}
|
|
91
|
+
}
|