firefox-css-theme-mcp 0.1.5 → 0.1.6

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 CHANGED
@@ -69,14 +69,16 @@ npm run build
69
69
 
70
70
  ## Command-line Flags
71
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`). |
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. |
75
76
 
76
77
  ## Available MCP Tools
77
78
 
78
79
  - `launch_browser`: Launches Firefox with chrome debugging capabilities enabled.
79
80
  - `close_browser`: Terminates the browser instance.
81
+ - `customize_toolbar`: Activates or controls the Firefox "Customize Toolbar" mode.
80
82
  - `get_ui_tree`: Dumps the hierarchical DOM tree of the chrome window.
81
83
  - `query_ui_elements`: Queries elements matching a CSS selector in the browser chrome.
82
84
  - `get_computed_styles`: Extracts computed CSS property values of a specific UI element.
package/dist/firefox.js CHANGED
@@ -1,60 +1,63 @@
1
1
  import { Builder, By } from "selenium-webdriver";
2
2
  import * as firefox from "selenium-webdriver/firefox.js";
3
- export class FirefoxBrowserManager {
4
- driverInstance = null;
3
+ export class FirefoxManager {
4
+ driver = null;
5
5
  async initialiseBrowser(binaryPath, profileDirectory) {
6
- if (this.driverInstance)
6
+ if (this.driver)
7
7
  return;
8
8
  const isNovaUiEnabled = process.argv.includes("--nova-ui");
9
- const firefoxOptions = new firefox.Options();
10
- firefoxOptions.addArguments("-no-remote", "-new-instance");
11
- firefoxOptions.setPreference("devtools.chrome.enabled", true);
12
- firefoxOptions.setPreference("devtools.debugger.remote-enabled", true);
13
- firefoxOptions.setPreference("devtools.debugger.prompt-connection", false);
14
- firefoxOptions.setPreference("toolkit.legacyUserProfileCustomizations.stylesheets", true);
15
- firefoxOptions.setPreference("marionette.allow-system-access", true);
16
- firefoxOptions.setPreference("remote.allow-system-access", true);
17
- firefoxOptions.setPreference("browser.nova.enabled", isNovaUiEnabled);
18
- firefoxOptions.setPreference("browser.newtabpage.activity-stream.nova.enabled", isNovaUiEnabled);
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);
19
22
  if (binaryPath)
20
- firefoxOptions.setBinary(binaryPath);
23
+ options.setBinary(binaryPath);
21
24
  if (profileDirectory)
22
- firefoxOptions.setProfile(profileDirectory);
23
- const firefoxService = new firefox.ServiceBuilder()
25
+ options.setProfile(profileDirectory);
26
+ const service = new firefox.ServiceBuilder()
24
27
  .enableVerboseLogging(true)
25
28
  .addArguments("--allow-system-access");
26
29
  try {
27
- this.driverInstance = await new Builder()
30
+ this.driver = await new Builder()
28
31
  .forBrowser("firefox")
29
- .setFirefoxOptions(firefoxOptions)
30
- .setFirefoxService(firefoxService)
32
+ .setFirefoxOptions(options)
33
+ .setFirefoxService(service)
31
34
  .build();
32
35
  await this.ensureChromeContext();
33
36
  await this.openBrowserToolbox().catch(() => { });
34
37
  }
35
38
  catch (error) {
36
- if (this.driverInstance) {
37
- await this.driverInstance.quit().catch(() => { });
38
- this.driverInstance = null;
39
+ if (this.driver) {
40
+ await this.driver.quit().catch(() => { });
41
+ this.driver = null;
39
42
  }
40
43
  throw error;
41
44
  }
42
45
  }
43
46
  async terminateBrowser() {
44
- if (!this.driverInstance)
47
+ if (!this.driver)
45
48
  return;
46
- await this.driverInstance.quit();
47
- this.driverInstance = null;
49
+ await this.driver.quit();
50
+ this.driver = null;
48
51
  }
49
52
  async ensureChromeContext() {
50
- if (!this.driverInstance) {
53
+ if (!this.driver) {
51
54
  throw new Error("Firefox driver instance is not initialised.");
52
55
  }
53
- await this.driverInstance.setContext("chrome");
56
+ await this.driver.setContext("chrome");
54
57
  }
55
58
  async executeChromeScript(script, ...argumentsList) {
56
59
  await this.ensureChromeContext();
57
- return (await this.driverInstance.executeScript(script, ...argumentsList));
60
+ return (await this.driver.executeScript(script, ...argumentsList));
58
61
  }
59
62
  async queryElements(selector) {
60
63
  await this.ensureChromeContext();
@@ -114,28 +117,92 @@ export class FirefoxBrowserManager {
114
117
  throw new Error("Root element not found for selector: " +
115
118
  targetSelector);
116
119
  }
117
- function serializeNode(node, currentDepth) {
118
- const attributeMap = {};
119
- for (const attribute of Array.from(node.attributes)) {
120
- attributeMap[attribute.name] = attribute.value;
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;
121
143
  }
122
- const serializedChildren = [];
123
- if (currentDepth < maximumTraversalDepth) {
124
- for (const child of Array.from(node.children)) {
125
- serializedChildren.push(serializeNode(child, currentDepth + 1));
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;
126
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
+ });
127
162
  }
128
- return {
129
- tagName: node.tagName.toLowerCase(),
130
- id: node.id || "",
131
- className: node.className || "",
132
- attributes: attributeMap,
133
- children: serializedChildren,
134
- };
135
163
  }
136
- return serializeNode(rootTarget, 0);
164
+ return rootNodeHierarchy;
137
165
  }, rootSelector, maximumDepth);
138
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
+ }
139
206
  async injectUserInterfaceStyle(cascadingStyleSheetContent, styleIdentifier = "mcp-injected-style") {
140
207
  await this.ensureChromeContext();
141
208
  return await this.executeChromeScript((identifier, cssSource) => {
@@ -164,11 +231,11 @@ export class FirefoxBrowserManager {
164
231
  async captureScreenshot(targetSelector) {
165
232
  await this.ensureChromeContext();
166
233
  if (targetSelector) {
167
- const targetWebElement = await this.driverInstance.findElement(By.css(targetSelector));
234
+ const targetWebElement = await this.driver.findElement(By.css(targetSelector));
168
235
  const elementScreenshotBase64 = await targetWebElement.takeScreenshot();
169
236
  return { base64Image: elementScreenshotBase64, format: "png" };
170
237
  }
171
- const fullWindowScreenshotBase64 = await this.driverInstance.takeScreenshot();
238
+ const fullWindowScreenshotBase64 = await this.driver.takeScreenshot();
172
239
  return { base64Image: fullWindowScreenshotBase64, format: "png" };
173
240
  }
174
241
  async openBrowserToolbox() {
@@ -185,4 +252,4 @@ export class FirefoxBrowserManager {
185
252
  });
186
253
  }
187
254
  }
188
- export const globalFirefoxManager = new FirefoxBrowserManager();
255
+ export const firefoxManager = new FirefoxManager();
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  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.5",
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,
9
11
  });
10
- serverInstance.registerTool("launch_browser", {
12
+ server.registerTool("launch_browser", {
11
13
  description: "Launch a Firefox instance with chrome debugging capabilities enabled.",
12
14
  inputSchema: {
13
15
  binaryPath: z
@@ -20,7 +22,7 @@ serverInstance.registerTool("launch_browser", {
20
22
  .describe("Optional path to custom Firefox profile directory."),
21
23
  },
22
24
  }, async (parameters) => {
23
- await globalFirefoxManager.initialiseBrowser(parameters.binaryPath, parameters.profileDirectory);
25
+ await firefoxManager.initialiseBrowser(parameters.binaryPath, parameters.profileDirectory);
24
26
  return {
25
27
  content: [
26
28
  {
@@ -30,13 +32,27 @@ serverInstance.registerTool("launch_browser", {
30
32
  ],
31
33
  };
32
34
  });
33
- serverInstance.registerTool("close_browser", { description: "Close the running Firefox instance." }, async () => {
34
- await globalFirefoxManager.terminateBrowser();
35
+ server.registerTool("close_browser", { description: "Close the running Firefox instance." }, async () => {
36
+ await firefoxManager.terminateBrowser();
35
37
  return {
36
38
  content: [{ type: "text", text: "Firefox closed successfully." }],
37
39
  };
38
40
  });
39
- serverInstance.registerTool("get_ui_tree", {
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", {
40
56
  description: "Retrieve the hierarchical DOM tree of the Firefox chrome user interface window.",
41
57
  inputSchema: {
42
58
  rootSelector: z
@@ -49,14 +65,14 @@ serverInstance.registerTool("get_ui_tree", {
49
65
  .describe("Maximum recursive depth to traverse."),
50
66
  },
51
67
  }, async (parameters) => {
52
- const treeData = await globalFirefoxManager.getUserInterfaceTree(parameters.rootSelector, parameters.maximumDepth);
68
+ const treeData = await firefoxManager.getUserInterfaceTree(parameters.rootSelector, parameters.maximumDepth);
53
69
  return {
54
70
  content: [
55
71
  { type: "text", text: JSON.stringify(treeData, null, 2) },
56
72
  ],
57
73
  };
58
74
  });
59
- serverInstance.registerTool("query_ui_elements", {
75
+ server.registerTool("query_ui_elements", {
60
76
  description: "Find elements in the Firefox UI chrome document matching a CSS selector.",
61
77
  inputSchema: {
62
78
  selector: z
@@ -64,14 +80,14 @@ serverInstance.registerTool("query_ui_elements", {
64
80
  .describe("CSS selector to query in chrome document (e.g. '#nav-bar', '#TabsToolbar')."),
65
81
  },
66
82
  }, async (parameters) => {
67
- const elements = await globalFirefoxManager.queryElements(parameters.selector);
83
+ const elements = await firefoxManager.queryElements(parameters.selector);
68
84
  return {
69
85
  content: [
70
86
  { type: "text", text: JSON.stringify(elements, null, 2) },
71
87
  ],
72
88
  };
73
89
  });
74
- serverInstance.registerTool("get_computed_styles", {
90
+ server.registerTool("get_computed_styles", {
75
91
  description: "Extract computed CSS properties for a Firefox UI chrome element.",
76
92
  inputSchema: {
77
93
  selector: z
@@ -83,12 +99,12 @@ serverInstance.registerTool("get_computed_styles", {
83
99
  .describe("Optional list of specific CSS property names."),
84
100
  },
85
101
  }, async (parameters) => {
86
- const styles = await globalFirefoxManager.getComputedStyles(parameters.selector, parameters.properties);
102
+ const styles = await firefoxManager.getComputedStyles(parameters.selector, parameters.properties);
87
103
  return {
88
104
  content: [{ type: "text", text: JSON.stringify(styles, null, 2) }],
89
105
  };
90
106
  });
91
- serverInstance.registerTool("inject_theme_css", {
107
+ server.registerTool("inject_theme_css", {
92
108
  description: "Inject or replace a live stylesheet in the Firefox chrome window for immediate visual testing.",
93
109
  inputSchema: {
94
110
  css: z
@@ -100,12 +116,12 @@ serverInstance.registerTool("inject_theme_css", {
100
116
  .describe("Identifier for the style tag."),
101
117
  },
102
118
  }, async (parameters) => {
103
- const result = await globalFirefoxManager.injectUserInterfaceStyle(parameters.css, parameters.styleId);
119
+ const result = await firefoxManager.injectUserInterfaceStyle(parameters.css, parameters.styleId);
104
120
  return {
105
121
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
106
122
  };
107
123
  });
108
- serverInstance.registerTool("remove_theme_css", {
124
+ server.registerTool("remove_theme_css", {
109
125
  description: "Remove an injected stylesheet from the Firefox chrome window.",
110
126
  inputSchema: {
111
127
  styleId: z
@@ -113,12 +129,12 @@ serverInstance.registerTool("remove_theme_css", {
113
129
  .describe("Identifier of the style tag to remove."),
114
130
  },
115
131
  }, async (parameters) => {
116
- const result = await globalFirefoxManager.removeUserInterfaceStyle(parameters.styleId);
132
+ const result = await firefoxManager.removeUserInterfaceStyle(parameters.styleId);
117
133
  return {
118
134
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
119
135
  };
120
136
  });
121
- serverInstance.registerTool("take_ui_screenshot", {
137
+ server.registerTool("take_ui_screenshot", {
122
138
  description: "Capture a PNG screenshot of the Firefox browser chrome window or a specific UI component.",
123
139
  inputSchema: {
124
140
  selector: z
@@ -127,7 +143,7 @@ serverInstance.registerTool("take_ui_screenshot", {
127
143
  .describe("Optional CSS selector to screenshot a specific element."),
128
144
  },
129
145
  }, async (parameters) => {
130
- const screenshot = await globalFirefoxManager.captureScreenshot(parameters.selector);
146
+ const screenshot = await firefoxManager.captureScreenshot(parameters.selector);
131
147
  return {
132
148
  content: [
133
149
  {
@@ -138,7 +154,7 @@ serverInstance.registerTool("take_ui_screenshot", {
138
154
  ],
139
155
  };
140
156
  });
141
- serverInstance.registerTool("execute_chrome_javascript", {
157
+ server.registerTool("execute_chrome_javascript", {
142
158
  description: "Execute privileged JavaScript code in the Firefox chrome window context.",
143
159
  inputSchema: {
144
160
  script: z
@@ -146,14 +162,14 @@ serverInstance.registerTool("execute_chrome_javascript", {
146
162
  .describe("JavaScript code string to evaluate in chrome context."),
147
163
  },
148
164
  }, async (parameters) => {
149
- const result = await globalFirefoxManager.executeChromeScript(parameters.script);
165
+ const result = await firefoxManager.executeChromeScript(parameters.script);
150
166
  return {
151
167
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
152
168
  };
153
169
  });
154
170
  async function main() {
155
- const transportInstance = new StdioServerTransport();
156
- await serverInstance.connect(transportInstance);
171
+ const transport = new StdioServerTransport();
172
+ await server.connect(transport);
157
173
  }
158
174
  main().catch((error) => {
159
175
  console.error("Server initialisation failure:", error);
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firefox-css-theme-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Model Context Protocol server for inspecting and live-debugging Firefox UI DOM and CSS themes.",
5
5
  "homepage": "https://github.com/easonwong-de/Firefox-CSS-Theme-MCP#readme",
6
6
  "bugs": {
@@ -24,7 +24,8 @@
24
24
  "build": "tsc",
25
25
  "format": "prettier --write --list-different .",
26
26
  "prepublishOnly": "npm run build",
27
- "start": "node dist/index.js"
27
+ "start": "node dist/index.js",
28
+ "test": "tsx tests/run.ts --headless"
28
29
  },
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -35,6 +36,7 @@
35
36
  "@types/node": "^26.2.0",
36
37
  "@types/selenium-webdriver": "^4.35.6",
37
38
  "prettier": "^3.9.6",
39
+ "tsx": "^4.23.12",
38
40
  "typescript": "^7.0.2"
39
41
  }
40
42
  }