This is page 1 of 3. Use http://codebase.md/hatrigt/hana-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .github │ └── pull_request_template.md ├── .gitignore ├── .npmignore ├── claude_template.json ├── docs │ ├── hana_mcp_architecture.svg │ └── hana_mcp_ui.gif ├── hana-mcp-server.js ├── hana-mcp-ui │ ├── .gitignore │ ├── bin │ │ └── cli.js │ ├── hana_mcp_ui.gif │ ├── index.html │ ├── logo.png │ ├── package.json │ ├── postcss.config.js │ ├── README.md │ ├── server │ │ └── index.js │ ├── src │ │ ├── App.jsx │ │ ├── components │ │ │ ├── ClaudeConfigTile.jsx │ │ │ ├── ClaudeDesktopView.jsx │ │ │ ├── ClaudeServerCard.jsx │ │ │ ├── ConfigurationModal.jsx │ │ │ ├── ConnectionDetailsModal.jsx │ │ │ ├── DashboardView.jsx │ │ │ ├── DatabaseListView.jsx │ │ │ ├── EnhancedServerCard.jsx │ │ │ ├── EnvironmentManager.jsx │ │ │ ├── EnvironmentSelector.jsx │ │ │ ├── layout │ │ │ │ ├── index.js │ │ │ │ └── VerticalSidebar.jsx │ │ │ ├── MainApp.jsx │ │ │ ├── PathConfigModal.jsx │ │ │ ├── PathSetupModal.jsx │ │ │ ├── SearchAndFilter.jsx │ │ │ └── ui │ │ │ ├── DatabaseTypeBadge.jsx │ │ │ ├── GlassCard.jsx │ │ │ ├── GlassWindow.jsx │ │ │ ├── GradientButton.jsx │ │ │ ├── IconComponent.jsx │ │ │ ├── index.js │ │ │ ├── LoadingSpinner.jsx │ │ │ ├── MetricCard.jsx │ │ │ ├── StatusBadge.jsx │ │ │ └── Tabs.jsx │ │ ├── index.css │ │ ├── main.jsx │ │ └── utils │ │ ├── cn.js │ │ ├── databaseTypes.js │ │ └── theme.js │ ├── start.js │ ├── tailwind.config.js │ └── vite.config.js ├── LICENSE ├── manifest.yml ├── package-lock.json ├── package.json ├── README.md ├── setup.sh ├── src │ ├── constants │ │ ├── mcp-constants.js │ │ └── tool-definitions.js │ ├── database │ │ ├── connection-manager.js │ │ ├── hana-client.js │ │ └── query-executor.js │ ├── server │ │ ├── index.js │ │ ├── lifecycle-manager.js │ │ └── mcp-handler.js │ ├── tools │ │ ├── config-tools.js │ │ ├── index-tools.js │ │ ├── index.js │ │ ├── query-tools.js │ │ ├── schema-tools.js │ │ └── table-tools.js │ └── utils │ ├── config.js │ ├── formatters.js │ ├── logger.js │ └── validators.js └── tests ├── automated │ └── test-mcp-inspector.js ├── manual │ └── manual-test.js ├── mcpInspector │ ├── mcp-inspector-config.json │ └── mcp-inspector-config.template.json ├── mcpTestingGuide.md └── README.md ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Server configuration 2 | MCP_TRANSPORT=http 3 | MCP_HOST=localhost 4 | MCP_PORT=3000 5 | 6 | # HANA connection configuration 7 | HANA_HOST=your-hana-host 8 | HANA_PORT=30015 9 | HANA_USER=your-username 10 | HANA_PASSWORD=your-password 11 | HANA_ENCRYPT=true 12 | HANA_VALIDATE_CERT=true 13 | 14 | # Security configuration 15 | MCP_READ_ONLY=true 16 | MCP_ALLOWED_SCHEMAS=SCHEMA1,SCHEMA2 17 | 18 | # Logging configuration 19 | LOG_LEVEL=info 20 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Production build 8 | dist/ 9 | build/ 10 | 11 | # Environment variables 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | # IDE 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | 24 | # OS 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Logs 29 | *.log 30 | logs/ 31 | 32 | # Runtime data 33 | pids/ 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Application data 39 | data/ 40 | 41 | # Temporary files 42 | tmp/ 43 | temp/ ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` 1 | # Development files 2 | tests/ 3 | docs/ 4 | setup.sh 5 | claude_template.json 6 | 7 | # Configuration files 8 | *.config.json 9 | *config*.json 10 | *credential*.json 11 | *secret*.json 12 | *password*.json 13 | *key*.json 14 | 15 | # Environment files 16 | .env* 17 | *.env 18 | 19 | # Logs 20 | *.log 21 | logs/ 22 | 23 | # Editor files 24 | .vscode/ 25 | .idea/ 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | # OS files 33 | .DS_Store 34 | Thumbs.db 35 | 36 | # Git 37 | .git/ 38 | .gitignore 39 | 40 | # Temporary files 41 | *.tmp 42 | *.temp 43 | *backup* 44 | *POC* 45 | 46 | # Build artifacts 47 | dist/ 48 | build/ 49 | coverage/ 50 | 51 | # Package manager files 52 | package-lock.json 53 | yarn.lock ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependency directories 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.production 11 | .env.staging 12 | 13 | # Security - Credential files 14 | *config*.json 15 | *credential*.json 16 | *secret*.json 17 | *password*.json 18 | *key*.json 19 | 20 | # Build output 21 | dist/ 22 | build/ 23 | coverage/ 24 | 25 | # Editor directories and files 26 | .idea/ 27 | .vscode/ 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | .DS_Store 34 | 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | 42 | # Runtime data 43 | pids 44 | *.pid 45 | *.seed 46 | *.pid.lock 47 | 48 | *claude_con* 49 | *POC 50 | *backup* 51 | image.png 52 | .qod* 53 | hana-mcp-ui/bun.lock 54 | hana-mcp-ui/bun.lockb 55 | ``` -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # HANA MCP Server Tests 2 | 3 | This folder contains various testing approaches for the HANA MCP Server. 4 | 5 | ## Folder Structure 6 | 7 | ``` 8 | tests/ 9 | ├── README.md # This file 10 | ├── mcpInspector/ # MCP Inspector configuration and setup 11 | │ └── mcp-inspector-config.json 12 | ├── manual/ # Manual testing scripts 13 | │ └── manual-test.js 14 | └── automated/ # Automated testing scripts 15 | └── test-mcp-inspector.js 16 | ``` 17 | 18 | ## Testing Approaches 19 | 20 | ### 1. MCP Inspector (Recommended) 21 | **Location**: `tests/mcpInspector/` 22 | 23 | The MCP Inspector provides a web-based UI for testing MCP servers. 24 | 25 | **Setup**: 26 | 1. Open https://modelcontextprotocol.io/inspector 27 | 2. Use the configuration from `mcp-inspector-config.json` 28 | 3. Connect and test tools interactively 29 | 30 | **Configuration**: 31 | - Command: `/opt/homebrew/bin/node` 32 | - Arguments: `/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js` 33 | - Environment variables: See `mcp-inspector-config.json` 34 | 35 | ### 2. Manual Testing 36 | **Location**: `tests/manual/` 37 | 38 | Interactive command-line testing with menu-driven interface. 39 | 40 | **Usage**: 41 | ```bash 42 | cd tests/manual 43 | node manual-test.js 44 | ``` 45 | 46 | ### 3. Automated Testing 47 | **Location**: `tests/automated/` 48 | 49 | Automated test suite that runs all tools and validates responses. 50 | 51 | **Usage**: 52 | ```bash 53 | cd tests/automated 54 | node test-mcp-inspector.js 55 | ``` 56 | 57 | ## Environment Variables Required 58 | 59 | All tests require these environment variables: 60 | - `HANA_HOST`: HANA database host 61 | - `HANA_PORT`: HANA database port (usually 443) 62 | - `HANA_USER`: HANA database username 63 | - `HANA_PASSWORD`: HANA database password 64 | - `HANA_SCHEMA`: HANA database schema 65 | - `HANA_SSL`: SSL enabled (true/false) 66 | - `HANA_ENCRYPT`: Encryption enabled (true/false) 67 | - `HANA_VALIDATE_CERT`: Certificate validation (true/false) 68 | 69 | ## Quick Start 70 | 71 | 1. **For interactive testing**: Use MCP Inspector 72 | 2. **For quick validation**: Run automated tests 73 | 3. **For debugging**: Use manual testing ``` -------------------------------------------------------------------------------- /hana-mcp-ui/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # HANA MCP UI 2 | 3 | [](https://www.npmjs.com/package/hana-mcp-ui) 4 | [](https://www.npmjs.com/package/hana-mcp-ui) 5 | [](https://nodejs.org/) 6 | [](LICENSE) 7 | 8 | > **Visual interface for managing HANA MCP server configurations with Claude Desktop integration** 9 | 10 | ## 🚀 Quick Start 11 | 12 | ### 1. Run the UI 13 | ```bash 14 | npx hana-mcp-ui 15 | ``` 16 | 17 | That's it! The UI will: 18 | - 📦 Install automatically (if not cached) 19 | - 🔧 Start the backend server on port 3001 20 | - ⚡ Start the React frontend on port 5173 21 | - 🌐 Open your browser automatically 22 | 23 | ### 2. First-Time Setup 24 | 25 | On first run, you'll be prompted to set your Claude Desktop config path: 26 | 27 | - **🍎 macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 28 | - **🪟 Windows**: `%APPDATA%\Claude/claude_desktop_config.json` 29 | - **🐧 Linux**: `~/.config/claude/claude_desktop_config.json` 30 | 31 | The system suggests the correct path for your OS. 32 | 33 | ## 🎯 What You Get 34 | 35 | ### Visual Database Management 36 | - **🌐 Web Interface**: Modern, responsive React UI 37 | - **🔄 Multi-Environment**: Configure Production, Development, Staging per server 38 | - **🤖 Claude Integration**: Deploy configurations directly to Claude Desktop 39 | - **📊 Real-time Status**: Monitor active and configured servers 40 | - **✅ Smart Validation**: Comprehensive form validation for database connections 41 | 42 | ### Key Features 43 | - **One-Click Deployment**: Add databases to Claude Desktop with a single click 44 | - **Environment Management**: Switch between different database environments 45 | - **Configuration Backup**: Automatic backups before making changes 46 | - **Connection Testing**: Test database connectivity before deployment 47 | - **Clean Interface**: Intuitive design with smooth animations 48 | 49 |  50 | 51 | ## 🛠️ How to Use 52 | 53 | ### 1. Add Database Configuration 54 | - Click **"+ Add Database"** 55 | - Enter database details (host, user, password, etc.) 56 | - Configure environments (Production, Development, Staging) 57 | 58 | ### 2. Add to Claude Desktop 59 | - Select a database from your list 60 | - Choose which environment to deploy 61 | - Click **"Add to Claude"** 62 | - Restart Claude Desktop to activate 63 | 64 | ### 3. Manage Active Connections 65 | - View all databases currently active in Claude 66 | - Remove connections when no longer needed 67 | - Monitor connection status 68 | 69 | ## ⚙️ Configuration Schema 70 | 71 | ### Required Fields 72 | | Parameter | Description | Example | 73 | |-----------|-------------|---------| 74 | | `HANA_HOST` | Database hostname or IP address | `hana.company.com` | 75 | | `HANA_USER` | Database username | `DBADMIN` | 76 | | `HANA_PASSWORD` | Database password | `your-secure-password` | 77 | 78 | ### Optional Fields 79 | | Parameter | Description | Default | Options | 80 | |-----------|-------------|---------|---------| 81 | | `HANA_PORT` | Database port | `443` | Any valid port number | 82 | | `HANA_SCHEMA` | Default schema name | - | Schema name | 83 | | `HANA_CONNECTION_TYPE` | Connection type | `auto` | `auto`, `single_container`, `mdc_system`, `mdc_tenant` | 84 | | `HANA_INSTANCE_NUMBER` | Instance number (MDC) | - | Instance number (e.g., `10`) | 85 | | `HANA_DATABASE_NAME` | Database name (MDC tenant) | - | Database name (e.g., `HQQ`) | 86 | | `HANA_SSL` | Enable SSL connection | `true` | `true`, `false` | 87 | | `HANA_ENCRYPT` | Enable encryption | `true` | `true`, `false` | 88 | | `HANA_VALIDATE_CERT` | Validate SSL certificates | `true` | `true`, `false` | 89 | | `LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` | 90 | | `ENABLE_FILE_LOGGING` | Enable file logging | `true` | `true`, `false` | 91 | | `ENABLE_CONSOLE_LOGGING` | Enable console logging | `false` | `true`, `false` | 92 | 93 | ### Database Connection Types 94 | 95 | #### 1. Single-Container Database 96 | Standard HANA database with single tenant. 97 | 98 | **Required**: `HANA_HOST`, `HANA_USER`, `HANA_PASSWORD` 99 | **Optional**: `HANA_PORT`, `HANA_SCHEMA` 100 | 101 | #### 2. MDC System Database 102 | Multi-tenant system database (manages tenants). 103 | 104 | **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_USER`, `HANA_PASSWORD` 105 | **Optional**: `HANA_SCHEMA` 106 | 107 | #### 3. MDC Tenant Database 108 | Multi-tenant tenant database (specific tenant). 109 | 110 | **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_DATABASE_NAME`, `HANA_USER`, `HANA_PASSWORD` 111 | **Optional**: `HANA_SCHEMA` 112 | 113 | #### Auto-Detection 114 | When `HANA_CONNECTION_TYPE` is set to `auto` (default), the server automatically detects the type: 115 | 116 | - If `HANA_INSTANCE_NUMBER` + `HANA_DATABASE_NAME` → **MDC Tenant** 117 | - If only `HANA_INSTANCE_NUMBER` → **MDC System** 118 | - If neither → **Single-Container** 119 | 120 | ## 🔌 Prerequisites 121 | 122 | Before using the UI, install the core server: 123 | 124 | ```bash 125 | npm install -g hana-mcp-server 126 | ``` 127 | 128 | The UI works as a management interface for the installed server. 129 | 130 | ## 🏗️ Architecture 131 | 132 | ### System Architecture 133 | 134 | ### Technology Stack 135 | - **Frontend**: React 19 with Vite build system 136 | - **Backend**: Express.js REST API 137 | - **Storage**: Local file system for configurations 138 | - **Integration**: Claude Desktop configuration management 139 | - **Styling**: Tailwind CSS with custom components 140 | - **Animations**: Framer Motion for smooth interactions 141 | - **Icons**: Heroicons for consistent iconography 142 | 143 | ### Component Architecture 144 | 145 | ``` 146 | hana-mcp-ui/ 147 | ├── 📁 bin/ 148 | │ └── cli.js # NPX entry point launcher 149 | ├── 📁 server/ 150 | │ └── index.js # Express backend server 151 | ├── 📁 src/ 152 | │ ├── main.jsx # React entry point 153 | │ ├── App.jsx # Main app component 154 | │ └── components/ 155 | │ ├── 🏠 MainApp.jsx # Main application container 156 | │ ├── 🎛️ ConfigurationModal.jsx # Server configuration modal 157 | │ ├── 📋 DatabaseListView.jsx # Database list management 158 | │ ├── 🤖 ClaudeDesktopView.jsx # Claude integration view 159 | │ ├── 📊 DashboardView.jsx # Dashboard overview 160 | │ ├── 🎯 EnvironmentSelector.jsx # Environment selection 161 | │ ├── 📱 VerticalSidebar.jsx # Navigation sidebar 162 | │ └── 🎨 ui/ # Reusable UI components 163 | │ ├── GlassWindow.jsx # Glass morphism container 164 | │ ├── StatusBadge.jsx # Status indicators 165 | │ ├── DatabaseTypeBadge.jsx # Database type badges 166 | │ └── LoadingSpinner.jsx # Loading states 167 | ├── 📁 dist/ # Built React app (production) 168 | ├── 📁 data/ # Local configuration storage 169 | ├── 📄 package.json # Dependencies and scripts 170 | ├── ⚙️ vite.config.js # Vite build configuration 171 | └── 🌐 index.html # HTML template 172 | ``` 173 | 174 | ## 📋 Requirements 175 | 176 | - **Node.js**: 18.0.0 or higher 177 | - **Claude Desktop**: For deployment features 178 | - **Browser**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ 179 | 180 | ## 🔧 Development 181 | 182 | ### Local Development 183 | ```bash 184 | git clone https://github.com/hatrigt/hana-mcp-server.git 185 | cd hana-mcp-server/hana-mcp-ui 186 | npm install 187 | npm run dev 188 | ``` 189 | 190 | ### Build for Production 191 | ```bash 192 | npm run build 193 | npm run preview 194 | ``` 195 | 196 | ## 🚀 Performance 197 | 198 | - **Startup**: < 5 seconds 199 | - **API Response**: < 500ms 200 | - **UI Interactions**: < 100ms 201 | - **Bundle Size**: ~264KB (gzipped: ~83KB) 202 | 203 | ## 🔒 Security 204 | 205 | - **Local-only API** (no external connections) 206 | - **Secure file access** patterns 207 | - **Automatic backups** before configuration changes 208 | - **Password masking** in UI forms 209 | 210 | ## 🤝 Support 211 | 212 | - **Issues**: [GitHub Issues](https://github.com/hatrigt/hana-mcp-server/issues) 213 | - **Main Package**: [HANA MCP Server](https://www.npmjs.com/package/hana-mcp-server) 214 | - **Documentation**: [Full Documentation](https://github.com/hatrigt/hana-mcp-server#readme) 215 | 216 | ## 📄 License 217 | 218 | MIT License - see LICENSE file for details. ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # HANA MCP Server 2 | 3 | [](https://www.npmjs.com/package/hana-mcp-server) 4 | [](https://www.npmjs.com/package/hana-mcp-server) 5 | [](https://nodejs.org/) 6 | [](LICENSE) 7 | [](https://modelcontextprotocol.io/) 8 | 9 | > **Model Context Protocol (MCP) server for seamless SAP HANA database integration with AI agents and development tools.** 10 | 11 | ## 🚀 Quick Start 12 | 13 | ### 1. Install 14 | ```bash 15 | npm install -g hana-mcp-server 16 | ``` 17 | 18 | ### 2. Configure Claude Desktop 19 | 20 | Update your Claude Desktop config file: 21 | 22 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 23 | **Windows**: `%APPDATA%\claude\claude_desktop_config.json` 24 | **Linux**: `~/.config/claude/claude_desktop_config.json` 25 | 26 | ```json 27 | { 28 | "mcpServers": { 29 | "HANA Database": { 30 | "command": "hana-mcp-server", 31 | "env": { 32 | "HANA_HOST": "your-hana-host.com", 33 | "HANA_PORT": "443", 34 | "HANA_USER": "your-username", 35 | "HANA_PASSWORD": "your-password", 36 | "HANA_SCHEMA": "your-schema", 37 | "HANA_SSL": "true", 38 | "HANA_ENCRYPT": "true", 39 | "HANA_VALIDATE_CERT": "true", 40 | "HANA_CONNECTION_TYPE": "auto", 41 | "HANA_INSTANCE_NUMBER": "10", 42 | "HANA_DATABASE_NAME": "HQQ", 43 | "LOG_LEVEL": "info", 44 | "ENABLE_FILE_LOGGING": "true", 45 | "ENABLE_CONSOLE_LOGGING": "false" 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ### 3. Restart Claude Desktop 53 | 54 | Close and reopen Claude Desktop to load the configuration. 55 | 56 | ### 4. Test It! 57 | 58 | Ask Claude: *"Show me the available schemas in my HANA database"* 59 | 60 | ## 🎯 What You Get 61 | 62 | ### Database Operations 63 | - **Schema Exploration**: List schemas, tables, and table structures 64 | - **Query Execution**: Run SQL queries with natural language 65 | - **Data Sampling**: Get sample data from tables 66 | - **System Information**: Monitor database status and performance 67 | 68 | ### AI Integration 69 | - **Natural Language Queries**: "Show me all tables in the SYSTEM schema" 70 | - **Query Building**: "Create a query to find customers with orders > $1000" 71 | - **Data Analysis**: "Get sample data from the ORDERS table" 72 | - **Schema Navigation**: "Describe the structure of table CUSTOMERS" 73 | 74 | ## 🖥️ Visual Configuration (Recommended) 75 | 76 | For easier setup and management, use the **HANA MCP UI**: 77 | 78 | ```bash 79 | npx hana-mcp-ui 80 | ``` 81 | 82 | This opens a web interface where you can: 83 | - Configure multiple database environments 84 | - Deploy configurations to Claude Desktop with one click 85 | - Manage active connections 86 | - Test database connectivity 87 | 88 |  89 | 90 | ## 🛠️ Configuration Options 91 | 92 | ### Required Parameters 93 | | Parameter | Description | Example | 94 | |-----------|-------------|---------| 95 | | `HANA_HOST` | Database hostname or IP address | `hana.company.com` | 96 | | `HANA_USER` | Database username | `DBADMIN` | 97 | | `HANA_PASSWORD` | Database password | `your-secure-password` | 98 | 99 | ### Optional Parameters 100 | | Parameter | Description | Default | Options | 101 | |-----------|-------------|---------|---------| 102 | | `HANA_PORT` | Database port | `443` | Any valid port number | 103 | | `HANA_SCHEMA` | Default schema name | - | Schema name | 104 | | `HANA_CONNECTION_TYPE` | Connection type | `auto` | `auto`, `single_container`, `mdc_system`, `mdc_tenant` | 105 | | `HANA_INSTANCE_NUMBER` | Instance number (MDC) | - | Instance number (e.g., `10`) | 106 | | `HANA_DATABASE_NAME` | Database name (MDC tenant) | - | Database name (e.g., `HQQ`) | 107 | | `HANA_SSL` | Enable SSL connection | `true` | `true`, `false` | 108 | | `HANA_ENCRYPT` | Enable encryption | `true` | `true`, `false` | 109 | | `HANA_VALIDATE_CERT` | Validate SSL certificates | `true` | `true`, `false` | 110 | | `LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` | 111 | | `ENABLE_FILE_LOGGING` | Enable file logging | `true` | `true`, `false` | 112 | | `ENABLE_CONSOLE_LOGGING` | Enable console logging | `false` | `true`, `false` | 113 | 114 | ### Database Connection Types 115 | 116 | #### 1. Single-Container Database 117 | Standard HANA database with single tenant. 118 | 119 | **Required**: `HANA_HOST`, `HANA_USER`, `HANA_PASSWORD` 120 | **Optional**: `HANA_PORT`, `HANA_SCHEMA` 121 | 122 | ```json 123 | { 124 | "HANA_HOST": "hana.company.com", 125 | "HANA_PORT": "443", 126 | "HANA_USER": "DBADMIN", 127 | "HANA_PASSWORD": "password", 128 | "HANA_SCHEMA": "SYSTEM", 129 | "HANA_CONNECTION_TYPE": "single_container" 130 | } 131 | ``` 132 | 133 | #### 2. MDC System Database 134 | Multi-tenant system database (manages tenants). 135 | 136 | **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_USER`, `HANA_PASSWORD` 137 | **Optional**: `HANA_SCHEMA` 138 | 139 | ```json 140 | { 141 | "HANA_HOST": "192.168.1.100", 142 | "HANA_PORT": "31013", 143 | "HANA_INSTANCE_NUMBER": "10", 144 | "HANA_USER": "SYSTEM", 145 | "HANA_PASSWORD": "password", 146 | "HANA_SCHEMA": "SYSTEM", 147 | "HANA_CONNECTION_TYPE": "mdc_system" 148 | } 149 | ``` 150 | 151 | #### 3. MDC Tenant Database 152 | Multi-tenant tenant database (specific tenant). 153 | 154 | **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_DATABASE_NAME`, `HANA_USER`, `HANA_PASSWORD` 155 | **Optional**: `HANA_SCHEMA` 156 | 157 | ```json 158 | { 159 | "HANA_HOST": "192.168.1.100", 160 | "HANA_PORT": "31013", 161 | "HANA_INSTANCE_NUMBER": "10", 162 | "HANA_DATABASE_NAME": "HQQ", 163 | "HANA_USER": "DBADMIN", 164 | "HANA_PASSWORD": "password", 165 | "HANA_SCHEMA": "SYSTEM", 166 | "HANA_CONNECTION_TYPE": "mdc_tenant" 167 | } 168 | ``` 169 | 170 | #### Auto-Detection 171 | When `HANA_CONNECTION_TYPE` is set to `auto` (default), the server automatically detects the type: 172 | 173 | - If `HANA_INSTANCE_NUMBER` + `HANA_DATABASE_NAME` → **MDC Tenant** 174 | - If only `HANA_INSTANCE_NUMBER` → **MDC System** 175 | - If neither → **Single-Container** 176 | 177 | ## 🏗️ Architecture 178 | 179 | ### System Architecture 180 | 181 |  182 | 183 | ### Component Structure 184 | 185 | ``` 186 | hana-mcp-server/ 187 | ├── 📁 src/ 188 | │ ├── 🏗️ server/ # MCP Protocol & Server Management 189 | │ │ ├── index.js # Main server entry point 190 | │ │ ├── mcp-handler.js # JSON-RPC 2.0 implementation 191 | │ │ └── lifecycle-manager.js # Server lifecycle management 192 | │ ├── 🛠️ tools/ # Tool Implementations 193 | │ │ ├── index.js # Tool registry & discovery 194 | │ │ ├── config-tools.js # Configuration management 195 | │ │ ├── schema-tools.js # Schema exploration 196 | │ │ ├── table-tools.js # Table operations 197 | │ │ ├── index-tools.js # Index management 198 | │ │ └── query-tools.js # Query execution 199 | │ ├── 🗄️ database/ # Database Layer 200 | │ │ ├── hana-client.js # HANA client wrapper 201 | │ │ ├── connection-manager.js # Connection management 202 | │ │ └── query-executor.js # Query execution utilities 203 | │ ├── 🔧 utils/ # Shared Utilities 204 | │ │ ├── logger.js # Structured logging 205 | │ │ ├── config.js # Configuration management 206 | │ │ ├── validators.js # Input validation 207 | │ │ └── formatters.js # Response formatting 208 | │ └── 📋 constants/ # Constants & Definitions 209 | │ ├── mcp-constants.js # MCP protocol constants 210 | │ └── tool-definitions.js # Tool schemas 211 | ├── 🧪 tests/ # Testing Framework 212 | ├── 📚 docs/ # Documentation 213 | ├── 📦 package.json # Dependencies & Scripts 214 | └── 🚀 hana-mcp-server.js # Main entry point 215 | ``` 216 | 217 | ## 📚 Available Commands 218 | 219 | Once configured, you can ask Claude to: 220 | 221 | - *"List all schemas in the database"* 222 | - *"Show me tables in the SYSTEM schema"* 223 | - *"Describe the CUSTOMERS table structure"* 224 | - *"Execute: SELECT * FROM SYSTEM.TABLES LIMIT 10"* 225 | - *"Get sample data from ORDERS table"* 226 | - *"Count rows in CUSTOMERS table"* 227 | 228 | ## 🔧 Troubleshooting 229 | 230 | ### Connection Issues 231 | - **"Connection refused"**: Check HANA host and port 232 | - **"Authentication failed"**: Verify username/password 233 | - **"SSL certificate error"**: Set `HANA_VALIDATE_CERT=false` or install valid certificates 234 | 235 | ### Debug Mode 236 | ```bash 237 | export LOG_LEVEL="debug" 238 | export ENABLE_CONSOLE_LOGGING="true" 239 | hana-mcp-server 240 | ``` 241 | 242 | ## 📦 Package Info 243 | 244 | - **Size**: 21.7 kB 245 | - **Dependencies**: @sap/hana-client, axios 246 | - **Node.js**: 18+ required 247 | - **Platforms**: macOS, Linux, Windows 248 | 249 | ## 🤝 Support 250 | 251 | - **Issues**: [GitHub Issues](https://github.com/hatrigt/hana-mcp-server/issues) 252 | - **UI Tool**: [HANA MCP UI](https://www.npmjs.com/package/hana-mcp-ui) 253 | 254 | ## 📄 License 255 | 256 | MIT License - see [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /hana-mcp-ui/postcss.config.js: -------------------------------------------------------------------------------- ```javascript 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/layout/index.js: -------------------------------------------------------------------------------- ```javascript 1 | // Layout Components Exports 2 | export { default as VerticalSidebar } from './VerticalSidebar' ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/cn.js: -------------------------------------------------------------------------------- ```javascript 1 | import { clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)) 6 | } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/main.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | <StrictMode> 8 | <App /> 9 | </StrictMode>, 10 | ) ``` -------------------------------------------------------------------------------- /hana-mcp-ui/vite.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | port: 5173, 8 | host: '0.0.0.0', 9 | strictPort: true 10 | }, 11 | build: { 12 | outDir: 'dist' 13 | } 14 | }) ``` -------------------------------------------------------------------------------- /hana-mcp-server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * HANA MCP Server - Main Entry Point 5 | * 6 | * This is a thin wrapper that starts the modular MCP server. 7 | * The actual implementation is in src/server/index.js 8 | */ 9 | 10 | // Start the modular server 11 | require('./src/server/index.js'); ``` -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- ```yaml 1 | applications: 2 | - name: hana-mcp-server 3 | memory: 256M 4 | disk_quota: 512M 5 | instances: 1 6 | buildpack: nodejs_buildpack 7 | command: node hana-mcp-server.js 8 | env: 9 | NODE_ENV: production 10 | PORT: 8080 11 | services: 12 | - hana-service # Your HANA service instance name ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown 1 | ## 📝 Description 2 | 3 | Brief description of changes 4 | 5 | ## 🔄 Type of Change 6 | 7 | - [ ] Bug fix 8 | - [ ] New feature 9 | - [ ] Documentation update 10 | - [ ] Performance improvement 11 | 12 | ## 🧪 Testing 13 | 14 | - [ ] MCP Inspector tests pass 15 | - [ ] Manual testing completed 16 | - [ ] No breaking changes 17 | 18 | ## ✅ Checklist 19 | 20 | - [ ] Code follows existing patterns 21 | - [ ] Self-review completed 22 | - [ ] Documentation updated 23 | - [ ] No breaking changes 24 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/App.jsx: -------------------------------------------------------------------------------- ```javascript 1 | 2 | import { Toaster } from 'react-hot-toast' 3 | import MainApp from './components/MainApp' 4 | 5 | function App() { 6 | return ( 7 | <> 8 | <MainApp /> 9 | 10 | <Toaster 11 | position="top-right" 12 | toastOptions={{ 13 | duration: 4000, 14 | style: { 15 | background: '#363636', 16 | color: '#fff', 17 | borderRadius: '8px', 18 | }, 19 | }} 20 | /> 21 | </> 22 | ) 23 | } 24 | 25 | export default App ``` -------------------------------------------------------------------------------- /tests/mcpInspector/mcp-inspector-config.template.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "HANA Database": { 4 | "command": "/opt/homebrew/bin/node", 5 | "args": [ 6 | "/path/to/your/hana-mcp-server/hana-mcp-server.js" 7 | ], 8 | "env": { 9 | "HANA_HOST": "your-hana-host.com", 10 | "HANA_PORT": "443", 11 | "HANA_USER": "your-username", 12 | "HANA_PASSWORD": "your-password", 13 | "HANA_SCHEMA": "your-schema", 14 | "HANA_SSL": "true", 15 | "HANA_ENCRYPT": "true", 16 | "HANA_VALIDATE_CERT": "true" 17 | } 18 | } 19 | } 20 | } ``` -------------------------------------------------------------------------------- /tests/mcpInspector/mcp-inspector-config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "HANA Database": { 4 | "command": "/opt/homebrew/bin/node", 5 | "args": [ 6 | "/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js" 7 | ], 8 | "env": { 9 | "HANA_HOST": "your-hana-host.com", 10 | "HANA_PORT": "443", 11 | "HANA_USER": "your-username", 12 | "HANA_PASSWORD": "your-password", 13 | "HANA_SCHEMA": "your-schema", 14 | "HANA_SSL": "true", 15 | "HANA_ENCRYPT": "true", 16 | "HANA_VALIDATE_CERT": "true" 17 | } 18 | } 19 | } 20 | } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/index.html: -------------------------------------------------------------------------------- ```html 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <link rel="icon" type="image/png" href="/logo.png" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 | <title>HANA MCP UI - Database Configuration Manager</title> 8 | <style> 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif; 13 | background: #f8fafc; 14 | } 15 | </style> 16 | </head> 17 | <body> 18 | <div id="root"></div> 19 | <script type="module" src="/src/main.jsx"></script> 20 | </body> 21 | </html> ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/index.js: -------------------------------------------------------------------------------- ```javascript 1 | // UI Components Exports 2 | export { default as GlassCard } from './GlassCard' 3 | export { default as GlassWindow } from './GlassWindow' 4 | export { default as GradientButton } from './GradientButton' 5 | export { default as StatusBadge, EnvironmentBadge } from './StatusBadge' 6 | export { default as LoadingSpinner, LoadingOverlay } from './LoadingSpinner' 7 | export { default as MetricCard } from './MetricCard' 8 | export { default as IconComponent } from './IconComponent' 9 | export { default as Tabs } from './Tabs' 10 | export { default as DatabaseTypeBadge } from './DatabaseTypeBadge' 11 | export { default as PathConfigModal } from '../PathConfigModal' 12 | 13 | ``` -------------------------------------------------------------------------------- /claude_template.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "HANA Database": { 4 | "command": "/opt/homebrew/bin/node", 5 | "args": [ 6 | "/path/to/hana-mcp-server/hana-mcp-server.js" 7 | ], 8 | "env": { 9 | "HANA_HOST": "your-hana-host.com", 10 | "HANA_PORT": "443", 11 | "HANA_USER": "your-username", 12 | "HANA_PASSWORD": "your-password", 13 | "HANA_SCHEMA": "your-schema", 14 | "HANA_INSTANCE_NUMBER": "10", 15 | "HANA_DATABASE_NAME": "HQQ", 16 | "HANA_CONNECTION_TYPE": "auto", 17 | "HANA_SSL": "true", 18 | "LOG_LEVEL": "info", 19 | "ENABLE_FILE_LOGGING": "true", 20 | "ENABLE_CONSOLE_LOGGING": "false" 21 | } 22 | } 23 | } 24 | } ``` -------------------------------------------------------------------------------- /src/tools/schema-tools.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Schema exploration tools for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const QueryExecutor = require('../database/query-executor'); 7 | const Formatters = require('../utils/formatters'); 8 | 9 | class SchemaTools { 10 | /** 11 | * List all schemas 12 | */ 13 | static async listSchemas(args) { 14 | logger.tool('hana_list_schemas'); 15 | 16 | try { 17 | const schemas = await QueryExecutor.getSchemas(); 18 | const formattedSchemas = Formatters.formatSchemaList(schemas); 19 | 20 | return Formatters.createResponse(formattedSchemas); 21 | } catch (error) { 22 | logger.error('Error listing schemas:', error.message); 23 | return Formatters.createErrorResponse('Error listing schemas', error.message); 24 | } 25 | } 26 | } 27 | 28 | module.exports = SchemaTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/MetricCard.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { cn } from '../../utils/cn' 3 | 4 | const MetricCard = ({ 5 | title, 6 | value, 7 | className 8 | }) => { 9 | return ( 10 | <motion.div 11 | className={cn( 12 | 'bg-white border border-gray-100 rounded-xl overflow-hidden', 13 | 'hover:border-gray-200 transition-all duration-300', 14 | className 15 | )} 16 | whileHover={{ y: -1 }} 17 | transition={{ duration: 0.2 }} 18 | > 19 | <div className="border-b border-gray-50 px-4 py-3"> 20 | <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{title}</h3> 21 | </div> 22 | <div className="px-4 py-4"> 23 | <div className="flex items-baseline"> 24 | <p className="text-2xl font-semibold text-gray-900">{value}</p> 25 | </div> 26 | </div> 27 | </motion.div> 28 | ) 29 | } 30 | 31 | export default MetricCard ``` -------------------------------------------------------------------------------- /hana-mcp-ui/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "hana-mcp-ui", 3 | "version": "1.0.9", 4 | "description": "UI for managing HANA MCP server configurations with Claude Desktop integration", 5 | "type": "module", 6 | "main": "bin/cli.js", 7 | "bin": { 8 | "hana-mcp-ui": "bin/cli.js" 9 | }, 10 | "engines": { 11 | "node": ">=18.0.0" 12 | }, 13 | "scripts": { 14 | "dev": "node start.js", 15 | "dev:safe": "./start-dev.sh", 16 | "dev:old": "node bin/cli.js", 17 | "vite": "vite", 18 | "build": "bun run vite build", 19 | "preview": "bun run vite preview" 20 | }, 21 | "dependencies": { 22 | "@heroicons/react": "^2.2.0", 23 | "@tailwindcss/forms": "^0.5.7", 24 | "@vitejs/plugin-react": "^4.7.0", 25 | "autoprefixer": "^10.4.16", 26 | "axios": "^1.11.0", 27 | "chalk": "^5.5.0", 28 | "clsx": "^2.0.0", 29 | "cors": "^2.8.5", 30 | "express": "^5.1.0", 31 | "framer-motion": "^12.23.12", 32 | "fs-extra": "^11.3.1", 33 | "open": "^10.2.0", 34 | "postcss": "^8.4.32", 35 | "react": "^19.1.1", 36 | "react-dom": "^19.1.1", 37 | "react-hot-toast": "^2.5.2", 38 | "tailwind-merge": "^2.3.0", 39 | "tailwindcss": "^3.4.0", 40 | "vite": "^7.1.3" 41 | }, 42 | "keywords": [ 43 | "hana", 44 | "mcp", 45 | "claude", 46 | "database", 47 | "ui", 48 | "react", 49 | "management" 50 | ], 51 | "author": "HANA MCP Team", 52 | "license": "MIT" 53 | } 54 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/Tabs.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { cn } from '../../utils/cn'; 3 | 4 | const Tabs = ({ 5 | tabs, 6 | activeTab, 7 | onChange, 8 | className 9 | }) => { 10 | return ( 11 | <div className={cn("border-b border-gray-200", className)}> 12 | <nav className="flex -mb-px space-x-6"> 13 | {tabs.map((tab) => ( 14 | <button 15 | key={tab.id} 16 | onClick={() => onChange(tab.id)} 17 | className={cn( 18 | "py-3 px-4 border-b-2 font-medium text-sm whitespace-nowrap transition-all", 19 | activeTab === tab.id 20 | ? "border-blue-600 text-blue-700 bg-blue-50/50" 21 | : "border-transparent text-gray-500 hover:text-gray-800 hover:border-gray-300 hover:bg-gray-50/50" 22 | )} 23 | aria-current={activeTab === tab.id ? "page" : undefined} 24 | > 25 | {tab.icon && ( 26 | <span className="mr-2">{tab.icon}</span> 27 | )} 28 | {tab.label} 29 | {tab.count !== undefined && ( 30 | <span className={cn( 31 | "ml-2 py-0.5 px-2 rounded-full text-xs font-semibold", 32 | activeTab === tab.id 33 | ? "bg-blue-100 text-blue-700" 34 | : "bg-gray-100 text-gray-600" 35 | )}> 36 | {tab.count} 37 | </span> 38 | )} 39 | </button> 40 | ))} 41 | </nav> 42 | </div> 43 | ); 44 | }; 45 | 46 | export default Tabs; 47 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/IconComponent.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import React from 'react'; 2 | import { cn } from '../../utils/cn'; 3 | 4 | /** 5 | * IconComponent - A standardized wrapper for icons 6 | * 7 | * @param {Object} props - Component props 8 | * @param {React.ElementType} props.icon - The icon component to render 9 | * @param {string} props.size - Size of the icon (sm, md, lg) 10 | * @param {string} props.variant - Visual variant (default, primary, secondary, etc.) 11 | * @param {string} props.className - Additional CSS classes 12 | * @param {string} props.label - Accessibility label (for icon-only buttons) 13 | * @returns {JSX.Element} - Rendered icon component 14 | */ 15 | const IconComponent = ({ 16 | icon: Icon, 17 | size = 'md', 18 | variant = 'default', 19 | className, 20 | label, 21 | ...props 22 | }) => { 23 | // Size mappings 24 | const sizes = { 25 | xs: 'w-3 h-3', 26 | sm: 'w-4 h-4', 27 | md: 'w-5 h-5', 28 | lg: 'w-6 h-6', 29 | xl: 'w-8 h-8' 30 | }; 31 | 32 | // Variant mappings 33 | const variants = { 34 | default: 'text-gray-600', 35 | primary: 'text-[#86a0ff]', 36 | secondary: 'text-gray-500', 37 | success: 'text-green-600', 38 | warning: 'text-amber-600', 39 | danger: 'text-red-500', 40 | white: 'text-white' 41 | }; 42 | 43 | // If no Icon is provided, return null 44 | if (!Icon) return null; 45 | 46 | return ( 47 | <Icon 48 | className={cn( 49 | sizes[size], 50 | variants[variant], 51 | className 52 | )} 53 | aria-label={label} 54 | aria-hidden={!label} 55 | role={label ? 'img' : undefined} 56 | {...props} 57 | /> 58 | ); 59 | }; 60 | 61 | export default IconComponent; 62 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "hana-mcp-server", 3 | "version": "0.1.4", 4 | "description": "🚀 Easy-to-use MCP server for SAP HANA database integration with AI agents like Claude Desktop. Connect to HANA databases with natural language queries.", 5 | "main": "hana-mcp-server.js", 6 | "bin": { 7 | "hana-mcp-server": "hana-mcp-server.js" 8 | }, 9 | "scripts": { 10 | "start": "node hana-mcp-server.js", 11 | "dev": "nodemon hana-mcp-server.js", 12 | "test": "node tests/automated/test-mcp-inspector.js" 13 | }, 14 | "keywords": [ 15 | "sap", 16 | "hana", 17 | "mcp", 18 | "btp", 19 | "hana-mcp", 20 | "mcp-server", 21 | "database", 22 | "ai", 23 | "model", 24 | "context", 25 | "protocol", 26 | "claude", 27 | "anthropic", 28 | "enterprise", 29 | "sql", 30 | "query" 31 | ], 32 | "author": { 33 | "name": "HANA MCP Server Contributors", 34 | "email": "[email protected]" 35 | }, 36 | "license": "MIT", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/hatrigt/hana-mcp-server.git" 40 | }, 41 | "homepage": "https://github.com/hatrigt/hana-mcp-server#readme", 42 | "bugs": { 43 | "url": "https://github.com/hatrigt/hana-mcp-server/issues" 44 | }, 45 | "readme": "README.md", 46 | "engines": { 47 | "node": ">=18.0.0" 48 | }, 49 | "os": [ 50 | "darwin", 51 | "linux", 52 | "win32" 53 | ], 54 | "dependencies": { 55 | "@sap/hana-client": "^2.17.22", 56 | "axios": "^1.12.2" 57 | }, 58 | "devDependencies": { 59 | "nodemon": "^3.0.2" 60 | }, 61 | "files": [ 62 | "hana-mcp-server.js", 63 | "src/", 64 | "README.md", 65 | "LICENSE" 66 | ] 67 | } 68 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/LoadingSpinner.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { cn } from '../../utils/cn' 3 | 4 | const LoadingSpinner = ({ 5 | size = 'md', 6 | color = 'white', 7 | className 8 | }) => { 9 | const sizes = { 10 | sm: 'w-4 h-4', 11 | md: 'w-6 h-6', 12 | lg: 'w-8 h-8', 13 | xl: 'w-12 h-12' 14 | } 15 | 16 | const colors = { 17 | white: 'border-gray-200 border-t-blue-600', 18 | primary: 'border-blue-200 border-t-blue-600', 19 | success: 'border-emerald-200 border-t-emerald-600', 20 | warning: 'border-amber-200 border-t-amber-600', 21 | danger: 'border-red-200 border-t-red-600' 22 | } 23 | 24 | return ( 25 | <motion.div 26 | className={cn( 27 | 'inline-block rounded-full border-2', 28 | sizes[size], 29 | colors[color] || colors.white, 30 | className 31 | )} 32 | animate={{ rotate: 360 }} 33 | transition={{ duration: 1, repeat: Infinity, ease: "linear" }} 34 | /> 35 | ) 36 | } 37 | 38 | // Full page loading overlay 39 | export const LoadingOverlay = ({ message = "Loading..." }) => ( 40 | <motion.div 41 | className="fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center" 42 | initial={{ opacity: 0 }} 43 | animate={{ opacity: 1 }} 44 | exit={{ opacity: 0 }} 45 | > 46 | <motion.div 47 | className="glass-card p-8 text-center" 48 | initial={{ scale: 0.8, opacity: 0 }} 49 | animate={{ scale: 1, opacity: 1 }} 50 | transition={{ delay: 0.1 }} 51 | > 52 | <LoadingSpinner size="xl" color="primary" className="mx-auto mb-4" /> 53 | <p className="text-gray-700">{message}</p> 54 | </motion.div> 55 | </motion.div> 56 | ) 57 | 58 | export default LoadingSpinner ``` -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Centralized logging utility for HANA MCP Server 3 | * Uses console.error to avoid interfering with JSON-RPC stdout 4 | */ 5 | 6 | const LOG_LEVELS = { 7 | ERROR: 0, 8 | WARN: 1, 9 | INFO: 2, 10 | DEBUG: 3 11 | }; 12 | 13 | const LOG_LEVEL_NAMES = { 14 | 0: 'ERROR', 15 | 1: 'WARN', 16 | 2: 'INFO', 17 | 3: 'DEBUG' 18 | }; 19 | 20 | class Logger { 21 | constructor(level = 'INFO') { 22 | this.level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO; 23 | this.prefix = '[HANA MCP Server]'; 24 | } 25 | 26 | _log(level, message, ...args) { 27 | if (level <= this.level) { 28 | const timestamp = new Date().toISOString(); 29 | const levelName = LOG_LEVEL_NAMES[level]; 30 | const formattedMessage = `${this.prefix} ${timestamp} [${levelName}]: ${message}`; 31 | 32 | // Use console.error to avoid interfering with JSON-RPC stdout 33 | console.error(formattedMessage, ...args); 34 | } 35 | } 36 | 37 | error(message, ...args) { 38 | this._log(LOG_LEVELS.ERROR, message, ...args); 39 | } 40 | 41 | warn(message, ...args) { 42 | this._log(LOG_LEVELS.WARN, message, ...args); 43 | } 44 | 45 | info(message, ...args) { 46 | this._log(LOG_LEVELS.INFO, message, ...args); 47 | } 48 | 49 | debug(message, ...args) { 50 | this._log(LOG_LEVELS.DEBUG, message, ...args); 51 | } 52 | 53 | // Convenience method for method calls 54 | method(methodName, ...args) { 55 | this.info(`Handling method: ${methodName}`, ...args); 56 | } 57 | 58 | // Convenience method for tool calls 59 | tool(toolName, ...args) { 60 | this.info(`Calling tool: ${toolName}`, ...args); 61 | } 62 | } 63 | 64 | // Create default logger instance 65 | const logger = new Logger(process.env.LOG_LEVEL || 'INFO'); 66 | 67 | module.exports = { Logger, logger }; ``` -------------------------------------------------------------------------------- /src/tools/query-tools.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Query execution tools for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const QueryExecutor = require('../database/query-executor'); 7 | const Validators = require('../utils/validators'); 8 | const Formatters = require('../utils/formatters'); 9 | 10 | class QueryTools { 11 | /** 12 | * Execute a custom SQL query 13 | */ 14 | static async executeQuery(args) { 15 | logger.tool('hana_execute_query', args); 16 | 17 | const { query, parameters = [] } = args || {}; 18 | 19 | // Validate required parameters 20 | const validation = Validators.validateRequired(args, ['query'], 'hana_execute_query'); 21 | if (!validation.valid) { 22 | return Formatters.createErrorResponse('Error: query parameter is required', validation.error); 23 | } 24 | 25 | // Validate query 26 | const queryValidation = Validators.validateQuery(query); 27 | if (!queryValidation.valid) { 28 | return Formatters.createErrorResponse('Invalid query', queryValidation.error); 29 | } 30 | 31 | // Validate parameters 32 | const paramValidation = Validators.validateParameters(parameters); 33 | if (!paramValidation.valid) { 34 | return Formatters.createErrorResponse('Invalid parameters', paramValidation.error); 35 | } 36 | 37 | try { 38 | const results = await QueryExecutor.executeQuery(query, parameters); 39 | const formattedResults = Formatters.formatQueryResults(results, query); 40 | 41 | return Formatters.createResponse(formattedResults); 42 | } catch (error) { 43 | logger.error('Query execution failed:', error.message); 44 | return Formatters.createErrorResponse('Query execution failed', error.message); 45 | } 46 | } 47 | } 48 | 49 | module.exports = QueryTools; ``` -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # HANA MCP Server Setup Script 4 | echo "🚀 Setting up HANA MCP Server" 5 | echo "=============================" 6 | 7 | # Get the current directory 8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 | echo "📁 Working directory: $SCRIPT_DIR" 10 | 11 | # Check if Node.js is available 12 | NODE_PATH="/opt/homebrew/bin/node" 13 | if [ ! -f "$NODE_PATH" ]; then 14 | # Try alternative paths 15 | if command -v node >/dev/null 2>&1; then 16 | NODE_PATH=$(which node) 17 | else 18 | echo "❌ Node.js not found" 19 | echo "Please install Node.js or update the path in this script" 20 | exit 1 21 | fi 22 | fi 23 | 24 | echo "✅ Node.js found: $($NODE_PATH --version)" 25 | 26 | # Check if dependencies are installed 27 | if [ ! -d "node_modules" ]; then 28 | echo "📦 Installing dependencies..." 29 | npm install 30 | if [ $? -eq 0 ]; then 31 | echo "✅ Dependencies installed successfully" 32 | else 33 | echo "❌ Failed to install dependencies" 34 | echo "Please run 'npm install' manually" 35 | exit 1 36 | fi 37 | else 38 | echo "✅ Dependencies already installed" 39 | fi 40 | 41 | # Make the server executable 42 | chmod +x "$SCRIPT_DIR/hana-mcp-server.js" 43 | echo "✅ Made server executable" 44 | 45 | # Display configuration instructions 46 | echo "" 47 | echo "📖 Configuration Instructions:" 48 | echo "==============================" 49 | echo "" 50 | echo "1. Copy the configuration template:" 51 | echo " cp $SCRIPT_DIR/claude_config_template.json ~/.config/claude/claude_desktop_config.json" 52 | echo "" 53 | echo "2. Edit the configuration file with your HANA database details:" 54 | echo " - Update the path to hana-mcp-server.js" 55 | echo " - Set your HANA_HOST, HANA_USER, HANA_PASSWORD, etc." 56 | echo "" 57 | echo "3. Restart Claude Desktop" 58 | echo "" 59 | echo "4. Test the connection using the hana_test_connection tool" 60 | echo "" 61 | echo "📚 For more information, see README.md" 62 | echo "" 63 | echo "✅ Setup complete!" ``` -------------------------------------------------------------------------------- /src/constants/mcp-constants.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * MCP Protocol Constants 3 | */ 4 | 5 | // MCP Protocol versions 6 | const PROTOCOL_VERSIONS = { 7 | LATEST: '2024-11-05', 8 | SUPPORTED: ['2024-11-05', '2025-03-26'] 9 | }; 10 | 11 | // MCP Methods 12 | const METHODS = { 13 | // Lifecycle 14 | INITIALIZE: 'initialize', 15 | NOTIFICATIONS_INITIALIZED: 'notifications/initialized', 16 | 17 | // Tools 18 | TOOLS_LIST: 'tools/list', 19 | TOOLS_CALL: 'tools/call', 20 | 21 | // Prompts 22 | PROMPTS_LIST: 'prompts/list', 23 | 24 | // Resources 25 | RESOURCES_LIST: 'resources/list', 26 | RESOURCES_READ: 'resources/read' 27 | }; 28 | 29 | // JSON-RPC Error Codes 30 | const ERROR_CODES = { 31 | // JSON-RPC 2.0 Standard Errors 32 | PARSE_ERROR: -32700, 33 | INVALID_REQUEST: -32600, 34 | METHOD_NOT_FOUND: -32601, 35 | INVALID_PARAMS: -32602, 36 | INTERNAL_ERROR: -32603, 37 | 38 | // MCP Specific Errors 39 | TOOL_NOT_FOUND: -32601, 40 | INVALID_TOOL_ARGS: -32602, 41 | DATABASE_ERROR: -32000, 42 | CONNECTION_ERROR: -32001, 43 | VALIDATION_ERROR: -32002 44 | }; 45 | 46 | // Error Messages 47 | const ERROR_MESSAGES = { 48 | [ERROR_CODES.PARSE_ERROR]: 'Parse error', 49 | [ERROR_CODES.INVALID_REQUEST]: 'Invalid Request', 50 | [ERROR_CODES.METHOD_NOT_FOUND]: 'Method not found', 51 | [ERROR_CODES.INVALID_PARAMS]: 'Invalid params', 52 | [ERROR_CODES.INTERNAL_ERROR]: 'Internal error', 53 | [ERROR_CODES.TOOL_NOT_FOUND]: 'Tool not found', 54 | [ERROR_CODES.INVALID_TOOL_ARGS]: 'Invalid tool arguments', 55 | [ERROR_CODES.DATABASE_ERROR]: 'Database error', 56 | [ERROR_CODES.CONNECTION_ERROR]: 'Connection error', 57 | [ERROR_CODES.VALIDATION_ERROR]: 'Validation error' 58 | }; 59 | 60 | // Server Information 61 | const SERVER_INFO = { 62 | name: 'HANA MCP Server', 63 | version: '1.0.0', 64 | description: 'Model Context Protocol server for SAP HANA databases' 65 | }; 66 | 67 | // Capabilities 68 | const CAPABILITIES = { 69 | tools: {}, 70 | resources: {}, 71 | prompts: {} 72 | }; 73 | 74 | module.exports = { 75 | PROTOCOL_VERSIONS, 76 | METHODS, 77 | ERROR_CODES, 78 | ERROR_MESSAGES, 79 | SERVER_INFO, 80 | CAPABILITIES 81 | }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/bin/cli.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import { spawn } from 'child_process'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname, join } from 'path'; 6 | import chalk from 'chalk'; 7 | import open from 'open'; 8 | import fs from 'fs-extra'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | const rootDir = dirname(__dirname); 13 | 14 | console.log(chalk.blue.bold('🚀 Starting HANA MCP UI...')); 15 | console.log(chalk.gray('Professional database configuration management')); 16 | 17 | // Check if we're in development or production 18 | const isDev = process.env.NODE_ENV === 'development' || fs.existsSync(join(rootDir, 'src')); 19 | 20 | let backendProcess, frontendProcess; 21 | 22 | // Graceful shutdown 23 | process.on('SIGINT', () => { 24 | console.log(chalk.yellow('\n🛑 Shutting down servers...')); 25 | if (backendProcess) backendProcess.kill(); 26 | if (frontendProcess) frontendProcess.kill(); 27 | process.exit(0); 28 | }); 29 | 30 | // Start backend server 31 | console.log(chalk.cyan('🔧 Starting backend server...')); 32 | backendProcess = spawn('node', [join(rootDir, 'server', 'index.js')], { 33 | stdio: 'inherit', 34 | env: { ...process.env, PORT: '3001' } 35 | }); 36 | 37 | backendProcess.on('error', (err) => { 38 | console.error(chalk.red('Backend server error:'), err); 39 | }); 40 | 41 | // Wait for backend to start 42 | setTimeout(() => { 43 | if (isDev) { 44 | // Development mode - start Vite dev server 45 | console.log(chalk.cyan('⚛️ Starting React dev server...')); 46 | frontendProcess = spawn('bun', ['vite', '--port', '5173', '--host'], { 47 | stdio: 'inherit', 48 | cwd: rootDir, 49 | shell: true 50 | }); 51 | } else { 52 | // Production mode - serve built files 53 | console.log(chalk.cyan('📦 Serving production build...')); 54 | frontendProcess = spawn('bun', ['vite', 'preview', '--port', '5173', '--host'], { 55 | stdio: 'inherit', 56 | cwd: rootDir, 57 | shell: true 58 | }); 59 | } 60 | 61 | frontendProcess.on('error', (err) => { 62 | console.error(chalk.red('Frontend server error:'), err); 63 | }); 64 | 65 | // Open browser after frontend starts 66 | setTimeout(() => { 67 | console.log(chalk.green.bold('\n✨ HANA MCP UI is ready!')); 68 | console.log(chalk.gray('Opening browser at http://localhost:5173')); 69 | open('http://localhost:5173'); 70 | }, 3000); 71 | }, 2000); ``` -------------------------------------------------------------------------------- /src/tools/config-tools.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Configuration-related tools for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { config } = require('../utils/config'); 7 | const { connectionManager } = require('../database/connection-manager'); 8 | const Formatters = require('../utils/formatters'); 9 | 10 | class ConfigTools { 11 | /** 12 | * Show HANA configuration 13 | */ 14 | static async showConfig(args) { 15 | logger.tool('hana_show_config'); 16 | 17 | const displayConfig = config.getDisplayConfig(); 18 | const formattedConfig = Formatters.formatConfig(displayConfig); 19 | 20 | return Formatters.createResponse(formattedConfig); 21 | } 22 | 23 | /** 24 | * Test HANA connection 25 | */ 26 | static async testConnection(args) { 27 | logger.tool('hana_test_connection'); 28 | 29 | if (!config.isHanaConfigured()) { 30 | const missingConfig = config.getDisplayConfig(); 31 | const errorMessage = Formatters.formatConnectionTest(missingConfig, false, 'Missing required configuration'); 32 | return Formatters.createErrorResponse('Connection test failed!', errorMessage); 33 | } 34 | 35 | try { 36 | const testResult = await connectionManager.testConnection(); 37 | const displayConfig = config.getDisplayConfig(); 38 | 39 | if (testResult.success) { 40 | const successMessage = Formatters.formatConnectionTest(displayConfig, true, null, testResult.result); 41 | return Formatters.createResponse(successMessage); 42 | } else { 43 | const errorMessage = Formatters.formatConnectionTest(displayConfig, false, testResult.error); 44 | return Formatters.createErrorResponse('Connection test failed!', errorMessage); 45 | } 46 | } catch (error) { 47 | logger.error('Connection test error:', error.message); 48 | const displayConfig = config.getDisplayConfig(); 49 | const errorMessage = Formatters.formatConnectionTest(displayConfig, false, error.message); 50 | return Formatters.createErrorResponse('Connection test failed!', errorMessage); 51 | } 52 | } 53 | 54 | /** 55 | * Show environment variables 56 | */ 57 | static async showEnvVars(args) { 58 | logger.tool('hana_show_env_vars'); 59 | 60 | const envVars = config.getEnvironmentVars(); 61 | const formattedEnvVars = Formatters.formatEnvironmentVars(envVars); 62 | 63 | return Formatters.createResponse(formattedEnvVars); 64 | } 65 | } 66 | 67 | module.exports = ConfigTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GlassCard.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { cn } from '../../utils/cn' 3 | import { colors, shadows, borderRadius } from '../../utils/theme' 4 | 5 | /** 6 | * GlassCard - A versatile card component with multiple variants 7 | * 8 | * @param {Object} props - Component props 9 | * @param {React.ReactNode} props.children - Card content 10 | * @param {string} props.variant - Visual variant (default, primary, success, warning, danger) 11 | * @param {boolean} props.hover - Whether to apply hover effects 12 | * @param {boolean} props.glow - Whether to apply glow effect on hover 13 | * @param {string} props.className - Additional CSS classes 14 | * @param {Object} props.headerProps - Props for the card header 15 | * @param {React.ReactNode} props.header - Card header content 16 | * @returns {JSX.Element} - Rendered card component 17 | */ 18 | const GlassCard = ({ 19 | children, 20 | variant = 'default', 21 | hover = true, 22 | glow = false, 23 | className, 24 | header, 25 | headerProps = {}, 26 | ...props 27 | }) => { 28 | // Card variants - enhanced with better shadows and borders 29 | const variants = { 30 | default: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', 31 | primary: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', 32 | success: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', 33 | warning: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', 34 | danger: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden' 35 | } 36 | 37 | // Header variants - with improved styling 38 | const headerVariants = { 39 | default: 'border-b border-gray-200 bg-white p-6', 40 | primary: 'border-b border-gray-200 bg-white p-6', 41 | success: 'border-b border-gray-200 bg-white p-6', 42 | warning: 'border-b border-gray-200 bg-white p-6', 43 | danger: 'border-b border-gray-200 bg-white p-6' 44 | } 45 | 46 | return ( 47 | <motion.div 48 | className={cn( 49 | variants[variant], 50 | hover && 'hover:shadow-md hover:-translate-y-0.5', 51 | glow && 'hover:shadow-gray-200', 52 | className 53 | )} 54 | whileHover={hover ? { y: -3, boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1)' } : {}} 55 | transition={{ type: "spring", stiffness: 300, damping: 20 }} 56 | {...props} 57 | > 58 | {header && ( 59 | <div className={cn(headerVariants[variant], headerProps.className)} {...headerProps}> 60 | {header} 61 | </div> 62 | )} 63 | <div className="relative z-10"> 64 | {children} 65 | </div> 66 | </motion.div> 67 | ) 68 | } 69 | 70 | export default GlassCard ``` -------------------------------------------------------------------------------- /src/tools/index.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Tool registry and management for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { TOOLS } = require('../constants/tool-definitions'); 7 | const ConfigTools = require('./config-tools'); 8 | const SchemaTools = require('./schema-tools'); 9 | const TableTools = require('./table-tools'); 10 | const IndexTools = require('./index-tools'); 11 | const QueryTools = require('./query-tools'); 12 | 13 | // Tool implementations mapping 14 | const TOOL_IMPLEMENTATIONS = { 15 | hana_show_config: ConfigTools.showConfig, 16 | hana_test_connection: ConfigTools.testConnection, 17 | hana_show_env_vars: ConfigTools.showEnvVars, 18 | hana_list_schemas: SchemaTools.listSchemas, 19 | hana_list_tables: TableTools.listTables, 20 | hana_describe_table: TableTools.describeTable, 21 | hana_list_indexes: IndexTools.listIndexes, 22 | hana_describe_index: IndexTools.describeIndex, 23 | hana_execute_query: QueryTools.executeQuery 24 | }; 25 | 26 | class ToolRegistry { 27 | /** 28 | * Get all available tools 29 | */ 30 | static getTools() { 31 | return TOOLS; 32 | } 33 | 34 | /** 35 | * Get tool by name 36 | */ 37 | static getTool(name) { 38 | return TOOLS.find(tool => tool.name === name); 39 | } 40 | 41 | /** 42 | * Check if tool exists 43 | */ 44 | static hasTool(name) { 45 | return TOOL_IMPLEMENTATIONS.hasOwnProperty(name); 46 | } 47 | 48 | /** 49 | * Execute a tool 50 | */ 51 | static async executeTool(name, args) { 52 | if (!this.hasTool(name)) { 53 | throw new Error(`Tool not found: ${name}`); 54 | } 55 | 56 | const implementation = TOOL_IMPLEMENTATIONS[name]; 57 | if (typeof implementation !== 'function') { 58 | throw new Error(`Tool implementation not found: ${name}`); 59 | } 60 | 61 | try { 62 | logger.debug(`Executing tool: ${name}`, args); 63 | const result = await implementation(args); 64 | logger.debug(`Tool ${name} executed successfully`); 65 | return result; 66 | } catch (error) { 67 | logger.error(`Tool ${name} execution failed:`, error.message); 68 | throw error; 69 | } 70 | } 71 | 72 | /** 73 | * Get tool implementation 74 | */ 75 | static getToolImplementation(name) { 76 | return TOOL_IMPLEMENTATIONS[name]; 77 | } 78 | 79 | /** 80 | * Get all tool names 81 | */ 82 | static getAllToolNames() { 83 | return Object.keys(TOOL_IMPLEMENTATIONS); 84 | } 85 | 86 | /** 87 | * Validate tool arguments against schema 88 | */ 89 | static validateToolArgs(name, args) { 90 | const tool = this.getTool(name); 91 | if (!tool) { 92 | return { valid: false, error: `Tool not found: ${name}` }; 93 | } 94 | 95 | const schema = tool.inputSchema; 96 | if (!schema || !schema.required) { 97 | return { valid: true }; // No validation required 98 | } 99 | 100 | const missing = []; 101 | for (const field of schema.required) { 102 | if (!args || args[field] === undefined || args[field] === null || args[field] === '') { 103 | missing.push(field); 104 | } 105 | } 106 | 107 | if (missing.length > 0) { 108 | return { 109 | valid: false, 110 | error: `Missing required parameters: ${missing.join(', ')}` 111 | }; 112 | } 113 | 114 | return { valid: true }; 115 | } 116 | } 117 | 118 | module.exports = ToolRegistry; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GlassWindow.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | const GlassWindow = ({ children, className = '', maxWidth = '7xl', maxHeight = '6xl' }) => { 5 | // Convert maxWidth to actual Tailwind class 6 | const getMaxWidthClass = (width) => { 7 | const widthMap = { 8 | 'sm': 'max-w-sm', 9 | 'md': 'max-w-md', 10 | 'lg': 'max-w-lg', 11 | 'xl': 'max-w-xl', 12 | '2xl': 'max-w-2xl', 13 | '3xl': 'max-w-3xl', 14 | '4xl': 'max-w-4xl', 15 | '5xl': 'max-w-5xl', 16 | '6xl': 'max-w-6xl', 17 | '7xl': 'max-w-7xl', 18 | 'full': 'max-w-full' 19 | }; 20 | return widthMap[width] || 'max-w-7xl'; 21 | }; 22 | 23 | const getMaxHeightClass = (height) => { 24 | const heightMap = { 25 | 'sm': 'max-h-sm', 26 | 'md': 'max-h-md', 27 | 'lg': 'max-h-lg', 28 | 'xl': 'max-h-xl', 29 | '2xl': 'max-h-2xl', 30 | '3xl': 'max-h-3xl', 31 | '4xl': 'max-h-4xl', 32 | '5xl': 'max-h-5xl', 33 | '6xl': 'max-h-6xl', 34 | 'full': 'max-h-full' 35 | }; 36 | return heightMap[height] || 'max-h-6xl'; 37 | }; 38 | 39 | return ( 40 | <div className="glass-window-container bg-gradient-to-br from-gray-50 via-blue-50/30 to-indigo-50/20 overflow-hidden"> 41 | {/* Background Pattern */} 42 | <div className="fixed inset-0 bg-dots opacity-20 sm:opacity-30 pointer-events-none" /> 43 | 44 | {/* Glass Window Container */} 45 | <motion.div 46 | initial={{ opacity: 0, scale: 0.95, y: 20 }} 47 | animate={{ opacity: 1, scale: 1, y: 0 }} 48 | transition={{ duration: 0.5, ease: "easeOut" }} 49 | className={` 50 | glass-window-content 51 | relative bg-white/80 backdrop-blur-xl 52 | border border-white/20 53 | rounded-2xl sm:rounded-3xl shadow-xl sm:shadow-2xl shadow-gray-900/10 54 | overflow-hidden 55 | ${className} 56 | `} 57 | style={{ 58 | backdropFilter: 'blur(20px)', 59 | WebkitBackdropFilter: 'blur(20px)', 60 | minHeight: '600px' 61 | }} 62 | > 63 | {/* Glass Window Header */} 64 | <div className="absolute top-0 left-0 right-0 h-12 sm:h-14 bg-gradient-to-r from-white/40 to-white/20 border-b border-white/20 backdrop-blur-sm"> 65 | {/* Window Controls */} 66 | <div className="flex items-center h-full px-4 sm:px-6 gap-2 sm:gap-3"> 67 | <div className="w-3 h-3 sm:w-4 sm:h-4 rounded-full bg-red-400/80 shadow-sm hover:bg-red-500/90 transition-colors" /> 68 | <div className="w-3 h-3 sm:w-4 sm:h-4 rounded-full bg-yellow-400/80 shadow-sm hover:bg-yellow-500/90 transition-colors" /> 69 | <div className="w-3 h-3 sm:w-4 sm:h-4 rounded-full bg-green-400/80 shadow-sm hover:bg-green-500/90 transition-colors" /> 70 | </div> 71 | </div> 72 | 73 | {/* Content Area */} 74 | <div className="pt-12 sm:pt-14 h-full p-3 pb-4"> 75 | {children} 76 | </div> 77 | 78 | {/* Subtle glow effect */} 79 | <div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-white/10 via-transparent to-blue-100/20 pointer-events-none" /> 80 | </motion.div> 81 | </div> 82 | ); 83 | }; 84 | 85 | export default GlassWindow; 86 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/DatabaseTypeBadge.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { getDatabaseTypeColor, getDatabaseTypeShortName } from '../../utils/databaseTypes' 2 | 3 | const DatabaseTypeBadge = ({ 4 | type, 5 | size = 'md', 6 | showIcon = true, 7 | className = '' 8 | }) => { 9 | const color = getDatabaseTypeColor(type) 10 | const shortName = getDatabaseTypeShortName(type) 11 | 12 | const sizeClasses = { 13 | xs: 'px-2 py-1 text-xs', 14 | sm: 'px-2.5 py-1 text-sm', 15 | md: 'px-3 py-1.5 text-sm', 16 | lg: 'px-4 py-2 text-base' 17 | } 18 | 19 | const colorClasses = { 20 | blue: 'bg-gradient-to-r from-blue-100 to-blue-50 text-blue-800 border-blue-200 shadow-sm', 21 | amber: 'bg-gradient-to-r from-amber-100 to-amber-50 text-amber-800 border-amber-200 shadow-sm', 22 | green: 'bg-gradient-to-r from-green-100 to-green-50 text-green-800 border-green-200 shadow-sm', 23 | gray: 'bg-gradient-to-r from-gray-100 to-gray-50 text-gray-800 border-gray-200 shadow-sm' 24 | } 25 | 26 | const iconClasses = { 27 | xs: 'w-3 h-3', 28 | sm: 'w-3.5 h-3.5', 29 | md: 'w-4 h-4', 30 | lg: 'w-5 h-5' 31 | } 32 | 33 | const getIcon = () => { 34 | switch (type) { 35 | case 'single_container': 36 | return ( 37 | <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> 38 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 39 | </svg> 40 | ) 41 | case 'mdc_system': 42 | return ( 43 | <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> 44 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> 45 | </svg> 46 | ) 47 | case 'mdc_tenant': 48 | return ( 49 | <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> 50 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /> 51 | </svg> 52 | ) 53 | default: 54 | return ( 55 | <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> 56 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> 57 | </svg> 58 | ) 59 | } 60 | } 61 | 62 | return ( 63 | <span 64 | className={` 65 | inline-flex items-center gap-1.5 font-semibold rounded-full border transition-all duration-200 66 | hover:shadow-md hover:scale-105 67 | ${sizeClasses[size]} 68 | ${colorClasses[color]} 69 | ${className} 70 | `} 71 | title={shortName} 72 | > 73 | {showIcon && getIcon()} 74 | <span className='tracking-wide'>{shortName}</span> 75 | </span> 76 | ) 77 | } 78 | 79 | export default DatabaseTypeBadge 80 | ``` -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Main HANA MCP Server Entry Point 5 | */ 6 | 7 | const readline = require('readline'); 8 | const { logger } = require('../utils/logger'); 9 | const { lifecycleManager } = require('./lifecycle-manager'); 10 | const MCPHandler = require('./mcp-handler'); 11 | const { ERROR_CODES } = require('../constants/mcp-constants'); 12 | 13 | class MCPServer { 14 | constructor() { 15 | this.rl = null; 16 | this.isShuttingDown = false; 17 | } 18 | 19 | /** 20 | * Start the MCP server 21 | */ 22 | async start() { 23 | try { 24 | // Setup lifecycle management 25 | lifecycleManager.setupEventHandlers(); 26 | await lifecycleManager.start(); 27 | 28 | // Setup readline interface for STDIO 29 | this.setupReadline(); 30 | 31 | logger.info('Server ready for requests'); 32 | } catch (error) { 33 | logger.error('Failed to start server:', error.message); 34 | process.exit(1); 35 | } 36 | } 37 | 38 | /** 39 | * Setup readline interface for STDIO communication 40 | */ 41 | setupReadline() { 42 | this.rl = readline.createInterface({ 43 | input: process.stdin, 44 | output: process.stdout, 45 | terminal: false 46 | }); 47 | 48 | // Handle incoming lines 49 | this.rl.on('line', async (line) => { 50 | if (this.isShuttingDown) return; 51 | 52 | await this.handleLine(line); 53 | }); 54 | 55 | // Handle readline close 56 | this.rl.on('close', async () => { 57 | if (!this.isShuttingDown) { 58 | logger.info('Readline closed, but keeping process alive'); 59 | } else { 60 | logger.info('Server shutting down'); 61 | await lifecycleManager.shutdown(); 62 | } 63 | }); 64 | } 65 | 66 | /** 67 | * Handle incoming line from STDIO 68 | */ 69 | async handleLine(line) { 70 | try { 71 | const request = JSON.parse(line); 72 | const response = await this.handleRequest(request); 73 | 74 | if (response) { 75 | console.log(JSON.stringify(response)); 76 | } 77 | } catch (error) { 78 | logger.error(`Parse error: ${error.message}`); 79 | const errorResponse = { 80 | jsonrpc: '2.0', 81 | id: null, 82 | error: { 83 | code: ERROR_CODES.PARSE_ERROR, 84 | message: 'Parse error' 85 | } 86 | }; 87 | console.log(JSON.stringify(errorResponse)); 88 | } 89 | } 90 | 91 | /** 92 | * Handle MCP request 93 | */ 94 | async handleRequest(request) { 95 | // Validate request 96 | const validation = MCPHandler.validateRequest(request); 97 | if (!validation.valid) { 98 | return { 99 | jsonrpc: '2.0', 100 | id: request.id || null, 101 | error: { 102 | code: ERROR_CODES.INVALID_REQUEST, 103 | message: validation.error 104 | } 105 | }; 106 | } 107 | 108 | // Handle request 109 | return await MCPHandler.handleRequest(request); 110 | } 111 | 112 | /** 113 | * Shutdown the server 114 | */ 115 | async shutdown() { 116 | this.isShuttingDown = true; 117 | 118 | if (this.rl) { 119 | this.rl.close(); 120 | } 121 | 122 | await lifecycleManager.shutdown(); 123 | } 124 | } 125 | 126 | // Create and start server 127 | const server = new MCPServer(); 128 | 129 | // Handle process termination 130 | process.on('SIGINT', async () => { 131 | logger.info('Received SIGINT'); 132 | await server.shutdown(); 133 | }); 134 | 135 | process.on('SIGTERM', async () => { 136 | logger.info('Received SIGTERM'); 137 | await server.shutdown(); 138 | }); 139 | 140 | // Start the server 141 | server.start().catch(error => { 142 | logger.error('Failed to start server:', error.message); 143 | process.exit(1); 144 | }); ``` -------------------------------------------------------------------------------- /src/server/lifecycle-manager.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Server lifecycle management for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { connectionManager } = require('../database/connection-manager'); 7 | 8 | class LifecycleManager { 9 | constructor() { 10 | this.isShuttingDown = false; 11 | this.isInitialized = false; 12 | } 13 | 14 | /** 15 | * Initialize the server 16 | */ 17 | async initialize() { 18 | if (this.isInitialized) { 19 | logger.warn('Server already initialized'); 20 | return; 21 | } 22 | 23 | logger.info('Initializing HANA MCP Server...'); 24 | 25 | try { 26 | // Validate configuration 27 | const { config } = require('../utils/config'); 28 | if (!config.validate()) { 29 | logger.warn('Configuration validation failed, but continuing...'); 30 | } 31 | 32 | this.isInitialized = true; 33 | logger.info('HANA MCP Server initialized successfully'); 34 | } catch (error) { 35 | logger.error('Failed to initialize server:', error.message); 36 | throw error; 37 | } 38 | } 39 | 40 | /** 41 | * Start the server 42 | */ 43 | async start() { 44 | logger.info('Starting HANA MCP Server...'); 45 | 46 | try { 47 | await this.initialize(); 48 | 49 | // Keep process alive 50 | this.keepAlive(); 51 | 52 | logger.info('HANA MCP Server started successfully'); 53 | } catch (error) { 54 | logger.error('Failed to start server:', error.message); 55 | throw error; 56 | } 57 | } 58 | 59 | /** 60 | * Shutdown the server gracefully 61 | */ 62 | async shutdown() { 63 | if (this.isShuttingDown) { 64 | logger.warn('Shutdown already in progress'); 65 | return; 66 | } 67 | 68 | logger.info('Shutting down HANA MCP Server...'); 69 | this.isShuttingDown = true; 70 | 71 | try { 72 | // Disconnect from HANA database 73 | await connectionManager.disconnect(); 74 | 75 | logger.info('HANA MCP Server shutdown completed'); 76 | } catch (error) { 77 | logger.error('Error during shutdown:', error.message); 78 | } finally { 79 | process.exit(0); 80 | } 81 | } 82 | 83 | /** 84 | * Keep the process alive 85 | */ 86 | keepAlive() { 87 | // Keep stdin open 88 | process.stdin.resume(); 89 | 90 | // Keep process alive with interval 91 | setInterval(() => { 92 | // This keeps the event loop active 93 | }, 1000); 94 | } 95 | 96 | /** 97 | * Setup process event handlers 98 | */ 99 | setupEventHandlers() { 100 | // Handle SIGINT (Ctrl+C) 101 | process.on('SIGINT', async () => { 102 | logger.info('Received SIGINT'); 103 | await this.shutdown(); 104 | }); 105 | 106 | // Handle SIGTERM 107 | process.on('SIGTERM', async () => { 108 | logger.info('Received SIGTERM'); 109 | await this.shutdown(); 110 | }); 111 | 112 | // Handle uncaught exceptions 113 | process.on('uncaughtException', (error) => { 114 | logger.error('Uncaught exception:', error.message); 115 | this.shutdown(); 116 | }); 117 | 118 | // Handle unhandled promise rejections 119 | process.on('unhandledRejection', (reason, promise) => { 120 | logger.error('Unhandled promise rejection:', reason); 121 | this.shutdown(); 122 | }); 123 | } 124 | 125 | /** 126 | * Get server status 127 | */ 128 | getStatus() { 129 | return { 130 | isInitialized: this.isInitialized, 131 | isShuttingDown: this.isShuttingDown, 132 | connectionStatus: connectionManager.getStatus() 133 | }; 134 | } 135 | } 136 | 137 | // Create singleton instance 138 | const lifecycleManager = new LifecycleManager(); 139 | 140 | module.exports = { LifecycleManager, lifecycleManager }; ``` -------------------------------------------------------------------------------- /tests/automated/test-mcp-inspector.js: -------------------------------------------------------------------------------- ```javascript 1 | const { spawn } = require('child_process'); 2 | 3 | console.log('🔍 HANA MCP Server Inspector'); 4 | console.log('============================\n'); 5 | 6 | // Spawn the MCP server process 7 | const server = spawn('/opt/homebrew/opt/node@20/bin/node', ['../../hana-mcp-server.js'], { 8 | stdio: ['pipe', 'pipe', 'pipe'], 9 | env: { 10 | HANA_HOST: "your-hana-host.com", 11 | HANA_PORT: "443", 12 | HANA_USER: "your-username", 13 | HANA_PASSWORD: "your-password", 14 | HANA_SCHEMA: "your-schema", 15 | HANA_SSL: "true", 16 | HANA_ENCRYPT: "true", 17 | HANA_VALIDATE_CERT: "true" 18 | } 19 | }); 20 | 21 | // Handle server output 22 | server.stdout.on('data', (data) => { 23 | try { 24 | const response = JSON.parse(data.toString().trim()); 25 | console.log('📤 Response:', JSON.stringify(response, null, 2)); 26 | } catch (error) { 27 | console.log('🔧 Server Log:', data.toString().trim()); 28 | } 29 | }); 30 | 31 | server.stderr.on('data', (data) => { 32 | console.log('🔧 Server Log:', data.toString().trim()); 33 | }); 34 | 35 | // Send request function 36 | function sendRequest(method, params = {}) { 37 | const request = { 38 | jsonrpc: '2.0', 39 | id: Date.now(), 40 | method, 41 | params 42 | }; 43 | 44 | server.stdin.write(JSON.stringify(request) + '\n'); 45 | } 46 | 47 | // Test functions 48 | async function testInitialize() { 49 | console.log('\n🧪 Testing: Initialize'); 50 | sendRequest('initialize', { 51 | protocolVersion: '2024-11-05', 52 | capabilities: {}, 53 | clientInfo: { name: 'test-client', version: '1.0.0' } 54 | }); 55 | await new Promise(resolve => setTimeout(resolve, 1000)); 56 | } 57 | 58 | async function testToolsList() { 59 | console.log('\n🧪 Testing: Tools List'); 60 | sendRequest('tools/list', {}); 61 | await new Promise(resolve => setTimeout(resolve, 1000)); 62 | } 63 | 64 | async function testShowConfig() { 65 | console.log('\n🧪 Testing: Show Config'); 66 | sendRequest('tools/call', { 67 | name: "hana_show_config", 68 | arguments: {} 69 | }); 70 | await new Promise(resolve => setTimeout(resolve, 1000)); 71 | } 72 | 73 | async function testListSchemas() { 74 | console.log('\n🧪 Testing: List Schemas'); 75 | sendRequest('tools/call', { 76 | name: "hana_list_schemas", 77 | arguments: {} 78 | }); 79 | await new Promise(resolve => setTimeout(resolve, 1000)); 80 | } 81 | 82 | async function testListTables() { 83 | console.log('\n🧪 Testing: List Tables'); 84 | sendRequest('tools/call', { 85 | name: "hana_list_tables", 86 | arguments: { schema_name: "SYSTEM" } 87 | }); 88 | await new Promise(resolve => setTimeout(resolve, 1000)); 89 | } 90 | 91 | async function testExecuteQuery() { 92 | console.log('\n🧪 Testing: Execute Query'); 93 | sendRequest('tools/call', { 94 | name: "hana_execute_query", 95 | arguments: { 96 | query: "SELECT 1 as test_value FROM DUMMY" 97 | } 98 | }); 99 | await new Promise(resolve => setTimeout(resolve, 1000)); 100 | } 101 | 102 | // Main test runner 103 | async function runTests() { 104 | try { 105 | await testInitialize(); 106 | await testToolsList(); 107 | await testShowConfig(); 108 | await testListSchemas(); 109 | await testListTables(); 110 | await testExecuteQuery(); 111 | 112 | console.log('\n✅ Tests completed!'); 113 | 114 | // Close server 115 | server.stdin.end(); 116 | server.kill(); 117 | 118 | } catch (error) { 119 | console.error('❌ Test error:', error); 120 | server.kill(); 121 | } 122 | } 123 | 124 | // Handle server exit 125 | server.on('close', (code) => { 126 | console.log(`\n🔚 Server closed with code ${code}`); 127 | }); 128 | 129 | server.on('error', (error) => { 130 | console.error('❌ Server error:', error); 131 | }); 132 | 133 | // Start tests 134 | runTests().catch(console.error); ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeConfigTile.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { cn } from '../utils/cn'; 4 | import PathConfigModal from './PathConfigModal'; 5 | 6 | const ClaudeConfigTile = ({ 7 | claudeConfigPath, 8 | claudeServers, 9 | onSetupPath, 10 | onConfigPathChange 11 | }) => { 12 | const [showPathModal, setShowPathModal] = useState(false); 13 | 14 | const handleEditPath = () => { 15 | setShowPathModal(true); 16 | }; 17 | 18 | return ( 19 | <> 20 | <motion.div 21 | className="bg-white rounded-xl border border-gray-100 overflow-hidden" 22 | initial={{ opacity: 0, y: 20 }} 23 | animate={{ opacity: 1, y: 0 }} 24 | transition={{ duration: 0.3 }} 25 | > 26 | <div className="border-b border-gray-100 px-4 py-3"> 27 | <div className="flex items-center justify-between"> 28 | <div> 29 | <h3 className="text-sm font-medium text-gray-900">Claude Desktop Configuration</h3> 30 | <p className="text-xs text-gray-500">Integration Status</p> 31 | </div> 32 | <div className="flex items-center gap-2"> 33 | <div className={cn( 34 | 'w-2 h-2 rounded-full', 35 | claudeServers.length > 0 ? 'bg-green-500' : 'bg-gray-300' 36 | )}></div> 37 | <span className="text-xs text-gray-600"> 38 | {claudeServers.length > 0 ? 'Online' : 'Offline'} 39 | </span> 40 | </div> 41 | </div> 42 | </div> 43 | 44 | <div className="p-4"> 45 | 46 | {claudeConfigPath ? ( 47 | <div className="bg-gray-50 rounded-lg p-3"> 48 | <div className="flex items-center justify-between mb-1"> 49 | <div className="text-xs font-medium text-gray-600"> 50 | Config Path 51 | </div> 52 | <button 53 | onClick={handleEditPath} 54 | className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-white bg-[#86a0ff] hover:bg-[#7990e6] rounded-lg transition-colors shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:ring-offset-2" 55 | title="Change config path" 56 | > 57 | <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 58 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> 59 | </svg> 60 | Edit 61 | </button> 62 | </div> 63 | <div className="text-xs font-mono text-gray-700 break-all"> 64 | {claudeConfigPath} 65 | </div> 66 | </div> 67 | ) : ( 68 | <div className="text-center py-4"> 69 | <p className="text-sm text-gray-500"> 70 | Claude Desktop configuration path not set 71 | </p> 72 | <button 73 | onClick={onSetupPath} 74 | className="mt-2 text-sm text-blue-600 hover:text-blue-800" 75 | > 76 | Set Configuration Path 77 | </button> 78 | </div> 79 | )} 80 | </div> 81 | </motion.div> 82 | 83 | {/* Path Configuration Modal */} 84 | <PathConfigModal 85 | isOpen={showPathModal} 86 | onClose={() => setShowPathModal(false)} 87 | onConfigPathChange={onConfigPathChange} 88 | currentPath={claudeConfigPath} 89 | /> 90 | </> 91 | ); 92 | }; 93 | 94 | export default ClaudeConfigTile; 95 | ``` -------------------------------------------------------------------------------- /src/tools/table-tools.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Table management tools for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { config } = require('../utils/config'); 7 | const QueryExecutor = require('../database/query-executor'); 8 | const Validators = require('../utils/validators'); 9 | const Formatters = require('../utils/formatters'); 10 | 11 | class TableTools { 12 | /** 13 | * List tables in a schema 14 | */ 15 | static async listTables(args) { 16 | logger.tool('hana_list_tables', args); 17 | 18 | let { schema_name } = args || {}; 19 | 20 | // Use default schema if not provided 21 | if (!schema_name) { 22 | if (config.hasDefaultSchema()) { 23 | schema_name = config.getDefaultSchema(); 24 | logger.info(`Using default schema: ${schema_name}`); 25 | } else { 26 | return Formatters.createErrorResponse( 27 | 'Schema name is required', 28 | 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' 29 | ); 30 | } 31 | } 32 | 33 | // Validate schema name 34 | const schemaValidation = Validators.validateSchemaName(schema_name); 35 | if (!schemaValidation.valid) { 36 | return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); 37 | } 38 | 39 | try { 40 | const tables = await QueryExecutor.getTables(schema_name); 41 | const formattedTables = Formatters.formatTableList(tables, schema_name); 42 | 43 | return Formatters.createResponse(formattedTables); 44 | } catch (error) { 45 | logger.error('Error listing tables:', error.message); 46 | return Formatters.createErrorResponse('Error listing tables', error.message); 47 | } 48 | } 49 | 50 | /** 51 | * Describe table structure 52 | */ 53 | static async describeTable(args) { 54 | logger.tool('hana_describe_table', args); 55 | 56 | let { schema_name, table_name } = args || {}; 57 | 58 | // Use default schema if not provided 59 | if (!schema_name) { 60 | if (config.hasDefaultSchema()) { 61 | schema_name = config.getDefaultSchema(); 62 | logger.info(`Using default schema: ${schema_name}`); 63 | } else { 64 | return Formatters.createErrorResponse( 65 | 'Schema name is required', 66 | 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' 67 | ); 68 | } 69 | } 70 | 71 | // Validate required parameters 72 | const validation = Validators.validateRequired(args, ['table_name'], 'hana_describe_table'); 73 | if (!validation.valid) { 74 | return Formatters.createErrorResponse('Error: table_name parameter is required', validation.error); 75 | } 76 | 77 | // Validate schema and table names 78 | const schemaValidation = Validators.validateSchemaName(schema_name); 79 | if (!schemaValidation.valid) { 80 | return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); 81 | } 82 | 83 | const tableValidation = Validators.validateTableName(table_name); 84 | if (!tableValidation.valid) { 85 | return Formatters.createErrorResponse('Invalid table name', tableValidation.error); 86 | } 87 | 88 | try { 89 | const columns = await QueryExecutor.getTableColumns(schema_name, table_name); 90 | 91 | if (columns.length === 0) { 92 | return Formatters.createErrorResponse(`Table '${schema_name}.${table_name}' not found or no columns available`); 93 | } 94 | 95 | const formattedStructure = Formatters.formatTableStructure(columns, schema_name, table_name); 96 | 97 | return Formatters.createResponse(formattedStructure); 98 | } catch (error) { 99 | logger.error('Error describing table:', error.message); 100 | return Formatters.createErrorResponse('Error describing table', error.message); 101 | } 102 | } 103 | } 104 | 105 | module.exports = TableTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/start.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import { spawn, execSync } from 'child_process'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname, join } from 'path'; 6 | import chalk from 'chalk'; 7 | import open from 'open'; 8 | import { networkInterfaces } from 'os'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | console.log(chalk.blue.bold('🚀 Starting HANA MCP UI...')); 14 | console.log(chalk.gray('Professional database configuration management')); 15 | 16 | let backendProcess, frontendProcess; 17 | 18 | // Graceful shutdown 19 | process.on('SIGINT', () => { 20 | console.log(chalk.yellow('\n🛑 Shutting down servers...')); 21 | if (backendProcess) backendProcess.kill(); 22 | if (frontendProcess) frontendProcess.kill(); 23 | process.exit(0); 24 | }); 25 | 26 | // Start backend server 27 | console.log(chalk.cyan('🔧 Starting backend server...')); 28 | backendProcess = spawn('node', [join(__dirname, 'server', 'index.js')], { 29 | stdio: 'inherit', 30 | env: { ...process.env, PORT: '3001' } 31 | }); 32 | 33 | backendProcess.on('error', (err) => { 34 | console.error(chalk.red('Backend server error:'), err); 35 | }); 36 | 37 | // Function to check if port is in use and kill the process if needed 38 | function checkPortAndKillProcess(port) { 39 | try { 40 | console.log(chalk.yellow(`🔍 Checking if port ${port} is already in use...`)); 41 | 42 | // Check if the port is in use 43 | const checkCommand = process.platform === 'win32' 44 | ? `netstat -ano | findstr :${port}` 45 | : `lsof -i :${port}`; 46 | 47 | try { 48 | const result = execSync(checkCommand, { encoding: 'utf8' }); 49 | 50 | if (result) { 51 | console.log(chalk.yellow(`⚠️ Port ${port} is already in use. Finding the process...`)); 52 | 53 | // Get the PID of the process using the port 54 | let pid; 55 | if (process.platform === 'win32') { 56 | // Extract PID from Windows netstat output 57 | const lines = result.split('\n'); 58 | for (const line of lines) { 59 | if (line.includes(`LISTENING`)) { 60 | pid = line.trim().split(/\s+/).pop(); 61 | break; 62 | } 63 | } 64 | } else { 65 | // Extract PID from lsof output 66 | const pidMatch = result.match(/\s+(\d+)\s+/); 67 | if (pidMatch && pidMatch[1]) { 68 | pid = pidMatch[1]; 69 | } 70 | } 71 | 72 | if (pid) { 73 | console.log(chalk.yellow(`🛑 Killing process ${pid} that's using port ${port}...`)); 74 | 75 | // Kill the process 76 | const killCommand = process.platform === 'win32' 77 | ? `taskkill /F /PID ${pid}` 78 | : `kill -9 ${pid}`; 79 | 80 | execSync(killCommand); 81 | console.log(chalk.green(`✅ Process terminated.`)); 82 | 83 | // Wait a moment for the port to be released 84 | execSync('sleep 1'); 85 | } else { 86 | console.log(chalk.red(`❌ Could not find the process using port ${port}.`)); 87 | } 88 | } 89 | } catch (error) { 90 | // If the command fails, it likely means no process is using the port 91 | console.log(chalk.green(`✅ Port ${port} is available.`)); 92 | } 93 | } catch (error) { 94 | console.error(chalk.red(`Error checking port ${port}:`, error.message)); 95 | } 96 | } 97 | 98 | // Check and clear port 5173 if needed 99 | checkPortAndKillProcess(5173); 100 | 101 | // Start frontend server 102 | console.log(chalk.cyan('⚛️ Starting React dev server...')); 103 | frontendProcess = spawn('vite', ['--port', '5173', '--host', '0.0.0.0'], { 104 | stdio: 'inherit', 105 | cwd: __dirname, 106 | shell: true 107 | }); 108 | 109 | frontendProcess.on('error', (err) => { 110 | console.error(chalk.red('Frontend server error:'), err); 111 | }); 112 | 113 | // Open browser after a delay 114 | setTimeout(() => { 115 | console.log(chalk.green.bold('\n✨ HANA MCP UI is ready!')); 116 | console.log(chalk.gray('Opening browser at http://localhost:5173')); 117 | open('http://localhost:5173'); 118 | }, 5000); 119 | 120 | 121 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/theme.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Design tokens for the HANA MCP UI 3 | * This file defines the core design values used throughout the application 4 | */ 5 | 6 | // Color palette 7 | export const colors = { 8 | // Primary brand colors 9 | primary: { 10 | 50: '#eef2ff', 11 | 100: '#e0e7ff', 12 | 200: '#c7d2fe', 13 | 300: '#a5b4fc', 14 | 400: '#818cf8', 15 | 500: '#6366f1', 16 | 600: '#4f46e5', 17 | 700: '#4338ca', 18 | 800: '#3730a3', 19 | 900: '#312e81', 20 | 950: '#1e1b4b', 21 | }, 22 | 23 | // Secondary accent colors 24 | secondary: { 25 | 50: '#f0f9ff', 26 | 100: '#e0f2fe', 27 | 200: '#bae6fd', 28 | 300: '#7dd3fc', 29 | 400: '#38bdf8', 30 | 500: '#0ea5e9', 31 | 600: '#0284c7', 32 | 700: '#0369a1', 33 | 800: '#075985', 34 | 900: '#0c4a6e', 35 | 950: '#082f49', 36 | }, 37 | 38 | // Neutral colors for text, backgrounds 39 | gray: { 40 | 50: '#f9fafb', 41 | 100: '#f3f4f6', 42 | 200: '#e5e7eb', 43 | 300: '#d1d5db', 44 | 400: '#9ca3af', 45 | 500: '#6b7280', 46 | 600: '#4b5563', 47 | 700: '#374151', 48 | 800: '#1f2937', 49 | 900: '#111827', 50 | 950: '#030712', 51 | }, 52 | 53 | // Semantic colors 54 | success: { 55 | 50: '#f0fdf4', 56 | 100: '#dcfce7', 57 | 200: '#bbf7d0', 58 | 300: '#86efac', 59 | 400: '#4ade80', 60 | 500: '#22c55e', 61 | 600: '#16a34a', 62 | 700: '#15803d', 63 | 800: '#166534', 64 | 900: '#14532d', 65 | 950: '#052e16', 66 | }, 67 | 68 | warning: { 69 | 50: '#fffbeb', 70 | 100: '#fef3c7', 71 | 200: '#fde68a', 72 | 300: '#fcd34d', 73 | 400: '#fbbf24', 74 | 500: '#f59e0b', 75 | 600: '#d97706', 76 | 700: '#b45309', 77 | 800: '#92400e', 78 | 900: '#78350f', 79 | 950: '#451a03', 80 | }, 81 | 82 | danger: { 83 | 50: '#fef2f2', 84 | 100: '#fee2e2', 85 | 200: '#fecaca', 86 | 300: '#fca5a5', 87 | 400: '#f87171', 88 | 500: '#ef4444', 89 | 600: '#dc2626', 90 | 700: '#b91c1c', 91 | 800: '#991b1b', 92 | 900: '#7f1d1d', 93 | 950: '#450a0a', 94 | }, 95 | }; 96 | 97 | // Spacing system (in pixels, following 8pt grid) 98 | export const spacing = { 99 | 0: '0', 100 | 1: '0.25rem', // 4px 101 | 2: '0.5rem', // 8px 102 | 3: '0.75rem', // 12px 103 | 4: '1rem', // 16px 104 | 5: '1.25rem', // 20px 105 | 6: '1.5rem', // 24px 106 | 8: '2rem', // 32px 107 | 10: '2.5rem', // 40px 108 | 12: '3rem', // 48px 109 | 16: '4rem', // 64px 110 | 20: '5rem', // 80px 111 | 24: '6rem', // 96px 112 | }; 113 | 114 | // Typography scale 115 | export const typography = { 116 | fontFamily: { 117 | sans: 'Inter, system-ui, -apple-system, sans-serif', 118 | mono: 'ui-monospace, SFMono-Regular, Menlo, monospace', 119 | }, 120 | fontSize: { 121 | xs: '0.75rem', // 12px 122 | sm: '0.875rem', // 14px 123 | base: '1rem', // 16px 124 | lg: '1.125rem', // 18px 125 | xl: '1.25rem', // 20px 126 | '2xl': '1.5rem', // 24px 127 | '3xl': '1.875rem', // 30px 128 | '4xl': '2.25rem', // 36px 129 | }, 130 | fontWeight: { 131 | normal: '400', 132 | medium: '500', 133 | semibold: '600', 134 | bold: '700', 135 | }, 136 | lineHeight: { 137 | none: '1', 138 | tight: '1.25', 139 | snug: '1.375', 140 | normal: '1.5', 141 | relaxed: '1.625', 142 | loose: '2', 143 | }, 144 | }; 145 | 146 | // Border radius 147 | export const borderRadius = { 148 | none: '0', 149 | sm: '0.125rem', // 2px 150 | DEFAULT: '0.25rem', // 4px 151 | md: '0.375rem', // 6px 152 | lg: '0.5rem', // 8px 153 | xl: '0.75rem', // 12px 154 | '2xl': '1rem', // 16px 155 | '3xl': '1.5rem', // 24px 156 | full: '9999px', 157 | }; 158 | 159 | // Shadows 160 | export const shadows = { 161 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', 162 | DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 163 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 164 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 165 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 166 | }; 167 | 168 | // Z-index scale 169 | export const zIndex = { 170 | 0: '0', 171 | 10: '10', 172 | 20: '20', 173 | 30: '30', 174 | 40: '40', 175 | 50: '50', 176 | auto: 'auto', 177 | modal: '100', 178 | tooltip: '110', 179 | popover: '90', 180 | }; 181 | 182 | // Transitions 183 | export const transitions = { 184 | DEFAULT: '150ms cubic-bezier(0.4, 0, 0.2, 1)', 185 | fast: '100ms cubic-bezier(0.4, 0, 0.2, 1)', 186 | slow: '300ms cubic-bezier(0.4, 0, 0.2, 1)', 187 | }; 188 | 189 | // Export the full theme 190 | export const theme = { 191 | colors, 192 | spacing, 193 | typography, 194 | borderRadius, 195 | shadows, 196 | zIndex, 197 | transitions, 198 | }; 199 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/EnhancedServerCard.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { GradientButton, EnvironmentBadge, DatabaseTypeBadge } from './ui'; 4 | import { cn } from '../utils/cn'; 5 | import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes'; 6 | 7 | const EnhancedServerCard = ({ 8 | name, 9 | server, 10 | index, 11 | activeEnvironment, 12 | isSelected = false, 13 | onSelect, 14 | onEdit, 15 | onAddToClaude, 16 | onDelete 17 | }) => { 18 | const environmentCount = Object.keys(server.environments || {}).length; 19 | const hasActiveConnection = !!activeEnvironment; 20 | 21 | // Real connection status 22 | const connectionStatus = hasActiveConnection ? 'active' : 'configured'; 23 | const lastModified = server.modified ? new Date(server.modified).toLocaleDateString() : 'Unknown'; 24 | 25 | // Count environments connected to Claude 26 | const claudeActiveCount = hasActiveConnection ? 1 : 0; 27 | 28 | // Detect database type from active environment 29 | const activeEnvData = activeEnvironment ? server.environments[activeEnvironment] : {}; 30 | const databaseType = detectDatabaseType(activeEnvData); 31 | 32 | const handleRowClick = () => { 33 | if (onSelect) { 34 | onSelect(name); 35 | } 36 | }; 37 | 38 | const handleRadioChange = (e) => { 39 | e.stopPropagation(); 40 | if (onSelect) { 41 | onSelect(name); 42 | } 43 | }; 44 | 45 | const getConnectionStatusColor = () => { 46 | switch (connectionStatus) { 47 | case 'active': return 'text-green-600'; 48 | case 'configured': return 'text-[#86a0ff]'; 49 | case 'error': return 'text-red-600'; 50 | default: return 'text-gray-400'; 51 | } 52 | }; 53 | 54 | return ( 55 | <motion.div 56 | className={cn( 57 | "border-b border-gray-200 transition-colors cursor-pointer", 58 | isSelected 59 | ? "bg-blue-50 border-blue-200" 60 | : "bg-white hover:bg-gray-50" 61 | )} 62 | initial={{ opacity: 0, x: -10 }} 63 | animate={{ opacity: 1, x: 0 }} 64 | transition={{ duration: 0.2, delay: index * 0.02 }} 65 | onClick={handleRowClick} 66 | > 67 | <div className="px-6 py-4"> 68 | <div className="grid grid-cols-12 gap-4 items-center"> 69 | {/* Selection Radio */} 70 | <div className="col-span-1"> 71 | <input 72 | type="radio" 73 | name="database-selection" 74 | checked={isSelected} 75 | onChange={handleRadioChange} 76 | className="w-4 h-4 text-[#86a0ff] border-gray-300 focus:ring-[#86a0ff]" 77 | /> 78 | </div> 79 | 80 | {/* Database Info */} 81 | <div className="col-span-4"> 82 | <div className="flex items-center space-x-3"> 83 | <div className="w-8 h-8 bg-blue-50 rounded-lg flex items-center justify-center flex-shrink-0"> 84 | <svg className="h-4 w-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 85 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 86 | </svg> 87 | </div> 88 | <div className="min-w-0"> 89 | <h3 className="text-base font-semibold text-gray-900 truncate">{name}</h3> 90 | </div> 91 | </div> 92 | </div> 93 | 94 | {/* Active Environment */} 95 | <div className="col-span-2"> 96 | {hasActiveConnection && activeEnvironment ? ( 97 | <EnvironmentBadge environment={activeEnvironment} active size="sm" /> 98 | ) : ( 99 | <span className="text-sm text-gray-500">None</span> 100 | )} 101 | </div> 102 | 103 | {/* Environment Count */} 104 | <div className="col-span-2"> 105 | <span className="text-sm font-medium text-gray-600">{environmentCount}</span> 106 | </div> 107 | 108 | {/* Description */} 109 | <div className="col-span-3"> 110 | {server.description && ( 111 | <span className="text-sm text-gray-500 truncate block">{server.description}</span> 112 | )} 113 | </div> 114 | </div> 115 | </div> 116 | </motion.div> 117 | ); 118 | 119 | }; 120 | 121 | export default EnhancedServerCard; 122 | ``` -------------------------------------------------------------------------------- /src/constants/tool-definitions.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Tool definitions for HANA MCP Server 3 | * 4 | * Note: For tools that accept schema_name as an optional parameter, 5 | * the HANA_SCHEMA environment variable will be used if schema_name is not provided. 6 | */ 7 | 8 | const TOOLS = [ 9 | { 10 | name: "hana_show_config", 11 | description: "Show the HANA database configuration", 12 | inputSchema: { 13 | type: "object", 14 | properties: {}, 15 | required: [] 16 | } 17 | }, 18 | { 19 | name: "hana_test_connection", 20 | description: "Test connection to HANA database", 21 | inputSchema: { 22 | type: "object", 23 | properties: {}, 24 | required: [] 25 | } 26 | }, 27 | { 28 | name: "hana_list_schemas", 29 | description: "List all schemas in the HANA database", 30 | inputSchema: { 31 | type: "object", 32 | properties: {}, 33 | required: [] 34 | } 35 | }, 36 | { 37 | name: "hana_show_env_vars", 38 | description: "Show all HANA-related environment variables (for debugging)", 39 | inputSchema: { 40 | type: "object", 41 | properties: {}, 42 | required: [] 43 | } 44 | }, 45 | { 46 | name: "hana_list_tables", 47 | description: "List all tables in a specific schema", 48 | inputSchema: { 49 | type: "object", 50 | properties: { 51 | schema_name: { 52 | type: "string", 53 | description: "Name of the schema to list tables from (optional)" 54 | } 55 | }, 56 | required: [] 57 | } 58 | }, 59 | { 60 | name: "hana_describe_table", 61 | description: "Describe the structure of a specific table", 62 | inputSchema: { 63 | type: "object", 64 | properties: { 65 | schema_name: { 66 | type: "string", 67 | description: "Name of the schema containing the table (optional)" 68 | }, 69 | table_name: { 70 | type: "string", 71 | description: "Name of the table to describe" 72 | } 73 | }, 74 | required: ["table_name"] 75 | } 76 | }, 77 | { 78 | name: "hana_list_indexes", 79 | description: "List all indexes for a specific table", 80 | inputSchema: { 81 | type: "object", 82 | properties: { 83 | schema_name: { 84 | type: "string", 85 | description: "Name of the schema containing the table (optional)" 86 | }, 87 | table_name: { 88 | type: "string", 89 | description: "Name of the table to list indexes for" 90 | } 91 | }, 92 | required: ["table_name"] 93 | } 94 | }, 95 | { 96 | name: "hana_describe_index", 97 | description: "Describe the structure of a specific index", 98 | inputSchema: { 99 | type: "object", 100 | properties: { 101 | schema_name: { 102 | type: "string", 103 | description: "Name of the schema containing the table (optional)" 104 | }, 105 | table_name: { 106 | type: "string", 107 | description: "Name of the table containing the index" 108 | }, 109 | index_name: { 110 | type: "string", 111 | description: "Name of the index to describe" 112 | } 113 | }, 114 | required: ["table_name", "index_name"] 115 | } 116 | }, 117 | { 118 | name: "hana_execute_query", 119 | description: "Execute a custom SQL query against the HANA database", 120 | inputSchema: { 121 | type: "object", 122 | properties: { 123 | query: { 124 | type: "string", 125 | description: "The SQL query to execute" 126 | }, 127 | parameters: { 128 | type: "array", 129 | description: "Optional parameters for the query (for prepared statements)", 130 | items: { 131 | type: "string" 132 | } 133 | } 134 | }, 135 | required: ["query"] 136 | } 137 | } 138 | ]; 139 | 140 | // Tool categories for organization 141 | const TOOL_CATEGORIES = { 142 | CONFIGURATION: ['hana_show_config', 'hana_test_connection', 'hana_show_env_vars'], 143 | SCHEMA: ['hana_list_schemas'], 144 | TABLE: ['hana_list_tables', 'hana_describe_table'], 145 | INDEX: ['hana_list_indexes', 'hana_describe_index'], 146 | QUERY: ['hana_execute_query'] 147 | }; 148 | 149 | // Get tool by name 150 | function getTool(name) { 151 | return TOOLS.find(tool => tool.name === name); 152 | } 153 | 154 | // Get tools by category 155 | function getToolsByCategory(category) { 156 | const toolNames = TOOL_CATEGORIES[category] || []; 157 | return TOOLS.filter(tool => toolNames.includes(tool.name)); 158 | } 159 | 160 | // Get all tool names 161 | function getAllToolNames() { 162 | return TOOLS.map(tool => tool.name); 163 | } 164 | 165 | module.exports = { 166 | TOOLS, 167 | TOOL_CATEGORIES, 168 | getTool, 169 | getToolsByCategory, 170 | getAllToolNames 171 | }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/databaseTypes.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Database type detection and display utilities for HANA MCP UI 3 | */ 4 | 5 | export const DATABASE_TYPES = { 6 | SINGLE_CONTAINER: 'single_container', 7 | MDC_SYSTEM: 'mdc_system', 8 | MDC_TENANT: 'mdc_tenant' 9 | } 10 | 11 | /** 12 | * Detect database type based on configuration data 13 | * @param {Object} data - Configuration data 14 | * @returns {string} Database type 15 | */ 16 | export const detectDatabaseType = (data) => { 17 | if (!data) return DATABASE_TYPES.SINGLE_CONTAINER 18 | 19 | if (data.HANA_INSTANCE_NUMBER && data.HANA_DATABASE_NAME) { 20 | return DATABASE_TYPES.MDC_TENANT 21 | } else if (data.HANA_INSTANCE_NUMBER && !data.HANA_DATABASE_NAME) { 22 | return DATABASE_TYPES.MDC_SYSTEM 23 | } else { 24 | return DATABASE_TYPES.SINGLE_CONTAINER 25 | } 26 | } 27 | 28 | /** 29 | * Get display name for database type 30 | * @param {string} type - Database type 31 | * @returns {string} Display name 32 | */ 33 | export const getDatabaseTypeDisplayName = (type) => { 34 | const displayNames = { 35 | [DATABASE_TYPES.SINGLE_CONTAINER]: 'Single-Container Database', 36 | [DATABASE_TYPES.MDC_SYSTEM]: 'MDC System Database', 37 | [DATABASE_TYPES.MDC_TENANT]: 'MDC Tenant Database' 38 | } 39 | return displayNames[type] || 'Unknown Database Type' 40 | } 41 | 42 | /** 43 | * Get short display name for database type 44 | * @param {string} type - Database type 45 | * @returns {string} Short display name 46 | */ 47 | export const getDatabaseTypeShortName = (type) => { 48 | const shortNames = { 49 | [DATABASE_TYPES.SINGLE_CONTAINER]: 'Single-Container', 50 | [DATABASE_TYPES.MDC_SYSTEM]: 'MDC System', 51 | [DATABASE_TYPES.MDC_TENANT]: 'MDC Tenant' 52 | } 53 | return shortNames[type] || 'Unknown' 54 | } 55 | 56 | /** 57 | * Get color for database type badge 58 | * @param {string} type - Database type 59 | * @returns {string} Color class 60 | */ 61 | export const getDatabaseTypeColor = (type) => { 62 | const colors = { 63 | [DATABASE_TYPES.SINGLE_CONTAINER]: 'blue', 64 | [DATABASE_TYPES.MDC_SYSTEM]: 'amber', 65 | [DATABASE_TYPES.MDC_TENANT]: 'green' 66 | } 67 | return colors[type] || 'gray' 68 | } 69 | 70 | /** 71 | * Check if MDC fields should be shown 72 | * @param {string} detectedType - Auto-detected type 73 | * @param {string} manualType - Manually selected type 74 | * @returns {boolean} Should show MDC fields 75 | */ 76 | export const shouldShowMDCFields = (detectedType, manualType) => { 77 | // Show MDC fields only for MDC system or tenant types 78 | return manualType === DATABASE_TYPES.MDC_SYSTEM || 79 | manualType === DATABASE_TYPES.MDC_TENANT 80 | } 81 | 82 | /** 83 | * Get required fields for database type 84 | * @param {string} type - Database type 85 | * @returns {Array} Required field names 86 | */ 87 | export const getRequiredFieldsForType = (type) => { 88 | const baseFields = ['HANA_HOST', 'HANA_USER', 'HANA_PASSWORD'] 89 | 90 | switch (type) { 91 | case DATABASE_TYPES.MDC_TENANT: 92 | return [...baseFields, 'HANA_INSTANCE_NUMBER', 'HANA_DATABASE_NAME'] 93 | case DATABASE_TYPES.MDC_SYSTEM: 94 | return [...baseFields, 'HANA_INSTANCE_NUMBER'] 95 | case DATABASE_TYPES.SINGLE_CONTAINER: 96 | default: 97 | return baseFields 98 | } 99 | } 100 | 101 | /** 102 | * Get recommended fields for database type 103 | * @param {string} type - Database type 104 | * @returns {Array} Recommended field names 105 | */ 106 | export const getRecommendedFieldsForType = (type) => { 107 | switch (type) { 108 | case DATABASE_TYPES.SINGLE_CONTAINER: 109 | return ['HANA_SCHEMA'] 110 | default: 111 | return [] 112 | } 113 | } 114 | 115 | /** 116 | * Validate configuration for specific database type 117 | * @param {Object} data - Configuration data 118 | * @param {string} type - Database type 119 | * @returns {Object} Validation result 120 | */ 121 | export const validateForDatabaseType = (data, type) => { 122 | const errors = {} 123 | const requiredFields = getRequiredFieldsForType(type) 124 | const recommendedFields = getRecommendedFieldsForType(type) 125 | 126 | // Check required fields 127 | requiredFields.forEach(field => { 128 | if (!data[field] || data[field].toString().trim() === '') { 129 | errors[field] = `${field.replace('HANA_', '')} is required for ${getDatabaseTypeShortName(type)}` 130 | } 131 | }) 132 | 133 | // Check recommended fields 134 | recommendedFields.forEach(field => { 135 | if (!data[field] || data[field].toString().trim() === '') { 136 | errors[field] = `${field.replace('HANA_', '')} is recommended for ${getDatabaseTypeShortName(type)}` 137 | } 138 | }) 139 | 140 | return { 141 | valid: Object.keys(errors).length === 0, 142 | errors, 143 | databaseType: type 144 | } 145 | } 146 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeServerCard.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { EnvironmentBadge, DatabaseTypeBadge } from './ui' 3 | import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes' 4 | 5 | const ClaudeServerCard = ({ 6 | server, 7 | index, 8 | activeEnvironment, 9 | onRemove 10 | }) => { 11 | // Detect database type from server environment data 12 | const databaseType = detectDatabaseType(server.env || {}) 13 | 14 | return ( 15 | <motion.div 16 | className="bg-gray-50 border border-gray-100 rounded-lg p-3 hover:bg-gray-100 transition-all duration-200" 17 | initial={{ opacity: 0, y: 10 }} 18 | animate={{ opacity: 1, y: 0 }} 19 | transition={{ duration: 0.2, delay: index * 0.05 }} 20 | whileHover={{ y: -1 }} 21 | > 22 | {/* Header */} 23 | <div className="flex items-start justify-between mb-2"> 24 | <div className="flex-1 min-w-0"> 25 | <div className="flex items-center gap-2 mb-1"> 26 | <div className="w-1.5 h-1.5 bg-green-500 rounded-full"></div> 27 | <h4 className="text-sm font-medium text-gray-900 truncate">{server.name}</h4> 28 | <DatabaseTypeBadge type={databaseType} size="xs" /> 29 | </div> 30 | 31 | {activeEnvironment && ( 32 | <EnvironmentBadge environment={activeEnvironment} active size="xs" /> 33 | )} 34 | </div> 35 | 36 | <button 37 | onClick={onRemove} 38 | className="text-gray-400 hover:text-red-500 transition-colors duration-200 p-1 rounded hover:bg-white/60" 39 | title="Remove from Claude" 40 | > 41 | <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 42 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> 43 | </svg> 44 | </button> 45 | </div> 46 | 47 | {/* Connection Info */} 48 | <div className="space-y-1"> 49 | <div className="flex items-center justify-between text-xs"> 50 | <span className="text-gray-500">Host:</span> 51 | <span className="font-mono text-gray-700 bg-white/70 px-1.5 py-0.5 rounded text-xs truncate max-w-[100px]" title={server.env.HANA_HOST}> 52 | {server.env.HANA_HOST} 53 | </span> 54 | </div> 55 | 56 | {/* Show MDC-specific info when applicable */} 57 | {databaseType === 'mdc_tenant' && server.env.HANA_DATABASE_NAME && ( 58 | <div className="flex items-center justify-between text-xs"> 59 | <span className="text-gray-500">Database:</span> 60 | <span className="font-mono text-gray-700 bg-white/70 px-1.5 py-0.5 rounded text-xs truncate max-w-[100px]" title={server.env.HANA_DATABASE_NAME}> 61 | {server.env.HANA_DATABASE_NAME} 62 | </span> 63 | </div> 64 | )} 65 | 66 | {databaseType === 'mdc_system' && server.env.HANA_INSTANCE_NUMBER && ( 67 | <div className="flex items-center justify-between text-xs"> 68 | <span className="text-gray-500">Instance:</span> 69 | <span className="font-mono text-gray-700 bg-white/70 px-1.5 py-0.5 rounded text-xs"> 70 | {server.env.HANA_INSTANCE_NUMBER} 71 | </span> 72 | </div> 73 | )} 74 | 75 | <div className="flex items-center justify-between text-xs"> 76 | <span className="text-gray-500">Schema:</span> 77 | <span className="font-mono text-gray-700 bg-white/70 px-1.5 py-0.5 rounded text-xs truncate max-w-[100px]" title={server.env.HANA_SCHEMA}> 78 | {server.env.HANA_SCHEMA} 79 | </span> 80 | </div> 81 | </div> 82 | 83 | {/* Status */} 84 | <div className="mt-2 pt-2 border-t border-gray-200"> 85 | <div className="flex items-center justify-between"> 86 | <span className="text-xs text-green-700 font-medium flex items-center gap-1"> 87 | <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"> 88 | <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> 89 | </svg> 90 | Connected 91 | </span> 92 | <span className="text-xs text-gray-400"> 93 | Active 94 | </span> 95 | </div> 96 | </div> 97 | </motion.div> 98 | ) 99 | } 100 | 101 | export default ClaudeServerCard ``` -------------------------------------------------------------------------------- /src/database/hana-client.js: -------------------------------------------------------------------------------- ```javascript 1 | const hana = require('@sap/hana-client'); 2 | 3 | // Simple logger that doesn't interfere with JSON-RPC 4 | const log = (msg) => console.error(`[HANA Client] ${new Date().toISOString()}: ${msg}`); 5 | 6 | /** 7 | * Create and configure a HANA client 8 | * @param {Object} config - HANA connection configuration 9 | * @returns {Object} HANA client wrapper 10 | */ 11 | async function createHanaClient(config) { 12 | try { 13 | // Create connection 14 | const connection = hana.createConnection(); 15 | 16 | // Use connection parameter building if available 17 | const connectionParams = config.getConnectionParams ? 18 | config.getConnectionParams() : 19 | buildLegacyConnectionParams(config); 20 | 21 | // Log database type information 22 | const dbType = config.getHanaDatabaseType ? config.getHanaDatabaseType() : 'single_container'; 23 | log(`Connecting to HANA ${dbType} database...`); 24 | 25 | // Connect to HANA 26 | await connect(connection, connectionParams); 27 | 28 | log(`Successfully connected to HANA ${dbType} database`); 29 | 30 | // Return client wrapper with utility methods 31 | return { 32 | /** 33 | * Execute a SQL query 34 | * @param {string} sql - SQL query to execute 35 | * @param {Array} params - Query parameters 36 | * @returns {Promise<Array>} Query results 37 | */ 38 | async query(sql, params = []) { 39 | try { 40 | const statement = connection.prepare(sql); 41 | const results = await executeStatement(statement, params); 42 | statement.drop(); 43 | return results; 44 | } catch (error) { 45 | log('Query execution error:', error); 46 | throw new Error(`Query execution failed: ${error.message}`); 47 | } 48 | }, 49 | 50 | /** 51 | * Execute a SQL query that returns a single value 52 | * @param {string} sql - SQL query to execute 53 | * @param {Array} params - Query parameters 54 | * @returns {Promise<any>} Query result 55 | */ 56 | async queryScalar(sql, params = []) { 57 | const results = await this.query(sql, params); 58 | if (results.length === 0) return null; 59 | 60 | const firstRow = results[0]; 61 | const keys = Object.keys(firstRow); 62 | if (keys.length === 0) return null; 63 | 64 | return firstRow[keys[0]]; 65 | }, 66 | 67 | /** 68 | * Disconnect from HANA database 69 | * @returns {Promise<void>} 70 | */ 71 | async disconnect() { 72 | return new Promise((resolve, reject) => { 73 | connection.disconnect(err => { 74 | if (err) { 75 | log('Error disconnecting from HANA:', err); 76 | reject(err); 77 | } else { 78 | log('Disconnected from HANA database'); 79 | resolve(); 80 | } 81 | }); 82 | }); 83 | } 84 | }; 85 | } catch (error) { 86 | log(`Failed to create HANA client: ${error.message}`); 87 | throw error; 88 | } 89 | } 90 | 91 | /** 92 | * Build legacy connection parameters for backward compatibility 93 | */ 94 | function buildLegacyConnectionParams(config) { 95 | return { 96 | serverNode: `${config.host}:${config.port}`, 97 | uid: config.user, 98 | pwd: config.password, 99 | encrypt: config.encrypt !== false, 100 | sslValidateCertificate: config.validateCert !== false, 101 | ...config.additionalParams 102 | }; 103 | } 104 | 105 | /** 106 | * Connect to HANA database 107 | * @param {Object} connection - HANA connection object 108 | * @param {Object} params - Connection parameters 109 | * @returns {Promise<void>} 110 | */ 111 | function connect(connection, params) { 112 | return new Promise((resolve, reject) => { 113 | connection.connect(params, (err) => { 114 | if (err) { 115 | reject(new Error(`HANA connection failed: ${err.message}`)); 116 | } else { 117 | resolve(); 118 | } 119 | }); 120 | }); 121 | } 122 | 123 | /** 124 | * Execute a prepared statement 125 | * @param {Object} statement - Prepared statement 126 | * @param {Array} params - Statement parameters 127 | * @returns {Promise<Array>} Query results 128 | */ 129 | function executeStatement(statement, params) { 130 | return new Promise((resolve, reject) => { 131 | statement.execQuery(params, (err, results) => { 132 | if (err) { 133 | reject(err); 134 | } else { 135 | // Convert results to array of objects 136 | const rows = []; 137 | while (results.next()) { 138 | rows.push(results.getValues()); 139 | } 140 | resolve(rows); 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | module.exports = { 147 | createHanaClient 148 | }; 149 | ``` -------------------------------------------------------------------------------- /src/database/connection-manager.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * HANA Database Connection Manager 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { config } = require('../utils/config'); 7 | const { createHanaClient } = require('./hana-client'); 8 | 9 | class ConnectionManager { 10 | constructor() { 11 | this.client = null; 12 | this.isConnecting = false; 13 | this.lastConnectionAttempt = null; 14 | this.connectionRetries = 0; 15 | this.maxRetries = 3; 16 | } 17 | 18 | /** 19 | * Get or create HANA client connection 20 | */ 21 | async getClient() { 22 | // Return existing client if available 23 | if (this.client) { 24 | return this.client; 25 | } 26 | 27 | // Prevent multiple simultaneous connection attempts 28 | if (this.isConnecting) { 29 | logger.debug('Connection already in progress, waiting...'); 30 | while (this.isConnecting) { 31 | await new Promise(resolve => setTimeout(resolve, 100)); 32 | } 33 | return this.client; 34 | } 35 | 36 | // Check if configuration is valid 37 | if (!config.isHanaConfigured()) { 38 | logger.warn('HANA configuration is incomplete'); 39 | return null; 40 | } 41 | 42 | return this.connect(); 43 | } 44 | 45 | /** 46 | * Establish connection to HANA database 47 | */ 48 | async connect() { 49 | this.isConnecting = true; 50 | this.lastConnectionAttempt = new Date(); 51 | 52 | try { 53 | logger.info('Connecting to HANA database...'); 54 | 55 | const hanaConfig = config.getHanaConfig(); 56 | const dbType = config.getHanaDatabaseType(); 57 | 58 | logger.info(`Detected HANA database type: ${dbType}`); 59 | 60 | // Pass the full config object so the client can access the methods 61 | this.client = await createHanaClient(config); 62 | 63 | this.connectionRetries = 0; 64 | logger.info(`HANA client connected successfully to ${dbType} database`); 65 | 66 | return this.client; 67 | } catch (error) { 68 | this.connectionRetries++; 69 | logger.error(`Failed to connect to HANA (attempt ${this.connectionRetries}):`, error.message); 70 | 71 | if (this.connectionRetries < this.maxRetries) { 72 | logger.info(`Retrying connection in 2 seconds...`); 73 | await new Promise(resolve => setTimeout(resolve, 2000)); 74 | this.isConnecting = false; 75 | return this.connect(); 76 | } else { 77 | logger.error('Max connection retries reached'); 78 | this.isConnecting = false; 79 | return null; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Test the connection 86 | */ 87 | async testConnection() { 88 | const client = await this.getClient(); 89 | if (!client) { 90 | return { success: false, error: 'No client available' }; 91 | } 92 | 93 | try { 94 | const testQuery = 'SELECT 1 as test_value FROM DUMMY'; 95 | const result = await client.query(testQuery); 96 | 97 | if (result && result.length > 0) { 98 | return { 99 | success: true, 100 | result: result[0].TEST_VALUE 101 | }; 102 | } else { 103 | return { 104 | success: false, 105 | error: 'Connection test returned no results' 106 | }; 107 | } 108 | } catch (error) { 109 | logger.error('Connection test failed:', error.message); 110 | return { 111 | success: false, 112 | error: error.message 113 | }; 114 | } 115 | } 116 | 117 | /** 118 | * Check if connection is healthy 119 | */ 120 | async isHealthy() { 121 | const test = await this.testConnection(); 122 | return test.success; 123 | } 124 | 125 | /** 126 | * Disconnect from HANA database 127 | */ 128 | async disconnect() { 129 | if (this.client) { 130 | try { 131 | await this.client.disconnect(); 132 | logger.info('HANA client disconnected'); 133 | } catch (error) { 134 | logger.error('Error disconnecting HANA client:', error.message); 135 | } finally { 136 | this.client = null; 137 | this.connectionRetries = 0; 138 | } 139 | } 140 | } 141 | 142 | /** 143 | * Reset connection (disconnect and reconnect) 144 | */ 145 | async resetConnection() { 146 | logger.info('Resetting HANA connection...'); 147 | await this.disconnect(); 148 | this.connectionRetries = 0; 149 | return this.getClient(); 150 | } 151 | 152 | /** 153 | * Get connection status 154 | */ 155 | getStatus() { 156 | const dbType = config.getHanaDatabaseType(); 157 | 158 | return { 159 | connected: !!this.client, 160 | isConnecting: this.isConnecting, 161 | lastConnectionAttempt: this.lastConnectionAttempt, 162 | connectionRetries: this.connectionRetries, 163 | maxRetries: this.maxRetries, 164 | databaseType: dbType 165 | }; 166 | } 167 | } 168 | 169 | // Create singleton instance 170 | const connectionManager = new ConnectionManager(); 171 | 172 | module.exports = { ConnectionManager, connectionManager }; ``` -------------------------------------------------------------------------------- /src/server/mcp-handler.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * MCP Protocol Handler for JSON-RPC 2.0 communication 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { METHODS, ERROR_CODES, ERROR_MESSAGES, PROTOCOL_VERSIONS, SERVER_INFO, CAPABILITIES } = require('../constants/mcp-constants'); 7 | const ToolRegistry = require('../tools'); 8 | 9 | class MCPHandler { 10 | /** 11 | * Handle MCP request 12 | */ 13 | static async handleRequest(request) { 14 | const { id, method, params } = request; 15 | 16 | logger.method(method); 17 | 18 | try { 19 | switch (method) { 20 | case METHODS.INITIALIZE: 21 | return this.handleInitialize(id, params); 22 | 23 | case METHODS.TOOLS_LIST: 24 | return this.handleToolsList(id, params); 25 | 26 | case METHODS.TOOLS_CALL: 27 | return this.handleToolsCall(id, params); 28 | 29 | case METHODS.NOTIFICATIONS_INITIALIZED: 30 | return this.handleInitialized(id, params); 31 | 32 | case METHODS.PROMPTS_LIST: 33 | return this.handlePromptsList(id, params); 34 | 35 | default: 36 | return this.createErrorResponse(id, ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${method}`); 37 | } 38 | } catch (error) { 39 | logger.error(`Error handling request: ${error.message}`); 40 | return this.createErrorResponse(id, ERROR_CODES.INTERNAL_ERROR, error.message); 41 | } 42 | } 43 | 44 | /** 45 | * Handle initialize request 46 | */ 47 | static handleInitialize(id, params) { 48 | logger.info('Initializing server'); 49 | 50 | return { 51 | jsonrpc: '2.0', 52 | id, 53 | result: { 54 | protocolVersion: PROTOCOL_VERSIONS.LATEST, 55 | capabilities: CAPABILITIES, 56 | serverInfo: SERVER_INFO 57 | } 58 | }; 59 | } 60 | 61 | /** 62 | * Handle tools/list request 63 | */ 64 | static handleToolsList(id, params) { 65 | logger.info('Listing tools'); 66 | 67 | const tools = ToolRegistry.getTools(); 68 | 69 | return { 70 | jsonrpc: '2.0', 71 | id, 72 | result: { tools } 73 | }; 74 | } 75 | 76 | /** 77 | * Handle tools/call request 78 | */ 79 | static async handleToolsCall(id, params) { 80 | const { name, arguments: args } = params; 81 | 82 | logger.tool(name, args); 83 | 84 | // Validate tool exists 85 | if (!ToolRegistry.hasTool(name)) { 86 | return this.createErrorResponse(id, ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${name}`); 87 | } 88 | 89 | // Validate tool arguments 90 | const validation = ToolRegistry.validateToolArgs(name, args); 91 | if (!validation.valid) { 92 | return this.createErrorResponse(id, ERROR_CODES.INVALID_PARAMS, validation.error); 93 | } 94 | 95 | try { 96 | const result = await ToolRegistry.executeTool(name, args); 97 | 98 | return { 99 | jsonrpc: '2.0', 100 | id, 101 | result 102 | }; 103 | } catch (error) { 104 | logger.error(`Tool execution failed: ${error.message}`); 105 | return this.createErrorResponse(id, ERROR_CODES.INTERNAL_ERROR, error.message); 106 | } 107 | } 108 | 109 | /** 110 | * Handle notifications/initialized 111 | */ 112 | static handleInitialized(id, params) { 113 | logger.info('Server initialized'); 114 | return null; // No response for notifications 115 | } 116 | 117 | /** 118 | * Handle prompts/list request 119 | */ 120 | static handlePromptsList(id, params) { 121 | logger.info('Listing prompts'); 122 | 123 | const prompts = [ 124 | { 125 | name: "hana_query_builder", 126 | description: "Build a SQL query for HANA database", 127 | template: "I need to build a SQL query for HANA database that {{goal}}." 128 | }, 129 | { 130 | name: "hana_schema_explorer", 131 | description: "Explore HANA database schemas and tables", 132 | template: "I want to explore the schemas and tables in my HANA database." 133 | }, 134 | { 135 | name: "hana_connection_test", 136 | description: "Test HANA database connection", 137 | template: "Please test my HANA database connection and show the configuration." 138 | } 139 | ]; 140 | 141 | return { 142 | jsonrpc: '2.0', 143 | id, 144 | result: { prompts } 145 | }; 146 | } 147 | 148 | /** 149 | * Create error response 150 | */ 151 | static createErrorResponse(id, code, message) { 152 | return { 153 | jsonrpc: '2.0', 154 | id, 155 | error: { 156 | code, 157 | message: message || ERROR_MESSAGES[code] || 'Unknown error' 158 | } 159 | }; 160 | } 161 | 162 | /** 163 | * Validate JSON-RPC request 164 | */ 165 | static validateRequest(request) { 166 | if (!request || typeof request !== 'object') { 167 | return { valid: false, error: 'Invalid request: must be an object' }; 168 | } 169 | 170 | if (request.jsonrpc !== '2.0') { 171 | return { valid: false, error: 'Invalid JSON-RPC version' }; 172 | } 173 | 174 | if (!request.method) { 175 | return { valid: false, error: 'Missing method' }; 176 | } 177 | 178 | if (typeof request.method !== 'string') { 179 | return { valid: false, error: 'Method must be a string' }; 180 | } 181 | 182 | return { valid: true }; 183 | } 184 | } 185 | 186 | module.exports = MCPHandler; ``` -------------------------------------------------------------------------------- /src/tools/index-tools.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Index management tools for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { config } = require('../utils/config'); 7 | const QueryExecutor = require('../database/query-executor'); 8 | const Validators = require('../utils/validators'); 9 | const Formatters = require('../utils/formatters'); 10 | 11 | class IndexTools { 12 | /** 13 | * List indexes for a table 14 | */ 15 | static async listIndexes(args) { 16 | logger.tool('hana_list_indexes', args); 17 | 18 | let { schema_name, table_name } = args || {}; 19 | 20 | // Use default schema if not provided 21 | if (!schema_name) { 22 | if (config.hasDefaultSchema()) { 23 | schema_name = config.getDefaultSchema(); 24 | logger.info(`Using default schema: ${schema_name}`); 25 | } else { 26 | return Formatters.createErrorResponse( 27 | 'Schema name is required', 28 | 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' 29 | ); 30 | } 31 | } 32 | 33 | // Validate required parameters 34 | const validation = Validators.validateRequired(args, ['table_name'], 'hana_list_indexes'); 35 | if (!validation.valid) { 36 | return Formatters.createErrorResponse('Error: table_name parameter is required', validation.error); 37 | } 38 | 39 | // Validate schema and table names 40 | const schemaValidation = Validators.validateSchemaName(schema_name); 41 | if (!schemaValidation.valid) { 42 | return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); 43 | } 44 | 45 | const tableValidation = Validators.validateTableName(table_name); 46 | if (!tableValidation.valid) { 47 | return Formatters.createErrorResponse('Invalid table name', tableValidation.error); 48 | } 49 | 50 | try { 51 | const results = await QueryExecutor.getTableIndexes(schema_name, table_name); 52 | 53 | if (results.length === 0) { 54 | return Formatters.createResponse(`📋 No indexes found for table '${schema_name}.${table_name}'.`); 55 | } 56 | 57 | // Group by index name 58 | const indexMap = {}; 59 | results.forEach(row => { 60 | if (!indexMap[row.INDEX_NAME]) { 61 | indexMap[row.INDEX_NAME] = { 62 | type: row.INDEX_TYPE, 63 | isUnique: row.IS_UNIQUE === 'TRUE', 64 | columns: [] 65 | }; 66 | } 67 | indexMap[row.INDEX_NAME].columns.push(row.COLUMN_NAME); 68 | }); 69 | 70 | const formattedIndexes = Formatters.formatIndexList(indexMap, schema_name, table_name); 71 | 72 | return Formatters.createResponse(formattedIndexes); 73 | } catch (error) { 74 | logger.error('Error listing indexes:', error.message); 75 | return Formatters.createErrorResponse('Error listing indexes', error.message); 76 | } 77 | } 78 | 79 | /** 80 | * Describe index details 81 | */ 82 | static async describeIndex(args) { 83 | logger.tool('hana_describe_index', args); 84 | 85 | let { schema_name, table_name, index_name } = args || {}; 86 | 87 | // Use default schema if not provided 88 | if (!schema_name) { 89 | if (config.hasDefaultSchema()) { 90 | schema_name = config.getDefaultSchema(); 91 | logger.info(`Using default schema: ${schema_name}`); 92 | } else { 93 | return Formatters.createErrorResponse( 94 | 'Schema name is required', 95 | 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' 96 | ); 97 | } 98 | } 99 | 100 | // Validate required parameters 101 | const validation = Validators.validateRequired(args, ['table_name', 'index_name'], 'hana_describe_index'); 102 | if (!validation.valid) { 103 | return Formatters.createErrorResponse('Error: table_name and index_name parameters are required', validation.error); 104 | } 105 | 106 | // Validate schema, table, and index names 107 | const schemaValidation = Validators.validateSchemaName(schema_name); 108 | if (!schemaValidation.valid) { 109 | return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); 110 | } 111 | 112 | const tableValidation = Validators.validateTableName(table_name); 113 | if (!tableValidation.valid) { 114 | return Formatters.createErrorResponse('Invalid table name', tableValidation.error); 115 | } 116 | 117 | const indexValidation = Validators.validateIndexName(index_name); 118 | if (!indexValidation.valid) { 119 | return Formatters.createErrorResponse('Invalid index name', indexValidation.error); 120 | } 121 | 122 | try { 123 | const results = await QueryExecutor.getIndexDetails(schema_name, table_name, index_name); 124 | 125 | const formattedDetails = Formatters.formatIndexDetails(results, schema_name, table_name, index_name); 126 | 127 | return Formatters.createResponse(formattedDetails); 128 | } catch (error) { 129 | logger.error('Error describing index:', error.message); 130 | return Formatters.createErrorResponse('Error describing index', error.message); 131 | } 132 | } 133 | } 134 | 135 | module.exports = IndexTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/EnvironmentSelector.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { useEffect } from 'react' 3 | import { XMarkIcon, PlusIcon, CheckCircleIcon } from '@heroicons/react/24/outline' 4 | import { EnvironmentBadge } from './ui' 5 | 6 | const EnvironmentSelector = ({ 7 | isOpen, 8 | onClose, 9 | serverName, 10 | environments, 11 | activeEnvironment, 12 | onDeploy, 13 | isLoading 14 | }) => { 15 | useEffect(() => { 16 | if (!isOpen) return 17 | const onKeyDown = (e) => { 18 | if (e.key === 'Escape') onClose() 19 | } 20 | window.addEventListener('keydown', onKeyDown) 21 | return () => window.removeEventListener('keydown', onKeyDown) 22 | }, [isOpen, onClose]) 23 | 24 | if (!isOpen) return null 25 | 26 | return ( 27 | <motion.div 28 | className="fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4" 29 | initial={{ opacity: 0 }} 30 | animate={{ opacity: 1 }} 31 | exit={{ opacity: 0 }} 32 | onClick={onClose} 33 | > 34 | <motion.div 35 | className="bg-white rounded-2xl shadow-xl max-w-2xl w-full border border-gray-200 overflow-hidden" 36 | initial={{ scale: 0.9, opacity: 0, y: 20 }} 37 | animate={{ scale: 1, opacity: 1, y: 0 }} 38 | exit={{ scale: 0.9, opacity: 0, y: 20 }} 39 | transition={{ type: "spring", stiffness: 300, damping: 25 }} 40 | onClick={(e) => e.stopPropagation()} 41 | > 42 | {/* Header */} 43 | <div className="px-6 py-4 border-b border-gray-100"> 44 | <div className="flex items-center justify-between"> 45 | <div className="flex items-center gap-3"> 46 | <div className="p-2 bg-gray-100 rounded-lg"> 47 | <PlusIcon className="w-5 h-5 text-gray-600" /> 48 | </div> 49 | <div> 50 | <h2 className="text-xl font-semibold text-gray-900">Add to Claude Config</h2> 51 | <p className="text-sm text-gray-500 mt-0.5">Select environment for {serverName}</p> 52 | </div> 53 | </div> 54 | <button 55 | onClick={onClose} 56 | className="p-2 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors" 57 | > 58 | <XMarkIcon className="w-5 h-5" /> 59 | </button> 60 | </div> 61 | </div> 62 | 63 | {/* Body */} 64 | <div className="p-6"> 65 | <p className="text-gray-600 mb-6 text-sm"> 66 | Choose which environment configuration to add to Claude Desktop. Multiple environments from different databases can be active simultaneously. Each environment will be added as a separate connection. 67 | </p> 68 | 69 | <div className="space-y-3"> 70 | {Object.entries(environments).map(([env, config], index) => ( 71 | <motion.button 72 | key={env} 73 | onClick={() => onDeploy(env)} 74 | disabled={isLoading} 75 | className="w-full p-4 border border-gray-200 rounded-xl text-left transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" 76 | initial={{ opacity: 0, y: 10 }} 77 | animate={{ opacity: 1, y: 0 }} 78 | transition={{ duration: 0.2, delay: index * 0.05 }} 79 | whileHover={!isLoading ? { y: -1 } : {}} 80 | > 81 | <div className="flex justify-between items-center mb-3"> 82 | <div className="flex items-center gap-3"> 83 | <h3 className="text-lg font-medium text-gray-900">{env}</h3> 84 | <EnvironmentBadge environment={env} size="sm" /> 85 | </div> 86 | {activeEnvironment === env && ( 87 | <div className="flex items-center gap-2 px-2 py-1 bg-green-100 rounded-full"> 88 | <CheckCircleIcon className="w-4 h-4 text-green-600" /> 89 | <span className="text-green-700 text-xs font-medium">ACTIVE</span> 90 | </div> 91 | )} 92 | </div> 93 | <div className="grid grid-cols-2 gap-4 text-sm"> 94 | <div> 95 | <span className="text-gray-500">Host:</span> 96 | <p className="text-gray-700 font-mono text-xs mt-0.5">{config.HANA_HOST}</p> 97 | </div> 98 | <div> 99 | <span className="text-gray-500">Schema:</span> 100 | <p className="text-gray-700 font-mono text-xs mt-0.5">{config.HANA_SCHEMA}</p> 101 | </div> 102 | </div> 103 | </motion.button> 104 | ))} 105 | </div> 106 | </div> 107 | 108 | {/* Footer */} 109 | <div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex justify-end"> 110 | <button 111 | onClick={onClose} 112 | disabled={isLoading} 113 | className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-400 disabled:opacity-50 transition-colors" 114 | > 115 | Cancel 116 | </button> 117 | </div> 118 | </motion.div> 119 | </motion.div> 120 | ) 121 | } 122 | 123 | export default EnvironmentSelector ``` -------------------------------------------------------------------------------- /src/utils/validators.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Input validation utilities for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('./logger'); 6 | 7 | class Validators { 8 | /** 9 | * Validate required parameters 10 | */ 11 | static validateRequired(params, requiredFields, toolName) { 12 | const missing = []; 13 | 14 | for (const field of requiredFields) { 15 | if (!params || params[field] === undefined || params[field] === null || params[field] === '') { 16 | missing.push(field); 17 | } 18 | } 19 | 20 | if (missing.length > 0) { 21 | const error = `Missing required parameters: ${missing.join(', ')}`; 22 | logger.warn(`Validation failed for ${toolName}:`, error); 23 | return { valid: false, error }; 24 | } 25 | 26 | return { valid: true }; 27 | } 28 | 29 | /** 30 | * Validate schema name 31 | */ 32 | static validateSchemaName(schemaName) { 33 | if (!schemaName || typeof schemaName !== 'string') { 34 | return { valid: false, error: 'Schema name must be a non-empty string' }; 35 | } 36 | 37 | if (schemaName.length > 128) { 38 | return { valid: false, error: 'Schema name too long (max 128 characters)' }; 39 | } 40 | 41 | // Basic SQL identifier validation 42 | if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schemaName)) { 43 | return { valid: false, error: 'Invalid schema name format' }; 44 | } 45 | 46 | return { valid: true }; 47 | } 48 | 49 | /** 50 | * Validate table name 51 | */ 52 | static validateTableName(tableName) { 53 | if (!tableName || typeof tableName !== 'string') { 54 | return { valid: false, error: 'Table name must be a non-empty string' }; 55 | } 56 | 57 | if (tableName.length > 128) { 58 | return { valid: false, error: 'Table name too long (max 128 characters)' }; 59 | } 60 | 61 | // Basic SQL identifier validation 62 | if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) { 63 | return { valid: false, error: 'Invalid table name format' }; 64 | } 65 | 66 | return { valid: true }; 67 | } 68 | 69 | /** 70 | * Validate index name 71 | */ 72 | static validateIndexName(indexName) { 73 | if (!indexName || typeof indexName !== 'string') { 74 | return { valid: false, error: 'Index name must be a non-empty string' }; 75 | } 76 | 77 | if (indexName.length > 128) { 78 | return { valid: false, error: 'Index name too long (max 128 characters)' }; 79 | } 80 | 81 | // Basic SQL identifier validation 82 | if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(indexName)) { 83 | return { valid: false, error: 'Invalid index name format' }; 84 | } 85 | 86 | return { valid: true }; 87 | } 88 | 89 | /** 90 | * Validate SQL query 91 | */ 92 | static validateQuery(query) { 93 | if (!query || typeof query !== 'string') { 94 | return { valid: false, error: 'Query must be a non-empty string' }; 95 | } 96 | 97 | if (query.trim().length === 0) { 98 | return { valid: false, error: 'Query cannot be empty' }; 99 | } 100 | 101 | // Basic SQL injection prevention - check for suspicious patterns 102 | const suspiciousPatterns = [ 103 | /;\s*drop\s+table/i, 104 | /;\s*delete\s+from/i, 105 | /;\s*truncate\s+table/i, 106 | /;\s*alter\s+table/i, 107 | /;\s*create\s+table/i, 108 | /;\s*drop\s+database/i, 109 | /;\s*shutdown/i 110 | ]; 111 | 112 | for (const pattern of suspiciousPatterns) { 113 | if (pattern.test(query)) { 114 | return { valid: false, error: 'Query contains potentially dangerous operations' }; 115 | } 116 | } 117 | 118 | return { valid: true }; 119 | } 120 | 121 | /** 122 | * Validate query parameters 123 | */ 124 | static validateParameters(parameters) { 125 | if (!parameters) { 126 | return { valid: true }; // Parameters are optional 127 | } 128 | 129 | if (!Array.isArray(parameters)) { 130 | return { valid: false, error: 'Parameters must be an array' }; 131 | } 132 | 133 | for (let i = 0; i < parameters.length; i++) { 134 | const param = parameters[i]; 135 | if (param === undefined || param === null) { 136 | return { valid: false, error: `Parameter at index ${i} cannot be null or undefined` }; 137 | } 138 | } 139 | 140 | return { valid: true }; 141 | } 142 | 143 | /** 144 | * Validate tool arguments 145 | */ 146 | static validateToolArgs(args, toolName) { 147 | if (!args || typeof args !== 'object') { 148 | return { valid: false, error: 'Arguments must be an object' }; 149 | } 150 | 151 | logger.debug(`Validating arguments for ${toolName}:`, args); 152 | return { valid: true }; 153 | } 154 | 155 | /** 156 | * Validate configuration for specific database type 157 | */ 158 | static validateForDatabaseType(config) { 159 | const dbType = config.getHanaDatabaseType ? config.getHanaDatabaseType() : 'single_container'; 160 | const errors = []; 161 | 162 | switch (dbType) { 163 | case 'mdc_tenant': 164 | if (!config.instanceNumber) { 165 | errors.push('HANA_INSTANCE_NUMBER is required for MDC Tenant Database'); 166 | } 167 | if (!config.databaseName) { 168 | errors.push('HANA_DATABASE_NAME is required for MDC Tenant Database'); 169 | } 170 | break; 171 | case 'mdc_system': 172 | if (!config.instanceNumber) { 173 | errors.push('HANA_INSTANCE_NUMBER is required for MDC System Database'); 174 | } 175 | break; 176 | case 'single_container': 177 | if (!config.schema) { 178 | errors.push('HANA_SCHEMA is recommended for Single-Container Database'); 179 | } 180 | break; 181 | } 182 | 183 | return { 184 | valid: errors.length === 0, 185 | errors: errors, 186 | databaseType: dbType 187 | }; 188 | } 189 | } 190 | 191 | module.exports = Validators; ``` -------------------------------------------------------------------------------- /src/database/query-executor.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Query execution utilities for HANA database 3 | */ 4 | 5 | const { logger } = require('../utils/logger'); 6 | const { connectionManager } = require('./connection-manager'); 7 | const Validators = require('../utils/validators'); 8 | 9 | class QueryExecutor { 10 | /** 11 | * Execute a query with parameters 12 | */ 13 | static async executeQuery(query, parameters = []) { 14 | // Validate query 15 | const queryValidation = Validators.validateQuery(query); 16 | if (!queryValidation.valid) { 17 | throw new Error(queryValidation.error); 18 | } 19 | 20 | // Validate parameters 21 | const paramValidation = Validators.validateParameters(parameters); 22 | if (!paramValidation.valid) { 23 | throw new Error(paramValidation.error); 24 | } 25 | 26 | const client = await connectionManager.getClient(); 27 | if (!client) { 28 | throw new Error('HANA client not connected. Please check your HANA configuration.'); 29 | } 30 | 31 | try { 32 | logger.debug(`Executing query: ${query}`, parameters.length > 0 ? `with ${parameters.length} parameters` : ''); 33 | const results = await client.query(query, parameters); 34 | logger.debug(`Query executed successfully, returned ${results.length} rows`); 35 | return results; 36 | } catch (error) { 37 | logger.error(`Query execution failed: ${error.message}`); 38 | throw error; 39 | } 40 | } 41 | 42 | /** 43 | * Execute a scalar query (returns single value) 44 | */ 45 | static async executeScalarQuery(query, parameters = []) { 46 | const results = await this.executeQuery(query, parameters); 47 | 48 | if (results.length === 0) { 49 | return null; 50 | } 51 | 52 | const firstRow = results[0]; 53 | const firstColumn = Object.keys(firstRow)[0]; 54 | 55 | return firstRow[firstColumn]; 56 | } 57 | 58 | /** 59 | * Get all schemas 60 | */ 61 | static async getSchemas() { 62 | const query = `SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME`; 63 | const results = await this.executeQuery(query); 64 | return results.map(row => row.SCHEMA_NAME); 65 | } 66 | 67 | /** 68 | * Get tables in a schema 69 | */ 70 | static async getTables(schemaName) { 71 | const query = ` 72 | SELECT TABLE_NAME 73 | FROM SYS.TABLES 74 | WHERE SCHEMA_NAME = ? 75 | ORDER BY TABLE_NAME 76 | `; 77 | 78 | const results = await this.executeQuery(query, [schemaName]); 79 | return results.map(row => row.TABLE_NAME); 80 | } 81 | 82 | /** 83 | * Get table columns 84 | */ 85 | static async getTableColumns(schemaName, tableName) { 86 | const query = ` 87 | SELECT 88 | COLUMN_NAME, 89 | DATA_TYPE_NAME, 90 | LENGTH, 91 | SCALE, 92 | IS_NULLABLE, 93 | DEFAULT_VALUE, 94 | POSITION, 95 | COMMENTS 96 | FROM 97 | SYS.TABLE_COLUMNS 98 | WHERE 99 | SCHEMA_NAME = ? AND TABLE_NAME = ? 100 | ORDER BY 101 | POSITION 102 | `; 103 | 104 | return await this.executeQuery(query, [schemaName, tableName]); 105 | } 106 | 107 | /** 108 | * Get table indexes 109 | */ 110 | static async getTableIndexes(schemaName, tableName) { 111 | const query = ` 112 | SELECT 113 | INDEX_NAME, 114 | INDEX_TYPE, 115 | IS_UNIQUE, 116 | COLUMN_NAME 117 | FROM 118 | SYS.INDEX_COLUMNS ic 119 | JOIN 120 | SYS.INDEXES i ON ic.INDEX_NAME = i.INDEX_NAME 121 | AND ic.SCHEMA_NAME = i.SCHEMA_NAME 122 | WHERE 123 | ic.SCHEMA_NAME = ? AND ic.TABLE_NAME = ? 124 | ORDER BY 125 | ic.INDEX_NAME, ic.POSITION 126 | `; 127 | 128 | return await this.executeQuery(query, [schemaName, tableName]); 129 | } 130 | 131 | /** 132 | * Get index details 133 | */ 134 | static async getIndexDetails(schemaName, tableName, indexName) { 135 | const query = ` 136 | SELECT 137 | i.INDEX_NAME, 138 | i.INDEX_TYPE, 139 | i.IS_UNIQUE, 140 | ic.COLUMN_NAME, 141 | ic.POSITION, 142 | ic.ORDER 143 | FROM 144 | SYS.INDEXES i 145 | JOIN 146 | SYS.INDEX_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME 147 | AND i.SCHEMA_NAME = ic.SCHEMA_NAME 148 | WHERE 149 | i.SCHEMA_NAME = ? AND i.TABLE_NAME = ? AND i.INDEX_NAME = ? 150 | ORDER BY 151 | ic.POSITION 152 | `; 153 | 154 | return await this.executeQuery(query, [schemaName, tableName, indexName]); 155 | } 156 | 157 | /** 158 | * Test database connection 159 | */ 160 | static async testConnection() { 161 | return await connectionManager.testConnection(); 162 | } 163 | 164 | /** 165 | * Get database information 166 | */ 167 | static async getDatabaseInfo() { 168 | try { 169 | const versionQuery = 'SELECT * FROM M_DATABASE'; 170 | const version = await this.executeQuery(versionQuery); 171 | 172 | const userQuery = 'SELECT CURRENT_USER, CURRENT_SCHEMA FROM DUMMY'; 173 | const user = await this.executeQuery(userQuery); 174 | 175 | return { 176 | version: version.length > 0 ? version[0] : null, 177 | currentUser: user.length > 0 ? user[0].CURRENT_USER : null, 178 | currentSchema: user.length > 0 ? user[0].CURRENT_SCHEMA : null 179 | }; 180 | } catch (error) { 181 | logger.error('Failed to get database info:', error.message); 182 | return { error: error.message }; 183 | } 184 | } 185 | 186 | /** 187 | * Get table row count 188 | */ 189 | static async getTableRowCount(schemaName, tableName) { 190 | const query = `SELECT COUNT(*) as ROW_COUNT FROM "${schemaName}"."${tableName}"`; 191 | const result = await this.executeScalarQuery(query); 192 | return result; 193 | } 194 | 195 | /** 196 | * Get table sample data 197 | */ 198 | static async getTableSample(schemaName, tableName, limit = 10) { 199 | const query = `SELECT * FROM "${schemaName}"."${tableName}" LIMIT ?`; 200 | return await this.executeQuery(query, [limit]); 201 | } 202 | } 203 | 204 | module.exports = QueryExecutor; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/StatusBadge.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { cn } from '../../utils/cn' 3 | 4 | const StatusBadge = ({ 5 | status, 6 | count, 7 | showPulse = true, 8 | size = 'md', 9 | children, 10 | className 11 | }) => { 12 | const statusConfig = { 13 | online: { 14 | color: 'bg-gray-700', 15 | glow: 'shadow-gray-200/50', 16 | text: 'Online', 17 | className: 'status-online' 18 | }, 19 | offline: { 20 | color: 'bg-gray-400', 21 | glow: 'shadow-gray-200/50', 22 | text: 'Offline', 23 | className: 'status-offline' 24 | }, 25 | warning: { 26 | color: 'bg-gray-600', 27 | glow: 'shadow-gray-200/50', 28 | text: 'Warning', 29 | className: 'status-warning' 30 | }, 31 | error: { 32 | color: 'bg-gray-800', 33 | glow: 'shadow-gray-200/50', 34 | text: 'Error', 35 | className: 'status-error' 36 | } 37 | } 38 | 39 | const sizes = { 40 | xs: { dot: 'w-1.5 h-1.5', text: 'text-xs', padding: 'px-1.5 py-0.5' }, 41 | sm: { dot: 'w-2 h-2', text: 'text-xs', padding: 'px-2 py-1' }, 42 | md: { dot: 'w-3 h-3', text: 'text-sm', padding: 'px-3 py-1' }, 43 | lg: { dot: 'w-4 h-4', text: 'text-base', padding: 'px-4 py-2' } 44 | } 45 | 46 | const config = statusConfig[status] || statusConfig.offline 47 | const sizeConfig = sizes[size] 48 | 49 | return ( 50 | <motion.div 51 | className={cn( 52 | 'inline-flex items-center gap-2 rounded-full', 53 | sizeConfig.padding, 54 | className 55 | )} 56 | initial={{ scale: 0.8, opacity: 0 }} 57 | animate={{ scale: 1, opacity: 1 }} 58 | transition={{ duration: 0.2 }} 59 | > 60 | <div className={cn('relative flex items-center justify-center rounded-full', sizeConfig.dot)}> 61 | <div className={cn('w-full h-full rounded-full shadow-lg', config.color, config.glow)} /> 62 | {showPulse && status === 'online' && ( 63 | <div className={cn( 64 | 'absolute inset-0 rounded-full animate-ping opacity-75', 65 | config.color 66 | )} /> 67 | )} 68 | </div> 69 | 70 | <span className={cn('font-medium', sizeConfig.text)}> 71 | {children || config.text} 72 | {count !== undefined && ( 73 | <span className="ml-1 px-2 py-0.5 bg-gray-200 text-gray-700 rounded-full text-xs"> 74 | {count} 75 | </span> 76 | )} 77 | </span> 78 | </motion.div> 79 | ) 80 | } 81 | 82 | // Environment Badge Component 83 | export const EnvironmentBadge = ({ environment, active = false, size = 'sm', className }) => { 84 | const envClasses = { 85 | Production: 'bg-green-50 border-green-200 text-green-800', 86 | Development: 'bg-[#86a0ff] border-[#86a0ff] text-white', 87 | Staging: 'bg-amber-50 border-amber-200 text-amber-800', 88 | STAGING: 'bg-amber-50 border-amber-200 text-amber-800', 89 | Testing: 'bg-purple-50 border-purple-200 text-purple-800', 90 | QA: 'bg-indigo-50 border-indigo-200 text-indigo-800' 91 | } 92 | 93 | const sizeClasses = { 94 | xs: 'px-1.5 py-0.5 text-xs', 95 | sm: 'px-2 py-1 text-xs', 96 | md: 'px-3 py-1 text-sm', 97 | lg: 'px-4 py-2 text-base' 98 | } 99 | 100 | const activeRingClasses = { 101 | Production: 'ring-2 ring-green-300 shadow-sm', 102 | Development: 'ring-2 ring-[#86a0ff]/30 shadow-sm', 103 | Staging: 'ring-2 ring-amber-300 shadow-sm', 104 | STAGING: 'ring-2 ring-amber-300 shadow-sm', 105 | Testing: 'ring-2 ring-purple-300 shadow-sm', 106 | QA: 'ring-2 ring-indigo-300 shadow-sm' 107 | } 108 | 109 | const dotClasses = { 110 | Production: 'bg-green-600', 111 | Development: 'bg-[#86a0ff]', 112 | Staging: 'bg-amber-600', 113 | STAGING: 'bg-amber-600', 114 | Testing: 'bg-purple-600', 115 | QA: 'bg-indigo-600' 116 | } 117 | 118 | // Enhanced active state styling 119 | const activeStateClasses = active ? { 120 | Production: 'bg-green-100 border-green-300 text-green-900 shadow-md', 121 | Development: 'bg-[#86a0ff]/90 border-[#86a0ff] text-white shadow-md', 122 | Staging: 'bg-amber-100 border-amber-300 text-amber-900 shadow-md', 123 | STAGING: 'bg-amber-100 border-amber-300 text-amber-900 shadow-md', 124 | Testing: 'bg-purple-100 border-purple-300 text-purple-900 shadow-md', 125 | QA: 'bg-indigo-100 border-indigo-300 text-indigo-900 shadow-md' 126 | } : {} 127 | 128 | return ( 129 | <motion.span 130 | className={cn( 131 | 'inline-flex items-center rounded-full font-medium', 132 | 'border transition-all duration-200', 133 | sizeClasses[size], 134 | active ? (activeStateClasses[environment] || 'bg-green-100 border-green-300 text-green-900 shadow-md') : 135 | (envClasses[environment] || 'bg-gray-50 border-gray-200 text-gray-700'), 136 | active && (activeRingClasses[environment] || 'ring-2 ring-green-300 shadow-sm'), 137 | className 138 | )} 139 | whileHover={{ scale: 1.05 }} 140 | transition={{ duration: 0.1 }} 141 | title={`${environment} environment${active ? ' (active)' : ''}`} 142 | > 143 | {environment} 144 | {active && ( 145 | <motion.div 146 | className="ml-1.5 flex items-center space-x-1" 147 | initial={{ opacity: 0, scale: 0.8 }} 148 | animate={{ opacity: 1, scale: 1 }} 149 | transition={{ duration: 0.2 }} 150 | > 151 | <div 152 | className={cn("w-2 h-2 rounded-full", dotClasses[environment] || 'bg-green-600')} 153 | animate={{ opacity: [1, 0.5, 1] }} 154 | transition={{ duration: 1.5, repeat: Infinity }} 155 | /> 156 | <svg className="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 157 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> 158 | </svg> 159 | </motion.div> 160 | )} 161 | </motion.span> 162 | ) 163 | } 164 | 165 | export default StatusBadge ```