Skip to main content
Glama
final-mcp-server.cjs41.8 kB
#!/usr/bin/env node const express = require('express'); const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const axios = require('axios'); const app = express(); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true })); // CORS app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); if (req.method === 'OPTIONS') return res.sendStatus(204); next(); }); // Configuration const JWT_SECRET = crypto.randomBytes(64).toString('hex'); const TOKEN_EXPIRY = 24 * 60 * 60; // 24 hours const BASE_URL = 'https://api.umbrellacost.io/api/v1'; // In-memory sessions (no passwords stored!) const activeSessions = new Map(); // Umbrella Authentication Helper async function authenticateUmbrella(username, password) { try { console.log(`[AUTH] Authenticating ${username}`); // For demo/test mode if (username === 'demo@test.com' && password === 'demo') { return { success: true, authType: 'demo', umbrellaToken: 'demo-token', apiKey: 'demo:9350:0' }; } const axiosInstance = axios.create({ baseURL: BASE_URL, timeout: 30000 }); // First detect user management system let isKeycloak = false; try { const realmResponse = await axiosInstance.get('/user-management/users/user-realm', { params: { username: username.toLowerCase() } }); if (realmResponse.status === 200 && realmResponse.data?.realm) { console.log(`[AUTH] User is on Keycloak UM 2.0, realm: ${realmResponse.data.realm}`); isKeycloak = true; } else { console.log('[AUTH] User is on Cognito Old UM'); } } catch (realmError) { console.log('[AUTH] Realm detection failed, defaulting to Cognito'); } // Try appropriate authentication method if (isKeycloak) { // Try Keycloak with Basic Auth header try { const basicAuth = Buffer.from(`${username}:${password}`).toString('base64'); const keycloakResponse = await axiosInstance.post('/authentication/token/generate', { username, password }, { headers: { 'Authorization': `Basic ${basicAuth}`, 'Content-Type': 'application/json' } }); if (keycloakResponse.data?.Authorization && keycloakResponse.data?.apikey) { console.log('[AUTH] Keycloak authentication successful'); // Build proper API key const tempApiKey = keycloakResponse.data.apikey; const userKey = tempApiKey.split(':')[0]; const properApiKey = await buildProperApiKey(axiosInstance, userKey, keycloakResponse.data.Authorization, true); return { success: true, authType: 'keycloak', umbrellaToken: keycloakResponse.data.Authorization, apiKey: properApiKey }; } } catch (keycloakError) { console.log(`[AUTH] Keycloak failed: ${keycloakError.response?.status}`); // Fall through to try Cognito isKeycloak = false; } } // Try Cognito (either as primary or fallback) if (!isKeycloak) { try { const cognitoResponse = await axiosInstance.post('/users/signin', { username, password }); if (cognitoResponse.data?.jwtToken) { console.log('[AUTH] Cognito authentication successful'); // Extract userKey from JWT token const tokenPayload = JSON.parse( Buffer.from(cognitoResponse.data.jwtToken.split('.')[1], 'base64').toString() ); const userKey = tokenPayload.username || tokenPayload['custom:username'] || tokenPayload.sub; // Build proper API key const properApiKey = await buildProperApiKey(axiosInstance, userKey, cognitoResponse.data.jwtToken, false); return { success: true, authType: 'cognito', umbrellaToken: cognitoResponse.data.jwtToken, apiKey: properApiKey }; } } catch (cognitoError) { console.log(`[AUTH] Cognito failed: ${cognitoError.response?.status}`); } } throw new Error('Authentication failed'); } catch (error) { console.error('[AUTH] Error:', error.message); throw error; } } // Helper function to build proper API key async function buildProperApiKey(axiosInstance, userKey, jwtToken, isKeycloak) { try { const tempApiKey = `${userKey}:-1`; const accountsEndpoint = isKeycloak ? '/user-management/accounts' : '/users'; console.log(`[AUTH] Getting account data from ${accountsEndpoint}`); const accountsResponse = await axiosInstance.get(accountsEndpoint, { headers: { 'Authorization': jwtToken, 'apikey': tempApiKey, 'Content-Type': 'application/json' } }); if (accountsResponse.status === 200 && accountsResponse.data) { let accounts = []; if (isKeycloak) { // Keycloak: direct array accounts = accountsResponse.data; } else { // Cognito: user object with accounts array accounts = accountsResponse.data.accounts || []; } if (accounts.length > 0) { // Look for account 9350 or use first account let targetAccount = accounts.find(acc => { const accountKey = acc.accountKey || acc.account_key || acc.accountId || acc.account_id; return String(accountKey) === '9350' || accountKey === 9350; }); if (!targetAccount) { targetAccount = accounts[0]; } const accountKey = targetAccount.accountKey || targetAccount.account_key || targetAccount.accountId || targetAccount.account_id || '9350'; console.log(`[AUTH] Using account key: ${accountKey}`); return `${userKey}:${accountKey}:0`; } } // Fallback return `${userKey}:9350:0`; } catch (error) { console.log(`[AUTH] Failed to build proper API key: ${error.message}`); return `${userKey}:9350:0`; } } // Home page app.get('/', (req, res) => { res.send(` <html> <head> <title>Umbrella MCP Server</title> <style> body { font-family: sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; } .box { background: #f0f8ff; padding: 20px; border-radius: 8px; margin: 20px 0; } code { background: #e0e0e0; padding: 2px 6px; border-radius: 3px; } </style> </head> <body> <h1>🚀 Umbrella MCP Server</h1> <div class="box"> <h3>Features:</h3> ✅ No password storage<br> ✅ 24-hour Bearer tokens<br> ✅ Simple and secure<br> ✅ Works with Claude Desktop </div> <div class="box"> <h3>Quick Start:</h3> 1. <a href="/login">Login</a> to get Bearer token<br> 2. Copy configuration to Claude Desktop<br> 3. Token expires in 24 hours (re-login required) </div> <div class="box"> <h3>Demo Mode:</h3> Email: <code>demo@test.com</code><br> Password: <code>demo</code> </div> </body> </html> `); }); // Login page app.get('/login', (req, res) => { res.send(` <html> <head> <title>Login - Umbrella MCP</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); max-width: 400px; width: 100%; } h2 { text-align: center; color: #333; margin-bottom: 30px; } input { width: 100%; padding: 12px; margin: 10px 0; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 16px; box-sizing: border-box; } button { width: 100%; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; margin-top: 20px; } button:disabled { opacity: 0.5; cursor: not-allowed; } .message { margin: 10px 0; padding: 10px; border-radius: 6px; display: none; } .error { background: #fee; color: #c00; border: 1px solid #fcc; } .success { background: #efe; color: #060; border: 1px solid #cfc; } .info { background: #f0f8ff; padding: 15px; border-radius: 6px; margin-bottom: 20px; } </style> </head> <body> <div class="container"> <h2>🔐 Umbrella MCP Login</h2> <div class="info"> <strong>Secure Authentication</strong><br> • No passwords stored<br> • Token valid for 24 hours<br> • Demo: demo@test.com / demo </div> <form id="loginForm"> <input type="email" id="username" placeholder="Email" required> <input type="password" id="password" placeholder="Password" required> <button type="submit" id="submitBtn">Login</button> <div class="message error" id="error"></div> <div class="message success" id="success"></div> </form> </div> <script> document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const errorDiv = document.getElementById('error'); const successDiv = document.getElementById('success'); const submitBtn = document.getElementById('submitBtn'); errorDiv.style.display = 'none'; successDiv.style.display = 'none'; submitBtn.disabled = true; submitBtn.textContent = 'Authenticating...'; try { const response = await fetch('/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const result = await response.json(); if (result.success) { successDiv.textContent = 'Success! Redirecting to configuration...'; successDiv.style.display = 'block'; setTimeout(() => { window.location.href = '/config?token=' + encodeURIComponent(result.bearerToken); }, 1000); } else { throw new Error(result.error || 'Authentication failed'); } } catch (err) { errorDiv.textContent = err.message; errorDiv.style.display = 'block'; submitBtn.disabled = false; submitBtn.textContent = 'Login'; } }); </script> </body> </html> `); }); // Authentication endpoint app.post('/auth', async (req, res) => { try { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Username and password required' }); } // Authenticate with Umbrella (or demo mode) const authResult = await authenticateUmbrella(username, password); // Create JWT Bearer token const bearerToken = jwt.sign( { username, umbrellaToken: authResult.umbrellaToken, apiKey: authResult.apiKey, authType: authResult.authType, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + TOKEN_EXPIRY }, JWT_SECRET ); // Store in memory activeSessions.set(bearerToken, { username, authResult, createdAt: Date.now() }); console.log(`[AUTH] ✅ User ${username} authenticated (${authResult.authType})`); res.json({ success: true, bearerToken, expiresIn: TOKEN_EXPIRY }); } catch (error) { console.error('[AUTH] Failed:', error.message); res.status(401).json({ error: error.message }); } }); // Configuration display page app.get('/config', (req, res) => { const token = req.query.token; if (!token) { return res.redirect('/login'); } // Decode token to show info let tokenInfo = { username: 'unknown', exp: 0 }; try { tokenInfo = jwt.decode(token); } catch (e) {} const expiryDate = new Date(tokenInfo.exp * 1000).toLocaleString(); res.send(` <html> <head> <title>Configuration - Umbrella MCP</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 900px; margin: 50px auto; padding: 20px; background: #f5f5f5; } .container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .header { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 30px; text-align: center; } .code-block { background: #263238; color: #aed581; padding: 20px; border-radius: 6px; font-family: 'Monaco', 'Menlo', monospace; font-size: 14px; line-height: 1.6; overflow-x: auto; white-space: pre; margin: 20px 0; } button { background: #28a745; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; } .warning { background: #fff3cd; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #ffc107; color: #856404; } .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin: 20px 0; } .info-box { background: #f8f9fa; padding: 15px; border-radius: 6px; } </style> </head> <body> <div class="container"> <div class="header"> <h1>✅ Authentication Successful!</h1> <p>User: ${tokenInfo.username}</p> </div> <div class="info-grid"> <div class="info-box"> <strong>Token Type:</strong> Bearer JWT<br> <strong>Auth Method:</strong> ${tokenInfo.authType} </div> <div class="info-box"> <strong>Expires:</strong> ${expiryDate}<br> <strong>Duration:</strong> 24 hours </div> </div> <h3>Claude Desktop Configuration</h3> <div class="code-block" id="config">{ "mcpServers": { "umbrella-cost": { "command": "node", "args": ["${__dirname}/mcp-client.cjs"], "env": { "MCP_SERVER": "http://localhost:${PORT}", "BEARER_TOKEN": "${token}" } } } }</div> <button onclick="copyConfig()">📋 Copy Configuration</button> <div class="warning"> <strong>⚠️ Important:</strong><br> • Token expires in 24 hours - you'll need to login again<br> • No passwords are stored on the server<br> • Server restart will invalidate all tokens </div> <h3>Alternative: Direct MCP Configuration</h3> <p>If you have mcp-client.cjs installed globally:</p> <div class="code-block" id="altConfig">{ "mcpServers": { "umbrella-cost": { "command": "npx", "args": [ "mcp-remote", "http://localhost:${PORT}/mcp" ], "env": { "AUTHORIZATION": "Bearer ${token}" } } } }</div> </div> <script> function copyConfig() { const configText = document.getElementById('config').textContent; navigator.clipboard.writeText(configText); event.target.textContent = '✅ Copied!'; setTimeout(() => { event.target.textContent = '📋 Copy Configuration'; }, 2000); } </script> </body> </html> `); }); // MCP endpoint (handles JSON-RPC messages) app.post('/mcp', async (req, res) => { // Check Bearer token const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Authorization required' } }); } const bearerToken = authHeader.substring(7); try { // Verify JWT const decoded = jwt.verify(bearerToken, JWT_SECRET); // Get the MCP message const message = req.body; // Handle MCP protocol let response; switch (message.method) { case 'initialize': response = { jsonrpc: '2.0', id: message.id, result: { protocolVersion: '1.0.0', serverName: 'umbrella-mcp', serverVersion: '1.0.0', capabilities: { tools: true } } }; break; case 'tools/list': response = { jsonrpc: '2.0', id: message.id, result: { tools: [ { name: 'get_costs', description: 'Get cost data from Umbrella', inputSchema: { type: 'object', properties: { startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' } }, required: ['startDate', 'endDate'] } }, { name: 'get_accounts', description: 'Get available accounts', inputSchema: { type: 'object', properties: {} } }, { name: 'get_recommendations', description: 'Get recommendations heatmap summary with potential savings', inputSchema: { type: 'object', properties: { userQuery: { type: 'string', description: 'Natural language query for customer detection' }, customer_account_key: { type: 'string', description: 'MSP customer account key' } } } }, { name: 'get_legacy_recommendations', description: 'Get legacy recommendation reports', inputSchema: { type: 'object', properties: { userQuery: { type: 'string', description: 'Natural language query for customer detection' }, customer_account_key: { type: 'string', description: 'MSP customer account key' } } } } ] } }; break; case 'tools/call': const toolName = message.params.name; const args = message.params.arguments || {}; // For demo mode, return mock data if (decoded.authType === 'demo') { response = { jsonrpc: '2.0', id: message.id, result: { content: [{ type: 'text', text: JSON.stringify({ demo: true, message: 'Demo mode - no real data available', tool: toolName, args: args }, null, 2) }] } }; } else { // Real API call try { // Build proper API key based on cloud_context (like Latest RC) let apiKey = decoded.apiKey; if (args.cloud_context) { const cloudContext = args.cloud_context.toLowerCase(); const userKey = decoded.apiKey.split(':')[0]; if (cloudContext === 'gcp') { // GCP: Use account 21112 (MasterBilling) apiKey = `${userKey}:21112:0`; } else if (cloudContext === 'azure') { // Azure: Use account 23105 (AzureAmortized) apiKey = `${userKey}:23105:0`; } // AWS uses default apiKey (9350) } // Both Keycloak and Cognito use the token directly + apikey const headers = { 'Authorization': decoded.umbrellaToken, 'apikey': apiKey, 'Content-Type': 'application/json' }; console.log(`[MCP] Calling Umbrella API for tool: ${toolName}`); console.log(`[MCP] Headers: Authorization=${headers.Authorization.substring(0, 50)}..., apikey=${headers.apikey}`); let apiResponse; switch (toolName) { case 'get_costs': // Use the working endpoint with DYNAMIC parameters const costsUrl = `${BASE_URL}/invoices/caui`; console.log(`[MCP] GET ${costsUrl}`); // Build params with GITHUB WORKING PARAMETERS (from comprehensive-native-questions-demo.ts) const params = { startDate: args.startDate || '2025-08-01', endDate: args.endDate || '2025-08-31', groupBy: args.groupBy || 'none', // CRITICAL: Required for data return per GitHub periodGranLevel: args.periodGranLevel || 'day', // Fixed: Use 'day' like RC5 costType: args.costType || ['cost', 'discount'] // REQUIRED parameter (array, not string) }; // Only add accountId if NOT using cloud_context (let API key determine account) if (!args.cloud_context && !args.accountId) { params.accountId = '932213950603'; // Default AWS account } else if (args.accountId) { params.accountId = args.accountId; // Explicit account override } // Add the specific cost type flags based on what was requested if (args.isUnblended !== undefined) { params.isUnblended = args.isUnblended; } else if (!args.isAmortized && !args.isNetAmortized && !args.skipUnblendedDefault) { // Default cost calculation: isUnblended=true (like Latest RC) params.isUnblended = true; } if (args.isAmortized !== undefined) { params.isAmortized = args.isAmortized; } if (args.isNetAmortized !== undefined) { params.isNetAmortized = args.isNetAmortized; } if (args.isNetUnblended !== undefined) { params.isNetUnblended = args.isNetUnblended; } if (args.cloud_context) { params.cloud_context = args.cloud_context; } if (args.groupBy) { params.groupBy = args.groupBy; } if (args.service) { params.service = args.service; } // Apply excludeFilters - RC5 DOES exclude tax by default for AWS if (args.excludeFilters) { params.excludeFilters = args.excludeFilters; console.log(`[EXCLUDE-FILTERS] Using provided excludeFilters: ${JSON.stringify(args.excludeFilters)}`); } else if (args.cloud_context === 'aws') { // RC5 excludes tax by default for AWS - confirmed from actual RC5 logs params.excludeFilters = { chargetype: ['Tax'] }; console.log(`[TAX-EXCLUSION] AWS detected - excluding tax by default (like RC5)`); } // Convert excludeFilters object to proper URL format like RC5 if (params.excludeFilters) { const excludeFiltersObj = params.excludeFilters; delete params.excludeFilters; // Remove the object version // Convert to URL parameter format: excludeFilters[chargetype][]=Tax for (const [filterKey, filterValue] of Object.entries(excludeFiltersObj)) { if (Array.isArray(filterValue)) { filterValue.forEach(item => { params[`excludeFilters[${filterKey}][]`] = item; }); } else { params[`excludeFilters[${filterKey}]`] = filterValue; } } console.log(`[EXCLUDE-FILTERS] Converted to URL format: ${JSON.stringify(excludeFiltersObj)}`); } console.log(`[MCP] HONEST Parameters: ${JSON.stringify(params)}`); apiResponse = await axios.get(costsUrl, { headers, params }); break; case 'get_accounts': // Use the working user management endpoint const accountsUrl = `${BASE_URL}/user-management/accounts`; console.log(`[MCP] GET ${accountsUrl}`); apiResponse = await axios.get(accountsUrl, { headers }); break; case 'get_recommendations': // Use the heatmap summary endpoint that actually returns savings data const recsUrl = `${BASE_URL}/recommendationsNew/heatmap/summary`; console.log(`[MCP] POST ${recsUrl}`); const recsParams = {}; if (args.accountId) { recsParams.accountId = args.accountId; } else { // Default to main AWS account if not specified recsParams.accountId = '932213950603'; } if (args.userQuery) recsParams.userQuery = args.userQuery; if (args.customer_account_key) recsParams.customer_account_key = args.customer_account_key; console.log(`[MCP] Recommendations parameters: ${JSON.stringify(recsParams)}`); // Create custom headers with customer account key if needed let recsHeaders = { ...headers }; if (args.customer_account_key) { const userKey = decoded.apiKey.split(':')[0]; recsHeaders.apikey = `${userKey}:${args.customer_account_key}:0`; console.log(`[MCP] Switching to customer account key for recommendations: ${args.customer_account_key}`); console.log(`[MCP] Updated Headers: Authorization=${recsHeaders.Authorization.substring(0, 50)}..., apikey=${recsHeaders.apikey}`); } apiResponse = await axios.post(recsUrl, recsParams, { headers: recsHeaders }); break; case 'get_legacy_recommendations': // Use the legacy recommendations endpoint const legacyRecsUrl = `${BASE_URL}/recommendations/report`; console.log(`[MCP] GET ${legacyRecsUrl}`); const legacyParams = {}; if (args.userQuery) legacyParams.userQuery = args.userQuery; if (args.customer_account_key) legacyParams.customer_account_key = args.customer_account_key; console.log(`[MCP] Legacy recommendations parameters: ${JSON.stringify(legacyParams)}`); apiResponse = await axios.get(legacyRecsUrl, { headers, params: legacyParams }); break; default: throw new Error(`Unknown tool: ${toolName}`); } // Special handling for recommendations heatmap response let responseText; if (toolName === 'get_recommendations' && apiResponse.data) { const heatmapData = apiResponse.data; const potentialSavings = heatmapData.potentialAnnualSavings || 0; const actualSavings = heatmapData.actualAnnualSavings || 0; const potentialCount = heatmapData.potentialSavingsRecommendationCount || 0; const actualCount = heatmapData.actualSavingsRecommendationCount || 0; // Use the higher savings value as the primary metric const totalSavings = Math.max(potentialSavings, actualSavings); responseText = `**🎯 Recommendations Summary:** **💰 Total Potential Annual Savings:** $${totalSavings.toLocaleString()} **📊 Recommendations Available:** ${Math.max(potentialCount, actualCount)} **✅ Actual Annual Savings:** $${actualSavings.toLocaleString()} **📈 Potential Annual Savings:** $${potentialSavings.toLocaleString()}`; if (heatmapData.expectedSavingsRatePercent) { responseText += `\n**📋 Expected Savings Rate:** ${heatmapData.expectedSavingsRatePercent}%`; } if (heatmapData.effectiveSavingsRatePercent) { responseText += `\n**⚡ Effective Savings Rate:** ${heatmapData.effectiveSavingsRatePercent}%`; } } else { responseText = JSON.stringify(apiResponse.data, null, 2); } response = { jsonrpc: '2.0', id: message.id, result: { content: [{ type: 'text', text: responseText }] } }; } catch (apiError) { response = { jsonrpc: '2.0', id: message.id, error: { code: -32603, message: apiError.message } }; } } break; default: response = { jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } }; } res.json(response); } catch (error) { if (error.name === 'TokenExpiredError') { res.status(401).json({ jsonrpc: '2.0', error: { code: -32002, message: 'Token expired. Please login again.' } }); } else { res.status(401).json({ jsonrpc: '2.0', error: { code: -32003, message: 'Invalid token' } }); } } }); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', activeSessions: activeSessions.size, timestamp: new Date().toISOString() }); }); // Store SSE connections const sseClients = new Map(); // SSE endpoint for Claude Desktop MCP connection (GET for event stream) app.get('/sse', (req, res) => { const clientId = Date.now().toString(); console.log(`[SSE] Client ${clientId} connected`); // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); // Store client connection sseClients.set(clientId, res); // Send client ID res.write(`data: {"type":"connection","clientId":"${clientId}"}\n\n`); // Keep alive const keepAlive = setInterval(() => { res.write(':keepalive\n\n'); }, 30000); // Clean up on disconnect req.on('close', () => { console.log(`[SSE] Client ${clientId} disconnected`); clearInterval(keepAlive); sseClients.delete(clientId); }); }); // POST endpoint for receiving MCP messages app.post('/sse', express.json(), async (req, res) => { console.log('[SSE POST] Received message:', JSON.stringify(req.body).substring(0, 200)); try { const message = req.body; // Handle MCP protocol messages if (message.method === 'initialize') { const response = { jsonrpc: '2.0', id: message.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {} }, serverInfo: { name: 'umbrella-mcp-server', version: '1.0.0' } } }; res.json(response); console.log('[SSE POST] Sent initialization response'); } else if (message.method === 'tools/list') { const response = { jsonrpc: '2.0', id: message.id, result: { tools: [ { name: 'authenticate_user', description: 'Authenticate with Umbrella Cost Platform', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Your email' }, password: { type: 'string', description: 'Your password' } }, required: ['username', 'password'] } }, { name: 'get_costs', description: 'Get cost data from Umbrella Cost Platform', inputSchema: { type: 'object', properties: { cloud_context: { type: 'string', description: 'Cloud context (aws/gcp/azure)' }, userQuery: { type: 'string' } } } }, { name: 'get_accounts', description: 'Get list of available accounts', inputSchema: { type: 'object', properties: {} } }, { name: 'get_recommendations', description: 'Get cost optimization recommendations', inputSchema: { type: 'object', properties: { accountId: { type: 'string' }, customer_account_key: { type: 'string' }, userQuery: { type: 'string' } } } } ] } }; res.json(response); console.log('[SSE POST] Sent tools list'); } else if (message.method === 'tools/call') { // Forward to the existing MCP handler const response = await handleToolCall(message.params); res.json({ jsonrpc: '2.0', id: message.id, result: response }); } else { res.json({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } }); } } catch (error) { console.error('[SSE POST] Error:', error); res.status(400).json({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }); } }); // Helper function to handle tool calls async function handleToolCall(params) { const { name: toolName, arguments: args } = params; console.log(`[SSE] Tool call: ${toolName}`); // Handle authentication separately if (toolName === 'authenticate_user') { try { const result = await authenticateUmbrella(args.username, args.password); if (result.success) { // Store in session const sessionId = `session_${Buffer.from(args.username).toString('base64')}`; activeSessions.set(sessionId, { username: args.username, umbrellaToken: result.umbrellaToken, apiKey: result.apiKey, authType: result.authType, createdAt: Date.now() }); return { content: [{ type: 'text', text: `✅ Successfully authenticated as ${args.username}\nSession created. You can now use other tools.` }] }; } else { throw new Error(result.error || 'Authentication failed'); } } catch (error) { return { content: [{ type: 'text', text: `❌ Authentication failed: ${error.message}` }] }; } } // For other tools, check if authenticated // Try to find a session (simplified for SSE endpoint) const sessions = Array.from(activeSessions.values()); const session = sessions[sessions.length - 1]; // Use most recent session if (!session) { return { content: [{ type: 'text', text: '❌ Not authenticated. Please use authenticate_user tool first.' }] }; } // Handle tools directly here instead of forwarding try { const headers = { 'Authorization': session.umbrellaToken, 'apikey': session.apiKey, 'Content-Type': 'application/json' }; let apiResponse; switch (toolName) { case 'get_costs': const costsUrl = `${BASE_URL}/invoices/caui`; const costsParams = { accountId: '932213950603' }; if (args.cloud_context) { const cloudContext = args.cloud_context.toLowerCase(); const userKey = session.apiKey.split(':')[0]; if (cloudContext === 'gcp') { headers.apikey = `${userKey}:21112:0`; } else if (cloudContext === 'azure') { headers.apikey = `${userKey}:23105:0`; } } apiResponse = await axios.get(costsUrl, { headers, params: costsParams }); break; case 'get_accounts': const accountsUrl = `${BASE_URL}/user-management/accounts`; apiResponse = await axios.get(accountsUrl, { headers }); break; case 'get_recommendations': const recsUrl = `${BASE_URL}/recommendationsNew/heatmap/summary`; const recsParams = {}; if (args.accountId) { recsParams.accountId = args.accountId; } else { recsParams.accountId = '932213950603'; } if (args.userQuery) recsParams.userQuery = args.userQuery; if (args.customer_account_key) { recsParams.customer_account_key = args.customer_account_key; // Update API key for customer account const userKey = session.apiKey.split(':')[0]; headers.apikey = `${userKey}:${args.customer_account_key}:0`; } apiResponse = await axios.post(recsUrl, recsParams, { headers }); break; default: return { content: [{ type: 'text', text: `❌ Unknown tool: ${toolName}` }] }; } // Format response based on tool let responseText; if (toolName === 'get_recommendations' && apiResponse.data) { const heatmapData = apiResponse.data; const potentialSavings = heatmapData.potentialAnnualSavings || 0; const actualSavings = heatmapData.actualAnnualSavings || 0; const totalSavings = Math.max(potentialSavings, actualSavings); responseText = `**💰 Total Potential Annual Savings:** $${totalSavings.toLocaleString()} **📊 Recommendations Available:** ${heatmapData.potentialSavingsRecommendationCount || 0}`; } else if (toolName === 'get_accounts' && apiResponse.data) { const accounts = apiResponse.data.accounts || []; responseText = `Found ${accounts.length} accounts:\n` + accounts.slice(0, 5).map(a => `- ${a.name} (Key: ${a.key})`).join('\n'); } else if (toolName === 'get_costs' && apiResponse.data) { const costs = apiResponse.data; responseText = `Cost data retrieved successfully`; } else { responseText = JSON.stringify(apiResponse.data, null, 2); } return { content: [{ type: 'text', text: responseText }] }; } catch (error) { console.error(`[SSE] Tool execution error:`, error.message); return { content: [{ type: 'text', text: `❌ Error: ${error.message}` }] }; } } // Start server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(` 🚀 Umbrella MCP Server (Final Version) ====================================== Server: http://localhost:${PORT} Login: http://localhost:${PORT}/login Health: http://localhost:${PORT}/health Features: ✅ No password storage ✅ Bearer JWT tokens (24hr expiry) ✅ MCP protocol support ✅ Demo mode for testing Demo credentials: Email: demo@test.com Password: demo `); });

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/daviddraiumbrella/invoice-monitoring'

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