Skip to main content
Glama
workflow-discovery-simple.tsβ€’9.3 kB
/** * Simplified Workflow Discovery Service * Handles communication with the Simplified API to discover available workflows */ import type { APIClient, Logger } from '../types/index.js'; import { AppError, ErrorType } from '../types/index.js'; /** * Simple workflow definition for MCP tools */ export interface SimpleWorkflowDefinition { id: string; name: string; description: string; category: string; version: string; inputSchema: { type: 'object'; properties: Record<string, any>; required: string[]; }; executionType: 'async'; metadata?: Record<string, any>; } /** * Simple workflow configuration */ export interface SimpleWorkflowConfig { enabled: boolean; filterPatterns?: string[]; } /** * Simple Workflow Discovery Service */ export class SimpleWorkflowDiscoveryService { private apiClient: APIClient; private logger: Logger; private config: SimpleWorkflowConfig; private cachedWorkflows: SimpleWorkflowDefinition[] = []; private lastDiscoveryTime: number = 0; private cacheValidityMs: number = 60000; // 1 minute cache constructor(apiClient: APIClient, logger: Logger, config: SimpleWorkflowConfig) { this.apiClient = apiClient; this.logger = logger; this.config = config; } /** * Discover available workflows from the Simplified API */ async listWorkflows(): Promise<SimpleWorkflowDefinition[]> { this.logger.debug('Starting workflow discovery...'); // Check cache first if (this.isCacheValid()) { this.logger.debug(`Returning ${this.cachedWorkflows.length} cached workflows`); return [...this.cachedWorkflows]; } try { // Call the Simplified workflows API const response = await this.apiClient.get('/api/v1/service/workflows/mcp'); if (!response.data || !response.data.results || !Array.isArray(response.data.results)) { this.logger.warn('Workflows API returned invalid response format'); return []; } this.logger.debug(`Workflows API returned ${response.data.results.length} workflows (total: ${response.data.count})`); // Transform API response to workflow definitions const workflows: SimpleWorkflowDefinition[] = response.data.results .map((apiWorkflow: any) => this.transformWorkflowFromAPI(apiWorkflow)) .filter((workflow: SimpleWorkflowDefinition | null) => workflow !== null) as SimpleWorkflowDefinition[]; // Apply filtering if configured const filteredWorkflows = this.applyFilters(workflows); // Update cache this.cachedWorkflows = filteredWorkflows; this.lastDiscoveryTime = Date.now(); this.logger.info(`Successfully discovered ${filteredWorkflows.length} workflows`); return [...filteredWorkflows]; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Workflow discovery failed: ${errorMessage}`); // Return cached workflows if available if (this.cachedWorkflows.length > 0) { this.logger.info(`Returning ${this.cachedWorkflows.length} cached workflows due to discovery failure`); return [...this.cachedWorkflows]; } return []; } } /** * Test connection to the workflows API */ async testConnection(): Promise<boolean> { try { this.logger.debug('Testing connection to workflows API...'); const response = await this.apiClient.get('/api/v1/service/workflows/mcp'); this.logger.debug('Workflows API connection test successful'); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Workflows API connection test failed: ${errorMessage}`); if (error instanceof Error && 'details' in error) { this.logger.error('Error details:', (error as any).details); } return false; } } /** * Transform workflow data from API format to workflow definition */ private transformWorkflowFromAPI(apiWorkflow: any): SimpleWorkflowDefinition | null { try { if (!apiWorkflow || typeof apiWorkflow !== 'object') { return null; } const workflowId = String(apiWorkflow.id || 'unknown'); const workflowTitle = apiWorkflow.title || `Workflow ${workflowId}`; const workflowName = this.sanitizeWorkflowName(workflowTitle); const description = apiWorkflow.description || `Workflow ${workflowId}`; // Extract input schema from the workflow inputs const inputSchema = this.extractInputSchema(apiWorkflow.inputs); return { id: workflowId, name: workflowName, description: description, category: 'workflow', version: '1.0.0', inputSchema: inputSchema, executionType: 'async', metadata: { originalId: apiWorkflow.id, originalTitle: apiWorkflow.title, originalInputs: apiWorkflow.inputs, source: 'simplified-api' } }; } catch (error) { this.logger.warn(`Failed to transform workflow: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Extract and validate input schema from workflow inputs */ private extractInputSchema(inputs: any): { type: 'object'; properties: Record<string, any>; required: string[] } { // Default schema if no inputs provided const defaultSchema = { type: 'object' as const, properties: { parameters: { type: 'object', description: 'Workflow parameters', additionalProperties: true } }, required: [] }; if (!inputs || typeof inputs !== 'object') { return defaultSchema; } try { // Validate the inputs structure matches JSON Schema format if (inputs.type === 'object' && inputs.properties && typeof inputs.properties === 'object') { return { type: 'object', properties: inputs.properties, required: Array.isArray(inputs.required) ? inputs.required : [] }; } // If inputs doesn't match expected format, log and use default this.logger.debug('Workflow inputs do not match expected JSON Schema format, using default schema'); return defaultSchema; } catch (error) { this.logger.warn(`Failed to parse workflow inputs: ${error instanceof Error ? error.message : String(error)}`); return defaultSchema; } } /** * Sanitize workflow name to be valid for MCP tool names */ private sanitizeWorkflowName(title: string): string { // Convert to lowercase and replace invalid characters let sanitized = title .toLowerCase() .trim() .replace(/[^a-z0-9\s_-]/g, '') // Remove invalid characters .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens // Ensure it starts with a letter if (!/^[a-z]/.test(sanitized)) { sanitized = `workflow-${sanitized}`; } // Ensure it's not empty and has reasonable length if (!sanitized || sanitized.length < 1) { sanitized = 'unnamed-workflow'; } if (sanitized.length > 50) { sanitized = sanitized.substring(0, 50).replace(/-+$/, ''); } return sanitized; } /** * Apply configured filters to workflows */ private applyFilters(workflows: SimpleWorkflowDefinition[]): SimpleWorkflowDefinition[] { if (!this.config.filterPatterns || this.config.filterPatterns.length === 0) { return workflows; } const filteredWorkflows = workflows.filter(workflow => { return this.config.filterPatterns!.some(pattern => { // Simple pattern matching with wildcards if (pattern.includes('*')) { const regexPattern = pattern.replace(/\*/g, '.*'); return new RegExp(regexPattern, 'i').test(workflow.name); } return workflow.name.toLowerCase().includes(pattern.toLowerCase()); }); }); this.logger.debug(`Applied filters: ${workflows.length} -> ${filteredWorkflows.length} workflows`); return filteredWorkflows; } /** * Check if cached workflows are still valid */ private isCacheValid(): boolean { if (this.cachedWorkflows.length === 0) { return false; } const cacheAge = Date.now() - this.lastDiscoveryTime; return cacheAge < this.cacheValidityMs; } /** * Clear the workflow cache */ public clearCache(): void { this.cachedWorkflows = []; this.lastDiscoveryTime = 0; this.logger.debug('Workflow cache cleared'); } /** * Get cache statistics */ public getCacheStats(): { cachedCount: number; lastDiscoveryTime: number; cacheAge: number; isValid: boolean; } { return { cachedCount: this.cachedWorkflows.length, lastDiscoveryTime: this.lastDiscoveryTime, cacheAge: Date.now() - this.lastDiscoveryTime, isValid: this.isCacheValid() }; } /** * Force refresh of workflows (bypass cache) */ public async refreshWorkflows(): Promise<SimpleWorkflowDefinition[]> { this.clearCache(); return this.listWorkflows(); } }

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