finops-mcp-server
Version:
MCP server for FinOps Center cost optimization integration
1 lines • 12 kB
JavaScript
import{Server}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema,ListToolsRequestSchema}from"@modelcontextprotocol/sdk/types.js";export class FinOpsMCPHandler{constructor(e,t,o){this.isInitialized=!1,this.registeredTools=new Map,this.apiClient=e,this.config=t,this.logger=o,this.server=new Server({name:"finops-mcp-server",version:"1.0.0"}),this.setupServerHandlers()}setupServerHandlers(){this.server.setRequestHandler(ListToolsRequestSchema,async()=>(this.logger?.debug("Handling list tools request",{operation:"mcp_list_tools",toolCount:this.registeredTools.size}),{tools:Array.from(this.registeredTools.values()).map(e=>({name:e.name,description:e.description||"",inputSchema:{type:"object",properties:e.inputSchema.properties||{},required:e.inputSchema.required||[]}}))})),this.server.setRequestHandler(CallToolRequestSchema,async e=>{const{name:t,arguments:o}=e.params;this.logger?.debug("Handling call tool request",{operation:"mcp_call_tool",toolName:t,hasArguments:!!o});try{return{content:(await this.callTool({name:t,arguments:o||{}})).content,isError:!1}}catch(e){return this.logger?.error("Tool call failed",e,{operation:"mcp_call_tool_error",toolName:t}),{content:[{type:"text",text:`Error: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}})}async initialize(){return this.logger?.info("Initializing MCP server",{operation:"mcp_initialize"}),this.registerTools(),this.isInitialized=!0,this.logger?.info("MCP server initialized successfully",{operation:"mcp_initialize_success",toolCount:this.registeredTools.size}),{protocolVersion:"2024-11-05",capabilities:{tools:{}},serverInfo:{name:"FinOps MCP Server",version:"1.0.0"}}}async start(){if(!this.isInitialized)throw new Error("Server must be initialized before starting");this.logger?.info("Starting MCP server",{operation:"mcp_start"});const e=new StdioServerTransport;await this.server.connect(e),this.logger?.info("MCP server started successfully",{operation:"mcp_start_success"})}async shutdown(){this.logger?.info("Shutting down MCP server",{operation:"mcp_shutdown"}),await this.server.close(),this.isInitialized=!1,this.logger?.info("MCP server shutdown completed",{operation:"mcp_shutdown_success"})}registerTools(){this.logger?.debug("Registering MCP tools",{operation:"register_tools"}),this.registeredTools.set("get-cost-optimization-recommendations",{name:"get-cost-optimization-recommendations",description:"Get AI-powered cost optimization recommendations for AWS resources",inputSchema:{type:"object",properties:{accountId:{type:"string",description:"AWS Account ID to analyze (required)"},element:{type:"string",description:"Element/region to focus on (optional)"},recommendationType:{type:"string",description:"Type of recommendation to filter by (optional)"},status:{type:"string",description:"Status to filter by (pending, approved, ignored, implemented)",enum:["pending","approved","ignored","implemented"]},priority:{type:"string",description:"Priority level to filter by",enum:["low","medium","high","critical"]},category:{type:"string",description:"Category to filter by (compute, storage, network, etc.)"},limit:{type:"number",description:"Maximum number of recommendations to return (default: 50)"}},required:["accountId"]}}),this.registeredTools.set("get-cost-trends",{name:"get-cost-trends",description:"Analyze cost trends and patterns over time",inputSchema:{type:"object",properties:{accountId:{type:"string",description:"AWS Account ID to analyze"},element:{type:"string",description:"Element/region to focus on"},startDate:{type:"string",description:"Start date for trend analysis (YYYY-MM-DD)"},endDate:{type:"string",description:"End date for trend analysis (YYYY-MM-DD)"},trendType:{type:"string",description:"Type of trend to analyze"},status:{type:"string",description:"Status to filter by"},limit:{type:"number",description:"Maximum number of trends to return"}},required:["accountId"]}}),this.registeredTools.set("process-cost-optimization-recommendation",{name:"process-cost-optimization-recommendation",description:"Process an action on an existing cost optimization recommendation (approve, ignore, or mark as complete)",inputSchema:{type:"object",properties:{recommendationId:{type:"string",description:"ID of the recommendation to process (required)"},actionType:{type:"string",description:"Action to take on the recommendation",enum:["approve","ignore","complete"]}},required:["recommendationId","actionType"]}}),this.logger?.info("MCP tools registered successfully",{operation:"register_tools_success",toolCount:this.registeredTools.size,tools:Array.from(this.registeredTools.keys())})}async callTool(e){const{name:t,arguments:o}=e;this.logger?.debug("Executing tool call",{operation:"call_tool",toolName:t,arguments:o});try{switch(t){case"get-cost-optimization-recommendations":return await this.getCostOptimizationRecommendations(o);case"get-cost-trends":return await this.getCostTrends(o);case"process-cost-optimization-recommendation":return await this.processCostOptimizationRecommendation(o);default:throw new Error(`Unknown tool: ${t}`)}}catch(e){return this.logger?.error("Tool execution failed",e,{operation:"call_tool_error",toolName:t}),{content:[{type:"text",text:`Error executing ${t}: ${e instanceof Error?e.message:String(e)}`}],isError:!0}}}async getCostOptimizationRecommendations(e){this.logger?.info("Getting cost optimization recommendations",{operation:"get_cost_optimization",account:e.accountId});try{const t={account:e.accountId,element:e.element,recommendation_type:e.recommendationType,status:e.status,priority:e.priority,category:e.category,limit:e.limit||50},o=await this.apiClient.getCostOptimizationRecommendations(t);if(o.error)throw new Error(o.error.message);let n="# Cost Optimization Recommendations\n\n";return o.summary&&(n+="## Summary\n",n+=`- Account ID: ${e.accountId}\n`,n+=`- Total Recommendations: ${o.summary.total_recommendations||0}\n`,n+=`- Total Potential Savings: $${o.summary.total_potential_savings?.toFixed(2)||"0.00"}\n`,o.summary.average_savings_per_recommendation&&(n+=`- Average Savings per Recommendation: $${o.summary.average_savings_per_recommendation.toFixed(2)}\n`),n+="\n",o.summary.status_breakdown&&o.summary.status_breakdown.length>0&&(n+="### Status Breakdown\n",o.summary.status_breakdown.forEach(e=>{n+=`- **${e.status}**: ${e.count} recommendations ($${e.total_savings.toFixed(2)})\n`}),n+="\n"),o.summary.category_breakdown&&o.summary.category_breakdown.length>0&&(n+="### Category Breakdown\n",o.summary.category_breakdown.forEach(e=>{n+=`- **${e.category}**: ${e.count} recommendations ($${e.total_savings.toFixed(2)})\n`}),n+="\n"),o.summary.priority_breakdown&&o.summary.priority_breakdown.length>0&&(n+="### Priority Breakdown\n",o.summary.priority_breakdown.forEach(e=>{n+=`- **${e.priority}**: ${e.count} recommendations ($${e.total_savings.toFixed(2)})\n`}),n+="\n")),n+="## Recommendations\n\n",o.data&&o.data.length>0?o.data.forEach((e,t)=>{n+=`### ${t+1}. ${e.recommendation_type||"Optimization"}\n`,n+=`- **ID**: ${e.recommendation_id}\n`,n+=`- **Priority**: ${e.priority||"Medium"}\n`,n+=`- **Status**: ${e.status||"Pending"}\n`,n+=`- **Potential Savings**: $${e.potential_savings?.toFixed(2)||"0.00"}\n`,n+=`- **Description**: ${e.description||"No description available"}\n`,e.category&&(n+=`- **Category**: ${e.category}\n`),e.region&&(n+=`- **Region**: ${e.region}\n`),e.service&&(n+=`- **Service**: ${e.service}\n`),e.resource_id&&(n+=`- **Resource ID**: ${e.resource_id}\n`),e.created_date&&(n+=`- **Created**: ${e.created_date}\n`),n+="\n"}):n+="No recommendations found for the specified criteria.\n",n+="\n## Next Steps\n",n+="To process any recommendation, use the `process-cost-optimization-recommendation` tool with:\n",n+="- `recommendationId`: The ID of the recommendation to process\n",n+='- `actionType`: Either "approve", "ignore", or "complete"\n',{content:[{type:"text",text:n}],isError:!1}}catch(e){throw new Error(`Failed to get cost optimization recommendations: ${e instanceof Error?e.message:String(e)}`)}}async getCostTrends(e){this.logger?.info("Getting cost trends",{operation:"get_cost_trends",account:e.accountId});try{const t={account:e.accountId,element:e.element,start_date:e.startDate,end_date:e.endDate,trend_type:e.trendType,status:e.status,limit:e.limit||50},o=await this.apiClient.analyzeCostTrends(t);if(o.error)throw new Error(o.error.message);let n="# Cost Trends Analysis\n\n";return o.summary&&(n+="## Summary\n",n+=`- Account ID: ${e.accountId}\n`,n+=`- Total Trends: ${o.summary.total_recommendations||0}\n`,n+=`- Total Potential Savings: $${o.summary.total_potential_savings?.toFixed(2)||"0.00"}\n`,n+=`- Total Actual Savings: $${o.summary.total_actual_savings?.toFixed(2)||"0.00"}\n`,n+=`- Average ROI: ${o.summary.average_roi_percentage?.toFixed(1)||"0.0"}%\n`,n+="\n"),n+="## Trend Analysis\n\n",o.data&&o.data.length>0?o.data.forEach((e,t)=>{n+=`### ${t+1}. Trend Analysis\n`,n+=`- **Recommendation ID**: ${e.recommendation_id}\n`,n+=`- **Trend Type**: ${e.trend_type||"General"}\n`,n+=`- **Status**: ${e.status||"Active"}\n`,n+=`- **Potential Savings**: $${e.potential_savings?.toFixed(2)||"0.00"}\n`,n+=`- **Actual Savings**: $${e.actual_savings?.toFixed(2)||"0.00"}\n`,n+=`- **ROI**: ${e.roi_percentage?.toFixed(1)||"0.0"}%\n`,e.created_date&&(n+=`- **Created**: ${e.created_date}\n`),e.implemented_date&&(n+=`- **Implemented**: ${e.implemented_date}\n`),n+="\n"}):n+="No cost trends found for the specified criteria.\n",{content:[{type:"text",text:n}],isError:!1}}catch(e){throw new Error(`Failed to get cost trends: ${e instanceof Error?e.message:String(e)}`)}}async processCostOptimizationRecommendation(e){this.logger?.info("Processing cost optimization recommendation",{operation:"process_cost_optimization_recommendation",recommendationId:e.recommendationId,actionType:e.actionType});try{const t={recommendation_id:e.recommendationId,action_type:e.actionType},o=await this.apiClient.processCostOptimizationRecommendation(t);if(o.error)throw new Error(o.error.message);let n="# Cost Optimization Recommendation Processed\n\n";switch(n+="## Action Completed\n",n+=`- **Recommendation ID**: ${e.recommendationId}\n`,n+=`- **Action**: ${e.actionType}\n`,n+="- **Status**: Success\n\n",o.data&&(n+="## Updated Recommendation Details\n",n+=`- **ID**: ${o.data.recommendation_id}\n`,n+=`- **Type**: ${o.data.recommendation_type||"N/A"}\n`,n+=`- **Status**: ${o.data.status||"N/A"}\n`,n+=`- **Priority**: ${o.data.priority||"N/A"}\n`,n+=`- **Potential Savings**: $${o.data.potential_savings?.toFixed(2)||"0.00"}\n`,n+=`- **Description**: ${o.data.description||"No description available"}\n`,o.data.updated_date&&(n+=`- **Last Updated**: ${o.data.updated_date}\n`),o.data.last_action_user&&(n+=`- **Last Action By**: ${o.data.last_action_user}\n`)),n+="\n## What Happens Next\n",e.actionType){case"approve":n+="- The recommendation has been approved and is ready for implementation\n",n+="- Consider scheduling the implementation with your infrastructure team\n",n+='- Use the "complete" action once the recommendation is implemented\n';break;case"ignore":n+="- The recommendation has been marked as ignored\n",n+="- It will not appear in active recommendation lists\n",n+="- You can still find it by filtering for ignored recommendations\n";break;case"complete":n+="- The recommendation has been marked as completed\n",n+="- Cost savings should start reflecting in your next billing cycle\n",n+="- The system will track actual vs. projected savings\n"}return{content:[{type:"text",text:n}],isError:!1}}catch(e){throw new Error(`Failed to process cost optimization recommendation: ${e instanceof Error?e.message:String(e)}`)}}async listTools(e){return this.logger?.debug("Listing available tools",{operation:"list_tools",toolCount:this.registeredTools.size}),{tools:Array.from(this.registeredTools.values())}}}