Skip to main content
Glama
handleGetEnhancements.tsâ€ĸ29.4 kB
import { McpError, ErrorCode } from '../lib/utils'; import { makeAdtRequestWithTimeout, return_error, return_response, getBaseUrl, logger, encodeSapObjectName, fetchNodeStructure } from '../lib/utils'; import { writeResultToFile } from '../lib/writeResultToFile'; export const TOOL_DEFINITION = { "name": "GetEnhancements", "description": "Retrieve a list of enhancements for a given ABAP object.", "inputSchema": { "type": "object", "properties": { "object_name": { "type": "string", "description": "Name of the ABAP object" }, "object_type": { "type": "string", "description": "Type of the ABAP object" } }, "required": [ "object_name", "object_type" ] } } as const; /** * Interface for enhancement implementation data */ export interface EnhancementImplementation { name: string; type: string; sourceCode?: string; description?: string; } /** * Interface for parsed enhancement response */ export interface EnhancementResponse { object_name: string; object_type: 'program' | 'include' | 'class'; context?: string; enhancements: EnhancementImplementation[]; detailed?: boolean; total_enhancements?: number; } /** * Parses enhancement XML to extract enhancement implementations with their source code * @param xmlData - Raw XML response from ADT * @returns Array of enhancement implementations */ export function parseEnhancementsFromXml(xmlData: string): EnhancementImplementation[] { const enhancements: EnhancementImplementation[] = []; try { // Extract <enh:source> elements which contain the base64 encoded enhancement source code const sourceRegex = /<enh:source[^>]*>([^<]*)<\/enh:source>/g; let match; let index = 0; while ((match = sourceRegex.exec(xmlData)) !== null) { const enhancement: EnhancementImplementation = { name: `enhancement_${index + 1}`, // Default name if not found in attributes type: 'enhancement', }; // Try to find enhancement name and type from parent elements or attributes const sourceStart = match.index; // Look backwards for parent enhancement element with name/type attributes const beforeSource = xmlData.substring(0, sourceStart); // Try multiple patterns to find enhancement name // Pattern 1: adtcore:name attribute let enhNameMatch = beforeSource.match(/adtcore:name="([^"]*)"[^>]*$/); // Pattern 2: enh:name attribute if (!enhNameMatch) { enhNameMatch = beforeSource.match(/enh:name="([^"]*)"[^>]*$/); } // Pattern 3: name attribute if (!enhNameMatch) { enhNameMatch = beforeSource.match(/name="([^"]*)"[^>]*$/); } // Pattern 4: Look for enhancement implementation name in nearby elements if (!enhNameMatch) { // Look for enhancement implementation tag with name const enhImplMatch = beforeSource.match(/<[^>]*enhancement[^>]*name="([^"]*)"[^>]*>/i); if (enhImplMatch && enhImplMatch[1]) { enhNameMatch = [enhImplMatch[0], enhImplMatch[1]]; } } // Try multiple patterns to find enhancement type let enhTypeMatch = beforeSource.match(/adtcore:type="([^"]*)"[^>]*$/); if (!enhTypeMatch) { enhTypeMatch = beforeSource.match(/enh:type="([^"]*)"[^>]*$/); } if (!enhTypeMatch) { enhTypeMatch = beforeSource.match(/type="([^"]*)"[^>]*$/); } if (enhNameMatch && enhNameMatch[1]) { enhancement.name = enhNameMatch[1]; } if (enhTypeMatch && enhTypeMatch[1]) { enhancement.type = enhTypeMatch[1]; } // Extract and decode the base64 source code const base64Source = match[1]; if (base64Source) { try { // Decode base64 source code const decodedSource = Buffer.from(base64Source, 'base64').toString('utf-8'); enhancement.sourceCode = decodedSource; } catch (decodeError) { logger.warn(`Failed to decode source code for enhancement ${enhancement.name}:`, decodeError); enhancement.sourceCode = base64Source; // Keep original if decode fails } } enhancements.push(enhancement); index++; } logger.info(`Parsed ${enhancements.length} enhancement implementations`); return enhancements; } catch (parseError) { logger.error('Failed to parse enhancement XML:', parseError); return []; } } /** * Determines if an object is a program, include, or class and returns appropriate URL path * @param objectName - Name of the object * @param manualProgramContext - Optional manual program context for includes * @returns Object with type, basePath, and context (if needed) */ async function determineObjectTypeAndPath(objectName: string, manualProgramContext?: string): Promise<{type: 'program' | 'include' | 'class', basePath: string, context?: string}> { try { // First try as a class const classUrl = `${await getBaseUrl()}/sap/bc/adt/oo/classes/${encodeSapObjectName(objectName)}`; try { const response = await makeAdtRequestWithTimeout(classUrl, 'GET', 'csrf', { 'Accept': 'application/vnd.sap.adt.oo.classes.v4+xml' }); if (response.status === 200) { logger.info(`${objectName} is a class`); return { type: 'class', basePath: `/sap/bc/adt/oo/classes/${encodeSapObjectName(objectName)}/source/main/enhancements/elements` }; } } catch (classError) { // If class request fails, try as program logger.info(`${objectName} is not a class, trying as program...`); } // Try as a program const programUrl = `${await getBaseUrl()}/sap/bc/adt/programs/programs/${encodeSapObjectName(objectName)}`; try { const response = await makeAdtRequestWithTimeout(programUrl, 'GET', 'csrf', { 'Accept': 'application/vnd.sap.adt.programs.v3+xml' }); if (response.status === 200) { logger.info(`${objectName} is a program`); return { type: 'program', basePath: `/sap/bc/adt/programs/programs/${encodeSapObjectName(objectName)}/source/main/enhancements/elements` }; } } catch (programError) { // If program request fails, try as include logger.info(`${objectName} is not a program, trying as include...`); } // Try as include const includeUrl = `${await getBaseUrl()}/sap/bc/adt/programs/includes/${encodeSapObjectName(objectName)}`; const response = await makeAdtRequestWithTimeout(includeUrl, 'GET', 'csrf', { 'Accept': 'application/vnd.sap.adt.programs.includes.v2+xml' }); if (response.status === 200) { logger.info(`${objectName} is an include`); let context: string; // Use manual program context if provided if (manualProgramContext) { context = `/sap/bc/adt/programs/programs/${manualProgramContext}`; logger.info(`Using manual program context for include ${objectName}: ${context}`); } else { // Auto-determine context from metadata const xmlData = response.data; const contextMatch = xmlData.match(/include:contextRef[^>]+adtcore:uri="([^"]+)"/); if (contextMatch && contextMatch[1]) { context = contextMatch[1]; logger.info(`Found auto-determined context for include ${objectName}: ${context}`); } else { throw new McpError( ErrorCode.InvalidParams, `Could not determine parent program context for include: ${objectName}. No contextRef found in metadata. Consider providing the 'program' parameter manually.` ); } } return { type: 'include', basePath: `/sap/bc/adt/programs/includes/${encodeSapObjectName(objectName)}/source/main/enhancements/elements`, context: context }; } throw new McpError( ErrorCode.InvalidParams, `Could not determine object type for: ${objectName}. Object is neither a valid class, program, nor include.` ); } catch (error) { if (error instanceof McpError) { throw error; } logger.error(`Failed to determine object type for ${objectName}:`, error); throw new McpError( ErrorCode.InvalidParams, `Failed to determine object type for: ${objectName}. ${error instanceof Error ? error.message : String(error)}` ); } } /** * Parses XML response to extract includes information * @param xmlData XML response data * @returns Array of include objects with name and node_id */ function parseIncludesFromXml(xmlData: string): Array<{name: string, node_id: string, label: string}> { const includes: Array<{name: string, node_id: string, label: string}> = []; try { // Simple regex-based parsing for XML // Look for OBJECT_TYPE entries that contain "PROG/I" (includes) const objectTypeRegex = /<SEU_ADT_OBJECT_TYPE_INFO>(.*?)<\/SEU_ADT_OBJECT_TYPE_INFO>/gs; const matches = xmlData.match(objectTypeRegex); if (matches) { for (const match of matches) { // Check if this is an include type if (match.includes('<OBJECT_TYPE>PROG/I</OBJECT_TYPE>')) { const nodeIdMatch = match.match(/<NODE_ID>(\d+)<\/NODE_ID>/); const labelMatch = match.match(/<OBJECT_TYPE_LABEL>(.*?)<\/OBJECT_TYPE_LABEL>/); if (nodeIdMatch && labelMatch) { includes.push({ name: 'PROG/I', node_id: nodeIdMatch[1], label: labelMatch[1] }); } } } } } catch (error) { logger.warn('Error parsing XML for includes:', error); } return includes; } /** * Parses XML response to extract actual include names from node structure * @param xmlData XML response data * @returns Array of include names */ function parseIncludeNamesFromXml(xmlData: string): string[] { const includeNames: string[] = []; try { // Look for SEU_ADT_REPOSITORY_OBJ_NODE entries with OBJECT_TYPE PROG/I const nodeRegex = /<SEU_ADT_REPOSITORY_OBJ_NODE>(.*?)<\/SEU_ADT_REPOSITORY_OBJ_NODE>/gs; const nodeMatches = xmlData.match(nodeRegex); if (nodeMatches) { for (const nodeMatch of nodeMatches) { // Check if this node is for includes (PROG/I) if (nodeMatch.includes('<OBJECT_TYPE>PROG/I</OBJECT_TYPE>')) { // Extract the object name const nameMatch = nodeMatch.match(/<OBJECT_NAME>([^<]+)<\/OBJECT_NAME>/); if (nameMatch && nameMatch[1].trim()) { const includeName = nameMatch[1].trim(); // Decode URL-encoded names if needed const decodedName = decodeURIComponent(includeName); includeNames.push(decodedName); } } } } // If no nodes found, try alternative parsing for OBJECT_NAME tags if (includeNames.length === 0) { const objectNameRegex = /<OBJECT_NAME>([^<]+)<\/OBJECT_NAME>/g; let match; while ((match = objectNameRegex.exec(xmlData)) !== null) { const name = match[1].trim(); if (name && name.length > 0) { const decodedName = decodeURIComponent(name); includeNames.push(decodedName); } } } } catch (error) { logger.warn('Error parsing XML for include names:', error); } return [...new Set(includeNames)]; // Remove duplicates } /** * Internal function to get includes list using SAP ADT API * @param objectName - Name of the object * @param objectType - Type of the object ('program' | 'include' | 'class') * @returns Array of include names */ async function getIncludesListInternal(objectName: string, objectType: 'program' | 'include' | 'class'): Promise<string[]> { try { // Classes don't have includes in the traditional sense if (objectType === 'class') { logger.info(`Classes don't have includes. Returning empty list for class '${objectName}'`); return []; } // For includes, we need to determine the parent program let parentName = objectName; let parentTechName = objectName; let parentType = 'PROG/P'; if (objectType === 'include') { // For includes, we assume they belong to a program with similar name // This is a simplification - in real scenarios, you might need additional logic // to determine the actual parent program logger.warn(`Include processing: assuming parent program for include ${objectName}`); } // Step 1: Get root node structure to find includes node (with timeout) const rootResponse = await Promise.race([ fetchNodeStructure( parentName.toUpperCase(), parentTechName.toUpperCase(), parentType, '000000', // Root node true // with descriptions ), new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`Timeout: Failed to get root node structure for ${objectName} within 10000ms`)), 10000) ) ]); // Step 2: Parse response to find includes node ID const includesInfo = parseIncludesFromXml(rootResponse.data); const includesNode = includesInfo.find(info => info.name === 'PROG/I'); if (!includesNode) { logger.info(`No includes node found for ${objectType} '${objectName}'`); return []; } // Step 3: Get includes list using the found node ID (with timeout) const includesResponse = await Promise.race([ fetchNodeStructure( parentName.toUpperCase(), parentTechName.toUpperCase(), parentType, includesNode.node_id, true // with descriptions ), new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`Timeout: Failed to get includes list for ${objectName} within 10000ms`)), 10000) ) ]); // Step 4: Parse the includes response to extract include names const includeNames = parseIncludeNamesFromXml(includesResponse.data); logger.info(`Found ${includeNames.length} includes for ${objectType} '${objectName}' using SAP ADT API`); return includeNames; } catch (error) { logger.error(`Failed to get includes list for ${objectType} '${objectName}':`, error); return []; } } /** * Gets enhancements for a single object (program or include) * @param objectName - Name of the object * @param manualProgramContext - Optional manual program context for includes * @returns Enhancement response for the single object */ async function getEnhancementsForSingleObject(objectName: string, manualProgramContext?: string): Promise<EnhancementResponse> { logger.info(`Getting enhancements for single object: ${objectName}`, manualProgramContext ? `with manual program context: ${manualProgramContext}` : ''); // Determine object type and get appropriate path and context const objectInfo = await determineObjectTypeAndPath(objectName, manualProgramContext); // Build URL based on object type let url = `${await getBaseUrl()}${objectInfo.basePath}`; // Add context parameter only for includes if (objectInfo.type === 'include' && objectInfo.context) { url += `?context=${encodeURIComponent(objectInfo.context)}`; logger.info(`Using context for include: ${objectInfo.context}`); } logger.info(`Final enhancement URL: ${url}`); const response = await makeAdtRequestWithTimeout(url, 'GET', 'default'); if (response.status === 200 && response.data) { // Parse the XML to extract enhancement implementations const enhancements = parseEnhancementsFromXml(response.data); const enhancementResponse: EnhancementResponse = { object_name: objectName, object_type: objectInfo.type, context: objectInfo.context, enhancements: enhancements }; return enhancementResponse; } else { throw new McpError(ErrorCode.InternalError, `Failed to retrieve enhancements for ${objectName}. Status: ${response.status}`); } } /** * Filters enhancement response to show minimal information */ function filterMinimalEnhancements(response: any): any { if (response.objects) { // For nested results, filter each object const filteredObjects = response.objects .filter((obj: any) => obj.enhancements && obj.enhancements.length > 0) // Only objects with enhancements .map((obj: any) => ({ object_name: obj.object_name, object_type: obj.object_type, enhancements: obj.enhancements.map((enh: any) => ({ name: enh.name, type: enh.type, // Include source code only if it's short (< 500 chars) or first 200 chars sourceCode: enh.sourceCode ? (enh.sourceCode.length <= 500 ? enh.sourceCode : enh.sourceCode.substring(0, 200) + '...[truncated]') : undefined })) })); return { ...response, detailed: false, total_objects_with_enhancements: filteredObjects.length, total_objects_analyzed: response.total_objects_analyzed, filtered_out: response.total_objects_analyzed - filteredObjects.length, objects: filteredObjects }; } else { // For single object results const filteredEnhancements = response.enhancements ? response.enhancements.map((enh: any) => ({ name: enh.name, type: enh.type, sourceCode: enh.sourceCode ? (enh.sourceCode.length <= 500 ? enh.sourceCode : enh.sourceCode.substring(0, 200) + '...[truncated]') : undefined })) : []; return { object_name: response.object_name, object_type: response.object_type, context: response.context, detailed: false, total_enhancements: filteredEnhancements.length, enhancements: filteredEnhancements }; } } /** * Handler to retrieve enhancement implementations for ABAP programs/includes * Automatically determines if object is a program or include and handles accordingly * * @param args - Tool arguments containing: * - object_name: Name of the ABAP object * - program: Optional manual program context for includes * - include_nested: Optional boolean - if true, also searches enhancements in all nested includes * - detailed: Optional boolean - if false (default), returns minimal info; if true, returns full details including raw XML * - timeout: Optional timeout in milliseconds for each ADT request (default: 30000ms = 30s) * - max_includes: Optional maximum number of includes to process (default: 50) * @returns Response with parsed enhancement data or error */ export async function handleGetEnhancements(args: any) { try { logger.info('handleGetEnhancements called with args:', args); if (!args?.object_name) { throw new McpError(ErrorCode.InvalidParams, 'Object name is required'); } const objectName = args.object_name; const manualProgram = args.program; // Optional manual program context for includes const includeNested = args.include_nested === true; // Optional boolean for recursive include search const isDetailed = args.detailed === true; // Optional boolean for detailed output // Simple timeout logic: one timeout for all ADT requests const requestTimeout = args.timeout ? parseInt(args.timeout, 10) : 30000; // Timeout for each ADT request (default: 30s) const maxIncludes = args.max_includes ? parseInt(args.max_includes, 10) : 50; // Maximum number of includes to process logger.info(`Getting enhancements for object: ${objectName}`, { manualProgram: manualProgram || '(not provided)', includeNested: includeNested, requestTimeout: requestTimeout, maxIncludes: maxIncludes }); // Get enhancements for the main object const mainEnhancementResponse = await getEnhancementsForSingleObject(objectName, manualProgram); if (!includeNested) { // Return only main object enhancements let response = mainEnhancementResponse; // Apply filtering if not detailed if (!isDetailed) { response = filterMinimalEnhancements(response); } else { // Add detailed flag for consistency response = { ...response, detailed: true }; } const result = { isError: false, content: [ { type: "text", text: JSON.stringify(response) } ] }; if (args.filePath) { writeResultToFile(JSON.stringify(result, null, 2), args.filePath); } return result; } // If include_nested is true, also get enhancements from all nested includes logger.info('Searching for nested includes and their enhancements...'); // Simplified nested processing - no recursion, just flat list const processNestedIncludes = async () => { // Get flat list of all includes using the optimized GetIncludesList logic let includesList = await getIncludesListInternal(objectName, mainEnhancementResponse.object_type); logger.info(`Found ${includesList.length} includes (flat list, no recursion):`, includesList); // Limit the number of includes to process to avoid timeout if (includesList.length > maxIncludes) { logger.warn(`Too many includes found (${includesList.length}). Limiting to first ${maxIncludes} includes to avoid timeout.`); includesList = includesList.slice(0, maxIncludes); } // Collect all enhancement responses const allEnhancementResponses: EnhancementResponse[] = [mainEnhancementResponse]; // Simple sequential processing with individual timeouts for (const includeName of includesList) { try { logger.info(`Getting enhancements for include: ${includeName}`); // Create a promise with individual timeout for each include const includeEnhancementsPromise = getEnhancementsForSingleObject(includeName, manualProgram); const includeEnhancements = await createPromiseWithTimeout( includeEnhancementsPromise, requestTimeout, `Timeout: Failed to get enhancements for include ${includeName} within ${requestTimeout}ms` ); allEnhancementResponses.push(includeEnhancements); } catch (error) { logger.warn(`Failed to get enhancements for include ${includeName}:`, error); // Continue with other includes even if one fails } } return allEnhancementResponses; }; // Execute nested processing without total timeout - each request has its own timeout let allEnhancementResponses: EnhancementResponse[]; try { allEnhancementResponses = await processNestedIncludes(); } catch (error) { logger.error('Nested enhancement processing failed or timed out:', error); // Return partial results with just the main object const fallbackResult = { content: [ { type: "text", text: JSON.stringify({ main_object: { name: objectName, type: mainEnhancementResponse.object_type }, include_nested: true, error: error instanceof Error ? error.message : String(error), partial_result: true, total_objects_analyzed: 1, total_enhancements_found: mainEnhancementResponse.enhancements.length, objects: [mainEnhancementResponse] }, null, 2) } ] }; if (args.filePath) { writeResultToFile(fallbackResult, args.filePath); } return fallbackResult; } // Create combined response let combinedResponse: any = { main_object: { name: objectName, type: mainEnhancementResponse.object_type }, include_nested: true, total_objects_analyzed: allEnhancementResponses.length, total_enhancements_found: allEnhancementResponses.reduce((sum, resp) => sum + resp.enhancements.length, 0), objects: allEnhancementResponses }; // Apply filtering if not detailed if (!isDetailed) { combinedResponse = filterMinimalEnhancements(combinedResponse); } else { // Add detailed flag for consistency combinedResponse = { ...combinedResponse, detailed: true }; } const result = { isError: false, content: [ { type: "text", text: JSON.stringify(combinedResponse) } ] }; if (args.filePath) { writeResultToFile(JSON.stringify(result, null, 2), args.filePath); } return result; } catch (error) { // MCP-compliant error response: always return content[] with type "text" return { isError: true, content: [ { type: "text", text: `ADT error: ${String(error)}` } ] }; } } /** * Creates a promise with timeout that properly cleans up the timeout when the promise resolves * @param promise - The promise to wrap with timeout * @param timeoutMs - Timeout in milliseconds * @param errorMessage - Error message to use when timeout occurs * @returns Promise that resolves with the original promise result or rejects with timeout error */ function createPromiseWithTimeout<T>( promise: Promise<T>, timeoutMs: number, errorMessage: string ): Promise<T> { let timeoutId: NodeJS.Timeout; const timeoutPromise = new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(errorMessage)); }, timeoutMs); }); return Promise.race([ promise.then(result => { // Clear timeout when main promise resolves successfully clearTimeout(timeoutId); return result; }).catch(error => { // Clear timeout when main promise rejects clearTimeout(timeoutId); throw error; }), timeoutPromise ]); }

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/fr0ster/mcp-abap-adt'

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