#!/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
`);
});