Skip to main content
Glama

HANA Cloud MCP Server

by HatriGt
index.js19.9 kB
import express from 'express'; import cors from 'cors'; import fs from 'fs-extra'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { homedir } from 'os'; import { existsSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 3001; // Middleware app.use(cors()); app.use(express.json()); // Storage paths - ensure they work regardless of where the server is started from const UI_ROOT = dirname(__dirname); // This is the hana-mcp-ui directory const DATA_DIR = join(UI_ROOT, 'data'); const SERVERS_FILE = join(DATA_DIR, 'hana-servers.json'); const CONFIG_FILE = join(DATA_DIR, 'config.json'); const BACKUPS_DIR = join(DATA_DIR, 'backups'); const BACKUP_HISTORY_FILE = join(DATA_DIR, 'backup-history.json'); // Ensure data directory exists fs.ensureDirSync(DATA_DIR); fs.ensureDirSync(BACKUPS_DIR); // Default Claude config paths by OS const getDefaultClaudeConfigPath = () => { const platform = process.platform; switch (platform) { case 'darwin': return join(homedir(), 'Library/Application Support/Claude/claude_desktop_config.json'); case 'win32': return join(homedir(), 'AppData/Roaming/Claude/claude_desktop_config.json'); default: return join(homedir(), '.config/claude/claude_desktop_config.json'); } }; // Helper functions const loadServers = async () => { try { if (await fs.pathExists(SERVERS_FILE)) { return await fs.readJson(SERVERS_FILE); } return {}; } catch (error) { console.error('Error loading servers:', error); return {}; } }; const saveServers = async (servers) => { try { await fs.writeJson(SERVERS_FILE, servers, { spaces: 2 }); } catch (error) { console.error('Error saving servers:', error); throw error; } }; const loadConfig = async () => { try { if (await fs.pathExists(CONFIG_FILE)) { return await fs.readJson(CONFIG_FILE); } return {}; } catch (error) { console.error('Error loading config:', error); return {}; } }; const saveConfig = async (config) => { try { await fs.writeJson(CONFIG_FILE, config, { spaces: 2 }); } catch (error) { console.error('Error saving config:', error); throw error; } }; const loadClaudeConfig = async (configPath) => { try { if (await fs.pathExists(configPath)) { return await fs.readJson(configPath); } return { mcpServers: {} }; } catch (error) { console.error('Error loading Claude config:', error); return { mcpServers: {} }; } }; const saveClaudeConfig = async (configPath, config, skipBackup = false) => { try { // Create backup before saving (unless explicitly skipped) if (!skipBackup && await fs.pathExists(configPath)) { await createBackup(configPath, 'Auto backup before save'); } await fs.ensureDir(dirname(configPath)); await fs.writeJson(configPath, config, { spaces: 2 }); } catch (error) { console.error('Error saving Claude config:', error); throw error; } }; // Helper function to identify HANA MCP servers const isHanaMcpServer = (server) => { // Must have the correct command if (server.command !== 'hana-mcp-server') { return false; } // Must have HANA-specific environment variables for a complete HANA server if (!server.env) { return false; } const hasHanaHost = server.env.HANA_HOST; const hasHanaUser = server.env.HANA_USER; const hasHanaSchema = server.env.HANA_SCHEMA; // Must have all core HANA environment variables return hasHanaHost && hasHanaUser && hasHanaSchema; }; // Helper function to create composite server name const createCompositeServerName = (serverName, environment) => { return `${serverName} - ${environment}`; }; // Helper function to parse composite server name const parseCompositeServerName = (compositeName) => { const parts = compositeName.split(' - '); if (parts.length >= 2) { const environment = parts.pop(); // Last part is environment const serverName = parts.join(' - '); // Everything else is server name return { serverName, environment }; } return { serverName: compositeName, environment: null }; }; // Helper function to filter only HANA MCP servers from Claude config const filterHanaMcpServers = (mcpServers) => { const hanaServers = {}; for (const [name, server] of Object.entries(mcpServers || {})) { if (isHanaMcpServer(server)) { hanaServers[name] = server; } } return hanaServers; }; // Helper function to merge HANA servers while preserving non-HANA servers const mergeWithPreservation = (originalConfig, hanaServers) => { const newConfig = { ...originalConfig }; // Start with original mcpServers newConfig.mcpServers = { ...originalConfig.mcpServers }; // add new HANA servers newConfig.mcpServers = { ...newConfig.mcpServers, ...hanaServers }; return newConfig; }; // Backup management functions const loadBackupHistory = async () => { try { if (await fs.pathExists(BACKUP_HISTORY_FILE)) { return await fs.readJson(BACKUP_HISTORY_FILE); } return []; } catch (error) { console.error('Error loading backup history:', error); return []; } }; const saveBackupHistory = async (history) => { try { await fs.writeJson(BACKUP_HISTORY_FILE, history, { spaces: 2 }); } catch (error) { console.error('Error saving backup history:', error); throw error; } }; const createBackup = async (configPath, reason = 'Manual backup') => { try { if (!await fs.pathExists(configPath)) { throw new Error('Config file does not exist'); } const timestamp = new Date().toISOString(); const backupId = `backup_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; const backupFileName = `${backupId}.json`; const backupFilePath = join(BACKUPS_DIR, backupFileName); // Read and backup the config const config = await fs.readJson(configPath); await fs.writeJson(backupFilePath, config, { spaces: 2 }); // Create backup metadata const backupEntry = { id: backupId, timestamp, fileName: backupFileName, reason, size: JSON.stringify(config).length, mcpServerCount: Object.keys(config.mcpServers || {}).length, hanaServerCount: Object.keys(filterHanaMcpServers(config.mcpServers || {})).length }; // Update history const history = await loadBackupHistory(); history.unshift(backupEntry); // Add to beginning (most recent first) // Keep only last 50 backups if (history.length > 50) { const oldBackups = history.splice(50); // Delete old backup files for (const oldBackup of oldBackups) { const oldBackupPath = join(BACKUPS_DIR, oldBackup.fileName); if (await fs.pathExists(oldBackupPath)) { await fs.remove(oldBackupPath); } } } await saveBackupHistory(history); return backupEntry; } catch (error) { console.error('Error creating backup:', error); throw error; } }; const restoreBackup = async (backupId, configPath) => { try { const history = await loadBackupHistory(); const backup = history.find(b => b.id === backupId); if (!backup) { throw new Error('Backup not found'); } const backupFilePath = join(BACKUPS_DIR, backup.fileName); if (!await fs.pathExists(backupFilePath)) { throw new Error('Backup file not found'); } // Create a backup of current state before restoring await createBackup(configPath, `Before restoring to ${backup.timestamp}`); // Restore the backup const backupConfig = await fs.readJson(backupFilePath); await saveClaudeConfig(configPath, backupConfig, true); // Skip backup when restoring return backup; } catch (error) { console.error('Error restoring backup:', error); throw error; } }; // API Routes // Get Claude Desktop config path app.get('/api/claude/config-path', async (req, res) => { try { const config = await loadConfig(); res.json({ configPath: config.claudeConfigPath || null, defaultPath: getDefaultClaudeConfigPath() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Set Claude Desktop config path app.post('/api/claude/config-path', async (req, res) => { try { const { configPath } = req.body; if (!configPath) { return res.status(400).json({ error: 'Config path is required' }); } // Validate path exists or can be created const dir = dirname(configPath); await fs.ensureDir(dir); const config = await loadConfig(); config.claudeConfigPath = configPath; await saveConfig(config); res.json({ success: true, configPath }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all local HANA servers app.get('/api/hana-servers', async (req, res) => { try { const servers = await loadServers(); res.json(servers); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create new HANA server app.post('/api/hana-servers', async (req, res) => { try { const serverConfig = req.body; if (!serverConfig.name) { return res.status(400).json({ error: 'Server name is required' }); } const servers = await loadServers(); if (servers[serverConfig.name]) { return res.status(409).json({ error: 'Server with this name already exists' }); } // Add metadata serverConfig.created = new Date().toISOString(); serverConfig.modified = new Date().toISOString(); serverConfig.version = '1.0.0'; servers[serverConfig.name] = serverConfig; await saveServers(servers); res.status(201).json(serverConfig); } catch (error) { res.status(500).json({ error: error.message }); } }); // Update HANA server app.put('/api/hana-servers/:name', async (req, res) => { try { const { name } = req.params; const updatedConfig = req.body; const servers = await loadServers(); if (!servers[name]) { return res.status(404).json({ error: 'Server not found' }); } // Preserve creation date, update modified date updatedConfig.created = servers[name].created; updatedConfig.modified = new Date().toISOString(); servers[name] = updatedConfig; await saveServers(servers); res.json(updatedConfig); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete HANA server app.delete('/api/hana-servers/:name', async (req, res) => { try { const { name } = req.params; const servers = await loadServers(); if (!servers[name]) { return res.status(404).json({ error: 'Server not found' }); } delete servers[name]; await saveServers(servers); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Apply server to Claude Desktop app.post('/api/apply-to-claude', async (req, res) => { try { const { serverName, environment } = req.body; if (!serverName || !environment) { return res.status(400).json({ error: 'Server name and environment are required' }); } const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const servers = await loadServers(); const server = servers[serverName]; if (!server) { return res.status(404).json({ error: 'Server not found' }); } // Find environment with case-insensitive matching let envConfig = server.environments?.[environment]; let actualEnvironmentName = environment; if (!envConfig) { // Try case-insensitive matching const envKeys = Object.keys(server.environments || {}); const matchingKey = envKeys.find(key => key.toLowerCase() === environment.toLowerCase()); if (matchingKey) { envConfig = server.environments[matchingKey]; actualEnvironmentName = matchingKey; } else { return res.status(404).json({ error: 'Environment not found' }); } } const claudeConfig = await loadClaudeConfig(claudeConfigPath); // create a new HANA server const newHanaServer = { [serverName]: { command: 'hana-mcp-server', env: envConfig } }; // Merge while preserving non-HANA servers const updatedConfig = mergeWithPreservation(claudeConfig, newHanaServer); await saveClaudeConfig(claudeConfigPath, updatedConfig); res.json({ success: true, serverName, environment: actualEnvironmentName }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Remove server from Claude Desktop app.delete('/api/claude/:serverName', async (req, res) => { try { const { serverName } = req.params; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const serverToDelete = claudeConfig.mcpServers[serverName]; if (!serverToDelete) { return res.status(404).json({ error: 'Server not found in Claude config' }); } // Only delete if it's a HANA MCP server if (!isHanaMcpServer(serverToDelete)) { return res.status(400).json({ error: 'Cannot delete non-HANA MCP server' }); } delete claudeConfig.mcpServers[serverName]; await saveClaudeConfig(claudeConfigPath, claudeConfig); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get Claude Desktop servers app.get('/api/claude', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.json([]); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const claudeServers = []; // Filter only HANA MCP servers const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); for (const [serverName, server] of Object.entries(hanaServers)) { const serverData = { name: serverName, environment: server.env?.ENVIRONMENT || 'Development', env: server.env || {} }; claudeServers.push(serverData); } res.json(claudeServers); } catch (error) { console.error('Error loading Claude servers:', error); res.status(500).json({ error: error.message }); } }); // Get active environments app.get('/api/claude/active-environments', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.json({}); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const servers = await loadServers(); const activeEnvironments = {}; // Filter only HANA MCP servers const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); for (const [serverName, claudeServer] of Object.entries(hanaServers)) { if (servers[serverName]) { // Store the active environment for this server activeEnvironments[serverName] = claudeServer.env?.ENVIRONMENT || 'Development'; } } res.json(activeEnvironments); } catch (error) { res.status(500).json({ error: error.message }); } }); // Validate connection app.post('/api/validate-connection', async (req, res) => { try { const config = req.body; // Basic validation const required = ['HANA_HOST', 'HANA_USER', 'HANA_PASSWORD', 'HANA_SCHEMA']; for (const field of required) { if (!config[field]) { return res.status(400).json({ valid: false, error: `${field} is required` }); } } // For now, just validate required fields // In a real implementation, you could test the actual connection res.json({ valid: true }); } catch (error) { res.status(500).json({ valid: false, error: error.message }); } }); // Backup Management APIs // Get backup history app.get('/api/claude/backups', async (req, res) => { try { const history = await loadBackupHistory(); res.json(history); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create manual backup app.post('/api/claude/backups', async (req, res) => { try { const { reason = 'Manual backup' } = req.body; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const backup = await createBackup(claudeConfigPath, reason); res.json(backup); } catch (error) { res.status(500).json({ error: error.message }); } }); // Restore backup app.post('/api/claude/backups/:backupId/restore', async (req, res) => { try { const { backupId } = req.params; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const backup = await restoreBackup(backupId, claudeConfigPath); res.json({ success: true, backup }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete backup app.delete('/api/claude/backups/:backupId', async (req, res) => { try { const { backupId } = req.params; const history = await loadBackupHistory(); const backupIndex = history.findIndex(b => b.id === backupId); if (backupIndex === -1) { return res.status(404).json({ error: 'Backup not found' }); } const backup = history[backupIndex]; const backupFilePath = join(BACKUPS_DIR, backup.fileName); // Remove from history history.splice(backupIndex, 1); await saveBackupHistory(history); // Delete backup file if (await fs.pathExists(backupFilePath)) { await fs.remove(backupFilePath); } res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get backup details app.get('/api/claude/backups/:backupId', async (req, res) => { try { const { backupId } = req.params; const history = await loadBackupHistory(); const backup = history.find(b => b.id === backupId); if (!backup) { return res.status(404).json({ error: 'Backup not found' }); } const backupFilePath = join(BACKUPS_DIR, backup.fileName); if (!await fs.pathExists(backupFilePath)) { return res.status(404).json({ error: 'Backup file not found' }); } const backupConfig = await fs.readJson(backupFilePath); res.json({ ...backup, config: backupConfig }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Health check app.get('/api/status', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; let claudeConfigExists = false; if (claudeConfigPath) { claudeConfigExists = await fs.pathExists(claudeConfigPath); } res.json({ status: 'healthy', version: '1.0.0', claudeConfigPath, claudeConfigExists, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(PORT, () => { console.log(`🚀 HANA MCP UI Backend running on port ${PORT}`); });

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/HatriGt/hana-mcp-server'

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