Skip to main content
Glama
base-tool.ts10.8 kB
import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '../utils/logger.js'; import { ErrorHandler, ValidationError, TimeoutError } from '../utils/errors.js'; import { SecurityManager, RateLimiter } from '../config/security.js'; import { resourceManager } from '../utils/resource-manager.js'; import { config } from '../config/index.js'; /** * Base class for all MCP tools with built-in safety features * Following WCGW: Every tool must validate, sanitize, and handle errors */ export abstract class BaseTool<TInput = any, TOutput = any> { public readonly name: string; public readonly description: string; public readonly inputSchema: any; protected rateLimiter: RateLimiter; protected readonly timeout: number; constructor(options: { name: string; description: string; inputSchema: any; rateLimit?: { windowMs?: number; maxRequests?: number }; timeout?: number; }) { this.name = options.name; this.description = options.description; this.inputSchema = options.inputSchema; this.timeout = options.timeout || config.getNumber('COMMAND_TIMEOUT_MS', 30000); // Setup rate limiting per tool this.rateLimiter = new RateLimiter( options.rateLimit?.windowMs || 60000, options.rateLimit?.maxRequests || 100 ); } /** * Execute the tool with full safety checks */ async execute(args: TInput): Promise<TOutput> { const executionId = uuidv4(); const startTime = Date.now(); // Set logging context logger.setContext({ toolName: this.name, executionId }); try { // Rate limiting if (!this.rateLimiter.isAllowed(this.name)) { throw new Error(`Rate limit exceeded for tool: ${this.name}`); } // Input validation const validatedInput = await this.validateInput(args); // Log execution start logger.info(`Executing tool: ${this.name}`, { input: this.sanitizeForLogging(validatedInput) }); // Execute with timeout const result = await this.executeWithTimeout( () => this.executeInternal(validatedInput), this.timeout ); // Validate output const validatedOutput = await this.validateOutput(result); // Log success const duration = Date.now() - startTime; logger.info(`Tool execution completed: ${this.name}`, { duration, success: true }); logger.metric(`tool.${this.name}.execution_time`, duration); return validatedOutput; } catch (error) { const duration = Date.now() - startTime; // Log failure logger.error(`Tool execution failed: ${this.name}`, error as Error, { duration, input: this.sanitizeForLogging(args) }); logger.metric(`tool.${this.name}.error`, 1, 'count', { errorType: (error as Error).name }); // Re-throw with context if (error instanceof Error) { error.message = `[${this.name}] ${error.message}`; } throw error; } finally { // Clear logging context logger.clearContext(); } } /** * Internal execution logic to be implemented by subclasses */ protected abstract executeInternal(input: TInput): Promise<TOutput>; /** * Validate input against schema */ protected async validateInput(input: any): Promise<TInput> { try { // Use Zod if schema is provided if (this.getZodSchema) { const schema = this.getZodSchema(); return schema.parse(input); } // Basic validation if (!input || typeof input !== 'object') { throw new ValidationError('Input must be an object'); } // Check required fields from inputSchema if (this.inputSchema.required) { for (const field of this.inputSchema.required) { if (!(field in input)) { throw new ValidationError(`Missing required field: ${field}`, field); } } } return input as TInput; } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( firstError.message, firstError.path.join('.'), firstError ); } throw error; } } /** * Optional Zod schema for validation */ protected getZodSchema?(): z.ZodSchema<TInput>; /** * Validate output */ protected async validateOutput(output: any): Promise<TOutput> { // Override in subclasses for output validation return output as TOutput; } /** * Execute with timeout */ private async executeWithTimeout<T>( fn: () => Promise<T>, timeoutMs: number ): Promise<T> { const timeoutPromise = new Promise<never>((_, reject) => { const timer = setTimeout(() => { reject(new TimeoutError(this.name, timeoutMs)); }, timeoutMs); // Register timer for cleanup resourceManager.registerTimer(`timeout_${this.name}_${Date.now()}`, timer); }); try { return await Promise.race([fn(), timeoutPromise]); } finally { // Cleanup will happen in resource manager } } /** * Sanitize input for logging (remove sensitive data) */ protected sanitizeForLogging(input: any): any { if (!input || typeof input !== 'object') { return input; } const sanitized = { ...input }; const sensitiveFields = [ 'password', 'token', 'secret', 'key', 'authorization', 'api_key', 'apiKey', 'access_token', 'accessToken', 'connectionString', 'database_url', 'databaseUrl' ]; for (const field of sensitiveFields) { if (field in sanitized) { sanitized[field] = '[REDACTED]'; } // Check nested fields const fieldLower = field.toLowerCase(); for (const key of Object.keys(sanitized)) { if (key.toLowerCase().includes(fieldLower)) { sanitized[key] = '[REDACTED]'; } } } return sanitized; } /** * Helper for secure command execution */ protected async executeCommand( command: string, options?: { cwd?: string; timeout?: number } ): Promise<{ stdout: string; stderr: string }> { return await SecurityManager.executeSecureCommand(command, { timeout: options?.timeout || this.timeout, cwd: options?.cwd }); } /** * Helper for secure file path handling */ protected sanitizePath(path: string): string { return SecurityManager.sanitizePath(path); } /** * Helper for retry logic */ protected async withRetry<T>( operation: () => Promise<T>, options?: { maxAttempts?: number; retryMessage?: string; } ): Promise<T> { return await ErrorHandler.retry(operation, { maxAttempts: options?.maxAttempts || 3, onRetry: (error, attempt) => { logger.warn(options?.retryMessage || 'Retrying operation', { toolName: this.name, attempt, error: error.message }); } }); } /** * Helper for circuit breaker pattern */ protected createCircuitBreaker<T>( operation: (...args: any[]) => Promise<T> ): (...args: any[]) => Promise<T> { return ErrorHandler.createCircuitBreaker(operation, { threshold: 5, timeout: this.timeout, resetTimeout: 30000 }); } } /** * Base class for tools that work with files */ export abstract class FileBasedTool<TInput = any, TOutput = any> extends BaseTool<TInput, TOutput> { protected readonly maxFileSize: number; constructor(options: any) { super(options); this.maxFileSize = config.getNumber('MAX_FILE_SIZE', 10 * 1024 * 1024); // 10MB } /** * Check file size before operations */ protected async checkFileSize(path: string): Promise<void> { const fs = await import('fs-extra'); const stats = await fs.stat(path); if (stats.size > this.maxFileSize) { throw new ValidationError( `File size ${stats.size} exceeds maximum allowed size ${this.maxFileSize}`, 'fileSize', stats.size ); } } /** * Read file with size check */ protected async readFileSecure(path: string): Promise<string> { const sanitizedPath = this.sanitizePath(path); await this.checkFileSize(sanitizedPath); const fs = await import('fs-extra'); return await fs.readFile(sanitizedPath, 'utf-8'); } /** * Write file with backup */ protected async writeFileSecure( path: string, content: string, options?: { backup?: boolean } ): Promise<void> { const sanitizedPath = this.sanitizePath(path); const fs = await import('fs-extra'); // Create backup if file exists if (options?.backup !== false && await fs.pathExists(sanitizedPath)) { const backupPath = `${sanitizedPath}.backup.${Date.now()}`; await fs.copy(sanitizedPath, backupPath); logger.info('Created file backup', { original: sanitizedPath, backup: backupPath }); } // Ensure directory exists await fs.ensureDir(await import('path').then(p => p.dirname(sanitizedPath))); // Write file await fs.writeFile(sanitizedPath, content, 'utf-8'); } } /** * Base class for tools that work with databases */ export abstract class DatabaseTool<TInput = any, TOutput = any> extends BaseTool<TInput, TOutput> { protected connectionPool?: any; /** * Get database connection from pool */ protected async getConnection(): Promise<{ id: string; connection: any }> { if (!this.connectionPool) { throw new Error('Database connection pool not initialized'); } return await this.connectionPool.acquire(); } /** * Release database connection back to pool */ protected async releaseConnection(id: string): Promise<void> { if (this.connectionPool) { await this.connectionPool.release(id); } } /** * Execute database operation with connection management */ protected async withConnection<T>( operation: (connection: any) => Promise<T> ): Promise<T> { const { id, connection } = await this.getConnection(); try { return await operation(connection); } finally { await this.releaseConnection(id); } } /** * Execute query with parameterized statements */ protected async executeQuery( query: string, params: any[] = [] ): Promise<any> { // This should be overridden by specific database implementations throw new Error('executeQuery must be implemented by subclass'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rajawatrajat/mcp-software-engineer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server