@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,269 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const config = require('../../config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tool Registry - Manages discovery, loading, and registration of tools
|
|
7
|
+
*/
|
|
8
|
+
class ToolRegistry {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.tools = new Map(); // toolName -> toolInstance
|
|
11
|
+
this.definitions = []; // Array of tool definitions for MCP
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Automatically discovers and loads tools from the tools directory
|
|
17
|
+
* @param {string} toolsDir - The root tools directory path
|
|
18
|
+
* @param {boolean} debugMode - Whether to enable debug logging
|
|
19
|
+
*/
|
|
20
|
+
async discoverTools(toolsDir, debugMode = false) {
|
|
21
|
+
this.debugMode = debugMode;
|
|
22
|
+
if (debugMode) {
|
|
23
|
+
console.error('[ToolRegistry] Starting tool discovery...');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await this._scanDirectory(toolsDir);
|
|
28
|
+
if (debugMode) {
|
|
29
|
+
console.error(`[ToolRegistry] Discovery complete. Found ${this.tools.size} tools.`);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[ToolRegistry] Error during tool discovery:', error.message);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Recursively scans a directory for tool files
|
|
39
|
+
* @param {string} dir - Directory to scan
|
|
40
|
+
*/
|
|
41
|
+
async _scanDirectory(dir) {
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
console.error(`[ToolRegistry] Tools directory not found: ${dir}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const items = fs.readdirSync(dir);
|
|
48
|
+
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const itemPath = path.join(dir, item);
|
|
51
|
+
const stat = fs.statSync(itemPath);
|
|
52
|
+
|
|
53
|
+
if (stat.isDirectory() && item !== 'base') {
|
|
54
|
+
// Recursively scan subdirectories (except 'base')
|
|
55
|
+
await this._scanDirectory(itemPath);
|
|
56
|
+
} else if (stat.isFile() && item.endsWith('.js') && !item.startsWith('index')) {
|
|
57
|
+
// Load tool files (skip index.js files)
|
|
58
|
+
await this._loadTool(itemPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Loads a single tool from a file
|
|
65
|
+
* @param {string} toolPath - Path to the tool file
|
|
66
|
+
*/
|
|
67
|
+
async _loadTool(toolPath) {
|
|
68
|
+
try {
|
|
69
|
+
const ToolClass = require(toolPath);
|
|
70
|
+
|
|
71
|
+
// Validate that it's a proper tool class
|
|
72
|
+
if (typeof ToolClass !== 'function') {
|
|
73
|
+
console.warn(`[ToolRegistry] Skipping ${toolPath}: Not a class/function export`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!ToolClass.definition) {
|
|
78
|
+
console.warn(`[ToolRegistry] Skipping ${toolPath}: No tool definition found`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const toolName = ToolClass.getName();
|
|
83
|
+
|
|
84
|
+
// Check feature flags before registering
|
|
85
|
+
const category = this._getToolCategory(toolName, toolPath);
|
|
86
|
+
const featureFlag = `enable${category.charAt(0).toUpperCase() + category.slice(1)}Tools`;
|
|
87
|
+
|
|
88
|
+
if (!this.config.isFeatureEnabled(featureFlag)) {
|
|
89
|
+
if (this.debugMode) {
|
|
90
|
+
console.error(`[ToolRegistry] Tool '${toolName}' disabled by feature flag: ${featureFlag}`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create an instance of the tool
|
|
96
|
+
const toolInstance = new ToolClass();
|
|
97
|
+
|
|
98
|
+
// Check for name conflicts
|
|
99
|
+
if (this.tools.has(toolName)) {
|
|
100
|
+
console.warn(`[ToolRegistry] Tool name conflict: '${toolName}' already registered. Skipping ${toolPath}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register the tool
|
|
105
|
+
this.tools.set(toolName, toolInstance);
|
|
106
|
+
this.definitions.push(ToolClass.getDefinition());
|
|
107
|
+
|
|
108
|
+
if (this.debugMode) {
|
|
109
|
+
console.error(`[ToolRegistry] Registered tool: ${toolName}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`[ToolRegistry] Failed to load tool from ${toolPath}:`, error.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Manually registers a tool instance
|
|
119
|
+
* @param {ToolBase} toolInstance - The tool instance to register
|
|
120
|
+
*/
|
|
121
|
+
registerTool(toolInstance) {
|
|
122
|
+
const toolName = toolInstance.constructor.getName();
|
|
123
|
+
|
|
124
|
+
if (this.tools.has(toolName)) {
|
|
125
|
+
throw new Error(`Tool '${toolName}' is already registered`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.tools.set(toolName, toolInstance);
|
|
129
|
+
this.definitions.push(toolInstance.constructor.getDefinition());
|
|
130
|
+
|
|
131
|
+
console.error(`[ToolRegistry] Manually registered tool: ${toolName}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gets a tool instance by name
|
|
136
|
+
* @param {string} toolName - The name of the tool
|
|
137
|
+
* @returns {ToolBase|undefined} - The tool instance or undefined if not found
|
|
138
|
+
*/
|
|
139
|
+
getTool(toolName) {
|
|
140
|
+
return this.tools.get(toolName);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Gets all tool definitions for MCP tools/list response
|
|
145
|
+
* @returns {Array} - Array of tool definitions
|
|
146
|
+
*/
|
|
147
|
+
getDefinitions() {
|
|
148
|
+
return this.definitions;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Gets all registered tool names
|
|
153
|
+
* @returns {Array<string>} - Array of tool names
|
|
154
|
+
*/
|
|
155
|
+
getToolNames() {
|
|
156
|
+
return Array.from(this.tools.keys());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Checks if a tool is registered
|
|
161
|
+
* @param {string} toolName - The name of the tool
|
|
162
|
+
* @returns {boolean} - True if the tool is registered
|
|
163
|
+
*/
|
|
164
|
+
hasTool(toolName) {
|
|
165
|
+
return this.tools.has(toolName);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Executes a tool by name
|
|
170
|
+
* @param {string} toolName - The name of the tool to execute
|
|
171
|
+
* @param {object} parameters - The parameters to pass to the tool
|
|
172
|
+
* @returns {Promise<object>} - The tool execution result
|
|
173
|
+
*/
|
|
174
|
+
async executeTool(toolName, parameters) {
|
|
175
|
+
const tool = this.getTool(toolName);
|
|
176
|
+
|
|
177
|
+
if (!tool) {
|
|
178
|
+
throw {
|
|
179
|
+
code: -32601,
|
|
180
|
+
message: `Tool '${toolName}' not found`,
|
|
181
|
+
data: {
|
|
182
|
+
available_tools: this.getToolNames(),
|
|
183
|
+
requested_tool: toolName
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return await tool.run(parameters);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Apply feature flags to filter available tools
|
|
193
|
+
*/
|
|
194
|
+
_applyFeatureFlags() {
|
|
195
|
+
const toolsToRemove = [];
|
|
196
|
+
|
|
197
|
+
for (const [toolName, toolInstance] of this.tools) {
|
|
198
|
+
const category = this._getToolCategory(toolName);
|
|
199
|
+
const featureFlag = `enable${category.charAt(0).toUpperCase() + category.slice(1)}Tools`;
|
|
200
|
+
|
|
201
|
+
if (!this.config.isFeatureEnabled(featureFlag)) {
|
|
202
|
+
if (this.debugMode) {
|
|
203
|
+
console.error(`[ToolRegistry] Tool '${toolName}' disabled by feature flag: ${featureFlag}`);
|
|
204
|
+
}
|
|
205
|
+
toolsToRemove.push(toolName);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Remove disabled tools
|
|
210
|
+
for (const toolName of toolsToRemove) {
|
|
211
|
+
this.tools.delete(toolName);
|
|
212
|
+
this.definitions = this.definitions.filter(def => def.name !== toolName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get tool category from tool name and file path
|
|
218
|
+
* @param {string} toolName - The tool name
|
|
219
|
+
* @param {string} toolPath - The tool file path (optional, for better categorization)
|
|
220
|
+
* @returns {string} - The tool category
|
|
221
|
+
*/
|
|
222
|
+
_getToolCategory(toolName, toolPath = '') {
|
|
223
|
+
// Check by tool name prefix first
|
|
224
|
+
if (toolName.startsWith('api_')) return 'api';
|
|
225
|
+
if (toolName.startsWith('browser_')) return 'browser';
|
|
226
|
+
if (toolName.startsWith('file_')) return 'file';
|
|
227
|
+
if (toolName.startsWith('network_')) return 'network';
|
|
228
|
+
|
|
229
|
+
// Check by file path if tool name doesn't have clear prefix
|
|
230
|
+
if (toolPath.includes('/api/') || toolPath.includes('\\api\\')) return 'api';
|
|
231
|
+
if (toolPath.includes('/browser/') || toolPath.includes('\\browser\\')) return 'browser';
|
|
232
|
+
if (toolPath.includes('/advanced/') || toolPath.includes('\\advanced\\')) return 'advanced';
|
|
233
|
+
if (toolPath.includes('/file/') || toolPath.includes('\\file\\')) return 'file';
|
|
234
|
+
if (toolPath.includes('/network/') || toolPath.includes('\\network\\')) return 'network';
|
|
235
|
+
|
|
236
|
+
// Default fallback
|
|
237
|
+
return 'other';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gets registry statistics
|
|
242
|
+
* @returns {object} - Registry statistics
|
|
243
|
+
*/
|
|
244
|
+
getStats() {
|
|
245
|
+
const categories = {};
|
|
246
|
+
for (const toolName of this.getToolNames()) {
|
|
247
|
+
const category = this._getToolCategory(toolName);
|
|
248
|
+
categories[category] = (categories[category] || 0) + 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
total_tools: this.tools.size,
|
|
253
|
+
tool_names: this.getToolNames(),
|
|
254
|
+
definitions_count: this.definitions.length,
|
|
255
|
+
categories: categories,
|
|
256
|
+
feature_flags: {
|
|
257
|
+
enableApiTools: this.config.isFeatureEnabled('enableApiTools'),
|
|
258
|
+
enableBrowserTools: this.config.isFeatureEnabled('enableBrowserTools'),
|
|
259
|
+
enableAdvancedTools: this.config.isFeatureEnabled('enableAdvancedTools'),
|
|
260
|
+
enableFileTools: this.config.isFeatureEnabled('enableFileTools'),
|
|
261
|
+
enableNetworkTools: this.config.isFeatureEnabled('enableNetworkTools'),
|
|
262
|
+
enableOtherTools: this.config.isFeatureEnabled('enableOtherTools'),
|
|
263
|
+
enableDebugMode: this.config.isFeatureEnabled('enableDebugMode')
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = ToolRegistry;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
const ToolBase = require('../../base/ToolBase');
|
|
2
|
+
const browserService = require('../../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Console Tool - Monitor and retrieve browser console messages
|
|
6
|
+
* Inspired by Playwright MCP console capabilities
|
|
7
|
+
*/
|
|
8
|
+
class BrowserConsoleTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_console",
|
|
11
|
+
description: "Monitor, retrieve, and manage browser console messages including logs, errors, and warnings.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance"
|
|
18
|
+
},
|
|
19
|
+
action: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["get", "clear", "monitor", "stopMonitor"],
|
|
22
|
+
description: "Console action to perform"
|
|
23
|
+
},
|
|
24
|
+
filter: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
level: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["log", "info", "warn", "error", "debug", "trace"],
|
|
30
|
+
description: "Filter by console message level"
|
|
31
|
+
},
|
|
32
|
+
text: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Filter messages containing this text"
|
|
35
|
+
},
|
|
36
|
+
source: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Filter by source URL pattern"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
description: "Filter criteria for console messages"
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: "number",
|
|
45
|
+
default: 100,
|
|
46
|
+
description: "Maximum number of messages to return"
|
|
47
|
+
},
|
|
48
|
+
realTime: {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
default: false,
|
|
51
|
+
description: "Whether to return real-time console monitoring (for monitor action)"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ["browserId", "action"]
|
|
55
|
+
},
|
|
56
|
+
output_schema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
success: { type: "boolean", description: "Whether the operation was successful" },
|
|
60
|
+
action: { type: "string", description: "The action that was performed" },
|
|
61
|
+
messages: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
level: { type: "string" },
|
|
67
|
+
text: { type: "string" },
|
|
68
|
+
url: { type: "string" },
|
|
69
|
+
lineNumber: { type: "number" },
|
|
70
|
+
timestamp: { type: "string" },
|
|
71
|
+
args: { type: "array" },
|
|
72
|
+
stackTrace: { type: "array" }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
description: "Array of console messages"
|
|
76
|
+
},
|
|
77
|
+
summary: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
total: { type: "number" },
|
|
81
|
+
errors: { type: "number" },
|
|
82
|
+
warnings: { type: "number" },
|
|
83
|
+
logs: { type: "number" }
|
|
84
|
+
},
|
|
85
|
+
description: "Summary of console messages"
|
|
86
|
+
},
|
|
87
|
+
monitoring: { type: "boolean", description: "Whether console monitoring is active" },
|
|
88
|
+
browserId: { type: "string", description: "Browser instance ID" }
|
|
89
|
+
},
|
|
90
|
+
required: ["success", "action", "browserId"]
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
super();
|
|
96
|
+
this.consoleData = new Map(); // browserId -> console messages
|
|
97
|
+
this.monitoring = new Map(); // browserId -> monitoring state
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async execute(parameters) {
|
|
101
|
+
const {
|
|
102
|
+
browserId,
|
|
103
|
+
action,
|
|
104
|
+
filter = {},
|
|
105
|
+
limit = 100,
|
|
106
|
+
realTime = false
|
|
107
|
+
} = parameters;
|
|
108
|
+
|
|
109
|
+
const browser = browserService.getBrowserInstance(browserId);
|
|
110
|
+
if (!browser) {
|
|
111
|
+
throw new Error(`Browser instance '${browserId}' not found`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const client = browser.client;
|
|
115
|
+
|
|
116
|
+
let result = {
|
|
117
|
+
success: false,
|
|
118
|
+
action: action,
|
|
119
|
+
browserId: browserId
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
switch (action) {
|
|
123
|
+
case 'monitor':
|
|
124
|
+
await this.startConsoleMonitoring(client, browserId, realTime);
|
|
125
|
+
result.success = true;
|
|
126
|
+
result.monitoring = true;
|
|
127
|
+
result.message = 'Console monitoring started';
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'stopMonitor':
|
|
131
|
+
this.stopConsoleMonitoring(browserId);
|
|
132
|
+
result.success = true;
|
|
133
|
+
result.monitoring = false;
|
|
134
|
+
result.message = 'Console monitoring stopped';
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'get':
|
|
138
|
+
const messages = this.getConsoleMessages(browserId, filter, limit);
|
|
139
|
+
result.success = true;
|
|
140
|
+
result.messages = messages.messages;
|
|
141
|
+
result.summary = messages.summary;
|
|
142
|
+
result.monitoring = this.monitoring.has(browserId);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'clear':
|
|
146
|
+
this.clearConsoleMessages(browserId);
|
|
147
|
+
result.success = true;
|
|
148
|
+
result.message = 'Console messages cleared';
|
|
149
|
+
result.monitoring = this.monitoring.has(browserId);
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
throw new Error(`Unsupported console action: ${action}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start monitoring console messages
|
|
161
|
+
*/
|
|
162
|
+
async startConsoleMonitoring(client, browserId, realTime = false) {
|
|
163
|
+
// Enable Runtime domain for console events
|
|
164
|
+
await client.Runtime.enable();
|
|
165
|
+
|
|
166
|
+
// Initialize storage
|
|
167
|
+
if (!this.consoleData.has(browserId)) {
|
|
168
|
+
this.consoleData.set(browserId, []);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const consoleMessages = this.consoleData.get(browserId);
|
|
172
|
+
|
|
173
|
+
// Set up console event listener
|
|
174
|
+
const consoleListener = (params) => {
|
|
175
|
+
const message = this.formatConsoleMessage(params);
|
|
176
|
+
consoleMessages.push(message);
|
|
177
|
+
|
|
178
|
+
// Keep only last 1000 messages to prevent memory issues
|
|
179
|
+
if (consoleMessages.length > 1000) {
|
|
180
|
+
consoleMessages.splice(0, consoleMessages.length - 1000);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (realTime) {
|
|
184
|
+
console.log(`[Console:${browserId}] ${message.level.toUpperCase()}: ${message.text}`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
client.Runtime.consoleAPICalled(consoleListener);
|
|
189
|
+
|
|
190
|
+
// Also listen for runtime exceptions
|
|
191
|
+
const exceptionListener = (params) => {
|
|
192
|
+
const message = this.formatExceptionMessage(params);
|
|
193
|
+
consoleMessages.push(message);
|
|
194
|
+
|
|
195
|
+
if (realTime) {
|
|
196
|
+
console.log(`[Console:${browserId}] ERROR: ${message.text}`);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
client.Runtime.exceptionThrown(exceptionListener);
|
|
201
|
+
|
|
202
|
+
// Store monitoring state
|
|
203
|
+
this.monitoring.set(browserId, {
|
|
204
|
+
consoleListener,
|
|
205
|
+
exceptionListener,
|
|
206
|
+
startTime: Date.now()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Stop monitoring console messages
|
|
212
|
+
*/
|
|
213
|
+
stopConsoleMonitoring(browserId) {
|
|
214
|
+
this.monitoring.delete(browserId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get console messages with filtering
|
|
219
|
+
*/
|
|
220
|
+
getConsoleMessages(browserId, filter, limit) {
|
|
221
|
+
const allMessages = this.consoleData.get(browserId) || [];
|
|
222
|
+
|
|
223
|
+
let filteredMessages = [...allMessages];
|
|
224
|
+
|
|
225
|
+
// Apply filters
|
|
226
|
+
if (filter.level) {
|
|
227
|
+
filteredMessages = filteredMessages.filter(msg => msg.level === filter.level);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (filter.text) {
|
|
231
|
+
const searchText = filter.text.toLowerCase();
|
|
232
|
+
filteredMessages = filteredMessages.filter(msg =>
|
|
233
|
+
msg.text.toLowerCase().includes(searchText)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (filter.source) {
|
|
238
|
+
const sourceRegex = new RegExp(filter.source, 'i');
|
|
239
|
+
filteredMessages = filteredMessages.filter(msg =>
|
|
240
|
+
msg.url && sourceRegex.test(msg.url)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Limit results
|
|
245
|
+
const messages = filteredMessages.slice(-limit);
|
|
246
|
+
|
|
247
|
+
// Create summary
|
|
248
|
+
const summary = this.createMessageSummary(filteredMessages);
|
|
249
|
+
|
|
250
|
+
return { messages, summary };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear console messages
|
|
255
|
+
*/
|
|
256
|
+
clearConsoleMessages(browserId) {
|
|
257
|
+
this.consoleData.set(browserId, []);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Format console API message
|
|
262
|
+
*/
|
|
263
|
+
formatConsoleMessage(params) {
|
|
264
|
+
const level = params.type || 'log';
|
|
265
|
+
const timestamp = new Date().toISOString();
|
|
266
|
+
|
|
267
|
+
// Extract text from console arguments
|
|
268
|
+
let text = '';
|
|
269
|
+
const args = [];
|
|
270
|
+
|
|
271
|
+
if (params.args) {
|
|
272
|
+
for (const arg of params.args) {
|
|
273
|
+
if (arg.value !== undefined) {
|
|
274
|
+
const value = arg.value;
|
|
275
|
+
text += String(value) + ' ';
|
|
276
|
+
args.push(value);
|
|
277
|
+
} else if (arg.description) {
|
|
278
|
+
text += arg.description + ' ';
|
|
279
|
+
args.push(arg.description);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
level: level,
|
|
286
|
+
text: text.trim(),
|
|
287
|
+
timestamp: timestamp,
|
|
288
|
+
args: args,
|
|
289
|
+
url: params.executionContextId ? 'unknown' : null,
|
|
290
|
+
lineNumber: null,
|
|
291
|
+
stackTrace: params.stackTrace ? this.formatStackTrace(params.stackTrace) : null
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Format runtime exception message
|
|
297
|
+
*/
|
|
298
|
+
formatExceptionMessage(params) {
|
|
299
|
+
const timestamp = new Date().toISOString();
|
|
300
|
+
const exceptionDetails = params.exceptionDetails;
|
|
301
|
+
|
|
302
|
+
let text = 'Uncaught ';
|
|
303
|
+
if (exceptionDetails.exception?.description) {
|
|
304
|
+
text += exceptionDetails.exception.description;
|
|
305
|
+
} else if (exceptionDetails.text) {
|
|
306
|
+
text += exceptionDetails.text;
|
|
307
|
+
} else {
|
|
308
|
+
text += 'Error';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
level: 'error',
|
|
313
|
+
text: text,
|
|
314
|
+
timestamp: timestamp,
|
|
315
|
+
url: exceptionDetails.url || null,
|
|
316
|
+
lineNumber: exceptionDetails.lineNumber || null,
|
|
317
|
+
columnNumber: exceptionDetails.columnNumber || null,
|
|
318
|
+
stackTrace: exceptionDetails.stackTrace ? this.formatStackTrace(exceptionDetails.stackTrace) : null
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format stack trace
|
|
324
|
+
*/
|
|
325
|
+
formatStackTrace(stackTrace) {
|
|
326
|
+
if (!stackTrace.callFrames) return null;
|
|
327
|
+
|
|
328
|
+
return stackTrace.callFrames.map(frame => ({
|
|
329
|
+
functionName: frame.functionName || '<anonymous>',
|
|
330
|
+
url: frame.url,
|
|
331
|
+
lineNumber: frame.lineNumber,
|
|
332
|
+
columnNumber: frame.columnNumber
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Create message summary
|
|
338
|
+
*/
|
|
339
|
+
createMessageSummary(messages) {
|
|
340
|
+
const summary = {
|
|
341
|
+
total: messages.length,
|
|
342
|
+
errors: 0,
|
|
343
|
+
warnings: 0,
|
|
344
|
+
logs: 0,
|
|
345
|
+
info: 0,
|
|
346
|
+
debug: 0
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
messages.forEach(msg => {
|
|
350
|
+
switch (msg.level) {
|
|
351
|
+
case 'error':
|
|
352
|
+
summary.errors++;
|
|
353
|
+
break;
|
|
354
|
+
case 'warn':
|
|
355
|
+
summary.warnings++;
|
|
356
|
+
break;
|
|
357
|
+
case 'log':
|
|
358
|
+
summary.logs++;
|
|
359
|
+
break;
|
|
360
|
+
case 'info':
|
|
361
|
+
summary.info++;
|
|
362
|
+
break;
|
|
363
|
+
case 'debug':
|
|
364
|
+
summary.debug++;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return summary;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get console monitoring status
|
|
374
|
+
*/
|
|
375
|
+
getMonitoringStatus(browserId) {
|
|
376
|
+
return {
|
|
377
|
+
monitoring: this.monitoring.has(browserId),
|
|
378
|
+
messageCount: (this.consoleData.get(browserId) || []).length,
|
|
379
|
+
startTime: this.monitoring.get(browserId)?.startTime
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = BrowserConsoleTool;
|