Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
destination-service.ts16.8 kB
import { getDestination, HttpDestination } from '@sap-cloud-sdk/connectivity'; import xsenv from '@sap/xsenv'; import { Logger } from '../utils/logger.js'; import { Config } from '../utils/config.js'; import { SecureErrorHandler } from '../utils/secure-error-handler.js'; import { JWTUtils } from '../utils/jwt-utils.js'; import { DestinationContext, DestinationType, getDestinationType, } from '../types/destination-types.js'; export class DestinationService { private config: Config; private vcapServices!: Record<string, unknown>; constructor( private logger: Logger, config?: Config ) { this.config = config || new Config(); } async initialize(): Promise<void> { try { // Load VCAP services xsenv.loadEnv(); this.vcapServices = xsenv.getServices({ destination: { label: 'destination' }, connectivity: { label: 'connectivity' }, xsuaa: { label: 'xsuaa' }, }); this.logger.info('Destination service initialized successfully'); } catch (error) { // Check if we're running in Cloud Foundry const isCloudFoundry = process.env.VCAP_SERVICES || process.env.CF_INSTANCE_INDEX; if (isCloudFoundry) { this.logger.error( '❌ Failed to initialize destination service in Cloud Foundry environment:', error ); throw error; // Critical error in CF - services must be available } else { this.logger.info( 'ℹ️ Running in local development mode - destination service fallback active' ); // Don't throw in local development - use fallback configuration this.vcapServices = { destination: null, connectivity: null, xsuaa: null, }; } } } /** * Get destination based on context (design-time or runtime) * This is the new context-aware method for dual destination architecture */ async getDestination(context: DestinationContext): Promise<HttpDestination> { const destinationType = context.type || getDestinationType(context.operation!); const destinationName = this.config.getDestination(destinationType); this.logger.debug( `Fetching ${destinationType} destination: ${destinationName} for operation: ${context.operation}` ); return await this.fetchDestinationByName(destinationName, context); } /** * Get design-time destination (for discovery and metadata operations) */ async getDesignTimeDestination(context?: Partial<DestinationContext>): Promise<HttpDestination> { return await this.getDestination({ type: 'design-time', operation: 'discovery', ...context, }); } /** * Get runtime destination (for CRUD operations) * @deprecated Use getRuntimeDestinationWithJWT for secure JWT handling */ async getRuntimeDestination(context?: Partial<DestinationContext>): Promise<HttpDestination> { return await this.getDestination({ type: 'runtime', operation: 'read', ...context, }); } /** * Get runtime destination with secure JWT handling (for CRUD operations) */ async getRuntimeDestinationWithJWT( jwt?: string, context?: Partial<DestinationContext> ): Promise<HttpDestination> { const destinationContext = { type: 'runtime' as const, operation: 'read' as const, ...context, }; return jwt ? await this.getDestinationWithJWT(destinationContext, jwt) : await this.getDestination(destinationContext); } /** * Get destination with JWT token passed directly (thread-safe) */ async getDestinationWithJWT(context: DestinationContext, jwt?: string): Promise<HttpDestination> { const destinationType = context.type || getDestinationType(context.operation!); const destinationName = this.config.getDestination(destinationType); return await this.fetchDestinationByNameWithJWT(destinationName, context, jwt); } /** * Fetch destination by name with JWT token passed directly (thread-safe) */ private async fetchDestinationByNameWithJWT( destinationName: string, context: DestinationContext, jwt?: string ): Promise<HttpDestination> { this.logger.debug( `Fetching destination: ${destinationName} for ${context.type} operation with explicit JWT` ); try { // Try environment variables first (for development/testing) const envDestination = await this.getFromEnvironment(destinationName); if (envDestination) { this.logger.info( `Successfully retrieved ${context.type} destination '${destinationName}' from environment variable.` ); return envDestination; } // For runtime destinations, log JWT availability if (context.type === 'runtime') { if (jwt) { this.logger.info( `JWT token provided for runtime destination '${destinationName}' - will use Principal Propagation if configured` ); } else { this.logger.info( `No JWT token provided for runtime destination '${destinationName}' - will use BasicAuth if configured` ); } } // Use JWT utility for consistent and secure token handling const destinationOptions = JWTUtils.createDestinationOptions(destinationName, jwt); // Log JWT validation info securely JWTUtils.logTokenInfo(jwt, `${context.type} destination '${destinationName}'`, this.logger); const destination = await getDestination(destinationOptions); if (!destination) { throw new Error( `Destination '${destinationName}' not found for ${context.type} operations` ); } // Log authentication type and actual usage for debugging if (destination.authentication) { const hasBasicCredentials = !!(destination.username && destination.password); const isPrincipalProp = destination.authentication === 'PrincipalPropagation'; const hasJWT = !!jwt; this.logger.info( `Destination '${destinationName}' uses authentication: ${destination.authentication}` ); if (isPrincipalProp && hasJWT) { this.logger.info(`✅ Runtime request will use Principal Propagation with user JWT token`); } else if (isPrincipalProp && !hasJWT && hasBasicCredentials) { this.logger.info( `⚠️ Principal Propagation configured but no JWT available - will fallback to BasicAuth` ); } else if (isPrincipalProp && !hasJWT && !hasBasicCredentials) { this.logger.warn( `❌ Principal Propagation configured but no JWT token or BasicAuth credentials available` ); } else if (hasBasicCredentials && !isPrincipalProp) { this.logger.info( `✅ Runtime request will use BasicAuthentication with destination credentials` ); } else { this.logger.info( `Authentication configured: ${destination.authentication}, hasJWT: ${hasJWT}, hasBasicAuth: ${hasBasicCredentials}` ); } } else { this.logger.warn(`Destination '${destinationName}' has no authentication information`); } this.logger.info(`Successfully retrieved ${context.type} destination: ${destinationName}`); return destination as HttpDestination; } catch (error) { this.logger.error(`Failed to get ${context.type} destination '${destinationName}':`, error); throw error; } } /** * Fetch destination by name with context information and JWT handling (legacy - uses environment variables) */ private async fetchDestinationByName( destinationName: string, context: DestinationContext ): Promise<HttpDestination> { try { // Try environment variables first (for development/testing) const envDestination = await this.getFromEnvironment(destinationName); if (envDestination) { this.logger.info( `Successfully retrieved ${context.type} destination '${destinationName}' from environment variable.` ); return envDestination; } // Get JWT token - this is critical for Principal Propagation const jwt = this.getJWT(context); // Use JWT utility for consistent token handling const destinationOptions = JWTUtils.createDestinationOptions(destinationName, jwt); // Log JWT info securely JWTUtils.logTokenInfo( jwt, `${context.type} destination '${destinationName}' (legacy)`, this.logger ); // Removed debug logging to reduce log spam const destination = await getDestination(destinationOptions); if (!destination) { throw new Error( `Destination '${destinationName}' not found for ${context.type} operations` ); } // Log authentication type and actual usage for debugging if (destination.authentication) { const hasBasicCredentials = !!(destination.username && destination.password); const isPrincipalProp = destination.authentication === 'PrincipalPropagation'; const hasJWT = !!jwt; this.logger.info( `Destination '${destinationName}' uses authentication: ${destination.authentication}` ); if (isPrincipalProp && hasJWT) { this.logger.info(`✅ Runtime request will use Principal Propagation with user JWT token`); } else if (isPrincipalProp && !hasJWT && hasBasicCredentials) { this.logger.info( `⚠️ Principal Propagation configured but no JWT available - will fallback to BasicAuth` ); } else if (isPrincipalProp && !hasJWT && !hasBasicCredentials) { this.logger.warn( `❌ Principal Propagation configured but no JWT token or BasicAuth credentials available` ); } else if (hasBasicCredentials && !isPrincipalProp) { this.logger.info( `✅ Runtime request will use BasicAuthentication with destination credentials` ); } else { this.logger.info( `Authentication configured: ${destination.authentication}, hasJWT: ${hasJWT}, hasBasicAuth: ${hasBasicCredentials}` ); } } else { this.logger.warn(`Destination '${destinationName}' has no authentication information`); } this.logger.info(`Successfully retrieved ${context.type} destination: ${destinationName}`); return destination as HttpDestination; } catch (error) { this.logger.error(`Failed to get ${context.type} destination '${destinationName}':`, error); throw error; } } /** * Get destination from environment variables */ private async getFromEnvironment(destinationName: string): Promise<HttpDestination | null> { try { const envDestinations = process.env.destinations; if (!envDestinations) { return null; } const destinations = JSON.parse(envDestinations); const envDest = destinations.find((d: Record<string, unknown>) => d.name === destinationName); if (envDest) { return { url: envDest.url, username: envDest.username, password: envDest.password, authentication: 'BasicAuthentication', } as HttpDestination; } } catch (envError) { this.logger.debug( `Failed to load destination '${destinationName}' from environment:`, envError ); } return null; } /** * Test if a destination is available and accessible * @deprecated Use testDestinationWithJWT for secure JWT handling */ async testDestination( destinationType: DestinationType ): Promise<{ available: boolean; error?: string }> { return this.testDestinationWithJWT(destinationType); } /** * Test if a destination is available and accessible with secure JWT handling */ async testDestinationWithJWT( destinationType: DestinationType, jwt?: string ): Promise<{ available: boolean; error?: string }> { try { const destinationName = this.config.getDestination(destinationType); // Use proper context with operation for runtime destinations const context = { type: destinationType, operation: destinationType === 'runtime' ? ('read' as const) : ('discovery' as const), }; // Use secure JWT passing instead of environment variables const destination = jwt ? await this.fetchDestinationByNameWithJWT(destinationName, context, jwt) : await this.fetchDestinationByName(destinationName, context); // Basic connectivity test - just verify we can get the destination if (destination && destination.url) { return { available: true }; } else { return { available: false, error: 'Destination configuration incomplete' }; } } catch (error) { return { available: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Check if a destination exists (without throwing errors) */ async exists(destinationName: string): Promise<boolean> { try { const envDest = await this.getFromEnvironment(destinationName); if (envDest) { return true; } const jwt = this.getJWT(); const destinationOptions = JWTUtils.createDestinationOptions(destinationName, jwt); const destination = await getDestination(destinationOptions); return destination !== null; } catch { return false; } } /** * Legacy method for backward compatibility * @deprecated Use getDesignTimeDestination() or getRuntimeDestination() instead */ async getSAPDestination(): Promise<HttpDestination> { // Use the same logic as getsapDestination, but update naming const destinationName = this.config.get('sap.destinationName', 'SAP_SYSTEM'); this.logger.debug(`Fetching destination: ${destinationName}`); try { const envDestinations = process.env.destinations; if (envDestinations) { const destinations = JSON.parse(envDestinations); const envDest = destinations.find( (d: Record<string, unknown>) => d.name === destinationName ); if (envDest) { this.logger.info( `Successfully retrieved destination '${destinationName}' from environment variable.` ); return { url: envDest.url, username: envDest.username, password: envDest.password, authentication: 'BasicAuthentication', } as HttpDestination; } } } catch (envError) { this.logger.debug('Failed to load from environment destinations:', envError); } try { // Fallback to SAP Cloud SDK getDestination const jwt = this.getJWT(); const destinationOptions = JWTUtils.createDestinationOptions(destinationName, jwt); const destination = await getDestination(destinationOptions); if (!destination) { throw new Error( `Destination '${destinationName}' not found in environment variables or BTP destination service` ); } this.logger.info(`Successfully retrieved destination: ${destinationName}`); return destination as HttpDestination; } catch (error) { this.logger.error('Failed to get SAP destination:', error); throw error; } } /** * @deprecated This method is deprecated and will be removed. Use getDestinationWithJWT() instead. * JWT tokens should be passed explicitly to methods rather than relying on global environment variables. */ private getJWT(context?: DestinationContext): string | undefined { // Note: This method provides secure fallback JWT handling for internal operations // Design-time operations use technical user JWT, runtime operations return undefined // For design-time operations, technical user JWT is acceptable for service discovery if (context?.type === 'design-time') { const jwt = process.env.USER_JWT || process.env.TECHNICAL_USER_JWT; if (jwt) { this.logger.debug('Using technical user JWT for design-time operations'); } return jwt; } // For runtime operations, no fallback to global JWTs - explicit JWT passing required this.logger.debug('Runtime operations require explicit JWT token passing for security'); return undefined; } getDestinationCredentials() { return (this.vcapServices?.destination as { credentials?: unknown })?.credentials; } getConnectivityCredentials() { return (this.vcapServices?.connectivity as { credentials?: unknown })?.credentials; } getXSUAACredentials() { return (this.vcapServices?.xsuaa as { credentials?: unknown })?.credentials; } }

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/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

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