UNPKG

finops-mcp-server

Version:

MCP server for FinOps Center cost optimization integration

1 lines 14 kB
export class HTTPClient{constructor(e,t,r){this.config=e,this.authManager=t,this.logger=r}async request(e){const t=Date.now();let r;for(let o=0;o<=(this.config.retryAttempts||3);o++)try{this.logger?.debug("Making HTTP request",{operation:"http_request",method:e.method,url:e.url,attempt:o+1,maxAttempts:(this.config.retryAttempts||3)+1});const r=await this.makeRequest(e),a=Date.now()-t;return this.logger?.info("HTTP request successful",{operation:"http_request_success",method:e.method,url:e.url,status:r.status,duration:a,attempt:o+1}),r}catch(a){r=a;const n=Date.now()-t;this.logger?.warn("HTTP request failed",{operation:"http_request_error",method:e.method,url:e.url,error:r.message,duration:n,attempt:o+1,maxAttempts:(this.config.retryAttempts||3)+1});const i=this.categorizeError(a);if(this.logger?.debug("Error categorized",{operation:"error_categorization",category:i.category,isRetryable:i.isRetryable,requiresBackoff:i.requiresBackoff,attempt:o+1}),"auth"===i.category)try{await this.authManager.handleAuthError(a)}catch(e){throw this.logger?.error("Authentication error handling failed",e,{operation:"auth_error_handling_failed",originalError:r.message}),e}if(!i.isRetryable)throw this.logger?.info("Error is not retryable, failing immediately",{operation:"non_retryable_error",category:i.category,errorCode:a.code,statusCode:a.statusCode}),a;if(o===(this.config.retryAttempts||3)){this.logger?.warn("Maximum retry attempts reached",{operation:"max_retries_reached",maxAttempts:(this.config.retryAttempts||3)+1,category:i.category});break}const s=this.calculateRetryDelay(a,o);s>0?(this.logger?.info("Retrying request after delay",{operation:"retry_with_backoff",delay:s,attempt:o+1,category:i.category,requiresBackoff:i.requiresBackoff}),await this.sleep(s)):this.logger?.debug("Retrying request immediately",{operation:"retry_immediate",attempt:o+1,category:i.category})}const o=Date.now()-t;throw this.logger?.error("HTTP request failed after all retries",r,{operation:"http_request_failed",method:e.method,url:e.url,totalDuration:o,attempts:(this.config.retryAttempts||3)+1}),r}async makeRequest(e){const{url:t,method:r,data:o,headers:a={},timeout:n}=e,i={method:r,headers:{...this.authManager.getAuthHeaders(),...a}};o&&(i.body=JSON.stringify(o)),n&&(i.signal=AbortSignal.timeout(n));try{const r=await fetch(t,i);let o;const a=r.headers.get("content-type");if(o=a&&a.includes("application/json")?await r.json():await r.text(),!r.ok)throw this.createHTTPError(r,o);const n={};return r.headers.forEach((e,t)=>{n[t]=e}),{data:o,status:r.status,statusText:r.statusText,headers:n,config:e}}catch(t){if(t instanceof TypeError&&t.message.includes("fetch")){const r=new Error(`Network error: ${t.message}`);throw r.code="NETWORK_ERROR",r.config=e,r}if("AbortError"===t.name){const t=new Error(`Request timeout after ${n}ms`);throw t.code="TIMEOUT_ERROR",t.config=e,t}throw t}}createHTTPError(e,t){let r=`HTTP ${e.status}: ${e.statusText}`;t&&"object"==typeof t&&(t.error&&t.error.message?r=t.error.message:t.errors&&Array.isArray(t.errors)&&t.errors.length>0?r=t.errors[0].message||r:t.message&&(r=t.message));const o=new Error(r);return o.code=this.getErrorCode(e.status),o.statusCode=e.status,429===e.status&&(o.rateLimitInfo=this.extractRateLimitInfo(e)),o}extractRateLimitInfo(e){const t=e.headers.get("retry-after"),r=t?parseInt(t,10):void 0,o={limit:parseInt(e.headers.get("x-ratelimit-limit")||"0",10),remaining:parseInt(e.headers.get("x-ratelimit-remaining")||"0",10),reset:parseInt(e.headers.get("x-ratelimit-reset")||"0",10)};return r&&r>0&&(o.retryAfter=r),o}getErrorCode(e){if(e>=400&&e<500)switch(e){case 400:return"BAD_REQUEST";case 401:return"UNAUTHORIZED";case 403:return"FORBIDDEN";case 404:return"NOT_FOUND";case 408:return"REQUEST_TIMEOUT";case 409:return"CONFLICT";case 422:return"VALIDATION_ERROR";case 429:return"RATE_LIMITED";default:return"CLIENT_ERROR"}else if(e>=500)switch(e){case 500:return"INTERNAL_SERVER_ERROR";case 502:return"BAD_GATEWAY";case 503:return"SERVICE_UNAVAILABLE";case 504:return"GATEWAY_TIMEOUT";default:return"SERVER_ERROR"}return"UNKNOWN_ERROR"}categorizeError(e){return"NETWORK_ERROR"===e.code||e instanceof TypeError?{category:"network",isRetryable:!0,requiresBackoff:!0}:"TIMEOUT_ERROR"===e.code||"AbortError"===e.name?{category:"timeout",isRetryable:!0,requiresBackoff:!0}:401===e.statusCode||403===e.statusCode?{category:"auth",isRetryable:!1,requiresBackoff:!1}:429===e.statusCode?{category:"rate_limit",isRetryable:!0,requiresBackoff:!0}:e.statusCode>=400&&e.statusCode<500?{category:"client",isRetryable:[408,409].includes(e.statusCode),requiresBackoff:!1}:e.statusCode>=500&&e.statusCode<600?{category:"server",isRetryable:!0,requiresBackoff:!0}:{category:"unknown",isRetryable:!1,requiresBackoff:!1}}calculateRetryDelay(e,t){const r=this.categorizeError(e);if(!r.requiresBackoff)return 0;if("rate_limit"===r.category){const t=e;if(t.rateLimitInfo?.retryAfter)return 1e3*t.rateLimitInfo.retryAfter}const o=1e3*Math.pow(2,t),a=1e3*Math.random();let n=3e4;switch(r.category){case"network":n=6e4;break;case"timeout":n=45e3;break;case"server":n=3e4}return Math.min(o+a,n)}sleep(e){return new Promise(t=>setTimeout(t,e))}}export class GraphQLClient{constructor(e,t,r){this.httpClient=e,this.config=t,this.logger=r}async query(e){const t={url:this.config.apiUrl,method:"POST",data:{query:e.query,variables:e.variables,operationName:e.operationName}};this.config.timeout&&(t.timeout=this.config.timeout),this.logger?.debug("Executing GraphQL query",{operation:"graphql_query",operationName:e.operationName,variableCount:e.variables?Object.keys(e.variables).length:0});try{const r=await this.httpClient.request(t);return r.data&&r.data.errors&&Array.isArray(r.data.errors)&&r.data.errors.length>0&&this.logger?.warn("GraphQL query returned errors",{operation:"graphql_errors",operationName:e.operationName,errorCount:r.data.errors.length,errors:r.data.errors.map(e=>e?.message)}),r.data}catch(t){throw this.logger?.error("GraphQL query failed",t,{operation:"graphql_query_error",operationName:e.operationName}),t}}async mutate(e){return this.query(e)}}export class FinOpsAPIClient{constructor(e,t,r,o){this.config=e,this.logger=r,this.performanceMonitor=o,this.httpClient=new HTTPClient(e,t,r),this.graphqlClient=new GraphQLClient(this.httpClient,e,r)}async getCostOptimizationRecommendations(e){const t=Date.now();let r,o=!0;const a={query:"\n query GetCostOptimizationRecommendations($params: CostOptimizationParams!) {\n getCostOptimizationRecommendations(params: $params) {\n data {\n recommendation_id\n element\n account\n recommendation_type\n status\n potential_savings\n description\n created_date\n priority\n category\n resource_id\n region\n service\n }\n summary {\n total_recommendations\n total_potential_savings\n average_savings_per_recommendation\n status_breakdown {\n status\n count\n total_savings\n }\n category_breakdown {\n category\n count\n total_savings\n }\n priority_breakdown {\n priority\n count\n total_savings\n }\n }\n error {\n statusCode\n message\n code\n }\n }\n }\n ",variables:{params:e},operationName:"GetCostOptimizationRecommendations"};this.logger?.info("Fetching cost optimization recommendations",{operation:"get_cost_optimization_recommendations",params:this.sanitizeParams(e)});try{const e=await this.graphqlClient.query(a);if(e.errors&&e.errors.length>0)throw new Error(`GraphQL errors: ${e.errors.map(e=>e.message).join(", ")}`);const t=e.data?.getCostOptimizationRecommendations;if(!t)throw new Error("No data returned from getCostOptimizationRecommendations query");return this.logger?.info("Cost optimization recommendations retrieved successfully",{operation:"get_cost_optimization_recommendations_success",recommendationCount:t.data?.length||0,totalPotentialSavings:t.summary?.total_potential_savings||0}),t}catch(t){return o=!1,r=t,this.logger?.error("Failed to get cost optimization recommendations",r,{operation:"get_cost_optimization_recommendations_error",params:this.sanitizeParams(e)}),{data:[],error:{message:r.message,code:"OPERATION_FAILED",details:JSON.stringify(r)}}}finally{if(this.performanceMonitor){const e=Date.now()-t;this.performanceMonitor.recordRequest("getCostOptimizationRecommendations",e,o,void 0,r?.name)}}}async analyzeCostTrends(e){let t;Date.now();const r={query:"\n query AnalyzeCostTrends(\n $element: String\n $account: String\n $start_date: String\n $end_date: String\n $trend_type: String\n $status: String\n $limit: Int\n $nextToken: String\n ) {\n analyzeCostTrends(\n element: $element\n account: $account\n start_date: $start_date\n end_date: $end_date\n trend_type: $trend_type\n status: $status\n limit: $limit\n nextToken: $nextToken\n ) {\n data {\n trend_id\n element\n account\n trend_type\n period\n cost_data {\n date\n cost\n usage\n }\n trend_analysis {\n direction\n percentage_change\n projected_savings\n confidence_level\n }\n recommendations\n created_date\n status\n }\n summary {\n total_trends\n trends_by_direction\n total_projected_savings\n average_confidence_level\n }\n nextToken\n error {\n message\n code\n details\n }\n }\n }\n ",variables:e,operationName:"AnalyzeCostTrends"};this.logger?.info("Analyzing cost trends",{operation:"analyze_cost_trends",params:this.sanitizeParams(e)});try{const e=await this.graphqlClient.query(r);if(e.errors&&e.errors.length>0)throw new Error(`GraphQL errors: ${e.errors.map(e=>e.message).join(", ")}`);const t=e.data?.analyzeCostTrends;if(!t)throw new Error("No data returned from analyzeCostTrends query");return this.logger?.info("Cost trends analyzed successfully",{operation:"analyze_cost_trends_success",trendCount:t.data?.length||0,totalProjectedSavings:t.summary?.total_projected_savings||0}),t}catch(t){return this.logger?.error("Failed to analyze cost trends",t,{operation:"analyze_cost_trends_error",params:this.sanitizeParams(e)}),{data:[],error:{message:t instanceof Error?t.message:String(t),code:"OPERATION_FAILED",details:JSON.stringify(t)}}}}async processCostOptimizationRecommendation(e){const t=Date.now();let r,o=!0;if(!e.recommendation_id)return this.logger?.error("Recommendation ID is required",new Error("Missing recommendation_id"),{operation:"process_cost_optimization_recommendation_validation_error",input:e}),{data:void 0,error:{message:"Recommendation ID is required",code:"VALIDATION_ERROR",details:JSON.stringify(e)}};if(!e.action_type||!["approve","ignore","complete"].includes(e.action_type))return this.logger?.error("Valid action type is required",new Error("Invalid action_type"),{operation:"process_cost_optimization_recommendation_validation_error",input:e}),{data:void 0,error:{message:"Action type must be one of: approve, ignore, complete",code:"VALIDATION_ERROR",details:JSON.stringify(e)}};const a={query:"\n mutation ProcessCostOptimizationRecommendation($action: CostOptimizationInput!) {\n processCostOptimizationRecommendation(action: $action) {\n data {\n recommendation_id\n element\n account\n recommendation_type\n status\n potential_savings\n description\n created_date\n priority\n category\n resource_id\n region\n service\n last_action_user\n last_action_role\n updated_date\n }\n error {\n statusCode\n message\n code\n }\n }\n }\n ",variables:{action:e},operationName:"ProcessCostOptimizationRecommendation"};this.logger?.info("Processing cost optimization recommendation",{operation:"process_cost_optimization_recommendation",recommendationId:e.recommendation_id,actionType:e.action_type});try{const t=await this.graphqlClient.mutate(a);if(t.errors&&t.errors.length>0)throw new Error(`GraphQL errors: ${t.errors.map(e=>e.message).join(", ")}`);const r=t.data?.processCostOptimizationRecommendation;if(!r)throw new Error("No data returned from processCostOptimizationRecommendation mutation");return this.logger?.info("Cost optimization recommendation processed successfully",{operation:"process_cost_optimization_recommendation_success",recommendationId:e.recommendation_id,actionType:e.action_type,newStatus:r.data?.status}),r}catch(t){return o=!1,r=t,this.logger?.error("Failed to process cost optimization recommendation",r,{operation:"process_cost_optimization_recommendation_error",recommendationId:e.recommendation_id,actionType:e.action_type}),{data:void 0,error:{message:r.message,code:"OPERATION_FAILED",details:JSON.stringify(r.stack)}}}finally{if(this.performanceMonitor){const e=Date.now()-t;this.performanceMonitor.recordRequest("processCostOptimizationRecommendation",e,o,void 0,r?.name)}}}sanitizeParams(e){return{...e}}}