@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,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;