Skip to main content
Glama
capability-scan-workflow.ts28.1 kB
/** * Core Capability Scan Workflow * * Handles the step-by-step capability scanning workflow including resource selection, * specification, processing mode selection, and the actual scanning process */ import { Logger } from './error-handling'; import { CapabilityVectorService } from './capability-vector-service'; import { KubernetesDiscovery } from './discovery'; import { CapabilityInferenceEngine } from './capabilities'; import { createAIProvider } from './ai-provider-factory'; // Types for shared utility functions (dependency injection) export type TransitionCapabilitySessionFn = (session: CapabilityScanSession, nextStep: CapabilityScanSession['currentStep'], updates: Partial<CapabilityScanSession>, args: any) => void; export type CleanupCapabilitySessionFn = (session: CapabilityScanSession, args: any, logger: Logger, requestId: string) => void; export type ParseNumericResponseFn = (response: string, validOptions: string[]) => string; export type CreateCapabilityScanCompletionResponseFn = (sessionId: string, totalProcessed: number, successful: number, failed: number, processingTime: string, mode: 'auto' | 'manual', stopped?: boolean) => any; // Progress tracking interface interface ProgressData { status: 'processing' | 'completed'; current: number; total: number; percentage: number; currentResource: string; startedAt: string; lastUpdated: string; completedAt?: string; estimatedTimeRemaining?: string; totalProcessingTime?: string; successfulResources: number; failedResources: number; errors: any[]; } // Session interface (should be imported from shared types, but defining here for now) interface CapabilityScanSession { sessionId: string; currentStep: 'resource-selection' | 'resource-specification' | 'scanning' | 'complete'; selectedResources?: string[] | 'all'; resourceList?: string; currentResourceIndex?: number; progress?: any; // Progress tracking for long-running operations startedAt: string; lastActivity: string; resourceMetadata?: Record<string, { apiVersion: string; version: string; group: string }>; // Store apiVersion info } /** * Create user-friendly error message for resource definition failures */ function createResourceDefinitionErrorMessage(resourceName: string, error: unknown): string { const errorMessage = error instanceof Error ? error.message : String(error); let userFriendlyMessage = `Cannot analyze resource '${resourceName}': `; if (errorMessage.includes('not found') || errorMessage.includes('NotFound')) { userFriendlyMessage += `Resource does not exist in cluster. Please ensure the CRD is installed.`; } else if (errorMessage.includes('connection refused') || errorMessage.includes('timeout')) { userFriendlyMessage += `Cannot connect to Kubernetes cluster. Please check cluster connectivity.`; } else if (errorMessage.includes('forbidden') || errorMessage.includes('Forbidden')) { userFriendlyMessage += `Insufficient permissions to read resource definitions. Please check RBAC settings.`; } else { userFriendlyMessage += `${errorMessage}`; } return userFriendlyMessage; } /** * Handle resource selection step */ export async function handleResourceSelection( session: CapabilityScanSession, args: any, logger: Logger, requestId: string, capabilityService: CapabilityVectorService, parseNumericResponse: ParseNumericResponseFn, transitionCapabilitySession: TransitionCapabilitySessionFn, cleanupCapabilitySession: CleanupCapabilitySessionFn, createCapabilityScanCompletionResponse: CreateCapabilityScanCompletionResponseFn, handleScanningFn: (session: CapabilityScanSession, args: any, logger: Logger, requestId: string, capabilityService: CapabilityVectorService, parseNumericResponse: ParseNumericResponseFn, transitionCapabilitySession: TransitionCapabilitySessionFn, cleanupCapabilitySession: CleanupCapabilitySessionFn, createCapabilityScanCompletionResponse: CreateCapabilityScanCompletionResponseFn) => Promise<any> ): Promise<any> { if (!args.response) { // Show initial resource selection prompt return { success: true, operation: 'scan', dataType: 'capabilities', // CRITICAL: Put required parameters at top level for maximum visibility REQUIRED_NEXT_CALL: { tool: 'dot-ai:manageOrgData', parameters: { dataType: 'capabilities', operation: 'scan', sessionId: session.sessionId, step: 'resource-selection', // MANDATORY PARAMETER response: 'user_choice_here' // Replace with actual user choice }, note: 'The step parameter is MANDATORY when sessionId is provided' }, workflow: { step: 'resource-selection', question: 'Scan all cluster resources or specify subset?', options: [ { number: 1, value: 'all', display: '1. all - Scan all available cluster resources' }, { number: 2, value: 'specific', display: '2. specific - Specify particular resource types to scan' } ], sessionId: session.sessionId, instruction: 'IMPORTANT: You MUST ask the user to make a choice. Do NOT automatically select an option.', userPrompt: 'Would you like to scan all cluster resources or specify a subset?', clientInstructions: { behavior: 'interactive', requirement: 'Ask user to choose between options', prohibit: 'Do not auto-select options', nextStep: `Call with step='resource-selection', sessionId='${session.sessionId}', and response parameter containing the semantic value (all or specific)`, responseFormat: 'Convert user input to semantic values: 1→all, 2→specific, or pass through semantic words directly', requiredParameters: { step: 'resource-selection', sessionId: session.sessionId, response: 'user choice (all or specific)' } } } }; } // Process user response const normalizedResponse = parseNumericResponse(args.response, ['all', 'specific']); if (normalizedResponse === 'all') { // Transition directly to scanning (auto mode only - manual mode removed) transitionCapabilitySession(session, 'scanning', { selectedResources: 'all', currentResourceIndex: 0 // Start with first resource }, args); // Begin actual capability scanning and return completion summary return await handleScanningFn(session, { ...args, response: undefined }, logger, requestId, capabilityService, parseNumericResponse, transitionCapabilitySession, cleanupCapabilitySession, createCapabilityScanCompletionResponse); } if (normalizedResponse === 'specific') { // Transition to resource specification transitionCapabilitySession(session, 'resource-specification', {}, args); return { success: true, operation: 'scan', dataType: 'capabilities', // CRITICAL: Put required parameters at top level for maximum visibility REQUIRED_NEXT_CALL: { tool: 'dot-ai:manageOrgData', parameters: { dataType: 'capabilities', operation: 'scan', sessionId: session.sessionId, step: 'resource-specification', // MANDATORY PARAMETER resourceList: 'user_resource_list_here' // Replace with actual resource list }, note: 'The step parameter is MANDATORY when sessionId is provided' }, workflow: { step: 'resource-specification', question: 'Which resources would you like to scan?', sessionId: session.sessionId, instruction: 'IMPORTANT: You MUST ask the user to specify which resources to scan. Do NOT provide a default list.', userPrompt: 'Please specify which resources you want to scan.', resourceFormat: { description: 'Specify resources using Kubernetes naming convention', format: 'Kind.group for CRDs, Kind for core resources', examples: { crds: ['SQL.devopstoolkit.live', 'Server.dbforpostgresql.azure.upbound.io'], core: ['Pod', 'Service', 'ConfigMap'], apps: ['Deployment.apps', 'StatefulSet.apps'] }, input: 'Comma-separated list (e.g.: SQL.devopstoolkit.live, Deployment.apps, Pod)' }, clientInstructions: { behavior: 'interactive', requirement: 'Ask user to provide specific resource list', prohibit: 'Do not suggest or auto-select resources', nextStep: `Call with step='resource-specification', sessionId='${session.sessionId}', and resourceList parameter`, requiredParameters: { step: 'resource-specification', sessionId: session.sessionId, resourceList: 'comma-separated list of resources' } } } }; } return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'Invalid resource selection response', details: `Expected 'all' or 'specific', got: ${args.response}`, currentStep: session.currentStep } }; } /** * Handle resource specification step */ export async function handleResourceSpecification( session: CapabilityScanSession, args: any, logger: Logger, requestId: string, capabilityService: CapabilityVectorService, parseNumericResponse: ParseNumericResponseFn, transitionCapabilitySession: TransitionCapabilitySessionFn, cleanupCapabilitySession: CleanupCapabilitySessionFn, createCapabilityScanCompletionResponse: CreateCapabilityScanCompletionResponseFn, handleScanningFn: (session: CapabilityScanSession, args: any, logger: Logger, requestId: string, capabilityService: CapabilityVectorService, parseNumericResponse: ParseNumericResponseFn, transitionCapabilitySession: TransitionCapabilitySessionFn, cleanupCapabilitySession: CleanupCapabilitySessionFn, createCapabilityScanCompletionResponse: CreateCapabilityScanCompletionResponseFn) => Promise<any> ): Promise<any> { if (!args.resourceList) { return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'Missing resource list', details: 'Expected resourceList parameter with comma-separated resource names', currentStep: session.currentStep, expectedCall: `Call with step='resource-specification' and resourceList parameter` } }; } // Parse and validate resource list const resources = args.resourceList.split(',').map((r: string) => r.trim()).filter((r: string) => r.length > 0); if (resources.length === 0) { return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'Empty resource list', details: 'Resource list cannot be empty', currentStep: session.currentStep } }; } // Transition directly to scanning (auto mode only - manual mode removed) transitionCapabilitySession(session, 'scanning', { selectedResources: resources, resourceList: args.resourceList, currentResourceIndex: 0 // Start with first resource }, args); // Begin actual capability scanning and return completion summary return await handleScanningFn(session, { ...args, response: undefined }, logger, requestId, capabilityService, parseNumericResponse, transitionCapabilitySession, cleanupCapabilitySession, createCapabilityScanCompletionResponse); } /** * Handle scanning step (actual capability analysis - auto mode only) */ export async function handleScanning( session: CapabilityScanSession, args: any, logger: Logger, requestId: string, capabilityService: CapabilityVectorService, parseNumericResponse: ParseNumericResponseFn, transitionCapabilitySession: TransitionCapabilitySessionFn, cleanupCapabilitySession: CleanupCapabilitySessionFn, createCapabilityScanCompletionResponse: CreateCapabilityScanCompletionResponseFn ): Promise<any> { try { // Validate and initialize AI provider let aiProvider; try { aiProvider = createAIProvider(); if (!aiProvider.isInitialized()) { return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'AI provider API key required for capability inference', details: 'Configure AI provider credentials to enable AI-powered capability analysis' } }; } } catch (error) { return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'AI provider initialization failed', details: error instanceof Error ? error.message : 'Unknown error' } }; } // Initialize capability engine const engine = new CapabilityInferenceEngine(aiProvider, logger); if (session.selectedResources === 'all') { // For 'all' mode, discover actual cluster resources first try { logger.info('Discovering all cluster resources for capability scanning', { requestId, sessionId: session.sessionId }); // Import discovery engine const discovery = new KubernetesDiscovery(); await discovery.connect(); // Discover all available resources const resourceMap = await discovery.discoverResources(); const allResources = [...resourceMap.resources, ...resourceMap.custom]; // Extract resource names AND preserve metadata for capability analysis const discoveredResourceNames: string[] = []; const resourceMetadata: Record<string, { apiVersion: string; version: string; group: string }> = {}; for (const resource of allResources) { let resourceName = 'unknown-resource'; // For CRDs (custom resources), prioritize full name format if (resource.name && resource.name.includes('.')) { resourceName = resource.name; } // For standard resources, use kind else if (resource.kind) { resourceName = resource.kind; } // Fallback to name if no kind else if (resource.name) { resourceName = resource.name; } if (resourceName !== 'unknown-resource') { discoveredResourceNames.push(resourceName); // Store apiVersion metadata for later use // Handle both EnhancedResource (has apiVersion) and EnhancedCRD (has group+version) let apiVersion = ''; let version = ''; let group = ''; if ('apiVersion' in resource) { // EnhancedResource type apiVersion = resource.apiVersion || ''; version = apiVersion.includes('/') ? apiVersion.split('/')[1] : apiVersion; group = resource.group || ''; } else { // EnhancedCRD type - construct apiVersion from group and version group = resource.group || ''; version = resource.version || ''; apiVersion = group ? `${group}/${version}` : version; } resourceMetadata[resourceName] = { apiVersion, version, group }; } } logger.info('Discovered cluster resources for capability scanning', { requestId, sessionId: session.sessionId, totalDiscovered: discoveredResourceNames.length, sampleResources: discoveredResourceNames.slice(0, 5), metadataPreserved: Object.keys(resourceMetadata).length }); if (discoveredResourceNames.length === 0) { return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'No resources discovered in cluster', details: 'Cluster resource discovery returned empty results. Check cluster connectivity and permissions.', sessionId: session.sessionId } }; } // Update session with discovered resources AND metadata transitionCapabilitySession(session, 'scanning', { selectedResources: discoveredResourceNames, resourceMetadata: resourceMetadata, currentResourceIndex: 0 }, args); // Fall through to batch processing with discovered resources } catch (error) { logger.error('Failed to discover cluster resources', error as Error, { requestId, sessionId: session.sessionId }); return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'Cluster resource discovery failed', details: error instanceof Error ? error.message : String(error), sessionId: session.sessionId, suggestedActions: [ 'Check cluster connectivity', 'Verify kubectl access permissions', 'Try specifying specific resources instead of "all"' ] } }; } } // Auto mode: Process ALL resources in batch without user interaction // At this point, selectedResources should always be an array (either discovered or specified) if (!Array.isArray(session.selectedResources)) { throw new Error(`Invalid selectedResources state: expected array, got ${typeof session.selectedResources}. This indicates a bug in resource discovery.`); } const resources = session.selectedResources; const totalResources = resources.length; const processedResults: any[] = []; const errors: any[] = []; logger.info('Starting auto batch processing', { requestId, sessionId: session.sessionId, totalResources, resources: resources }); // Initialize progress tracking const startTime = Date.now(); const updateProgress = (current: number, currentResource: string, successful: number, failed: number, recentErrors: any[]) => { const elapsed = Date.now() - startTime; const percentage = Math.round((current / totalResources) * 100); // Calculate estimated time remaining let estimatedTimeRemaining: string | undefined; if (current > 0) { const avgTimePerResource = elapsed / current; const remainingResources = totalResources - current; const estimatedRemainingMs = remainingResources * avgTimePerResource; const estimatedMinutes = Math.round(estimatedRemainingMs / 60000 * 10) / 10; estimatedTimeRemaining = estimatedMinutes > 1 ? `${estimatedMinutes} minutes` : `${Math.round(estimatedRemainingMs / 1000)} seconds`; } const progressData: ProgressData = { status: 'processing', current: current, total: totalResources, percentage: percentage, currentResource: currentResource, startedAt: new Date(startTime).toISOString(), lastUpdated: new Date().toISOString(), estimatedTimeRemaining, successfulResources: successful, failedResources: failed, errors: recentErrors.slice(-5) // Keep last 5 errors for troubleshooting }; // Update session file with progress transitionCapabilitySession(session, 'scanning', { selectedResources: session.selectedResources, currentResourceIndex: current - 1, progress: progressData }, args); }; // Setup kubectl access for getting complete resource definitions let discovery: any; try { discovery = new KubernetesDiscovery(); await discovery.connect(); logger.info('Connected to Kubernetes for batch resource definition retrieval', { requestId, sessionId: session.sessionId }); } catch (error) { logger.warn('Could not connect to Kubernetes for batch processing, falling back to name-based inference', { requestId, sessionId: session.sessionId, error: error instanceof Error ? error.message : String(error) }); } // Process each resource in the batch with progress tracking for (let i = 0; i < resources.length; i++) { const currentResource = resources[i]; // Get complete resource definition for this resource let currentResourceDefinition: string | undefined; if (discovery) { try { // Try kubectl explain with full name first (works for CRDs and core resources) try { currentResourceDefinition = await discovery.explainResource(currentResource); } catch (explainError) { // If explain fails and resource has a dot (like Deployment.apps), try with just the Kind if (currentResource.includes('.')) { const resourceKind = currentResource.split('.')[0]; logger.info(`kubectl explain failed for ${currentResource}, attempting with Kind only: ${resourceKind}`, { requestId, sessionId: session.sessionId, resource: currentResource, resourceKind }); currentResourceDefinition = await discovery.explainResource(resourceKind); } else { // Re-throw explain error for resources without dots throw explainError; } } } catch (error) { logger.error(`Failed to get resource definition for ${currentResource}`, error as Error, { requestId, sessionId: session.sessionId, resource: currentResource }); // Add to errors array and skip processing this resource errors.push({ resource: currentResource, error: createResourceDefinitionErrorMessage(currentResource, error), timestamp: new Date().toISOString() }); // Skip processing this resource continue; } } // Update progress before processing updateProgress(i + 1, currentResource, processedResults.length, errors.length, errors); try { logger.info(`Processing resource ${i + 1}/${totalResources}`, { requestId, sessionId: session.sessionId, resource: currentResource, percentage: Math.round(((i + 1) / totalResources) * 100) }); // Get metadata for this resource - first try session metadata, then parse from kubectl explain let metadata = session.resourceMetadata?.[currentResource]; // If no session metadata and we have resource definition, parse from kubectl explain output if (!metadata && currentResourceDefinition) { const lines = currentResourceDefinition.split('\n'); const groupLine = lines.find((line: string) => line.startsWith('GROUP:')); const versionLine = lines.find((line: string) => line.startsWith('VERSION:')); // Extract metadata if version is found (group is optional for core resources) if (versionLine) { const group = groupLine ? groupLine.replace('GROUP:', '').trim() : ''; const version = versionLine.replace('VERSION:', '').trim(); const apiVersion = group ? `${group}/${version}` : version; metadata = { apiVersion, version, group }; } } const capability = await engine.inferCapabilities( currentResource, currentResourceDefinition, args.interaction_id, metadata?.apiVersion, metadata?.version, metadata?.group ); const capabilityId = CapabilityInferenceEngine.generateCapabilityId(currentResource); // Store capability in Vector DB await capabilityService.storeCapability(capability); processedResults.push({ resource: currentResource, id: capabilityId, capabilities: capability.capabilities, providers: capability.providers, complexity: capability.complexity, confidence: capability.confidence }); logger.info(`Successfully processed resource ${i + 1}/${totalResources}`, { requestId, sessionId: session.sessionId, resource: currentResource, capabilitiesFound: capability.capabilities.length, percentage: Math.round(((i + 1) / totalResources) * 100) }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const errorDetail = { resource: currentResource, error: errorMessage, index: i + 1, timestamp: new Date().toISOString() }; logger.error(`Failed to process resource ${i + 1}/${totalResources}`, error as Error, { requestId, sessionId: session.sessionId, resource: currentResource, percentage: Math.round(((i + 1) / totalResources) * 100) }); errors.push(errorDetail); } } // Final progress update - mark as completed const finalElapsed = Date.now() - startTime; const finalMinutes = Math.round(finalElapsed / 60000 * 10) / 10; const successful = processedResults.length; const failed = errors.length; const completionData: ProgressData = { status: 'completed', current: totalResources, total: totalResources, percentage: 100, currentResource: 'Processing complete', startedAt: new Date(startTime).toISOString(), lastUpdated: new Date().toISOString(), completedAt: new Date().toISOString(), totalProcessingTime: finalMinutes > 1 ? `${finalMinutes} minutes` : `${Math.round(finalElapsed / 1000)} seconds`, successfulResources: successful, failedResources: failed, errors: errors.slice(-5) }; // Update session with completion status transitionCapabilitySession(session, 'complete', { progress: completionData }, args); logger.info('Auto batch processing completed', { requestId, sessionId: session.sessionId, processed: totalResources, successful, failed, processingTime: completionData.totalProcessingTime }); // Clean up session file after a brief delay to allow progress viewing setTimeout(() => { cleanupCapabilitySession(session, args, logger, requestId); }, 30000); // Keep for 30 seconds after completion return createCapabilityScanCompletionResponse( session.sessionId, totalResources, successful, failed, completionData.totalProcessingTime || 'completed', 'auto' ); } catch (error) { logger.error('Capability scanning failed', error as Error, { requestId, sessionId: session.sessionId, resource: (error && typeof error === 'object' && 'resourceName' in error) ? (error as any).resourceName : 'unknown', step: session.currentStep }); // Clean up session on error cleanupCapabilitySession(session, args, logger, requestId); return { success: false, operation: 'scan', dataType: 'capabilities', error: { message: 'Capability scanning failed', details: error instanceof Error ? error.message : String(error) } }; } }

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/vfarcic/dot-ai'

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