@democratize-quality/mcp-server 1.0.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 +15 -0
- package/README.md +423 -0
- package/browserControl.js +113 -0
- package/cli.js +187 -0
- package/docs/api/tool-reference.md +317 -0
- package/docs/api_tools_usage.md +477 -0
- package/docs/development/adding-tools.md +274 -0
- package/docs/development/configuration.md +332 -0
- package/docs/examples/authentication.md +124 -0
- package/docs/examples/basic-automation.md +105 -0
- package/docs/getting-started.md +214 -0
- package/docs/index.md +61 -0
- package/mcpServer.js +280 -0
- package/package.json +83 -0
- package/run-server.js +140 -0
- package/src/config/environments/api-only.js +53 -0
- package/src/config/environments/development.js +54 -0
- package/src/config/environments/production.js +69 -0
- package/src/config/index.js +341 -0
- package/src/config/server.js +41 -0
- package/src/config/tools/api.js +67 -0
- package/src/config/tools/browser.js +90 -0
- package/src/config/tools/default.js +32 -0
- package/src/services/browserService.js +325 -0
- package/src/tools/api/api-request.js +641 -0
- package/src/tools/api/api-session-report.js +1262 -0
- package/src/tools/api/api-session-status.js +395 -0
- package/src/tools/base/ToolBase.js +230 -0
- package/src/tools/base/ToolRegistry.js +269 -0
- package/src/tools/browser/advanced/browser-console.js +384 -0
- package/src/tools/browser/advanced/browser-dialog.js +319 -0
- package/src/tools/browser/advanced/browser-evaluate.js +337 -0
- package/src/tools/browser/advanced/browser-file.js +480 -0
- package/src/tools/browser/advanced/browser-keyboard.js +343 -0
- package/src/tools/browser/advanced/browser-mouse.js +332 -0
- package/src/tools/browser/advanced/browser-network.js +421 -0
- package/src/tools/browser/advanced/browser-pdf.js +407 -0
- package/src/tools/browser/advanced/browser-tabs.js +497 -0
- package/src/tools/browser/advanced/browser-wait.js +378 -0
- package/src/tools/browser/click.js +168 -0
- package/src/tools/browser/close.js +60 -0
- package/src/tools/browser/dom.js +70 -0
- package/src/tools/browser/launch.js +67 -0
- package/src/tools/browser/navigate.js +270 -0
- package/src/tools/browser/screenshot.js +351 -0
- package/src/tools/browser/type.js +174 -0
- package/src/tools/index.js +95 -0
- package/src/utils/browserHelpers.js +83 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser Launch Tool
|
|
6
|
+
* Launches a new web browser instance and returns a unique browserId
|
|
7
|
+
*/
|
|
8
|
+
class BrowserLaunchTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_launch",
|
|
11
|
+
description: "Launches a new web browser instance. Returns a unique browserId. Use this before any other browser actions.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
headless: {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
description: "Whether to launch the browser in headless mode (no UI). Defaults to true. Set to false for manual login.",
|
|
18
|
+
default: true
|
|
19
|
+
},
|
|
20
|
+
userDataDir: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Optional. A path (relative to the server) to a directory to store persistent user data (e.g., login sessions, cookies). Use for authenticated sessions. If not provided, a temporary profile is used."
|
|
23
|
+
},
|
|
24
|
+
port: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Optional. The port for remote debugging. If not provided, Chrome will choose an available port."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
output_schema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
browserId: { type: "string", description: "The unique ID of the launched browser instance." },
|
|
34
|
+
port: { type: "number", description: "The port the browser instance is running on for remote debugging." },
|
|
35
|
+
userDataDir: { type: "string", description: "The absolute path to the user data directory used." }
|
|
36
|
+
},
|
|
37
|
+
required: ["browserId", "port"]
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async execute(parameters) {
|
|
42
|
+
// Set defaults
|
|
43
|
+
const headless = parameters.headless !== undefined ? parameters.headless : true;
|
|
44
|
+
const port = parameters.port || undefined; // Let chrome-launcher choose if not specified
|
|
45
|
+
const userDataDir = parameters.userDataDir || null;
|
|
46
|
+
|
|
47
|
+
console.error(`[BrowserLaunchTool] Launching browser with headless=${headless}, port=${port}, userDataDir=${userDataDir}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await browserService.launchBrowser(headless, port, userDataDir);
|
|
51
|
+
|
|
52
|
+
console.error(`[BrowserLaunchTool] Successfully launched browser: ${result.browserId}`);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
browserId: result.browserId,
|
|
56
|
+
port: result.port,
|
|
57
|
+
userDataDir: result.userDataDir || null
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(`[BrowserLaunchTool] Failed to launch browser:`, error.message);
|
|
62
|
+
throw new Error(`Failed to launch browser: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = BrowserLaunchTool;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhanced Browser Navigate Tool
|
|
6
|
+
* Navigates a specific browser instance to a given URL with history navigation support
|
|
7
|
+
* Inspired by Playwright MCP navigate capabilities
|
|
8
|
+
*/
|
|
9
|
+
class BrowserNavigateTool extends ToolBase {
|
|
10
|
+
static definition = {
|
|
11
|
+
name: "browser_navigate",
|
|
12
|
+
description: "Navigate browser pages with full history support including go to URL, back, forward, and refresh operations.",
|
|
13
|
+
input_schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
browserId: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The ID of the browser instance to navigate."
|
|
19
|
+
},
|
|
20
|
+
action: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["goto", "back", "forward", "refresh", "reload"],
|
|
23
|
+
default: "goto",
|
|
24
|
+
description: "Navigation action to perform"
|
|
25
|
+
},
|
|
26
|
+
url: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "The URL to navigate to (required for 'goto' action). Must include protocol (http:// or https://)."
|
|
29
|
+
},
|
|
30
|
+
waitForNavigation: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: true,
|
|
33
|
+
description: "Whether to wait for navigation to complete"
|
|
34
|
+
},
|
|
35
|
+
timeout: {
|
|
36
|
+
type: "number",
|
|
37
|
+
default: 30000,
|
|
38
|
+
description: "Navigation timeout in milliseconds"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["browserId"]
|
|
42
|
+
},
|
|
43
|
+
output_schema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
success: { type: "boolean", description: "Whether the navigation was successful" },
|
|
47
|
+
action: { type: "string", description: "The navigation action that was performed" },
|
|
48
|
+
message: { type: "string", description: "Confirmation message of the navigation result" },
|
|
49
|
+
url: { type: "string", description: "The current URL after navigation" },
|
|
50
|
+
previousUrl: { type: "string", description: "The previous URL (for back/forward actions)" },
|
|
51
|
+
canGoBack: { type: "boolean", description: "Whether browser can navigate back" },
|
|
52
|
+
canGoForward: { type: "boolean", description: "Whether browser can navigate forward" },
|
|
53
|
+
browserId: { type: "string", description: "The browser instance ID that was used" }
|
|
54
|
+
},
|
|
55
|
+
required: ["success", "action", "message", "browserId"]
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async execute(parameters) {
|
|
60
|
+
const {
|
|
61
|
+
browserId,
|
|
62
|
+
action = "goto",
|
|
63
|
+
url,
|
|
64
|
+
waitForNavigation = true,
|
|
65
|
+
timeout = 30000
|
|
66
|
+
} = parameters;
|
|
67
|
+
|
|
68
|
+
// Validate required parameters
|
|
69
|
+
if (action === "goto" && !url) {
|
|
70
|
+
throw new Error("URL is required for 'goto' action");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (url && !url.startsWith('http://') && !url.startsWith('https://')) {
|
|
74
|
+
throw new Error("URL must include protocol (http:// or https://)");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const browser = browserService.getBrowserInstance(browserId);
|
|
78
|
+
if (!browser) {
|
|
79
|
+
throw new Error(`Browser instance '${browserId}' not found. Please launch a browser first using browser_launch.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const client = browser.cdpClient;
|
|
83
|
+
|
|
84
|
+
console.error(`[BrowserNavigateTool] Performing ${action} action on browser ${browserId}`);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
let result = {
|
|
88
|
+
success: false,
|
|
89
|
+
action: action,
|
|
90
|
+
browserId: browserId
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Get current URL and navigation state before action
|
|
94
|
+
const currentInfo = await this.getCurrentNavigationState(client);
|
|
95
|
+
|
|
96
|
+
switch (action) {
|
|
97
|
+
case 'goto':
|
|
98
|
+
await this.performGoto(client, url, waitForNavigation, timeout);
|
|
99
|
+
result.success = true;
|
|
100
|
+
result.message = `Successfully navigated to ${url}`;
|
|
101
|
+
result.url = url;
|
|
102
|
+
result.previousUrl = currentInfo.url;
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'back':
|
|
106
|
+
if (!currentInfo.canGoBack) {
|
|
107
|
+
throw new Error("Cannot go back - no previous page in history");
|
|
108
|
+
}
|
|
109
|
+
await this.performBack(client, waitForNavigation);
|
|
110
|
+
const backInfo = await this.getCurrentNavigationState(client);
|
|
111
|
+
result.success = true;
|
|
112
|
+
result.message = "Successfully navigated back";
|
|
113
|
+
result.url = backInfo.url;
|
|
114
|
+
result.previousUrl = currentInfo.url;
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'forward':
|
|
118
|
+
if (!currentInfo.canGoForward) {
|
|
119
|
+
throw new Error("Cannot go forward - no next page in history");
|
|
120
|
+
}
|
|
121
|
+
await this.performForward(client, waitForNavigation);
|
|
122
|
+
const forwardInfo = await this.getCurrentNavigationState(client);
|
|
123
|
+
result.success = true;
|
|
124
|
+
result.message = "Successfully navigated forward";
|
|
125
|
+
result.url = forwardInfo.url;
|
|
126
|
+
result.previousUrl = currentInfo.url;
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'refresh':
|
|
130
|
+
case 'reload':
|
|
131
|
+
await this.performRefresh(client, waitForNavigation);
|
|
132
|
+
result.success = true;
|
|
133
|
+
result.message = "Successfully refreshed page";
|
|
134
|
+
result.url = currentInfo.url;
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
throw new Error(`Unsupported navigation action: ${action}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get final navigation state
|
|
142
|
+
const finalInfo = await this.getCurrentNavigationState(client);
|
|
143
|
+
result.canGoBack = finalInfo.canGoBack;
|
|
144
|
+
result.canGoForward = finalInfo.canGoForward;
|
|
145
|
+
|
|
146
|
+
if (!result.url) {
|
|
147
|
+
result.url = finalInfo.url;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.error(`[BrowserNavigateTool] Successfully completed ${action} action`);
|
|
151
|
+
return result;
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`[BrowserNavigateTool] Navigation failed:`, error.message);
|
|
155
|
+
throw new Error(`Failed to perform ${action}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Navigate to a specific URL
|
|
161
|
+
*/
|
|
162
|
+
async performGoto(client, url, waitForNavigation, timeout) {
|
|
163
|
+
await client.Page.enable();
|
|
164
|
+
|
|
165
|
+
if (waitForNavigation) {
|
|
166
|
+
// Set up navigation promise
|
|
167
|
+
const navigationPromise = new Promise((resolve, reject) => {
|
|
168
|
+
const timeoutId = setTimeout(() => {
|
|
169
|
+
reject(new Error(`Navigation timeout after ${timeout}ms`));
|
|
170
|
+
}, timeout);
|
|
171
|
+
|
|
172
|
+
client.Page.loadEventFired(() => {
|
|
173
|
+
clearTimeout(timeoutId);
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Navigate and wait
|
|
179
|
+
await client.Page.navigate({ url });
|
|
180
|
+
await navigationPromise;
|
|
181
|
+
} else {
|
|
182
|
+
await client.Page.navigate({ url });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Navigate back in history
|
|
188
|
+
*/
|
|
189
|
+
async performBack(client, waitForNavigation) {
|
|
190
|
+
const history = await client.Page.getNavigationHistory();
|
|
191
|
+
|
|
192
|
+
if (history.currentIndex > 0) {
|
|
193
|
+
const previousEntry = history.entries[history.currentIndex - 1];
|
|
194
|
+
|
|
195
|
+
if (waitForNavigation) {
|
|
196
|
+
const navigationPromise = new Promise((resolve) => {
|
|
197
|
+
client.Page.loadEventFired(resolve);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await client.Page.navigateToHistoryEntry({ entryId: previousEntry.id });
|
|
201
|
+
await navigationPromise;
|
|
202
|
+
} else {
|
|
203
|
+
await client.Page.navigateToHistoryEntry({ entryId: previousEntry.id });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Navigate forward in history
|
|
210
|
+
*/
|
|
211
|
+
async performForward(client, waitForNavigation) {
|
|
212
|
+
const history = await client.Page.getNavigationHistory();
|
|
213
|
+
|
|
214
|
+
if (history.currentIndex < history.entries.length - 1) {
|
|
215
|
+
const nextEntry = history.entries[history.currentIndex + 1];
|
|
216
|
+
|
|
217
|
+
if (waitForNavigation) {
|
|
218
|
+
const navigationPromise = new Promise((resolve) => {
|
|
219
|
+
client.Page.loadEventFired(resolve);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await client.Page.navigateToHistoryEntry({ entryId: nextEntry.id });
|
|
223
|
+
await navigationPromise;
|
|
224
|
+
} else {
|
|
225
|
+
await client.Page.navigateToHistoryEntry({ entryId: nextEntry.id });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Refresh/reload the current page
|
|
232
|
+
*/
|
|
233
|
+
async performRefresh(client, waitForNavigation) {
|
|
234
|
+
if (waitForNavigation) {
|
|
235
|
+
const navigationPromise = new Promise((resolve) => {
|
|
236
|
+
client.Page.loadEventFired(resolve);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await client.Page.reload();
|
|
240
|
+
await navigationPromise;
|
|
241
|
+
} else {
|
|
242
|
+
await client.Page.reload();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get current navigation state
|
|
248
|
+
*/
|
|
249
|
+
async getCurrentNavigationState(client) {
|
|
250
|
+
await client.Page.enable();
|
|
251
|
+
|
|
252
|
+
// Get current URL
|
|
253
|
+
const urlResult = await client.Runtime.evaluate({
|
|
254
|
+
expression: 'window.location.href'
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Get navigation history
|
|
258
|
+
const history = await client.Page.getNavigationHistory();
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
url: urlResult.result.value,
|
|
262
|
+
canGoBack: history.currentIndex > 0,
|
|
263
|
+
canGoForward: history.currentIndex < history.entries.length - 1,
|
|
264
|
+
historyLength: history.entries.length,
|
|
265
|
+
currentIndex: history.currentIndex
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = BrowserNavigateTool;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
const ToolBase = require('../base/ToolBase');
|
|
2
|
+
const browserService = require('../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhanced Browser Screenshot Tool
|
|
6
|
+
* Captures screenshots with advanced options including element-specific and full-page captures
|
|
7
|
+
* Inspired by Playwright MCP screenshot capabilities
|
|
8
|
+
*/
|
|
9
|
+
class BrowserScreenshotTool extends ToolBase {
|
|
10
|
+
static definition = {
|
|
11
|
+
name: "browser_screenshot",
|
|
12
|
+
description: "Capture screenshots with advanced options including full page, element-specific, and custom formats with quality control.",
|
|
13
|
+
input_schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
browserId: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The ID of the browser instance."
|
|
19
|
+
},
|
|
20
|
+
fileName: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Optional. The name of the file to save the screenshot as (e.g., 'my_page.png'). Saved to the server's configured output directory. If not provided, a timestamped name is used."
|
|
23
|
+
},
|
|
24
|
+
saveToDisk: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
description: "Optional. Whether to save the screenshot to disk on the server. Defaults to true. Set to false to only receive base64 data.",
|
|
27
|
+
default: true
|
|
28
|
+
},
|
|
29
|
+
options: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
fullPage: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
default: false,
|
|
35
|
+
description: "Capture full scrollable page instead of just viewport"
|
|
36
|
+
},
|
|
37
|
+
element: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "CSS selector of element to screenshot (captures only that element)"
|
|
40
|
+
},
|
|
41
|
+
format: {
|
|
42
|
+
type: "string",
|
|
43
|
+
enum: ["png", "jpeg", "webp"],
|
|
44
|
+
default: "png",
|
|
45
|
+
description: "Image format"
|
|
46
|
+
},
|
|
47
|
+
quality: {
|
|
48
|
+
type: "number",
|
|
49
|
+
minimum: 0,
|
|
50
|
+
maximum: 100,
|
|
51
|
+
description: "JPEG/WebP quality (0-100, ignored for PNG)"
|
|
52
|
+
},
|
|
53
|
+
clip: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
x: { type: "number", description: "X coordinate of clip area" },
|
|
57
|
+
y: { type: "number", description: "Y coordinate of clip area" },
|
|
58
|
+
width: { type: "number", description: "Width of clip area" },
|
|
59
|
+
height: { type: "number", description: "Height of clip area" },
|
|
60
|
+
scale: { type: "number", description: "Scale factor for clip area" }
|
|
61
|
+
},
|
|
62
|
+
description: "Specific area to capture"
|
|
63
|
+
},
|
|
64
|
+
omitBackground: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
default: false,
|
|
67
|
+
description: "Hide default white background for transparent screenshots"
|
|
68
|
+
},
|
|
69
|
+
captureBeyondViewport: {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
default: false,
|
|
72
|
+
description: "Capture content outside viewport"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
description: "Screenshot capture options"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: ["browserId"]
|
|
79
|
+
},
|
|
80
|
+
output_schema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
success: { type: "boolean", description: "Whether screenshot was captured successfully" },
|
|
84
|
+
imageData: { type: "string", description: "Base64 encoded image data" },
|
|
85
|
+
format: { type: "string", description: "Image format used" },
|
|
86
|
+
fileName: { type: "string", description: "The file name if saved to disk" },
|
|
87
|
+
dimensions: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
width: { type: "number" },
|
|
91
|
+
height: { type: "number" }
|
|
92
|
+
},
|
|
93
|
+
description: "Image dimensions"
|
|
94
|
+
},
|
|
95
|
+
fileSize: { type: "number", description: "File size in bytes" },
|
|
96
|
+
element: { type: "string", description: "CSS selector if element screenshot was taken" },
|
|
97
|
+
browserId: { type: "string", description: "The browser instance ID that was used" }
|
|
98
|
+
},
|
|
99
|
+
required: ["success", "imageData", "format", "browserId"]
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
async execute(parameters) {
|
|
104
|
+
const {
|
|
105
|
+
browserId,
|
|
106
|
+
fileName,
|
|
107
|
+
saveToDisk = true,
|
|
108
|
+
options = {}
|
|
109
|
+
} = parameters;
|
|
110
|
+
|
|
111
|
+
const browser = browserService.getBrowserInstance(browserId);
|
|
112
|
+
if (!browser) {
|
|
113
|
+
throw new Error(`Browser instance '${browserId}' not found. Please launch a browser first using browser_launch.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generate filename if not provided
|
|
117
|
+
const fileExtension = this.getFileExtension(options.format || 'png');
|
|
118
|
+
const finalFileName = fileName || `screenshot_${browserId}_${Date.now()}.${fileExtension}`;
|
|
119
|
+
|
|
120
|
+
console.error(`[BrowserScreenshotTool] Taking screenshot of browser ${browserId}, fileName: ${finalFileName}, saveToDisk: ${saveToDisk}`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
let screenshotResult;
|
|
124
|
+
|
|
125
|
+
if (options.element) {
|
|
126
|
+
screenshotResult = await this.captureElementScreenshot(browser.client, options);
|
|
127
|
+
} else {
|
|
128
|
+
screenshotResult = await this.capturePageScreenshot(browser.client, options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Save to disk if requested
|
|
132
|
+
let savedPath = null;
|
|
133
|
+
if (saveToDisk) {
|
|
134
|
+
savedPath = await this.saveScreenshot(screenshotResult.data, finalFileName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.error(`[BrowserScreenshotTool] Successfully captured screenshot for browser: ${browserId}`);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
imageData: screenshotResult.data,
|
|
142
|
+
format: options.format || 'png',
|
|
143
|
+
fileName: savedPath ? finalFileName : null,
|
|
144
|
+
dimensions: screenshotResult.dimensions,
|
|
145
|
+
fileSize: screenshotResult.fileSize,
|
|
146
|
+
element: options.element || null,
|
|
147
|
+
browserId: browserId
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`[BrowserScreenshotTool] Failed to take screenshot:`, error.message);
|
|
152
|
+
throw new Error(`Failed to take screenshot of browser ${browserId}: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Capture page screenshot with options
|
|
158
|
+
*/
|
|
159
|
+
async capturePageScreenshot(client, options) {
|
|
160
|
+
await client.Page.enable();
|
|
161
|
+
|
|
162
|
+
const screenshotOptions = {
|
|
163
|
+
format: options.format || 'png',
|
|
164
|
+
quality: options.format !== 'png' ? options.quality : undefined,
|
|
165
|
+
optimizeForSpeed: false,
|
|
166
|
+
captureBeyondViewport: options.captureBeyondViewport || options.fullPage || false
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Handle clip area
|
|
170
|
+
if (options.clip) {
|
|
171
|
+
screenshotOptions.clip = options.clip;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle full page screenshot
|
|
175
|
+
if (options.fullPage && !options.clip) {
|
|
176
|
+
const layoutMetrics = await client.Page.getLayoutMetrics();
|
|
177
|
+
screenshotOptions.clip = {
|
|
178
|
+
x: 0,
|
|
179
|
+
y: 0,
|
|
180
|
+
width: layoutMetrics.contentSize.width,
|
|
181
|
+
height: layoutMetrics.contentSize.height,
|
|
182
|
+
scale: 1
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle transparent background
|
|
187
|
+
if (options.omitBackground) {
|
|
188
|
+
// Set background to transparent (requires format support)
|
|
189
|
+
await client.Emulation.setDefaultBackgroundColorOverride({
|
|
190
|
+
color: { r: 0, g: 0, b: 0, a: 0 }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result = await client.Page.captureScreenshot(screenshotOptions);
|
|
195
|
+
|
|
196
|
+
// Reset background if it was changed
|
|
197
|
+
if (options.omitBackground) {
|
|
198
|
+
await client.Emulation.setDefaultBackgroundColorOverride();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
data: result.data,
|
|
203
|
+
dimensions: this.getImageDimensions(screenshotOptions.clip),
|
|
204
|
+
fileSize: Buffer.from(result.data, 'base64').length
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Capture element screenshot
|
|
210
|
+
*/
|
|
211
|
+
async captureElementScreenshot(client, options) {
|
|
212
|
+
await client.DOM.enable();
|
|
213
|
+
await client.Page.enable();
|
|
214
|
+
|
|
215
|
+
// Find the element
|
|
216
|
+
const document = await client.DOM.getDocument();
|
|
217
|
+
const element = await client.DOM.querySelector({
|
|
218
|
+
nodeId: document.root.nodeId,
|
|
219
|
+
selector: options.element
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!element.nodeId) {
|
|
223
|
+
throw new Error(`Element not found with selector: ${options.element}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get element bounds
|
|
227
|
+
const box = await client.DOM.getBoxModel({ nodeId: element.nodeId });
|
|
228
|
+
if (!box.model) {
|
|
229
|
+
throw new Error(`Could not get bounds for element: ${options.element}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Use content bounds for the clip
|
|
233
|
+
const bounds = box.model.content;
|
|
234
|
+
const clip = {
|
|
235
|
+
x: Math.round(bounds[0]),
|
|
236
|
+
y: Math.round(bounds[1]),
|
|
237
|
+
width: Math.round(bounds[4] - bounds[0]),
|
|
238
|
+
height: Math.round(bounds[5] - bounds[1]),
|
|
239
|
+
scale: 1
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const screenshotOptions = {
|
|
243
|
+
format: options.format || 'png',
|
|
244
|
+
quality: options.format !== 'png' ? options.quality : undefined,
|
|
245
|
+
clip: clip,
|
|
246
|
+
optimizeForSpeed: false
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Handle transparent background for element
|
|
250
|
+
if (options.omitBackground) {
|
|
251
|
+
await client.Emulation.setDefaultBackgroundColorOverride({
|
|
252
|
+
color: { r: 0, g: 0, b: 0, a: 0 }
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = await client.Page.captureScreenshot(screenshotOptions);
|
|
257
|
+
|
|
258
|
+
if (options.omitBackground) {
|
|
259
|
+
await client.Emulation.setDefaultBackgroundColorOverride();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
data: result.data,
|
|
264
|
+
dimensions: { width: clip.width, height: clip.height },
|
|
265
|
+
fileSize: Buffer.from(result.data, 'base64').length
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save screenshot to disk
|
|
271
|
+
*/
|
|
272
|
+
async saveScreenshot(base64Data, fileName) {
|
|
273
|
+
const fs = require('fs').promises;
|
|
274
|
+
const path = require('path');
|
|
275
|
+
const os = require('os');
|
|
276
|
+
|
|
277
|
+
// Use output directory from environment or default to user home directory
|
|
278
|
+
const defaultOutputDir = process.env.HOME
|
|
279
|
+
? path.join(process.env.HOME, '.mcp-browser-control')
|
|
280
|
+
: path.join(os.tmpdir(), 'mcp-browser-control');
|
|
281
|
+
const outputDir = process.env.OUTPUT_DIR || defaultOutputDir;
|
|
282
|
+
const filePath = path.join(outputDir, fileName);
|
|
283
|
+
|
|
284
|
+
// Ensure directory exists
|
|
285
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
286
|
+
|
|
287
|
+
// Save file
|
|
288
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
289
|
+
await fs.writeFile(filePath, buffer);
|
|
290
|
+
|
|
291
|
+
return filePath;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get file extension for format
|
|
296
|
+
*/
|
|
297
|
+
getFileExtension(format) {
|
|
298
|
+
const extensions = {
|
|
299
|
+
'png': 'png',
|
|
300
|
+
'jpeg': 'jpg',
|
|
301
|
+
'webp': 'webp'
|
|
302
|
+
};
|
|
303
|
+
return extensions[format] || 'png';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get image dimensions from clip or default
|
|
308
|
+
*/
|
|
309
|
+
getImageDimensions(clip) {
|
|
310
|
+
if (clip) {
|
|
311
|
+
return {
|
|
312
|
+
width: clip.width,
|
|
313
|
+
height: clip.height
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return null; // Will be determined by actual screenshot
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Capture screenshot with scroll handling
|
|
321
|
+
*/
|
|
322
|
+
async captureWithScroll(client, options) {
|
|
323
|
+
// Save current scroll position
|
|
324
|
+
const scrollPosition = await client.Runtime.evaluate({
|
|
325
|
+
expression: '({ x: window.scrollX, y: window.scrollY })'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Scroll to top for full page capture
|
|
330
|
+
if (options.fullPage) {
|
|
331
|
+
await client.Runtime.evaluate({
|
|
332
|
+
expression: 'window.scrollTo(0, 0)'
|
|
333
|
+
});
|
|
334
|
+
// Wait for scroll to complete
|
|
335
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const result = await this.capturePageScreenshot(client, options);
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
} finally {
|
|
342
|
+
// Restore original scroll position
|
|
343
|
+
const original = scrollPosition.result.value;
|
|
344
|
+
await client.Runtime.evaluate({
|
|
345
|
+
expression: `window.scrollTo(${original.x}, ${original.y})`
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = BrowserScreenshotTool;
|