@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.
Files changed (48) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +423 -0
  3. package/browserControl.js +113 -0
  4. package/cli.js +187 -0
  5. package/docs/api/tool-reference.md +317 -0
  6. package/docs/api_tools_usage.md +477 -0
  7. package/docs/development/adding-tools.md +274 -0
  8. package/docs/development/configuration.md +332 -0
  9. package/docs/examples/authentication.md +124 -0
  10. package/docs/examples/basic-automation.md +105 -0
  11. package/docs/getting-started.md +214 -0
  12. package/docs/index.md +61 -0
  13. package/mcpServer.js +280 -0
  14. package/package.json +83 -0
  15. package/run-server.js +140 -0
  16. package/src/config/environments/api-only.js +53 -0
  17. package/src/config/environments/development.js +54 -0
  18. package/src/config/environments/production.js +69 -0
  19. package/src/config/index.js +341 -0
  20. package/src/config/server.js +41 -0
  21. package/src/config/tools/api.js +67 -0
  22. package/src/config/tools/browser.js +90 -0
  23. package/src/config/tools/default.js +32 -0
  24. package/src/services/browserService.js +325 -0
  25. package/src/tools/api/api-request.js +641 -0
  26. package/src/tools/api/api-session-report.js +1262 -0
  27. package/src/tools/api/api-session-status.js +395 -0
  28. package/src/tools/base/ToolBase.js +230 -0
  29. package/src/tools/base/ToolRegistry.js +269 -0
  30. package/src/tools/browser/advanced/browser-console.js +384 -0
  31. package/src/tools/browser/advanced/browser-dialog.js +319 -0
  32. package/src/tools/browser/advanced/browser-evaluate.js +337 -0
  33. package/src/tools/browser/advanced/browser-file.js +480 -0
  34. package/src/tools/browser/advanced/browser-keyboard.js +343 -0
  35. package/src/tools/browser/advanced/browser-mouse.js +332 -0
  36. package/src/tools/browser/advanced/browser-network.js +421 -0
  37. package/src/tools/browser/advanced/browser-pdf.js +407 -0
  38. package/src/tools/browser/advanced/browser-tabs.js +497 -0
  39. package/src/tools/browser/advanced/browser-wait.js +378 -0
  40. package/src/tools/browser/click.js +168 -0
  41. package/src/tools/browser/close.js +60 -0
  42. package/src/tools/browser/dom.js +70 -0
  43. package/src/tools/browser/launch.js +67 -0
  44. package/src/tools/browser/navigate.js +270 -0
  45. package/src/tools/browser/screenshot.js +351 -0
  46. package/src/tools/browser/type.js +174 -0
  47. package/src/tools/index.js +95 -0
  48. 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;