Skip to main content
Glama
workflow-tool-generator.ts20.8 kB
/** * WorkflowToolGenerator - Converts WorkflowDefinitions to ToolDefinitions * Handles dynamic tool generation, parameter validation, and execution */ import type { WorkflowDefinition, ToolDefinition, APIClient, Logger, WorkflowExecutionResult } from '../types/index.js'; import { AppError, ErrorType } from '../types/index.js'; import { WorkflowExecutionService } from './workflow-execution.js'; import { WorkflowErrorHandler } from '../utils/workflow-error-handler.js'; /** * Configuration for workflow tool generation */ export interface WorkflowToolGeneratorConfig { executionTimeout: number; // Default execution timeout in ms statusCheckInterval: number; // Status polling interval in ms maxRetryAttempts: number; // Max retry attempts for failed requests toolNamePrefix?: string; // Optional prefix for generated tool names } /** * Interface for WorkflowToolGenerator */ export interface IWorkflowToolGenerator { // Tool generation convertWorkflowToTool(workflow: WorkflowDefinition): ToolDefinition; generateToolName(workflow: WorkflowDefinition, existingNames?: Set<string>): string; // Parameter handling validateWorkflowParameters(workflow: WorkflowDefinition, params: any): void; buildExecutionPayload(params: any): { input: any; source: string }; // Execution handling executeWorkflowTool(workflow: WorkflowDefinition, params: any, apiClient: APIClient): Promise<any>; formatExecutionResult(result: WorkflowExecutionResult, workflow: WorkflowDefinition): any; // Cache and statistics management clearGeneratedNames(): void; getGenerationStats(): { totalGenerated: number; cacheHits: number; errors: number }; } /** * WorkflowToolGenerator implementation * Handles conversion from WorkflowDefinition to ToolDefinition */ export class WorkflowToolGenerator implements IWorkflowToolGenerator { private logger: Logger; private config: WorkflowToolGeneratorConfig; private executionService: WorkflowExecutionService; private errorHandler: WorkflowErrorHandler; private generatedToolNames: Set<string> = new Set(); private cacheHits: number = 0; private errors: number = 0; constructor( logger: Logger, config: WorkflowToolGeneratorConfig, executionService: WorkflowExecutionService ) { this.logger = logger; this.config = config; this.executionService = executionService; this.errorHandler = new WorkflowErrorHandler(logger); } /** * Convert a WorkflowDefinition to a ToolDefinition * @param workflow Workflow definition to convert * @returns ToolDefinition MCP tool definition */ convertWorkflowToTool(workflow: WorkflowDefinition): ToolDefinition { // this.logger.debug(`Converting workflow to tool: ${workflow.name} (${workflow.id})`); // Validate workflow definition this.validateWorkflowDefinition(workflow); // Generate unique tool name const toolName = this.generateToolName(workflow, this.generatedToolNames); this.generatedToolNames.add(toolName); // Convert input schema with validation const inputSchema = this.convertInputSchema(workflow.inputSchema); // Create tool definition const toolDefinition: ToolDefinition = { name: toolName, description: this.enhanceToolDescription(workflow), category: workflow.category || 'workflow', version: workflow.version || '1.0.0', inputSchema, handler: async (params: any, apiClient: APIClient) => { return this.executeWorkflowTool(workflow, params, apiClient); } }; // this.logger.debug(`Successfully converted workflow to tool: ${toolName}`); return toolDefinition; } /** * Generate a unique tool name from workflow definition * Handles naming conflicts and ensures MCP compliance * @param workflow Workflow definition * @param existingNames Set of existing tool names to avoid conflicts * @returns string Unique tool name */ generateToolName(workflow: WorkflowDefinition, existingNames?: Set<string>): string { const existing = existingNames || new Set(); // Start with sanitized workflow name let baseName = this.sanitizeToolName(workflow.name); // Add prefix if configured if (this.config.toolNamePrefix) { baseName = `${this.config.toolNamePrefix}_${baseName}`; } // Check for conflicts and generate unique name let toolName = baseName; let counter = 1; while (existing.has(toolName)) { this.cacheHits++; // Track cache hits when name conflicts occur // Try with workflow ID first if (counter === 1) { toolName = `${baseName}_${this.sanitizeToolName(workflow.id)}`; } else { // Use numeric suffix toolName = `${baseName}_${counter}`; } counter++; // Prevent infinite loop if (counter > 1000) { this.errors++; // Track error throw new AppError( ErrorType.VALIDATION_ERROR, `Unable to generate unique tool name for workflow: ${workflow.name}`, { workflowId: workflow.id, baseName, attempts: counter } ); } } // this.logger.debug(`Generated tool name: ${toolName} for workflow: ${workflow.name}`); return toolName; } /** * Validate workflow parameters against the workflow's input schema * @param workflow Workflow definition with input schema * @param params Parameters to validate * @throws AppError if validation fails */ validateWorkflowParameters(workflow: WorkflowDefinition, params: any): void { const schema = workflow.inputSchema; // Validate required parameters if (schema.required && Array.isArray(schema.required)) { for (const requiredField of schema.required) { if (!(requiredField in params)) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Missing required parameter: ${requiredField}`, { workflowId: workflow.id, requiredField, providedParams: Object.keys(params) } ); } } } // Validate parameter types and constraints if (schema.properties) { for (const [key, value] of Object.entries(params)) { const propSchema = schema.properties[key]; if (propSchema) { this.validateParameterValue(key, value, propSchema, workflow.id); } else { // Log warning for unexpected parameters but don't fail this.logger.warn( `Unexpected parameter '${key}' for workflow ${workflow.id}, ignoring` ); } } } } /** * Build execution payload in the required format for Simplified API * @param params Raw parameters from MCP tool call * @returns Object with input and source fields */ buildExecutionPayload(params: any): { input: any; source: string } { return { input: params || {}, source: 'application' }; } /** * Execute a workflow tool with proper error handling and result formatting * @param workflow Workflow definition * @param params Tool parameters * @param apiClient API client instance * @returns Promise<any> Formatted execution result */ async executeWorkflowTool( workflow: WorkflowDefinition, params: any, apiClient: APIClient ): Promise<any> { this.logger.info(`Executing workflow tool: ${workflow.name} (${workflow.id})`); try { // Validate parameters this.validateWorkflowParameters(workflow, params); // Execute the workflow const result = await this.executionService.executeWorkflow(workflow.id, params); // Format and return result return this.formatExecutionResult(result, workflow); } catch (error) { this.errors++; // Track error // Use error handler to create MCP-formatted error response const mcpError = this.errorHandler.createWorkflowToolErrorResponse( error instanceof Error ? error : new Error(String(error)), workflow.name, params, { workflowId: workflow.id, operation: 'workflow_tool_execution' } ); // Throw an error that includes the MCP error data const enhancedError = new AppError( ErrorType.TOOL_ERROR, mcpError.message, mcpError.data ); throw enhancedError; } } /** * Format workflow execution result for MCP response * @param result Workflow execution result * @param workflow Workflow definition * @returns Formatted result object */ formatExecutionResult(result: WorkflowExecutionResult, workflow: WorkflowDefinition): any { if (result.success) { this.logger.info( `Workflow tool execution successful: ${workflow.name}, duration: ${result.executionDuration}ms` ); return { content: [ { type: 'text', text: JSON.stringify({ success: true, workflowId: workflow.id, workflowName: workflow.name, correlationId: result.correlationId, executionDuration: result.executionDuration, status: result.status, result: result.output || {}, metadata: { startTime: result.startTime, endTime: result.endTime, createTime: result.createTime, updateTime: result.updateTime } }, null, 2) } ] }; } else { this.logger.error( `Workflow tool execution failed: ${workflow.name}, error: ${result.error}` ); return { content: [ { type: 'text', text: JSON.stringify({ success: false, workflowId: workflow.id, workflowName: workflow.name, correlationId: result.correlationId, status: result.status, error: result.error, metadata: { startTime: result.startTime, endTime: result.endTime, createTime: result.createTime, updateTime: result.updateTime } }, null, 2) } ], isError: true }; } } /** * Clear generated tool names cache */ clearGeneratedNames(): void { this.generatedToolNames.clear(); this.cacheHits = 0; this.errors = 0; } /** * Get all generated tool names * @returns string[] Array of generated tool names */ getGeneratedToolNames(): string[] { return Array.from(this.generatedToolNames); } /** * Get generation statistics * @returns Object with generation statistics */ getGenerationStats(): { totalGenerated: number; cacheHits: number; errors: number } { return { totalGenerated: this.generatedToolNames.size, cacheHits: this.cacheHits, errors: this.errors }; } /** * Validate workflow definition structure * @param workflow Workflow definition to validate * @throws AppError if validation fails */ private validateWorkflowDefinition(workflow: WorkflowDefinition): void { if (!workflow.id || typeof workflow.id !== 'string') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow ID is required and must be a string' ); } if (!workflow.name || typeof workflow.name !== 'string') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow name is required and must be a string' ); } if (!workflow.description || typeof workflow.description !== 'string') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow description is required and must be a string' ); } if (!workflow.inputSchema || typeof workflow.inputSchema !== 'object') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow inputSchema is required and must be an object' ); } if (workflow.inputSchema.type !== 'object') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow inputSchema type must be "object"' ); } if (!workflow.inputSchema.properties || typeof workflow.inputSchema.properties !== 'object') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, 'Workflow inputSchema must have properties object' ); } } /** * Convert workflow input schema to tool input schema * @param inputSchema Workflow input schema * @returns Converted input schema */ private convertInputSchema(inputSchema: WorkflowDefinition['inputSchema']): ToolDefinition['inputSchema'] { // For now, we pass through the schema as-is since they have the same structure // In the future, we might need to transform certain schema elements const convertedSchema: ToolDefinition['inputSchema'] = { type: 'object', properties: { ...inputSchema.properties } }; // Handle optional required array - only set if it exists and has items if (inputSchema.required && inputSchema.required.length > 0) { convertedSchema.required = [...inputSchema.required]; } return convertedSchema; } /** * Enhance tool description with workflow metadata * @param workflow Workflow definition * @returns Enhanced description */ private enhanceToolDescription(workflow: WorkflowDefinition): string { let description = workflow.description; // Add workflow ID for reference description += ` (Workflow ID: ${workflow.id})`; // Add execution type if specified if (workflow.executionType) { description += ` [${workflow.executionType}]`; } return description; } /** * Sanitize tool name to be valid for MCP * @param name Raw tool name * @returns string Sanitized tool name */ private sanitizeToolName(name: string): string { // Replace invalid characters (including hyphens) with underscores let sanitized = name.replace(/[^a-zA-Z0-9_]/g, '_'); // Ensure it starts with a letter if (!/^[a-zA-Z]/.test(sanitized)) { sanitized = `workflow_${sanitized}`; } // Remove consecutive underscores sanitized = sanitized.replace(/_+/g, '_'); // Remove trailing underscores sanitized = sanitized.replace(/_$/, ''); // Ensure minimum length if (sanitized.length === 0) { sanitized = 'workflow_tool'; } return sanitized; } /** * Validate individual parameter value against its schema * @param paramName Parameter name * @param value Parameter value * @param schema Parameter schema * @param workflowId Workflow ID for error context */ private validateParameterValue(paramName: string, value: any, schema: any, workflowId: string): void { if (schema.type) { const actualType = this.getValueType(value); const expectedType = schema.type; if (expectedType === 'string' && actualType !== 'string') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be a string, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } if (expectedType === 'number' && actualType !== 'number') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be a number, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } if (expectedType === 'integer' && (!Number.isInteger(value) || actualType !== 'number')) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be an integer, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } if (expectedType === 'boolean' && actualType !== 'boolean') { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be a boolean, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } if (expectedType === 'array' && !Array.isArray(value)) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be an array, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } if (expectedType === 'object' && (actualType !== 'object' || value === null || Array.isArray(value))) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be an object, got ${actualType}`, { workflowId, paramName, expectedType, actualType } ); } } // Validate enum values if (schema.enum && Array.isArray(schema.enum)) { if (!schema.enum.includes(value)) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be one of: ${schema.enum.join(', ')}, got: ${value}`, { workflowId, paramName, allowedValues: schema.enum, actualValue: value } ); } } // Validate string constraints if (schema.type === 'string' && typeof value === 'string') { if (schema.minLength && value.length < schema.minLength) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be at least ${schema.minLength} characters long`, { workflowId, paramName, minLength: schema.minLength, actualLength: value.length } ); } if (schema.maxLength && value.length > schema.maxLength) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be at most ${schema.maxLength} characters long`, { workflowId, paramName, maxLength: schema.maxLength, actualLength: value.length } ); } if (schema.pattern && !new RegExp(schema.pattern).test(value)) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' does not match required pattern: ${schema.pattern}`, { workflowId, paramName, pattern: schema.pattern, actualValue: value } ); } } // Validate number constraints if ((schema.type === 'number' || schema.type === 'integer') && typeof value === 'number') { if (schema.minimum !== undefined && value < schema.minimum) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be at least ${schema.minimum}`, { workflowId, paramName, minimum: schema.minimum, actualValue: value } ); } if (schema.maximum !== undefined && value > schema.maximum) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must be at most ${schema.maximum}`, { workflowId, paramName, maximum: schema.maximum, actualValue: value } ); } } // Validate array constraints if (schema.type === 'array' && Array.isArray(value)) { if (schema.minItems !== undefined && value.length < schema.minItems) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must have at least ${schema.minItems} items`, { workflowId, paramName, minItems: schema.minItems, actualItems: value.length } ); } if (schema.maxItems !== undefined && value.length > schema.maxItems) { this.errors++; throw new AppError( ErrorType.VALIDATION_ERROR, `Parameter '${paramName}' must have at most ${schema.maxItems} items`, { workflowId, paramName, maxItems: schema.maxItems, actualItems: value.length } ); } // Validate array items if schema is provided if (schema.items) { for (let i = 0; i < value.length; i++) { this.validateParameterValue(`${paramName}[${i}]`, value[i], schema.items, workflowId); } } } } /** * Get the type of a value for validation * @param value Value to check * @returns string Type name */ private getValueType(value: any): string { if (value === null) return 'null'; if (Array.isArray(value)) return 'array'; return typeof value; } }

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/celeryhq/simplified-mcp-server'

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