firefox-css-theme-mcp 0.1.4 → 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 +7 -4
- package/dist/firefox.js +114 -47
- package/dist/index.js +40 -24
- package/dist/types.js +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol (MCP) server for inspecting, querying, and live-debugging Firefox UI DOM and CSS themes (`userChrome.css`).
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
9
|
- Node.js >= 18
|
|
8
10
|
- Firefox Browser
|
|
9
|
-
- `geckodriver` available on your `PATH` or managed via Selenium
|
|
10
11
|
|
|
11
12
|
## Installation & Usage
|
|
12
13
|
|
|
@@ -68,14 +69,16 @@ npm run build
|
|
|
68
69
|
|
|
69
70
|
## Command-line Flags
|
|
70
71
|
|
|
71
|
-
| Flag
|
|
72
|
-
|
|
|
73
|
-
| `--nova-ui`
|
|
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. |
|
|
74
76
|
|
|
75
77
|
## Available MCP Tools
|
|
76
78
|
|
|
77
79
|
- `launch_browser`: Launches Firefox with chrome debugging capabilities enabled.
|
|
78
80
|
- `close_browser`: Terminates the browser instance.
|
|
81
|
+
- `customize_toolbar`: Activates or controls the Firefox "Customize Toolbar" mode.
|
|
79
82
|
- `get_ui_tree`: Dumps the hierarchical DOM tree of the chrome window.
|
|
80
83
|
- `query_ui_elements`: Queries elements matching a CSS selector in the browser chrome.
|
|
81
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
|
|
4
|
-
|
|
3
|
+
export class FirefoxManager {
|
|
4
|
+
driver = null;
|
|
5
5
|
async initialiseBrowser(binaryPath, profileDirectory) {
|
|
6
|
-
if (this.
|
|
6
|
+
if (this.driver)
|
|
7
7
|
return;
|
|
8
8
|
const isNovaUiEnabled = process.argv.includes("--nova-ui");
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
+
options.setBinary(binaryPath);
|
|
21
24
|
if (profileDirectory)
|
|
22
|
-
|
|
23
|
-
const
|
|
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.
|
|
30
|
+
this.driver = await new Builder()
|
|
28
31
|
.forBrowser("firefox")
|
|
29
|
-
.setFirefoxOptions(
|
|
30
|
-
.setFirefoxService(
|
|
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.
|
|
37
|
-
await this.
|
|
38
|
-
this.
|
|
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.
|
|
47
|
+
if (!this.driver)
|
|
45
48
|
return;
|
|
46
|
-
await this.
|
|
47
|
-
this.
|
|
49
|
+
await this.driver.quit();
|
|
50
|
+
this.driver = null;
|
|
48
51
|
}
|
|
49
52
|
async ensureChromeContext() {
|
|
50
|
-
if (!this.
|
|
53
|
+
if (!this.driver) {
|
|
51
54
|
throw new Error("Firefox driver instance is not initialised.");
|
|
52
55
|
}
|
|
53
|
-
await this.
|
|
56
|
+
await this.driver.setContext("chrome");
|
|
54
57
|
}
|
|
55
58
|
async executeChromeScript(script, ...argumentsList) {
|
|
56
59
|
await this.ensureChromeContext();
|
|
57
|
-
return (await this.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
for (const
|
|
125
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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 {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
156
|
-
await
|
|
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.
|
|
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
|
}
|