Skip to main content
Glama
IBM
by IBM
toolFactory.ts11.4 kB
/** * @fileoverview SQL execution engine for YAML-defined tools * Processes SQL parameters and executes queries using SourceManager * * @module src/utils/yaml/yamlSqlExecutor */ import { SourceManager } from "../../services/sourceManager.js"; import { AuthenticatedPoolManager } from "../../services/authenticatedPoolManager.js"; import { SqlToolParameter, SqlToolSecurityConfig, SqlToolExecutionResult, } from "@/ibmi-mcp-server/schemas/index.js"; import { ErrorHandler, logger } from "@/utils/internal/index.js"; import { requestContextService, RequestContext, } from "@/utils/internal/requestContext.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; import { ParameterProcessor } from "../sql/parameterProcessor.js"; import { authContext } from "@/mcp-server/transports/auth/index.js"; import { QueryResult } from "@ibm/mapepire-js"; /** * SQL execution engine for YAML-defined tools * Handles parameter binding and multi-source query execution */ export class SQLToolFactory { private static sourceManager: SourceManager; /** * Initialize the SQL executor with a source manager * @param sourceManager - Source manager instance */ static initialize(sourceManager: SourceManager): void { this.sourceManager = sourceManager; logger.info( { requestId: "sql-executor-init", timestamp: new Date().toISOString(), }, "YAML SQL Executor initialized successfully", ); } /** * Validate SQL statement for common issues * @param statement - SQL statement to validate * @param toolName - Tool name for error reporting * @param context - Request context * @private */ static async validateSqlStatement( statement: string, toolName: string, context: RequestContext, ): Promise<void> { logger.debug( { ...context, statementLength: statement.length, }, `Validating SQL statement for tool: ${toolName}`, ); // Basic SQL validation if (!statement?.trim()) { throw new McpError( JsonRpcErrorCode.ValidationError, `SQL statement cannot be empty for tool: ${toolName}`, { toolName }, ); } // Check for potential SQL injection patterns const suspiciousPatterns = [ /;\s*(drop|delete|truncate|alter)\s+/i, /union\s+select/i, /exec\s*\(/i, ]; for (const pattern of suspiciousPatterns) { if (pattern.test(statement)) { logger.warning( { ...context, pattern: pattern.source, }, `Potentially unsafe SQL pattern detected in tool: ${toolName}`, ); } } } /** * Execute a SQL statement with parameter binding * @param toolName - Name of the tool * @param sourceName - Source to execute against * @param sqlStatement - SQL statement with parameter placeholders * @param parameters - Parameters for processing * @param parameterDefinitions - Parameter definitions for validation * @param context - Request context (optional) * @param securityConfig - Security configuration for validation (optional) * @returns Execution result */ static async executeStatementWithParameters<T = Record<string, unknown>>( toolName: string, sourceName: string, sqlStatement: string, parameters: Record<string, unknown>, parameterDefinitions: SqlToolParameter[] = [], context?: RequestContext, securityConfig?: SqlToolSecurityConfig, ): Promise<SqlToolExecutionResult> { const operationContext = context || requestContextService.createRequestContext({ operation: "ExecuteYamlStatementWithParameters", toolName, sourceName, }); const startTime = Date.now(); return ErrorHandler.tryCatch( async () => { if (!this.sourceManager) { throw new McpError( JsonRpcErrorCode.InternalError, "YAML SQL executor not initialized. Call YamlSqlExecutor.initialize() first.", ); } logger.debug( { ...operationContext, sqlLength: sqlStatement.length, parameterCount: Object.keys(parameters).length, definitionCount: parameterDefinitions.length, }, `Processing SQL statement with parameters for tool: ${toolName}`, ); let processedSql: string; let bindingParameters: (string | number | (string | number)[])[] = []; // Special handling for direct SQL substitution (e.g., execute_sql tool) if ( parameterDefinitions.length === 1 && parameterDefinitions[0] && sqlStatement.trim() === `:${parameterDefinitions[0].name}` ) { // This is a direct SQL substitution case (like execute_sql) const paramName = parameterDefinitions[0].name; if ( paramName in parameters && typeof parameters[paramName] === "string" ) { processedSql = parameters[paramName] as string; bindingParameters = []; logger.debug( { ...operationContext, originalStatement: sqlStatement, substitutedSql: processedSql.substring(0, 100) + (processedSql.length > 100 ? "..." : ""), parameterName: paramName, }, `Applied direct SQL substitution for tool: ${toolName}`, ); } else { throw new McpError( JsonRpcErrorCode.ValidationError, `Missing or invalid SQL parameter '${paramName}' for direct substitution`, { paramName, toolName }, ); } } else if (parameterDefinitions.length > 0) { // Process with unified parameter validation and binding const result = await ParameterProcessor.process( sqlStatement, parameters, parameterDefinitions, { detailedLogging: true, validateSyntax: true, context: operationContext, strictTypeValidation: true, }, ); processedSql = result.sql; bindingParameters = result.parameters; if (result.missingParameters.length > 0) { logger.warning( { ...operationContext, missingParameters: result.missingParameters, }, `Missing parameters for tool ${toolName}`, ); } logger.debug( { ...operationContext, mode: result.mode, parameterCount: bindingParameters.length, parametersUsed: result.parameterNames, }, `SQL processed with parameter binding`, ); } else { // No parameter definitions - use SQL as-is processedSql = sqlStatement; bindingParameters = []; logger.debug( { ...operationContext, sqlLength: sqlStatement.length, }, `SQL used as-is (no parameters defined)`, ); } // Execute the query with auth-aware routing const result = await this.executeWithAuthRouting<T>( processedSql, bindingParameters, sourceName, operationContext, securityConfig, ); const executionTime = Date.now() - startTime; logger.debug( { ...operationContext, executionTime, rowCount: result.data?.length || 0, success: result.success, }, `SQL executed successfully for tool: ${toolName}`, ); const simplifiedColumns = (result.metadata?.columns ?? []).map( (column, index) => { const record = column as { name?: string; type?: string; label?: string; }; const name = record.name ?? record.label ?? `column_${index}`; return { name, type: record.type, label: record.label ?? record.name, }; }, ); return { data: result.data || [], rowCount: result.data?.length || 0, affectedRows: (result.metadata as { affectedRows?: number }) ?.affectedRows, columns: simplifiedColumns, executionTime, parameterMetadata: { mode: parameterDefinitions.length > 0 ? "parameters" : "none", parameterCount: bindingParameters.length, processedParameters: parameterDefinitions.map((p) => p.name), }, }; }, { operation: "ExecuteYamlStatementWithParameters", context: operationContext, errorCode: JsonRpcErrorCode.InternalError, }, ); } /** * Execute a query with auth-aware routing * Routes to authenticated pools when IBM i tokens are present, otherwise uses source manager * @param sql - Processed SQL statement * @param parameters - Binding parameters * @param sourceName - Source name for fallback routing * @param context - Request context * @param securityConfig - Optional security configuration * @returns Query execution result * @private */ private static async executeWithAuthRouting<T>( sql: string, parameters: (string | number | (string | number)[])[], sourceName: string, context: RequestContext, securityConfig?: SqlToolSecurityConfig, ): Promise<QueryResult<T>> { // Check for IBM i authentication context const authInfo = authContext.getStore()?.authInfo; if (authInfo?.ibmiToken) { // Use authenticated pool manager when IBM i token is present logger.debug( { ...context, authClientId: authInfo.clientId, ibmiHost: authInfo.ibmiHost, routingMode: "authenticated", }, "Executing SQL via authenticated pool", ); const poolManager = AuthenticatedPoolManager.getInstance(); return poolManager.executeQuery<T>( authInfo.ibmiToken, sql, parameters, context, ); } else { // Fall back to regular source manager (environment credentials) logger.debug( { ...context, sourceName, routingMode: "environment", }, "Executing SQL via source manager", ); return securityConfig ? this.sourceManager.executeQuery<T>( sourceName, sql, parameters, context, securityConfig, ) : this.sourceManager.executeQuery<T>( sourceName, sql, parameters, context, ); } } /** * Get initialized state * @returns Whether the executor is initialized */ static isInitialized(): boolean { return !!this.sourceManager; } /** * Get the source manager instance (for testing) * @returns Source manager instance * @internal */ static getSourceManager(): SourceManager | undefined { return this.sourceManager; } }

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/IBM/ibmi-mcp'

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