@democratize-quality/mcp-server 1.2.0 → 1.2.1
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/cli.js +248 -0
- package/package.json +7 -5
- package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +409 -0
- package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +494 -0
- package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +954 -0
- package/src/config/environments/api-only.js +72 -0
- package/src/config/environments/development.js +73 -0
- package/src/config/environments/production.js +88 -0
- package/src/config/index.js +360 -0
- package/src/config/server.js +60 -0
- package/src/config/tools/api.js +86 -0
- package/src/config/tools/browser.js +109 -0
- package/src/config/tools/default.js +51 -0
- package/src/docs/Agent_README.md +310 -0
- package/src/docs/QUICK_REFERENCE.md +111 -0
- package/src/server.ts +234 -0
- package/src/services/browserService.js +344 -0
- package/src/skills/api-planning/SKILL.md +224 -0
- package/src/skills/test-execution/SKILL.md +777 -0
- package/src/skills/test-generation/SKILL.md +309 -0
- package/src/skills/test-healing/SKILL.md +405 -0
- package/src/tools/api/api-generator.js +1884 -0
- package/src/tools/api/api-healer.js +636 -0
- package/src/tools/api/api-planner.js +2617 -0
- package/src/tools/api/api-project-setup.js +332 -0
- package/src/tools/api/api-request.js +660 -0
- package/src/tools/api/api-session-report.js +1297 -0
- package/src/tools/api/api-session-status.js +414 -0
- package/src/tools/api/prompts/README.md +293 -0
- package/src/tools/api/prompts/generation-prompts.js +722 -0
- package/src/tools/api/prompts/healing-prompts.js +214 -0
- package/src/tools/api/prompts/index.js +44 -0
- package/src/tools/api/prompts/orchestrator.js +353 -0
- package/src/tools/api/prompts/validation-rules.js +358 -0
- package/src/tools/base/ToolBase.js +249 -0
- package/src/tools/base/ToolRegistry.js +288 -0
- package/src/tools/browser/advanced/browser-console.js +403 -0
- package/src/tools/browser/advanced/browser-dialog.js +338 -0
- package/src/tools/browser/advanced/browser-evaluate.js +356 -0
- package/src/tools/browser/advanced/browser-file.js +499 -0
- package/src/tools/browser/advanced/browser-keyboard.js +362 -0
- package/src/tools/browser/advanced/browser-mouse.js +351 -0
- package/src/tools/browser/advanced/browser-network.js +440 -0
- package/src/tools/browser/advanced/browser-pdf.js +426 -0
- package/src/tools/browser/advanced/browser-tabs.js +516 -0
- package/src/tools/browser/advanced/browser-wait.js +397 -0
- package/src/tools/browser/click.js +187 -0
- package/src/tools/browser/close.js +79 -0
- package/src/tools/browser/dom.js +89 -0
- package/src/tools/browser/launch.js +86 -0
- package/src/tools/browser/navigate.js +289 -0
- package/src/tools/browser/screenshot.js +370 -0
- package/src/tools/browser/type.js +193 -0
- package/src/tools/index.js +114 -0
- package/src/utils/agentInstaller.js +437 -0
- package/src/utils/browserHelpers.js +102 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copyright (C) 2025 Democratize Quality
|
|
5
|
+
*
|
|
6
|
+
* This file is part of Democratize Quality MCP Server.
|
|
7
|
+
*
|
|
8
|
+
* Democratize Quality MCP Server is free software: you can redistribute it and/or modify
|
|
9
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
10
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
* (at your option) any later version.
|
|
12
|
+
*
|
|
13
|
+
* Democratize Quality MCP Server is distributed in the hope that it will be useful,
|
|
14
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
* GNU Affero General Public License for more details.
|
|
17
|
+
*
|
|
18
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
19
|
+
* along with Democratize Quality MCP Server. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @democratize-quality/mcp-server
|
|
25
|
+
* Main MCP server implementation for API testing and browser automation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
29
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
30
|
+
import {
|
|
31
|
+
ListToolsRequestSchema,
|
|
32
|
+
CallToolRequestSchema,
|
|
33
|
+
Tool,
|
|
34
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
35
|
+
import * as path from 'path';
|
|
36
|
+
|
|
37
|
+
// Import existing CommonJS modules - use paths relative to project root
|
|
38
|
+
// When compiled, this will be in dist/, so we need to go up one level to find src/
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
40
|
+
const browserService = require(path.join(__dirname, '..', 'src', 'services', 'browserService.js'));
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
42
|
+
const toolsModule = require(path.join(__dirname, '..', 'src', 'tools', 'index.js'));
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
44
|
+
const config = require(path.join(__dirname, '..', 'src', 'config', 'index.js'));
|
|
45
|
+
|
|
46
|
+
interface ServerConfig {
|
|
47
|
+
enableDebugMode?: boolean;
|
|
48
|
+
enableApiTools?: boolean;
|
|
49
|
+
enableBrowserTools?: boolean;
|
|
50
|
+
enableAdvancedTools?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class DemocratizeQualityMCPServer {
|
|
54
|
+
private server: Server;
|
|
55
|
+
private toolDefinitions: any[] = [];
|
|
56
|
+
private config: ServerConfig;
|
|
57
|
+
private isDebugMode: boolean;
|
|
58
|
+
|
|
59
|
+
constructor(serverConfig: ServerConfig = {}) {
|
|
60
|
+
// Check if debug mode is requested via environment variable
|
|
61
|
+
const debugFromEnv = process.env.MCP_FEATURES_ENABLEDEBUGMODE === 'true' ||
|
|
62
|
+
process.env.NODE_ENV === 'development';
|
|
63
|
+
|
|
64
|
+
this.isDebugMode = config.get('features.enableDebugMode', false) || debugFromEnv || serverConfig.enableDebugMode || false;
|
|
65
|
+
|
|
66
|
+
// Set quiet mode if not in debug
|
|
67
|
+
config.setQuiet(!this.isDebugMode);
|
|
68
|
+
|
|
69
|
+
this.config = serverConfig;
|
|
70
|
+
|
|
71
|
+
// Initialize MCP Server with SDK
|
|
72
|
+
this.server = new Server(
|
|
73
|
+
{
|
|
74
|
+
name: '@democratize-quality/mcp-server',
|
|
75
|
+
version: '1.2.0',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
capabilities: {
|
|
79
|
+
tools: {},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this.setupHandlers();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private debugLog(...args: any[]): void {
|
|
88
|
+
if (this.isDebugMode) {
|
|
89
|
+
console.error('[Democratize Quality MCP]', ...args);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private log(...args: any[]): void {
|
|
94
|
+
console.error('[Democratize Quality MCP]', ...args);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private setupHandlers(): void {
|
|
98
|
+
// List available tools
|
|
99
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
100
|
+
this.debugLog(`Received 'tools/list' request.`);
|
|
101
|
+
this.debugLog(`Returning ${this.toolDefinitions.length} tools`);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
tools: this.getAvailableTools(),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle tool calls
|
|
109
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
110
|
+
const { name, arguments: args } = request.params;
|
|
111
|
+
this.debugLog(`Received 'tools/call' for method: ${name}`);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Use the existing tool system to execute the tool
|
|
115
|
+
const result = await toolsModule.executeTool(name, args || {});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text' as const,
|
|
121
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.log(`Error executing tool '${name}':`, (error as Error).message);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text' as const,
|
|
132
|
+
text: `Error executing ${name}: ${(error as Error).message}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private getAvailableTools(): Tool[] {
|
|
142
|
+
// Convert existing tool definitions to MCP SDK format
|
|
143
|
+
return this.toolDefinitions.map((tool) => {
|
|
144
|
+
const sdkTool: Tool = {
|
|
145
|
+
name: tool.name,
|
|
146
|
+
description: tool.description,
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object' as const,
|
|
149
|
+
properties: tool.input_schema?.properties || tool.inputSchema?.properties || {},
|
|
150
|
+
...(tool.input_schema?.required && tool.input_schema.required.length > 0
|
|
151
|
+
? { required: tool.input_schema.required }
|
|
152
|
+
: {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return sdkTool;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async initialize(): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
this.debugLog('Initializing tool system...');
|
|
162
|
+
|
|
163
|
+
// Initialize the existing tool system
|
|
164
|
+
await toolsModule.initializeTools(this.isDebugMode);
|
|
165
|
+
|
|
166
|
+
// Get tool definitions from the existing system
|
|
167
|
+
this.toolDefinitions = toolsModule.getToolDefinitions();
|
|
168
|
+
|
|
169
|
+
this.log(`Tool system initialized with ${this.toolDefinitions.length} tools`);
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
this.log('Failed to initialize tool system:', (error as Error).message);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async start(): Promise<void> {
|
|
178
|
+
// Initialize tools first
|
|
179
|
+
await this.initialize();
|
|
180
|
+
|
|
181
|
+
// Create transport and connect
|
|
182
|
+
const transport = new StdioServerTransport();
|
|
183
|
+
await this.server.connect(transport);
|
|
184
|
+
|
|
185
|
+
this.log('Server started and ready');
|
|
186
|
+
if (this.isDebugMode) {
|
|
187
|
+
this.log('Debug mode enabled - showing detailed logs');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async stop(): Promise<void> {
|
|
192
|
+
try {
|
|
193
|
+
await browserService.shutdownAllBrowsers();
|
|
194
|
+
this.log('All browser instances closed');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.log('Error during shutdown:', (error as Error).message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Main entry point
|
|
202
|
+
async function main(): Promise<void> {
|
|
203
|
+
const server = new DemocratizeQualityMCPServer();
|
|
204
|
+
|
|
205
|
+
// Handle graceful shutdown
|
|
206
|
+
process.on('SIGINT', async () => {
|
|
207
|
+
console.error('\nSIGINT received. Shutting down...');
|
|
208
|
+
await server.stop();
|
|
209
|
+
process.exit(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
process.on('SIGTERM', async () => {
|
|
213
|
+
console.error('\nSIGTERM received. Shutting down...');
|
|
214
|
+
await server.stop();
|
|
215
|
+
process.exit(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await server.start();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('Failed to start server:', error);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Start the server if this file is being executed directly
|
|
227
|
+
if (require.main === module) {
|
|
228
|
+
main().catch((error) => {
|
|
229
|
+
console.error('Unhandled error:', error);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { DemocratizeQualityMCPServer };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2025 Democratize Quality
|
|
3
|
+
*
|
|
4
|
+
* This file is part of Democratize Quality MCP Server.
|
|
5
|
+
*
|
|
6
|
+
* Democratize Quality MCP Server is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
* (at your option) any later version.
|
|
10
|
+
*
|
|
11
|
+
* Democratize Quality MCP Server is distributed in the hope that it will be useful,
|
|
12
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
* GNU Affero General Public License for more details.
|
|
15
|
+
*
|
|
16
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
* along with Democratize Quality MCP Server. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CDP = require('chrome-remote-interface');
|
|
21
|
+
//const launchChrome = require('chrome-launcher').launch;
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { findNodeBySelector, getElementClickCoordinates } = require('../utils/browserHelpers'); // Import helpers
|
|
25
|
+
const config = require('../config');
|
|
26
|
+
|
|
27
|
+
// A private in-memory store for our browser instances within the service
|
|
28
|
+
// Each key will be a unique browserId, value will be { chromeInstance, cdpClient, userDataDir }
|
|
29
|
+
const activeBrowsers = {};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ensures a user data directory exists.
|
|
33
|
+
* @param {string} dirPath - The absolute path to the user data directory.
|
|
34
|
+
*/
|
|
35
|
+
function ensureUserDataDir(dirPath) {
|
|
36
|
+
if (!fs.existsSync(dirPath)) {
|
|
37
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
38
|
+
console.log(`[BrowserService] Created user data directory: ${dirPath}`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`[BrowserService] Using existing user data directory: ${dirPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Retrieves a browser instance by ID.
|
|
46
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
47
|
+
* @returns {object|null} - The browser instance object or null if not found.
|
|
48
|
+
*/
|
|
49
|
+
function getBrowserInstance(browserId) {
|
|
50
|
+
return activeBrowsers[browserId];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Launches a new Chrome instance.
|
|
55
|
+
* @param {boolean} headless - Whether to run Chrome in headless mode.
|
|
56
|
+
* @param {number} port - The port for remote debugging.
|
|
57
|
+
* @param {string|null} userDataDir - Path to the user data directory for persistent profiles.
|
|
58
|
+
* @returns {Promise<object>} - Object containing browserId, port, and resolvedUserDataDir.
|
|
59
|
+
*/
|
|
60
|
+
async function launchBrowser(headless, port, userDataDir) {
|
|
61
|
+
let chrome;
|
|
62
|
+
let client;
|
|
63
|
+
const { launch: launchChrome } = await import('chrome-launcher');
|
|
64
|
+
let resolvedUserDataDir = null;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (userDataDir) {
|
|
68
|
+
resolvedUserDataDir = path.resolve(process.cwd(), userDataDir);
|
|
69
|
+
ensureUserDataDir(resolvedUserDataDir);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`[BrowserService] Launching Chrome (headless: ${headless}, userDataDir: ${resolvedUserDataDir || 'temporary'})...`);
|
|
73
|
+
|
|
74
|
+
const launchOptions = {
|
|
75
|
+
port: port,
|
|
76
|
+
userDataDir: resolvedUserDataDir, // Set userDataDir if provided
|
|
77
|
+
chromeFlags: [
|
|
78
|
+
headless ? '--headless=new' : '',
|
|
79
|
+
'--disable-gpu',
|
|
80
|
+
'--disable-setuid-sandbox',
|
|
81
|
+
'--no-sandbox'
|
|
82
|
+
].filter(Boolean)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
chrome = await launchChrome(launchOptions);
|
|
86
|
+
|
|
87
|
+
// Generate browserId: profile-name if userDataDir is used, otherwise a unique timestamped ID
|
|
88
|
+
const browserId = userDataDir ? `profile-${path.basename(resolvedUserDataDir)}` : `browser-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
89
|
+
|
|
90
|
+
if (activeBrowsers[browserId]) {
|
|
91
|
+
console.warn(`[BrowserService] Warning: Browser ID '${browserId}' already exists. Overwriting.`);
|
|
92
|
+
// In a real scenario, you might want more sophisticated handling here,
|
|
93
|
+
// e.g., error if ID exists, or try to attach to existing.
|
|
94
|
+
// For now, we're assuming a new launch means a fresh start or overwrite.
|
|
95
|
+
try { // Attempt to clean up old instance if it exists
|
|
96
|
+
if (activeBrowsers[browserId].cdpClient) activeBrowsers[browserId].cdpClient.close();
|
|
97
|
+
if (activeBrowsers[browserId].chromeInstance) await activeBrowsers[browserId].chromeInstance.kill();
|
|
98
|
+
} catch (cleanupErr) {
|
|
99
|
+
console.error(`[BrowserService] Error cleaning up old instance for ${browserId}:`, cleanupErr.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
activeBrowsers[browserId] = { chromeInstance: chrome, cdpClient: null, userDataDir: resolvedUserDataDir };
|
|
104
|
+
console.log(`[BrowserService] Chrome launched on port ${chrome.port} with ID: ${browserId}`);
|
|
105
|
+
|
|
106
|
+
client = await CDP({ port: chrome.port });
|
|
107
|
+
activeBrowsers[browserId].cdpClient = client;
|
|
108
|
+
|
|
109
|
+
const { Page, Runtime, DOM, Network, Security, Input } = client; // Enable Input domain here
|
|
110
|
+
await Page.enable();
|
|
111
|
+
await Runtime.enable();
|
|
112
|
+
await DOM.enable();
|
|
113
|
+
await Network.enable();
|
|
114
|
+
await Security.enable();
|
|
115
|
+
//await Input.enable(); // Enable Input domain
|
|
116
|
+
|
|
117
|
+
console.log(`[BrowserService] CDP client connected and domains enabled for ${browserId}.`);
|
|
118
|
+
|
|
119
|
+
return { browserId, port: chrome.port, userDataDir: resolvedUserDataDir };
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`[BrowserService] Error launching browser:`, error);
|
|
123
|
+
if (chrome && !client) { // If chrome launched but CDP connection failed
|
|
124
|
+
try {
|
|
125
|
+
await chrome.kill();
|
|
126
|
+
console.log(`[BrowserService] Partially launched Chrome instance killed due to error.`);
|
|
127
|
+
} catch (killError) {
|
|
128
|
+
console.error(`[BrowserService] Error killing partially launched Chrome:`, killError);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw error; // Re-throw to be caught by the route handler
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Navigates a specific browser instance to a URL.
|
|
137
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
138
|
+
* @param {string} url - The URL to navigate to.
|
|
139
|
+
* @returns {Promise<void>}
|
|
140
|
+
*/
|
|
141
|
+
async function navigateBrowser(browserId, url) {
|
|
142
|
+
const instance = getBrowserInstance(browserId);
|
|
143
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
144
|
+
|
|
145
|
+
const { cdpClient } = instance;
|
|
146
|
+
console.log(`[BrowserService] Browser ${browserId} navigating to: ${url}`);
|
|
147
|
+
await cdpClient.Page.navigate({ url: url });
|
|
148
|
+
await cdpClient.Page.loadEventFired(); // Wait for page to load
|
|
149
|
+
console.log(`[BrowserService] Browser ${browserId} successfully navigated to ${url}.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Takes a screenshot of a specific browser page and can save it to disk.
|
|
154
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
155
|
+
* @param {string} [fileName='screenshot.png'] - Optional: The name of the file to save the screenshot as.
|
|
156
|
+
* @param {boolean} [saveToDisk=true] - Optional: Whether to save the screenshot to disk.
|
|
157
|
+
* @returns {Promise<string>} - Base64 encoded screenshot data.
|
|
158
|
+
*/
|
|
159
|
+
async function takeScreenshot(browserId, fileName = 'screenshot.png', saveToDisk = true) { // Added fileName and saveToDisk params
|
|
160
|
+
const instance = getBrowserInstance(browserId);
|
|
161
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
162
|
+
|
|
163
|
+
const { cdpClient } = instance;
|
|
164
|
+
console.log(`[BrowserService] Taking screenshot for browser ${browserId}...`);
|
|
165
|
+
const screenshot = await cdpClient.Page.captureScreenshot({ format: 'png', quality: 80 });
|
|
166
|
+
|
|
167
|
+
if (saveToDisk) {
|
|
168
|
+
const screenshotBuffer = Buffer.from(screenshot.data, 'base64');
|
|
169
|
+
// Ensure the output directory exists
|
|
170
|
+
if (!fs.existsSync(config.OUTPUT_DIR)) {
|
|
171
|
+
fs.mkdirSync(config.OUTPUT_DIR, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
const filePath = path.join(config.OUTPUT_DIR, fileName);
|
|
174
|
+
fs.writeFileSync(filePath, screenshotBuffer);
|
|
175
|
+
console.log(`[BrowserService] Screenshot saved to ${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`[BrowserService] Screenshot captured for browser ${browserId}.`);
|
|
179
|
+
return screenshot.data; // Always return base64 data to the caller (e.g., AI agent)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Gets the current DOM content of a specific browser page.
|
|
184
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
185
|
+
* @returns {Promise<string>} - The outer HTML of the document.
|
|
186
|
+
*/
|
|
187
|
+
async function getDomContent(browserId) {
|
|
188
|
+
const instance = getBrowserInstance(browserId);
|
|
189
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
190
|
+
|
|
191
|
+
const { cdpClient } = instance;
|
|
192
|
+
console.log(`[BrowserService] Getting DOM for browser ${browserId}...`);
|
|
193
|
+
const documentNode = await cdpClient.DOM.getDocument({ depth: -1 });
|
|
194
|
+
const outerHTML = await cdpClient.DOM.getOuterHTML({ nodeId: documentNode.root.nodeId });
|
|
195
|
+
console.log(`[BrowserService] DOM content retrieved for browser ${browserId}.`);
|
|
196
|
+
return outerHTML.outerHTML;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clicks an element identified by a locator.
|
|
201
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
202
|
+
* @param {object} locator - { type: 'css'|'xpath', value: 'selector' }
|
|
203
|
+
* @returns {Promise<object>} - Coordinates of the click.
|
|
204
|
+
*/
|
|
205
|
+
async function clickElement(browserId, locator) {
|
|
206
|
+
const instance = getBrowserInstance(browserId);
|
|
207
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
208
|
+
|
|
209
|
+
const { cdpClient } = instance;
|
|
210
|
+
const { Input } = cdpClient;
|
|
211
|
+
|
|
212
|
+
console.log(`[BrowserService] Browser ${browserId}: Attempting to click element with locator:`, locator);
|
|
213
|
+
|
|
214
|
+
const nodeId = await findNodeBySelector(cdpClient, locator.type, locator.value);
|
|
215
|
+
if (!nodeId) {
|
|
216
|
+
throw new Error(`Element not found for locator: ${JSON.stringify(locator)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const coords = await getElementClickCoordinates(cdpClient, nodeId);
|
|
220
|
+
if (!coords) {
|
|
221
|
+
throw new Error(`Could not determine click coordinates for element: ${JSON.stringify(locator)}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await Input.dispatchMouseEvent({
|
|
225
|
+
type: 'mousePressed',
|
|
226
|
+
button: 'left',
|
|
227
|
+
x: coords.x,
|
|
228
|
+
y: coords.y,
|
|
229
|
+
clickCount: 1
|
|
230
|
+
});
|
|
231
|
+
await Input.dispatchMouseEvent({
|
|
232
|
+
type: 'mouseReleased',
|
|
233
|
+
button: 'left',
|
|
234
|
+
x: coords.x,
|
|
235
|
+
y: coords.y,
|
|
236
|
+
clickCount: 1
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
console.log(`[BrowserService] Browser ${browserId}: Clicked element at x: ${coords.x}, y: ${coords.y}`);
|
|
240
|
+
return coords;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Types text into an element identified by a locator.
|
|
245
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
246
|
+
* @param {object} locator - { type: 'css'|'xpath', value: 'selector' }
|
|
247
|
+
* @param {string} text - The text to type.
|
|
248
|
+
* @returns {Promise<void>}
|
|
249
|
+
*/
|
|
250
|
+
async function typeIntoElement(browserId, locator, text) {
|
|
251
|
+
const instance = getBrowserInstance(browserId);
|
|
252
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
253
|
+
|
|
254
|
+
const { cdpClient } = instance;
|
|
255
|
+
const { DOM, Input } = cdpClient;
|
|
256
|
+
|
|
257
|
+
console.log(`[BrowserService] Browser ${browserId}: Attempting to type "${text}" into element with locator:`, locator);
|
|
258
|
+
|
|
259
|
+
const nodeId = await findNodeBySelector(cdpClient, locator.type, locator.value);
|
|
260
|
+
if (!nodeId) {
|
|
261
|
+
throw new Error(`Element not found for locator: ${JSON.stringify(locator)}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await DOM.focus({ nodeId: nodeId });
|
|
265
|
+
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay for focus
|
|
266
|
+
|
|
267
|
+
// Clear existing text: Cmd/Ctrl+A then Backspace
|
|
268
|
+
await Input.dispatchKeyEvent({ type: 'keyDown', text: 'a', modifiers: (process.platform === 'darwin' ? 4 : 2) }); // 4 for Meta (Cmd), 2 for Control
|
|
269
|
+
await Input.dispatchKeyEvent({ type: 'keyUp', text: 'a', modifiers: (process.platform === 'darwin' ? 4 : 2) });
|
|
270
|
+
await Input.dispatchKeyEvent({ type: 'keyDown', key: 'Backspace' });
|
|
271
|
+
await Input.dispatchKeyEvent({ type: 'keyUp', key: 'Backspace' });
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay for clear
|
|
273
|
+
|
|
274
|
+
for (const char of text) {
|
|
275
|
+
await Input.dispatchKeyEvent({ type: 'keyDown', text: char, key: char });
|
|
276
|
+
await Input.dispatchKeyEvent({ type: 'keyUp', text: char, key: char });
|
|
277
|
+
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay for realism
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(`[BrowserService] Browser ${browserId}: Typed "${text}" into element.`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Closes a specific browser instance.
|
|
285
|
+
* @param {string} browserId - The ID of the browser instance.
|
|
286
|
+
* @returns {Promise<void>}
|
|
287
|
+
*/
|
|
288
|
+
async function closeBrowser(browserId) {
|
|
289
|
+
const instance = getBrowserInstance(browserId);
|
|
290
|
+
if (!instance) throw new Error(`Browser instance with ID '${browserId}' not found.`);
|
|
291
|
+
|
|
292
|
+
const { chromeInstance, cdpClient, userDataDir } = instance;
|
|
293
|
+
|
|
294
|
+
console.log(`[BrowserService] Closing browser ${browserId} (profile: ${userDataDir || 'temporary'})...`);
|
|
295
|
+
if (cdpClient) {
|
|
296
|
+
try {
|
|
297
|
+
cdpClient.close();
|
|
298
|
+
console.log(`[BrowserService] CDP client disconnected for ${browserId}.`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.warn(`[BrowserService] Error during CDP client close for ${browserId}:`, err.message);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (chromeInstance) {
|
|
304
|
+
try {
|
|
305
|
+
await chromeInstance.kill();
|
|
306
|
+
console.log(`[BrowserService] Chrome instance ${browserId} killed.`);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.warn(`[BrowserService] Error during Chrome instance kill for ${browserId}:`, err.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
delete activeBrowsers[browserId]; // Remove from our store
|
|
312
|
+
console.log(`[BrowserService] Browser ${browserId} removed from active list.`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Shuts down all active browser instances. Used for graceful server shutdown.
|
|
317
|
+
* @returns {Promise<void>}
|
|
318
|
+
*/
|
|
319
|
+
async function shutdownAllBrowsers() {
|
|
320
|
+
const browserIds = Object.keys(activeBrowsers);
|
|
321
|
+
if (browserIds.length === 0) {
|
|
322
|
+
console.log('[BrowserService] No active browsers to shut down.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.log(`[BrowserService] Shutting down ${browserIds.length} active browser(s)...`);
|
|
326
|
+
await Promise.all(browserIds.map(id => closeBrowser(id).catch(err => {
|
|
327
|
+
console.error(`[BrowserService] Failed to gracefully close browser ${id}:`, err.message);
|
|
328
|
+
// Continue with other shutdowns even if one fails
|
|
329
|
+
})));
|
|
330
|
+
console.log('[BrowserService] All active browsers shut down.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
launchBrowser,
|
|
336
|
+
navigateBrowser,
|
|
337
|
+
getBrowserInstance,
|
|
338
|
+
takeScreenshot,
|
|
339
|
+
getDomContent,
|
|
340
|
+
clickElement,
|
|
341
|
+
typeIntoElement,
|
|
342
|
+
closeBrowser,
|
|
343
|
+
shutdownAllBrowsers // Export for server.js to use
|
|
344
|
+
};
|