This is page 1 of 2. Use http://codebase.md/hatrigt/hana-mcp-server?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: -------------------------------------------------------------------------------- ``` # Server configuration MCP_TRANSPORT=http MCP_HOST=localhost MCP_PORT=3000 # HANA connection configuration HANA_HOST=your-hana-host HANA_PORT=30015 HANA_USER=your-username HANA_PASSWORD=your-password HANA_ENCRYPT=true HANA_VALIDATE_CERT=true # Security configuration MCP_READ_ONLY=true MCP_ALLOWED_SCHEMAS=SCHEMA1,SCHEMA2 # Logging configuration LOG_LEVEL=info ``` -------------------------------------------------------------------------------- /hana-mcp-ui/.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Production build dist/ build/ # Environment variables .env .env.local .env.development.local .env.test.local .env.production.local # IDE .vscode/ .idea/ *.swp *.swo # OS .DS_Store Thumbs.db # Logs *.log logs/ # Runtime data pids/ *.pid *.seed *.pid.lock # Application data data/ # Temporary files tmp/ temp/ ``` -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- ``` # Development files tests/ docs/ setup.sh claude_template.json # Configuration files *.config.json *config*.json *credential*.json *secret*.json *password*.json *key*.json # Environment files .env* *.env # Logs *.log logs/ # Editor files .vscode/ .idea/ *.suo *.ntvs* *.njsproj *.sln *.sw? # OS files .DS_Store Thumbs.db # Git .git/ .gitignore # Temporary files *.tmp *.temp *backup* *POC* # Build artifacts dist/ build/ coverage/ # Package manager files package-lock.json yarn.lock ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependency directories node_modules/ npm-debug.log yarn-debug.log yarn-error.log # Environment variables .env .env.local .env.production .env.staging # Security - Credential files *config*.json *credential*.json *secret*.json *password*.json *key*.json # Build output dist/ build/ coverage/ # Editor directories and files .idea/ .vscode/ *.suo *.ntvs* *.njsproj *.sln *.sw? .DS_Store # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock *claude_con* *POC *backup* image.png .qod* hana-mcp-ui/bun.lock hana-mcp-ui/bun.lockb ``` -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- ```markdown # HANA MCP Server Tests This folder contains various testing approaches for the HANA MCP Server. ## Folder Structure ``` tests/ ├── README.md # This file ├── mcpInspector/ # MCP Inspector configuration and setup │ └── mcp-inspector-config.json ├── manual/ # Manual testing scripts │ └── manual-test.js └── automated/ # Automated testing scripts └── test-mcp-inspector.js ``` ## Testing Approaches ### 1. MCP Inspector (Recommended) **Location**: `tests/mcpInspector/` The MCP Inspector provides a web-based UI for testing MCP servers. **Setup**: 1. Open https://modelcontextprotocol.io/inspector 2. Use the configuration from `mcp-inspector-config.json` 3. Connect and test tools interactively **Configuration**: - Command: `/opt/homebrew/bin/node` - Arguments: `/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js` - Environment variables: See `mcp-inspector-config.json` ### 2. Manual Testing **Location**: `tests/manual/` Interactive command-line testing with menu-driven interface. **Usage**: ```bash cd tests/manual node manual-test.js ``` ### 3. Automated Testing **Location**: `tests/automated/` Automated test suite that runs all tools and validates responses. **Usage**: ```bash cd tests/automated node test-mcp-inspector.js ``` ## Environment Variables Required All tests require these environment variables: - `HANA_HOST`: HANA database host - `HANA_PORT`: HANA database port (usually 443) - `HANA_USER`: HANA database username - `HANA_PASSWORD`: HANA database password - `HANA_SCHEMA`: HANA database schema - `HANA_SSL`: SSL enabled (true/false) - `HANA_ENCRYPT`: Encryption enabled (true/false) - `HANA_VALIDATE_CERT`: Certificate validation (true/false) ## Quick Start 1. **For interactive testing**: Use MCP Inspector 2. **For quick validation**: Run automated tests 3. **For debugging**: Use manual testing ``` -------------------------------------------------------------------------------- /hana-mcp-ui/README.md: -------------------------------------------------------------------------------- ```markdown # HANA MCP UI [](https://www.npmjs.com/package/hana-mcp-ui) [](https://www.npmjs.com/package/hana-mcp-ui) [](https://nodejs.org/) [](LICENSE) > **Visual interface for managing HANA MCP server configurations with Claude Desktop integration** ## 🚀 Quick Start ### 1. Run the UI ```bash npx hana-mcp-ui ``` That's it! The UI will: - 📦 Install automatically (if not cached) - 🔧 Start the backend server on port 3001 - ⚡ Start the React frontend on port 5173 - 🌐 Open your browser automatically ### 2. First-Time Setup On first run, you'll be prompted to set your Claude Desktop config path: - **🍎 macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - **🪟 Windows**: `%APPDATA%\Claude/claude_desktop_config.json` - **🐧 Linux**: `~/.config/claude/claude_desktop_config.json` The system suggests the correct path for your OS. ## 🎯 What You Get ### Visual Database Management - **🌐 Web Interface**: Modern, responsive React UI - **🔄 Multi-Environment**: Configure Production, Development, Staging per server - **🤖 Claude Integration**: Deploy configurations directly to Claude Desktop - **📊 Real-time Status**: Monitor active and configured servers - **✅ Smart Validation**: Comprehensive form validation for database connections ### Key Features - **One-Click Deployment**: Add databases to Claude Desktop with a single click - **Environment Management**: Switch between different database environments - **Configuration Backup**: Automatic backups before making changes - **Connection Testing**: Test database connectivity before deployment - **Clean Interface**: Intuitive design with smooth animations  ## 🛠️ How to Use ### 1. Add Database Configuration - Click **"+ Add Database"** - Enter database details (host, user, password, etc.) - Configure environments (Production, Development, Staging) ### 2. Add to Claude Desktop - Select a database from your list - Choose which environment to deploy - Click **"Add to Claude"** - Restart Claude Desktop to activate ### 3. Manage Active Connections - View all databases currently active in Claude - Remove connections when no longer needed - Monitor connection status ## ⚙️ Configuration Schema ### Required Fields | Parameter | Description | Example | |-----------|-------------|---------| | `HANA_HOST` | Database hostname or IP address | `hana.company.com` | | `HANA_USER` | Database username | `DBADMIN` | | `HANA_PASSWORD` | Database password | `your-secure-password` | ### Optional Fields | Parameter | Description | Default | Options | |-----------|-------------|---------|---------| | `HANA_PORT` | Database port | `443` | Any valid port number | | `HANA_SCHEMA` | Default schema name | - | Schema name | | `HANA_CONNECTION_TYPE` | Connection type | `auto` | `auto`, `single_container`, `mdc_system`, `mdc_tenant` | | `HANA_INSTANCE_NUMBER` | Instance number (MDC) | - | Instance number (e.g., `10`) | | `HANA_DATABASE_NAME` | Database name (MDC tenant) | - | Database name (e.g., `HQQ`) | | `HANA_SSL` | Enable SSL connection | `true` | `true`, `false` | | `HANA_ENCRYPT` | Enable encryption | `true` | `true`, `false` | | `HANA_VALIDATE_CERT` | Validate SSL certificates | `true` | `true`, `false` | | `LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` | | `ENABLE_FILE_LOGGING` | Enable file logging | `true` | `true`, `false` | | `ENABLE_CONSOLE_LOGGING` | Enable console logging | `false` | `true`, `false` | ### Database Connection Types #### 1. Single-Container Database Standard HANA database with single tenant. **Required**: `HANA_HOST`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_PORT`, `HANA_SCHEMA` #### 2. MDC System Database Multi-tenant system database (manages tenants). **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_SCHEMA` #### 3. MDC Tenant Database Multi-tenant tenant database (specific tenant). **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_DATABASE_NAME`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_SCHEMA` #### Auto-Detection When `HANA_CONNECTION_TYPE` is set to `auto` (default), the server automatically detects the type: - If `HANA_INSTANCE_NUMBER` + `HANA_DATABASE_NAME` → **MDC Tenant** - If only `HANA_INSTANCE_NUMBER` → **MDC System** - If neither → **Single-Container** ## 🔌 Prerequisites Before using the UI, install the core server: ```bash npm install -g hana-mcp-server ``` The UI works as a management interface for the installed server. ## 🏗️ Architecture ### System Architecture ### Technology Stack - **Frontend**: React 19 with Vite build system - **Backend**: Express.js REST API - **Storage**: Local file system for configurations - **Integration**: Claude Desktop configuration management - **Styling**: Tailwind CSS with custom components - **Animations**: Framer Motion for smooth interactions - **Icons**: Heroicons for consistent iconography ### Component Architecture ``` hana-mcp-ui/ ├── 📁 bin/ │ └── cli.js # NPX entry point launcher ├── 📁 server/ │ └── index.js # Express backend server ├── 📁 src/ │ ├── main.jsx # React entry point │ ├── App.jsx # Main app component │ └── components/ │ ├── 🏠 MainApp.jsx # Main application container │ ├── 🎛️ ConfigurationModal.jsx # Server configuration modal │ ├── 📋 DatabaseListView.jsx # Database list management │ ├── 🤖 ClaudeDesktopView.jsx # Claude integration view │ ├── 📊 DashboardView.jsx # Dashboard overview │ ├── 🎯 EnvironmentSelector.jsx # Environment selection │ ├── 📱 VerticalSidebar.jsx # Navigation sidebar │ └── 🎨 ui/ # Reusable UI components │ ├── GlassWindow.jsx # Glass morphism container │ ├── StatusBadge.jsx # Status indicators │ ├── DatabaseTypeBadge.jsx # Database type badges │ └── LoadingSpinner.jsx # Loading states ├── 📁 dist/ # Built React app (production) ├── 📁 data/ # Local configuration storage ├── 📄 package.json # Dependencies and scripts ├── ⚙️ vite.config.js # Vite build configuration └── 🌐 index.html # HTML template ``` ## 📋 Requirements - **Node.js**: 18.0.0 or higher - **Claude Desktop**: For deployment features - **Browser**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ ## 🔧 Development ### Local Development ```bash git clone https://github.com/hatrigt/hana-mcp-server.git cd hana-mcp-server/hana-mcp-ui npm install npm run dev ``` ### Build for Production ```bash npm run build npm run preview ``` ## 🚀 Performance - **Startup**: < 5 seconds - **API Response**: < 500ms - **UI Interactions**: < 100ms - **Bundle Size**: ~264KB (gzipped: ~83KB) ## 🔒 Security - **Local-only API** (no external connections) - **Secure file access** patterns - **Automatic backups** before configuration changes - **Password masking** in UI forms ## 🤝 Support - **Issues**: [GitHub Issues](https://github.com/hatrigt/hana-mcp-server/issues) - **Main Package**: [HANA MCP Server](https://www.npmjs.com/package/hana-mcp-server) - **Documentation**: [Full Documentation](https://github.com/hatrigt/hana-mcp-server#readme) ## 📄 License MIT License - see LICENSE file for details. ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # HANA MCP Server [](https://www.npmjs.com/package/hana-mcp-server) [](https://www.npmjs.com/package/hana-mcp-server) [](https://nodejs.org/) [](LICENSE) [](https://modelcontextprotocol.io/) > **Model Context Protocol (MCP) server for seamless SAP HANA database integration with AI agents and development tools.** ## 🚀 Quick Start ### 1. Install ```bash npm install -g hana-mcp-server ``` ### 2. Configure Claude Desktop Update your Claude Desktop config file: **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows**: `%APPDATA%\claude\claude_desktop_config.json` **Linux**: `~/.config/claude/claude_desktop_config.json` ```json { "mcpServers": { "HANA Database": { "command": "hana-mcp-server", "env": { "HANA_HOST": "your-hana-host.com", "HANA_PORT": "443", "HANA_USER": "your-username", "HANA_PASSWORD": "your-password", "HANA_SCHEMA": "your-schema", "HANA_SSL": "true", "HANA_ENCRYPT": "true", "HANA_VALIDATE_CERT": "true", "HANA_CONNECTION_TYPE": "auto", "HANA_INSTANCE_NUMBER": "10", "HANA_DATABASE_NAME": "HQQ", "LOG_LEVEL": "info", "ENABLE_FILE_LOGGING": "true", "ENABLE_CONSOLE_LOGGING": "false" } } } } ``` ### 3. Restart Claude Desktop Close and reopen Claude Desktop to load the configuration. ### 4. Test It! Ask Claude: *"Show me the available schemas in my HANA database"* ## 🎯 What You Get ### Database Operations - **Schema Exploration**: List schemas, tables, and table structures - **Query Execution**: Run SQL queries with natural language - **Data Sampling**: Get sample data from tables - **System Information**: Monitor database status and performance ### AI Integration - **Natural Language Queries**: "Show me all tables in the SYSTEM schema" - **Query Building**: "Create a query to find customers with orders > $1000" - **Data Analysis**: "Get sample data from the ORDERS table" - **Schema Navigation**: "Describe the structure of table CUSTOMERS" ## 🖥️ Visual Configuration (Recommended) For easier setup and management, use the **HANA MCP UI**: ```bash npx hana-mcp-ui ``` This opens a web interface where you can: - Configure multiple database environments - Deploy configurations to Claude Desktop with one click - Manage active connections - Test database connectivity  ## 🛠️ Configuration Options ### Required Parameters | Parameter | Description | Example | |-----------|-------------|---------| | `HANA_HOST` | Database hostname or IP address | `hana.company.com` | | `HANA_USER` | Database username | `DBADMIN` | | `HANA_PASSWORD` | Database password | `your-secure-password` | ### Optional Parameters | Parameter | Description | Default | Options | |-----------|-------------|---------|---------| | `HANA_PORT` | Database port | `443` | Any valid port number | | `HANA_SCHEMA` | Default schema name | - | Schema name | | `HANA_CONNECTION_TYPE` | Connection type | `auto` | `auto`, `single_container`, `mdc_system`, `mdc_tenant` | | `HANA_INSTANCE_NUMBER` | Instance number (MDC) | - | Instance number (e.g., `10`) | | `HANA_DATABASE_NAME` | Database name (MDC tenant) | - | Database name (e.g., `HQQ`) | | `HANA_SSL` | Enable SSL connection | `true` | `true`, `false` | | `HANA_ENCRYPT` | Enable encryption | `true` | `true`, `false` | | `HANA_VALIDATE_CERT` | Validate SSL certificates | `true` | `true`, `false` | | `LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` | | `ENABLE_FILE_LOGGING` | Enable file logging | `true` | `true`, `false` | | `ENABLE_CONSOLE_LOGGING` | Enable console logging | `false` | `true`, `false` | ### Database Connection Types #### 1. Single-Container Database Standard HANA database with single tenant. **Required**: `HANA_HOST`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_PORT`, `HANA_SCHEMA` ```json { "HANA_HOST": "hana.company.com", "HANA_PORT": "443", "HANA_USER": "DBADMIN", "HANA_PASSWORD": "password", "HANA_SCHEMA": "SYSTEM", "HANA_CONNECTION_TYPE": "single_container" } ``` #### 2. MDC System Database Multi-tenant system database (manages tenants). **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_SCHEMA` ```json { "HANA_HOST": "192.168.1.100", "HANA_PORT": "31013", "HANA_INSTANCE_NUMBER": "10", "HANA_USER": "SYSTEM", "HANA_PASSWORD": "password", "HANA_SCHEMA": "SYSTEM", "HANA_CONNECTION_TYPE": "mdc_system" } ``` #### 3. MDC Tenant Database Multi-tenant tenant database (specific tenant). **Required**: `HANA_HOST`, `HANA_PORT`, `HANA_INSTANCE_NUMBER`, `HANA_DATABASE_NAME`, `HANA_USER`, `HANA_PASSWORD` **Optional**: `HANA_SCHEMA` ```json { "HANA_HOST": "192.168.1.100", "HANA_PORT": "31013", "HANA_INSTANCE_NUMBER": "10", "HANA_DATABASE_NAME": "HQQ", "HANA_USER": "DBADMIN", "HANA_PASSWORD": "password", "HANA_SCHEMA": "SYSTEM", "HANA_CONNECTION_TYPE": "mdc_tenant" } ``` #### Auto-Detection When `HANA_CONNECTION_TYPE` is set to `auto` (default), the server automatically detects the type: - If `HANA_INSTANCE_NUMBER` + `HANA_DATABASE_NAME` → **MDC Tenant** - If only `HANA_INSTANCE_NUMBER` → **MDC System** - If neither → **Single-Container** ## 🏗️ Architecture ### System Architecture  ### Component Structure ``` hana-mcp-server/ ├── 📁 src/ │ ├── 🏗️ server/ # MCP Protocol & Server Management │ │ ├── index.js # Main server entry point │ │ ├── mcp-handler.js # JSON-RPC 2.0 implementation │ │ └── lifecycle-manager.js # Server lifecycle management │ ├── 🛠️ tools/ # Tool Implementations │ │ ├── index.js # Tool registry & discovery │ │ ├── config-tools.js # Configuration management │ │ ├── schema-tools.js # Schema exploration │ │ ├── table-tools.js # Table operations │ │ ├── index-tools.js # Index management │ │ └── query-tools.js # Query execution │ ├── 🗄️ database/ # Database Layer │ │ ├── hana-client.js # HANA client wrapper │ │ ├── connection-manager.js # Connection management │ │ └── query-executor.js # Query execution utilities │ ├── 🔧 utils/ # Shared Utilities │ │ ├── logger.js # Structured logging │ │ ├── config.js # Configuration management │ │ ├── validators.js # Input validation │ │ └── formatters.js # Response formatting │ └── 📋 constants/ # Constants & Definitions │ ├── mcp-constants.js # MCP protocol constants │ └── tool-definitions.js # Tool schemas ├── 🧪 tests/ # Testing Framework ├── 📚 docs/ # Documentation ├── 📦 package.json # Dependencies & Scripts └── 🚀 hana-mcp-server.js # Main entry point ``` ## 📚 Available Commands Once configured, you can ask Claude to: - *"List all schemas in the database"* - *"Show me tables in the SYSTEM schema"* - *"Describe the CUSTOMERS table structure"* - *"Execute: SELECT * FROM SYSTEM.TABLES LIMIT 10"* - *"Get sample data from ORDERS table"* - *"Count rows in CUSTOMERS table"* ## 🔧 Troubleshooting ### Connection Issues - **"Connection refused"**: Check HANA host and port - **"Authentication failed"**: Verify username/password - **"SSL certificate error"**: Set `HANA_VALIDATE_CERT=false` or install valid certificates ### Debug Mode ```bash export LOG_LEVEL="debug" export ENABLE_CONSOLE_LOGGING="true" hana-mcp-server ``` ## 📦 Package Info - **Size**: 21.7 kB - **Dependencies**: @sap/hana-client, axios - **Node.js**: 18+ required - **Platforms**: macOS, Linux, Windows ## 🤝 Support - **Issues**: [GitHub Issues](https://github.com/hatrigt/hana-mcp-server/issues) - **UI Tool**: [HANA MCP UI](https://www.npmjs.com/package/hana-mcp-ui) ## 📄 License MIT License - see [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /hana-mcp-ui/postcss.config.js: -------------------------------------------------------------------------------- ```javascript export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/layout/index.js: -------------------------------------------------------------------------------- ```javascript // Layout Components Exports export { default as VerticalSidebar } from './VerticalSidebar' ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/cn.js: -------------------------------------------------------------------------------- ```javascript import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs) { return twMerge(clsx(inputs)) } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/main.jsx: -------------------------------------------------------------------------------- ```javascript import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.jsx' import './index.css' createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode>, ) ``` -------------------------------------------------------------------------------- /hana-mcp-ui/vite.config.js: -------------------------------------------------------------------------------- ```javascript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { port: 5173, host: '0.0.0.0', strictPort: true }, build: { outDir: 'dist' } }) ``` -------------------------------------------------------------------------------- /hana-mcp-server.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * HANA MCP Server - Main Entry Point * * This is a thin wrapper that starts the modular MCP server. * The actual implementation is in src/server/index.js */ // Start the modular server require('./src/server/index.js'); ``` -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- ```yaml applications: - name: hana-mcp-server memory: 256M disk_quota: 512M instances: 1 buildpack: nodejs_buildpack command: node hana-mcp-server.js env: NODE_ENV: production PORT: 8080 services: - hana-service # Your HANA service instance name ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown ## 📝 Description Brief description of changes ## 🔄 Type of Change - [ ] Bug fix - [ ] New feature - [ ] Documentation update - [ ] Performance improvement ## 🧪 Testing - [ ] MCP Inspector tests pass - [ ] Manual testing completed - [ ] No breaking changes ## ✅ Checklist - [ ] Code follows existing patterns - [ ] Self-review completed - [ ] Documentation updated - [ ] No breaking changes ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/App.jsx: -------------------------------------------------------------------------------- ```javascript import { Toaster } from 'react-hot-toast' import MainApp from './components/MainApp' function App() { return ( <> <MainApp /> <Toaster position="top-right" toastOptions={{ duration: 4000, style: { background: '#363636', color: '#fff', borderRadius: '8px', }, }} /> </> ) } export default App ``` -------------------------------------------------------------------------------- /tests/mcpInspector/mcp-inspector-config.template.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "HANA Database": { "command": "/opt/homebrew/bin/node", "args": [ "/path/to/your/hana-mcp-server/hana-mcp-server.js" ], "env": { "HANA_HOST": "your-hana-host.com", "HANA_PORT": "443", "HANA_USER": "your-username", "HANA_PASSWORD": "your-password", "HANA_SCHEMA": "your-schema", "HANA_SSL": "true", "HANA_ENCRYPT": "true", "HANA_VALIDATE_CERT": "true" } } } } ``` -------------------------------------------------------------------------------- /tests/mcpInspector/mcp-inspector-config.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "HANA Database": { "command": "/opt/homebrew/bin/node", "args": [ "/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js" ], "env": { "HANA_HOST": "your-hana-host.com", "HANA_PORT": "443", "HANA_USER": "your-username", "HANA_PASSWORD": "your-password", "HANA_SCHEMA": "your-schema", "HANA_SSL": "true", "HANA_ENCRYPT": "true", "HANA_VALIDATE_CERT": "true" } } } } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/index.html: -------------------------------------------------------------------------------- ```html <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/png" href="/logo.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>HANA MCP UI - Database Configuration Manager</title> <style> body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif; background: #f8fafc; } </style> </head> <body> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html> ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/index.js: -------------------------------------------------------------------------------- ```javascript // UI Components Exports export { default as GlassCard } from './GlassCard' export { default as GlassWindow } from './GlassWindow' export { default as GradientButton } from './GradientButton' export { default as StatusBadge, EnvironmentBadge } from './StatusBadge' export { default as LoadingSpinner, LoadingOverlay } from './LoadingSpinner' export { default as MetricCard } from './MetricCard' export { default as IconComponent } from './IconComponent' export { default as Tabs } from './Tabs' export { default as DatabaseTypeBadge } from './DatabaseTypeBadge' export { default as PathConfigModal } from '../PathConfigModal' ``` -------------------------------------------------------------------------------- /claude_template.json: -------------------------------------------------------------------------------- ```json { "mcpServers": { "HANA Database": { "command": "/opt/homebrew/bin/node", "args": [ "/path/to/hana-mcp-server/hana-mcp-server.js" ], "env": { "HANA_HOST": "your-hana-host.com", "HANA_PORT": "443", "HANA_USER": "your-username", "HANA_PASSWORD": "your-password", "HANA_SCHEMA": "your-schema", "HANA_INSTANCE_NUMBER": "10", "HANA_DATABASE_NAME": "HQQ", "HANA_CONNECTION_TYPE": "auto", "HANA_SSL": "true", "LOG_LEVEL": "info", "ENABLE_FILE_LOGGING": "true", "ENABLE_CONSOLE_LOGGING": "false" } } } } ``` -------------------------------------------------------------------------------- /src/tools/schema-tools.js: -------------------------------------------------------------------------------- ```javascript /** * Schema exploration tools for HANA MCP Server */ const { logger } = require('../utils/logger'); const QueryExecutor = require('../database/query-executor'); const Formatters = require('../utils/formatters'); class SchemaTools { /** * List all schemas */ static async listSchemas(args) { logger.tool('hana_list_schemas'); try { const schemas = await QueryExecutor.getSchemas(); const formattedSchemas = Formatters.formatSchemaList(schemas); return Formatters.createResponse(formattedSchemas); } catch (error) { logger.error('Error listing schemas:', error.message); return Formatters.createErrorResponse('Error listing schemas', error.message); } } } module.exports = SchemaTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/MetricCard.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { cn } from '../../utils/cn' const MetricCard = ({ title, value, className }) => { return ( <motion.div className={cn( 'bg-white border border-gray-100 rounded-xl overflow-hidden', 'hover:border-gray-200 transition-all duration-300', className )} whileHover={{ y: -1 }} transition={{ duration: 0.2 }} > <div className="border-b border-gray-50 px-4 py-3"> <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wide">{title}</h3> </div> <div className="px-4 py-4"> <div className="flex items-baseline"> <p className="text-2xl font-semibold text-gray-900">{value}</p> </div> </div> </motion.div> ) } export default MetricCard ``` -------------------------------------------------------------------------------- /hana-mcp-ui/package.json: -------------------------------------------------------------------------------- ```json { "name": "hana-mcp-ui", "version": "1.0.9", "description": "UI for managing HANA MCP server configurations with Claude Desktop integration", "type": "module", "main": "bin/cli.js", "bin": { "hana-mcp-ui": "bin/cli.js" }, "engines": { "node": ">=18.0.0" }, "scripts": { "dev": "node start.js", "dev:safe": "./start-dev.sh", "dev:old": "node bin/cli.js", "vite": "vite", "build": "bun run vite build", "preview": "bun run vite preview" }, "dependencies": { "@heroicons/react": "^2.2.0", "@tailwindcss/forms": "^0.5.7", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.16", "axios": "^1.11.0", "chalk": "^5.5.0", "clsx": "^2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "framer-motion": "^12.23.12", "fs-extra": "^11.3.1", "open": "^10.2.0", "postcss": "^8.4.32", "react": "^19.1.1", "react-dom": "^19.1.1", "react-hot-toast": "^2.5.2", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.0", "vite": "^7.1.3" }, "keywords": [ "hana", "mcp", "claude", "database", "ui", "react", "management" ], "author": "HANA MCP Team", "license": "MIT" } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/Tabs.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { cn } from '../../utils/cn'; const Tabs = ({ tabs, activeTab, onChange, className }) => { return ( <div className={cn("border-b border-gray-200", className)}> <nav className="flex -mb-px space-x-6"> {tabs.map((tab) => ( <button key={tab.id} onClick={() => onChange(tab.id)} className={cn( "py-3 px-4 border-b-2 font-medium text-sm whitespace-nowrap transition-all", activeTab === tab.id ? "border-blue-600 text-blue-700 bg-blue-50/50" : "border-transparent text-gray-500 hover:text-gray-800 hover:border-gray-300 hover:bg-gray-50/50" )} aria-current={activeTab === tab.id ? "page" : undefined} > {tab.icon && ( <span className="mr-2">{tab.icon}</span> )} {tab.label} {tab.count !== undefined && ( <span className={cn( "ml-2 py-0.5 px-2 rounded-full text-xs font-semibold", activeTab === tab.id ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-600" )}> {tab.count} </span> )} </button> ))} </nav> </div> ); }; export default Tabs; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/IconComponent.jsx: -------------------------------------------------------------------------------- ```javascript import React from 'react'; import { cn } from '../../utils/cn'; /** * IconComponent - A standardized wrapper for icons * * @param {Object} props - Component props * @param {React.ElementType} props.icon - The icon component to render * @param {string} props.size - Size of the icon (sm, md, lg) * @param {string} props.variant - Visual variant (default, primary, secondary, etc.) * @param {string} props.className - Additional CSS classes * @param {string} props.label - Accessibility label (for icon-only buttons) * @returns {JSX.Element} - Rendered icon component */ const IconComponent = ({ icon: Icon, size = 'md', variant = 'default', className, label, ...props }) => { // Size mappings const sizes = { xs: 'w-3 h-3', sm: 'w-4 h-4', md: 'w-5 h-5', lg: 'w-6 h-6', xl: 'w-8 h-8' }; // Variant mappings const variants = { default: 'text-gray-600', primary: 'text-[#86a0ff]', secondary: 'text-gray-500', success: 'text-green-600', warning: 'text-amber-600', danger: 'text-red-500', white: 'text-white' }; // If no Icon is provided, return null if (!Icon) return null; return ( <Icon className={cn( sizes[size], variants[variant], className )} aria-label={label} aria-hidden={!label} role={label ? 'img' : undefined} {...props} /> ); }; export default IconComponent; ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "hana-mcp-server", "version": "0.1.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.", "main": "hana-mcp-server.js", "bin": { "hana-mcp-server": "hana-mcp-server.js" }, "scripts": { "start": "node hana-mcp-server.js", "dev": "nodemon hana-mcp-server.js", "test": "node tests/automated/test-mcp-inspector.js" }, "keywords": [ "sap", "hana", "mcp", "btp", "hana-mcp", "mcp-server", "database", "ai", "model", "context", "protocol", "claude", "anthropic", "enterprise", "sql", "query" ], "author": { "name": "HANA MCP Server Contributors", "email": "[email protected]" }, "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/hatrigt/hana-mcp-server.git" }, "homepage": "https://github.com/hatrigt/hana-mcp-server#readme", "bugs": { "url": "https://github.com/hatrigt/hana-mcp-server/issues" }, "readme": "README.md", "engines": { "node": ">=18.0.0" }, "os": [ "darwin", "linux", "win32" ], "dependencies": { "@sap/hana-client": "^2.17.22", "axios": "^1.12.2" }, "devDependencies": { "nodemon": "^3.0.2" }, "files": [ "hana-mcp-server.js", "src/", "README.md", "LICENSE" ] } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/LoadingSpinner.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { cn } from '../../utils/cn' const LoadingSpinner = ({ size = 'md', color = 'white', className }) => { const sizes = { sm: 'w-4 h-4', md: 'w-6 h-6', lg: 'w-8 h-8', xl: 'w-12 h-12' } const colors = { white: 'border-gray-200 border-t-blue-600', primary: 'border-blue-200 border-t-blue-600', success: 'border-emerald-200 border-t-emerald-600', warning: 'border-amber-200 border-t-amber-600', danger: 'border-red-200 border-t-red-600' } return ( <motion.div className={cn( 'inline-block rounded-full border-2', sizes[size], colors[color] || colors.white, className )} animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} /> ) } // Full page loading overlay export const LoadingOverlay = ({ message = "Loading..." }) => ( <motion.div className="fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} > <motion.div className="glass-card p-8 text-center" initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: 0.1 }} > <LoadingSpinner size="xl" color="primary" className="mx-auto mb-4" /> <p className="text-gray-700">{message}</p> </motion.div> </motion.div> ) export default LoadingSpinner ``` -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- ```javascript /** * Centralized logging utility for HANA MCP Server * Uses console.error to avoid interfering with JSON-RPC stdout */ const LOG_LEVELS = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 }; const LOG_LEVEL_NAMES = { 0: 'ERROR', 1: 'WARN', 2: 'INFO', 3: 'DEBUG' }; class Logger { constructor(level = 'INFO') { this.level = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO; this.prefix = '[HANA MCP Server]'; } _log(level, message, ...args) { if (level <= this.level) { const timestamp = new Date().toISOString(); const levelName = LOG_LEVEL_NAMES[level]; const formattedMessage = `${this.prefix} ${timestamp} [${levelName}]: ${message}`; // Use console.error to avoid interfering with JSON-RPC stdout console.error(formattedMessage, ...args); } } error(message, ...args) { this._log(LOG_LEVELS.ERROR, message, ...args); } warn(message, ...args) { this._log(LOG_LEVELS.WARN, message, ...args); } info(message, ...args) { this._log(LOG_LEVELS.INFO, message, ...args); } debug(message, ...args) { this._log(LOG_LEVELS.DEBUG, message, ...args); } // Convenience method for method calls method(methodName, ...args) { this.info(`Handling method: ${methodName}`, ...args); } // Convenience method for tool calls tool(toolName, ...args) { this.info(`Calling tool: ${toolName}`, ...args); } } // Create default logger instance const logger = new Logger(process.env.LOG_LEVEL || 'INFO'); module.exports = { Logger, logger }; ``` -------------------------------------------------------------------------------- /src/tools/query-tools.js: -------------------------------------------------------------------------------- ```javascript /** * Query execution tools for HANA MCP Server */ const { logger } = require('../utils/logger'); const QueryExecutor = require('../database/query-executor'); const Validators = require('../utils/validators'); const Formatters = require('../utils/formatters'); class QueryTools { /** * Execute a custom SQL query */ static async executeQuery(args) { logger.tool('hana_execute_query', args); const { query, parameters = [] } = args || {}; // Validate required parameters const validation = Validators.validateRequired(args, ['query'], 'hana_execute_query'); if (!validation.valid) { return Formatters.createErrorResponse('Error: query parameter is required', validation.error); } // Validate query const queryValidation = Validators.validateQuery(query); if (!queryValidation.valid) { return Formatters.createErrorResponse('Invalid query', queryValidation.error); } // Validate parameters const paramValidation = Validators.validateParameters(parameters); if (!paramValidation.valid) { return Formatters.createErrorResponse('Invalid parameters', paramValidation.error); } try { const results = await QueryExecutor.executeQuery(query, parameters); const formattedResults = Formatters.formatQueryResults(results, query); return Formatters.createResponse(formattedResults); } catch (error) { logger.error('Query execution failed:', error.message); return Formatters.createErrorResponse('Query execution failed', error.message); } } } module.exports = QueryTools; ``` -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- ```bash #!/bin/bash # HANA MCP Server Setup Script echo "🚀 Setting up HANA MCP Server" echo "=============================" # Get the current directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo "📁 Working directory: $SCRIPT_DIR" # Check if Node.js is available NODE_PATH="/opt/homebrew/bin/node" if [ ! -f "$NODE_PATH" ]; then # Try alternative paths if command -v node >/dev/null 2>&1; then NODE_PATH=$(which node) else echo "❌ Node.js not found" echo "Please install Node.js or update the path in this script" exit 1 fi fi echo "✅ Node.js found: $($NODE_PATH --version)" # Check if dependencies are installed if [ ! -d "node_modules" ]; then echo "📦 Installing dependencies..." npm install if [ $? -eq 0 ]; then echo "✅ Dependencies installed successfully" else echo "❌ Failed to install dependencies" echo "Please run 'npm install' manually" exit 1 fi else echo "✅ Dependencies already installed" fi # Make the server executable chmod +x "$SCRIPT_DIR/hana-mcp-server.js" echo "✅ Made server executable" # Display configuration instructions echo "" echo "📖 Configuration Instructions:" echo "==============================" echo "" echo "1. Copy the configuration template:" echo " cp $SCRIPT_DIR/claude_config_template.json ~/.config/claude/claude_desktop_config.json" echo "" echo "2. Edit the configuration file with your HANA database details:" echo " - Update the path to hana-mcp-server.js" echo " - Set your HANA_HOST, HANA_USER, HANA_PASSWORD, etc." echo "" echo "3. Restart Claude Desktop" echo "" echo "4. Test the connection using the hana_test_connection tool" echo "" echo "📚 For more information, see README.md" echo "" echo "✅ Setup complete!" ``` -------------------------------------------------------------------------------- /src/constants/mcp-constants.js: -------------------------------------------------------------------------------- ```javascript /** * MCP Protocol Constants */ // MCP Protocol versions const PROTOCOL_VERSIONS = { LATEST: '2024-11-05', SUPPORTED: ['2024-11-05', '2025-03-26'] }; // MCP Methods const METHODS = { // Lifecycle INITIALIZE: 'initialize', NOTIFICATIONS_INITIALIZED: 'notifications/initialized', // Tools TOOLS_LIST: 'tools/list', TOOLS_CALL: 'tools/call', // Prompts PROMPTS_LIST: 'prompts/list', // Resources RESOURCES_LIST: 'resources/list', RESOURCES_READ: 'resources/read' }; // JSON-RPC Error Codes const ERROR_CODES = { // JSON-RPC 2.0 Standard Errors PARSE_ERROR: -32700, INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603, // MCP Specific Errors TOOL_NOT_FOUND: -32601, INVALID_TOOL_ARGS: -32602, DATABASE_ERROR: -32000, CONNECTION_ERROR: -32001, VALIDATION_ERROR: -32002 }; // Error Messages const ERROR_MESSAGES = { [ERROR_CODES.PARSE_ERROR]: 'Parse error', [ERROR_CODES.INVALID_REQUEST]: 'Invalid Request', [ERROR_CODES.METHOD_NOT_FOUND]: 'Method not found', [ERROR_CODES.INVALID_PARAMS]: 'Invalid params', [ERROR_CODES.INTERNAL_ERROR]: 'Internal error', [ERROR_CODES.TOOL_NOT_FOUND]: 'Tool not found', [ERROR_CODES.INVALID_TOOL_ARGS]: 'Invalid tool arguments', [ERROR_CODES.DATABASE_ERROR]: 'Database error', [ERROR_CODES.CONNECTION_ERROR]: 'Connection error', [ERROR_CODES.VALIDATION_ERROR]: 'Validation error' }; // Server Information const SERVER_INFO = { name: 'HANA MCP Server', version: '1.0.0', description: 'Model Context Protocol server for SAP HANA databases' }; // Capabilities const CAPABILITIES = { tools: {}, resources: {}, prompts: {} }; module.exports = { PROTOCOL_VERSIONS, METHODS, ERROR_CODES, ERROR_MESSAGES, SERVER_INFO, CAPABILITIES }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/bin/cli.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import chalk from 'chalk'; import open from 'open'; import fs from 'fs-extra'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const rootDir = dirname(__dirname); console.log(chalk.blue.bold('🚀 Starting HANA MCP UI...')); console.log(chalk.gray('Professional database configuration management')); // Check if we're in development or production const isDev = process.env.NODE_ENV === 'development' || fs.existsSync(join(rootDir, 'src')); let backendProcess, frontendProcess; // Graceful shutdown process.on('SIGINT', () => { console.log(chalk.yellow('\n🛑 Shutting down servers...')); if (backendProcess) backendProcess.kill(); if (frontendProcess) frontendProcess.kill(); process.exit(0); }); // Start backend server console.log(chalk.cyan('🔧 Starting backend server...')); backendProcess = spawn('node', [join(rootDir, 'server', 'index.js')], { stdio: 'inherit', env: { ...process.env, PORT: '3001' } }); backendProcess.on('error', (err) => { console.error(chalk.red('Backend server error:'), err); }); // Wait for backend to start setTimeout(() => { if (isDev) { // Development mode - start Vite dev server console.log(chalk.cyan('⚛️ Starting React dev server...')); frontendProcess = spawn('bun', ['vite', '--port', '5173', '--host'], { stdio: 'inherit', cwd: rootDir, shell: true }); } else { // Production mode - serve built files console.log(chalk.cyan('📦 Serving production build...')); frontendProcess = spawn('bun', ['vite', 'preview', '--port', '5173', '--host'], { stdio: 'inherit', cwd: rootDir, shell: true }); } frontendProcess.on('error', (err) => { console.error(chalk.red('Frontend server error:'), err); }); // Open browser after frontend starts setTimeout(() => { console.log(chalk.green.bold('\n✨ HANA MCP UI is ready!')); console.log(chalk.gray('Opening browser at http://localhost:5173')); open('http://localhost:5173'); }, 3000); }, 2000); ``` -------------------------------------------------------------------------------- /src/tools/config-tools.js: -------------------------------------------------------------------------------- ```javascript /** * Configuration-related tools for HANA MCP Server */ const { logger } = require('../utils/logger'); const { config } = require('../utils/config'); const { connectionManager } = require('../database/connection-manager'); const Formatters = require('../utils/formatters'); class ConfigTools { /** * Show HANA configuration */ static async showConfig(args) { logger.tool('hana_show_config'); const displayConfig = config.getDisplayConfig(); const formattedConfig = Formatters.formatConfig(displayConfig); return Formatters.createResponse(formattedConfig); } /** * Test HANA connection */ static async testConnection(args) { logger.tool('hana_test_connection'); if (!config.isHanaConfigured()) { const missingConfig = config.getDisplayConfig(); const errorMessage = Formatters.formatConnectionTest(missingConfig, false, 'Missing required configuration'); return Formatters.createErrorResponse('Connection test failed!', errorMessage); } try { const testResult = await connectionManager.testConnection(); const displayConfig = config.getDisplayConfig(); if (testResult.success) { const successMessage = Formatters.formatConnectionTest(displayConfig, true, null, testResult.result); return Formatters.createResponse(successMessage); } else { const errorMessage = Formatters.formatConnectionTest(displayConfig, false, testResult.error); return Formatters.createErrorResponse('Connection test failed!', errorMessage); } } catch (error) { logger.error('Connection test error:', error.message); const displayConfig = config.getDisplayConfig(); const errorMessage = Formatters.formatConnectionTest(displayConfig, false, error.message); return Formatters.createErrorResponse('Connection test failed!', errorMessage); } } /** * Show environment variables */ static async showEnvVars(args) { logger.tool('hana_show_env_vars'); const envVars = config.getEnvironmentVars(); const formattedEnvVars = Formatters.formatEnvironmentVars(envVars); return Formatters.createResponse(formattedEnvVars); } } module.exports = ConfigTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GlassCard.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { cn } from '../../utils/cn' import { colors, shadows, borderRadius } from '../../utils/theme' /** * GlassCard - A versatile card component with multiple variants * * @param {Object} props - Component props * @param {React.ReactNode} props.children - Card content * @param {string} props.variant - Visual variant (default, primary, success, warning, danger) * @param {boolean} props.hover - Whether to apply hover effects * @param {boolean} props.glow - Whether to apply glow effect on hover * @param {string} props.className - Additional CSS classes * @param {Object} props.headerProps - Props for the card header * @param {React.ReactNode} props.header - Card header content * @returns {JSX.Element} - Rendered card component */ const GlassCard = ({ children, variant = 'default', hover = true, glow = false, className, header, headerProps = {}, ...props }) => { // Card variants - enhanced with better shadows and borders const variants = { default: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', primary: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', success: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', warning: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden', danger: 'bg-white border border-gray-200 shadow-[0_2px_8px_rgba(0,0,0,0.08)] rounded-xl overflow-hidden' } // Header variants - with improved styling const headerVariants = { default: 'border-b border-gray-200 bg-white p-6', primary: 'border-b border-gray-200 bg-white p-6', success: 'border-b border-gray-200 bg-white p-6', warning: 'border-b border-gray-200 bg-white p-6', danger: 'border-b border-gray-200 bg-white p-6' } return ( <motion.div className={cn( variants[variant], hover && 'hover:shadow-md hover:-translate-y-0.5', glow && 'hover:shadow-gray-200', className )} whileHover={hover ? { y: -3, boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1)' } : {}} transition={{ type: "spring", stiffness: 300, damping: 20 }} {...props} > {header && ( <div className={cn(headerVariants[variant], headerProps.className)} {...headerProps}> {header} </div> )} <div className="relative z-10"> {children} </div> </motion.div> ) } export default GlassCard ``` -------------------------------------------------------------------------------- /src/tools/index.js: -------------------------------------------------------------------------------- ```javascript /** * Tool registry and management for HANA MCP Server */ const { logger } = require('../utils/logger'); const { TOOLS } = require('../constants/tool-definitions'); const ConfigTools = require('./config-tools'); const SchemaTools = require('./schema-tools'); const TableTools = require('./table-tools'); const IndexTools = require('./index-tools'); const QueryTools = require('./query-tools'); // Tool implementations mapping const TOOL_IMPLEMENTATIONS = { hana_show_config: ConfigTools.showConfig, hana_test_connection: ConfigTools.testConnection, hana_show_env_vars: ConfigTools.showEnvVars, hana_list_schemas: SchemaTools.listSchemas, hana_list_tables: TableTools.listTables, hana_describe_table: TableTools.describeTable, hana_list_indexes: IndexTools.listIndexes, hana_describe_index: IndexTools.describeIndex, hana_execute_query: QueryTools.executeQuery }; class ToolRegistry { /** * Get all available tools */ static getTools() { return TOOLS; } /** * Get tool by name */ static getTool(name) { return TOOLS.find(tool => tool.name === name); } /** * Check if tool exists */ static hasTool(name) { return TOOL_IMPLEMENTATIONS.hasOwnProperty(name); } /** * Execute a tool */ static async executeTool(name, args) { if (!this.hasTool(name)) { throw new Error(`Tool not found: ${name}`); } const implementation = TOOL_IMPLEMENTATIONS[name]; if (typeof implementation !== 'function') { throw new Error(`Tool implementation not found: ${name}`); } try { logger.debug(`Executing tool: ${name}`, args); const result = await implementation(args); logger.debug(`Tool ${name} executed successfully`); return result; } catch (error) { logger.error(`Tool ${name} execution failed:`, error.message); throw error; } } /** * Get tool implementation */ static getToolImplementation(name) { return TOOL_IMPLEMENTATIONS[name]; } /** * Get all tool names */ static getAllToolNames() { return Object.keys(TOOL_IMPLEMENTATIONS); } /** * Validate tool arguments against schema */ static validateToolArgs(name, args) { const tool = this.getTool(name); if (!tool) { return { valid: false, error: `Tool not found: ${name}` }; } const schema = tool.inputSchema; if (!schema || !schema.required) { return { valid: true }; // No validation required } const missing = []; for (const field of schema.required) { if (!args || args[field] === undefined || args[field] === null || args[field] === '') { missing.push(field); } } if (missing.length > 0) { return { valid: false, error: `Missing required parameters: ${missing.join(', ')}` }; } return { valid: true }; } } module.exports = ToolRegistry; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GlassWindow.jsx: -------------------------------------------------------------------------------- ```javascript import React from 'react'; import { motion } from 'framer-motion'; const GlassWindow = ({ children, className = '', maxWidth = '7xl', maxHeight = '6xl' }) => { // Convert maxWidth to actual Tailwind class const getMaxWidthClass = (width) => { const widthMap = { 'sm': 'max-w-sm', 'md': 'max-w-md', 'lg': 'max-w-lg', 'xl': 'max-w-xl', '2xl': 'max-w-2xl', '3xl': 'max-w-3xl', '4xl': 'max-w-4xl', '5xl': 'max-w-5xl', '6xl': 'max-w-6xl', '7xl': 'max-w-7xl', 'full': 'max-w-full' }; return widthMap[width] || 'max-w-7xl'; }; const getMaxHeightClass = (height) => { const heightMap = { 'sm': 'max-h-sm', 'md': 'max-h-md', 'lg': 'max-h-lg', 'xl': 'max-h-xl', '2xl': 'max-h-2xl', '3xl': 'max-h-3xl', '4xl': 'max-h-4xl', '5xl': 'max-h-5xl', '6xl': 'max-h-6xl', 'full': 'max-h-full' }; return heightMap[height] || 'max-h-6xl'; }; return ( <div className="glass-window-container bg-gradient-to-br from-gray-50 via-blue-50/30 to-indigo-50/20 overflow-hidden"> {/* Background Pattern */} <div className="fixed inset-0 bg-dots opacity-20 sm:opacity-30 pointer-events-none" /> {/* Glass Window Container */} <motion.div initial={{ opacity: 0, scale: 0.95, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} transition={{ duration: 0.5, ease: "easeOut" }} className={` glass-window-content relative bg-white/80 backdrop-blur-xl border border-white/20 rounded-2xl sm:rounded-3xl shadow-xl sm:shadow-2xl shadow-gray-900/10 overflow-hidden ${className} `} style={{ backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', minHeight: '600px' }} > {/* Glass Window Header */} <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"> {/* Window Controls */} <div className="flex items-center h-full px-4 sm:px-6 gap-2 sm:gap-3"> <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" /> <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" /> <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" /> </div> </div> {/* Content Area */} <div className="pt-12 sm:pt-14 h-full p-3 pb-4"> {children} </div> {/* Subtle glow effect */} <div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-white/10 via-transparent to-blue-100/20 pointer-events-none" /> </motion.div> </div> ); }; export default GlassWindow; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/DatabaseTypeBadge.jsx: -------------------------------------------------------------------------------- ```javascript import { getDatabaseTypeColor, getDatabaseTypeShortName } from '../../utils/databaseTypes' const DatabaseTypeBadge = ({ type, size = 'md', showIcon = true, className = '' }) => { const color = getDatabaseTypeColor(type) const shortName = getDatabaseTypeShortName(type) const sizeClasses = { xs: 'px-2 py-1 text-xs', sm: 'px-2.5 py-1 text-sm', md: 'px-3 py-1.5 text-sm', lg: 'px-4 py-2 text-base' } const colorClasses = { blue: 'bg-gradient-to-r from-blue-100 to-blue-50 text-blue-800 border-blue-200 shadow-sm', amber: 'bg-gradient-to-r from-amber-100 to-amber-50 text-amber-800 border-amber-200 shadow-sm', green: 'bg-gradient-to-r from-green-100 to-green-50 text-green-800 border-green-200 shadow-sm', gray: 'bg-gradient-to-r from-gray-100 to-gray-50 text-gray-800 border-gray-200 shadow-sm' } const iconClasses = { xs: 'w-3 h-3', sm: 'w-3.5 h-3.5', md: 'w-4 h-4', lg: 'w-5 h-5' } const getIcon = () => { switch (type) { case 'single_container': return ( <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ) case 'mdc_system': return ( <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ) case 'mdc_tenant': return ( <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ) default: return ( <svg className={iconClasses[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ) } } return ( <span className={` inline-flex items-center gap-1.5 font-semibold rounded-full border transition-all duration-200 hover:shadow-md hover:scale-105 ${sizeClasses[size]} ${colorClasses[color]} ${className} `} title={shortName} > {showIcon && getIcon()} <span className='tracking-wide'>{shortName}</span> </span> ) } export default DatabaseTypeBadge ``` -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Main HANA MCP Server Entry Point */ const readline = require('readline'); const { logger } = require('../utils/logger'); const { lifecycleManager } = require('./lifecycle-manager'); const MCPHandler = require('./mcp-handler'); const { ERROR_CODES } = require('../constants/mcp-constants'); class MCPServer { constructor() { this.rl = null; this.isShuttingDown = false; } /** * Start the MCP server */ async start() { try { // Setup lifecycle management lifecycleManager.setupEventHandlers(); await lifecycleManager.start(); // Setup readline interface for STDIO this.setupReadline(); logger.info('Server ready for requests'); } catch (error) { logger.error('Failed to start server:', error.message); process.exit(1); } } /** * Setup readline interface for STDIO communication */ setupReadline() { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Handle incoming lines this.rl.on('line', async (line) => { if (this.isShuttingDown) return; await this.handleLine(line); }); // Handle readline close this.rl.on('close', async () => { if (!this.isShuttingDown) { logger.info('Readline closed, but keeping process alive'); } else { logger.info('Server shutting down'); await lifecycleManager.shutdown(); } }); } /** * Handle incoming line from STDIO */ async handleLine(line) { try { const request = JSON.parse(line); const response = await this.handleRequest(request); if (response) { console.log(JSON.stringify(response)); } } catch (error) { logger.error(`Parse error: ${error.message}`); const errorResponse = { jsonrpc: '2.0', id: null, error: { code: ERROR_CODES.PARSE_ERROR, message: 'Parse error' } }; console.log(JSON.stringify(errorResponse)); } } /** * Handle MCP request */ async handleRequest(request) { // Validate request const validation = MCPHandler.validateRequest(request); if (!validation.valid) { return { jsonrpc: '2.0', id: request.id || null, error: { code: ERROR_CODES.INVALID_REQUEST, message: validation.error } }; } // Handle request return await MCPHandler.handleRequest(request); } /** * Shutdown the server */ async shutdown() { this.isShuttingDown = true; if (this.rl) { this.rl.close(); } await lifecycleManager.shutdown(); } } // Create and start server const server = new MCPServer(); // Handle process termination process.on('SIGINT', async () => { logger.info('Received SIGINT'); await server.shutdown(); }); process.on('SIGTERM', async () => { logger.info('Received SIGTERM'); await server.shutdown(); }); // Start the server server.start().catch(error => { logger.error('Failed to start server:', error.message); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/server/lifecycle-manager.js: -------------------------------------------------------------------------------- ```javascript /** * Server lifecycle management for HANA MCP Server */ const { logger } = require('../utils/logger'); const { connectionManager } = require('../database/connection-manager'); class LifecycleManager { constructor() { this.isShuttingDown = false; this.isInitialized = false; } /** * Initialize the server */ async initialize() { if (this.isInitialized) { logger.warn('Server already initialized'); return; } logger.info('Initializing HANA MCP Server...'); try { // Validate configuration const { config } = require('../utils/config'); if (!config.validate()) { logger.warn('Configuration validation failed, but continuing...'); } this.isInitialized = true; logger.info('HANA MCP Server initialized successfully'); } catch (error) { logger.error('Failed to initialize server:', error.message); throw error; } } /** * Start the server */ async start() { logger.info('Starting HANA MCP Server...'); try { await this.initialize(); // Keep process alive this.keepAlive(); logger.info('HANA MCP Server started successfully'); } catch (error) { logger.error('Failed to start server:', error.message); throw error; } } /** * Shutdown the server gracefully */ async shutdown() { if (this.isShuttingDown) { logger.warn('Shutdown already in progress'); return; } logger.info('Shutting down HANA MCP Server...'); this.isShuttingDown = true; try { // Disconnect from HANA database await connectionManager.disconnect(); logger.info('HANA MCP Server shutdown completed'); } catch (error) { logger.error('Error during shutdown:', error.message); } finally { process.exit(0); } } /** * Keep the process alive */ keepAlive() { // Keep stdin open process.stdin.resume(); // Keep process alive with interval setInterval(() => { // This keeps the event loop active }, 1000); } /** * Setup process event handlers */ setupEventHandlers() { // Handle SIGINT (Ctrl+C) process.on('SIGINT', async () => { logger.info('Received SIGINT'); await this.shutdown(); }); // Handle SIGTERM process.on('SIGTERM', async () => { logger.info('Received SIGTERM'); await this.shutdown(); }); // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error('Uncaught exception:', error.message); this.shutdown(); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled promise rejection:', reason); this.shutdown(); }); } /** * Get server status */ getStatus() { return { isInitialized: this.isInitialized, isShuttingDown: this.isShuttingDown, connectionStatus: connectionManager.getStatus() }; } } // Create singleton instance const lifecycleManager = new LifecycleManager(); module.exports = { LifecycleManager, lifecycleManager }; ``` -------------------------------------------------------------------------------- /tests/automated/test-mcp-inspector.js: -------------------------------------------------------------------------------- ```javascript const { spawn } = require('child_process'); console.log('🔍 HANA MCP Server Inspector'); console.log('============================\n'); // Spawn the MCP server process const server = spawn('/opt/homebrew/opt/node@20/bin/node', ['../../hana-mcp-server.js'], { stdio: ['pipe', 'pipe', 'pipe'], env: { HANA_HOST: "your-hana-host.com", HANA_PORT: "443", HANA_USER: "your-username", HANA_PASSWORD: "your-password", HANA_SCHEMA: "your-schema", HANA_SSL: "true", HANA_ENCRYPT: "true", HANA_VALIDATE_CERT: "true" } }); // Handle server output server.stdout.on('data', (data) => { try { const response = JSON.parse(data.toString().trim()); console.log('📤 Response:', JSON.stringify(response, null, 2)); } catch (error) { console.log('🔧 Server Log:', data.toString().trim()); } }); server.stderr.on('data', (data) => { console.log('🔧 Server Log:', data.toString().trim()); }); // Send request function function sendRequest(method, params = {}) { const request = { jsonrpc: '2.0', id: Date.now(), method, params }; server.stdin.write(JSON.stringify(request) + '\n'); } // Test functions async function testInitialize() { console.log('\n🧪 Testing: Initialize'); sendRequest('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test-client', version: '1.0.0' } }); await new Promise(resolve => setTimeout(resolve, 1000)); } async function testToolsList() { console.log('\n🧪 Testing: Tools List'); sendRequest('tools/list', {}); await new Promise(resolve => setTimeout(resolve, 1000)); } async function testShowConfig() { console.log('\n🧪 Testing: Show Config'); sendRequest('tools/call', { name: "hana_show_config", arguments: {} }); await new Promise(resolve => setTimeout(resolve, 1000)); } async function testListSchemas() { console.log('\n🧪 Testing: List Schemas'); sendRequest('tools/call', { name: "hana_list_schemas", arguments: {} }); await new Promise(resolve => setTimeout(resolve, 1000)); } async function testListTables() { console.log('\n🧪 Testing: List Tables'); sendRequest('tools/call', { name: "hana_list_tables", arguments: { schema_name: "SYSTEM" } }); await new Promise(resolve => setTimeout(resolve, 1000)); } async function testExecuteQuery() { console.log('\n🧪 Testing: Execute Query'); sendRequest('tools/call', { name: "hana_execute_query", arguments: { query: "SELECT 1 as test_value FROM DUMMY" } }); await new Promise(resolve => setTimeout(resolve, 1000)); } // Main test runner async function runTests() { try { await testInitialize(); await testToolsList(); await testShowConfig(); await testListSchemas(); await testListTables(); await testExecuteQuery(); console.log('\n✅ Tests completed!'); // Close server server.stdin.end(); server.kill(); } catch (error) { console.error('❌ Test error:', error); server.kill(); } } // Handle server exit server.on('close', (code) => { console.log(`\n🔚 Server closed with code ${code}`); }); server.on('error', (error) => { console.error('❌ Server error:', error); }); // Start tests runTests().catch(console.error); ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeConfigTile.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { motion } from 'framer-motion'; import { cn } from '../utils/cn'; import PathConfigModal from './PathConfigModal'; const ClaudeConfigTile = ({ claudeConfigPath, claudeServers, onSetupPath, onConfigPathChange }) => { const [showPathModal, setShowPathModal] = useState(false); const handleEditPath = () => { setShowPathModal(true); }; return ( <> <motion.div className="bg-white rounded-xl border border-gray-100 overflow-hidden" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} > <div className="border-b border-gray-100 px-4 py-3"> <div className="flex items-center justify-between"> <div> <h3 className="text-sm font-medium text-gray-900">Claude Desktop Configuration</h3> <p className="text-xs text-gray-500">Integration Status</p> </div> <div className="flex items-center gap-2"> <div className={cn( 'w-2 h-2 rounded-full', claudeServers.length > 0 ? 'bg-green-500' : 'bg-gray-300' )}></div> <span className="text-xs text-gray-600"> {claudeServers.length > 0 ? 'Online' : 'Offline'} </span> </div> </div> </div> <div className="p-4"> {claudeConfigPath ? ( <div className="bg-gray-50 rounded-lg p-3"> <div className="flex items-center justify-between mb-1"> <div className="text-xs font-medium text-gray-600"> Config Path </div> <button onClick={handleEditPath} 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" title="Change config path" > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> Edit </button> </div> <div className="text-xs font-mono text-gray-700 break-all"> {claudeConfigPath} </div> </div> ) : ( <div className="text-center py-4"> <p className="text-sm text-gray-500"> Claude Desktop configuration path not set </p> <button onClick={onSetupPath} className="mt-2 text-sm text-blue-600 hover:text-blue-800" > Set Configuration Path </button> </div> )} </div> </motion.div> {/* Path Configuration Modal */} <PathConfigModal isOpen={showPathModal} onClose={() => setShowPathModal(false)} onConfigPathChange={onConfigPathChange} currentPath={claudeConfigPath} /> </> ); }; export default ClaudeConfigTile; ``` -------------------------------------------------------------------------------- /src/tools/table-tools.js: -------------------------------------------------------------------------------- ```javascript /** * Table management tools for HANA MCP Server */ const { logger } = require('../utils/logger'); const { config } = require('../utils/config'); const QueryExecutor = require('../database/query-executor'); const Validators = require('../utils/validators'); const Formatters = require('../utils/formatters'); class TableTools { /** * List tables in a schema */ static async listTables(args) { logger.tool('hana_list_tables', args); let { schema_name } = args || {}; // Use default schema if not provided if (!schema_name) { if (config.hasDefaultSchema()) { schema_name = config.getDefaultSchema(); logger.info(`Using default schema: ${schema_name}`); } else { return Formatters.createErrorResponse( 'Schema name is required', 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' ); } } // Validate schema name const schemaValidation = Validators.validateSchemaName(schema_name); if (!schemaValidation.valid) { return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); } try { const tables = await QueryExecutor.getTables(schema_name); const formattedTables = Formatters.formatTableList(tables, schema_name); return Formatters.createResponse(formattedTables); } catch (error) { logger.error('Error listing tables:', error.message); return Formatters.createErrorResponse('Error listing tables', error.message); } } /** * Describe table structure */ static async describeTable(args) { logger.tool('hana_describe_table', args); let { schema_name, table_name } = args || {}; // Use default schema if not provided if (!schema_name) { if (config.hasDefaultSchema()) { schema_name = config.getDefaultSchema(); logger.info(`Using default schema: ${schema_name}`); } else { return Formatters.createErrorResponse( 'Schema name is required', 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' ); } } // Validate required parameters const validation = Validators.validateRequired(args, ['table_name'], 'hana_describe_table'); if (!validation.valid) { return Formatters.createErrorResponse('Error: table_name parameter is required', validation.error); } // Validate schema and table names const schemaValidation = Validators.validateSchemaName(schema_name); if (!schemaValidation.valid) { return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); } const tableValidation = Validators.validateTableName(table_name); if (!tableValidation.valid) { return Formatters.createErrorResponse('Invalid table name', tableValidation.error); } try { const columns = await QueryExecutor.getTableColumns(schema_name, table_name); if (columns.length === 0) { return Formatters.createErrorResponse(`Table '${schema_name}.${table_name}' not found or no columns available`); } const formattedStructure = Formatters.formatTableStructure(columns, schema_name, table_name); return Formatters.createResponse(formattedStructure); } catch (error) { logger.error('Error describing table:', error.message); return Formatters.createErrorResponse('Error describing table', error.message); } } } module.exports = TableTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/start.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import { spawn, execSync } from 'child_process'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import chalk from 'chalk'; import open from 'open'; import { networkInterfaces } from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); console.log(chalk.blue.bold('🚀 Starting HANA MCP UI...')); console.log(chalk.gray('Professional database configuration management')); let backendProcess, frontendProcess; // Graceful shutdown process.on('SIGINT', () => { console.log(chalk.yellow('\n🛑 Shutting down servers...')); if (backendProcess) backendProcess.kill(); if (frontendProcess) frontendProcess.kill(); process.exit(0); }); // Start backend server console.log(chalk.cyan('🔧 Starting backend server...')); backendProcess = spawn('node', [join(__dirname, 'server', 'index.js')], { stdio: 'inherit', env: { ...process.env, PORT: '3001' } }); backendProcess.on('error', (err) => { console.error(chalk.red('Backend server error:'), err); }); // Function to check if port is in use and kill the process if needed function checkPortAndKillProcess(port) { try { console.log(chalk.yellow(`🔍 Checking if port ${port} is already in use...`)); // Check if the port is in use const checkCommand = process.platform === 'win32' ? `netstat -ano | findstr :${port}` : `lsof -i :${port}`; try { const result = execSync(checkCommand, { encoding: 'utf8' }); if (result) { console.log(chalk.yellow(`⚠️ Port ${port} is already in use. Finding the process...`)); // Get the PID of the process using the port let pid; if (process.platform === 'win32') { // Extract PID from Windows netstat output const lines = result.split('\n'); for (const line of lines) { if (line.includes(`LISTENING`)) { pid = line.trim().split(/\s+/).pop(); break; } } } else { // Extract PID from lsof output const pidMatch = result.match(/\s+(\d+)\s+/); if (pidMatch && pidMatch[1]) { pid = pidMatch[1]; } } if (pid) { console.log(chalk.yellow(`🛑 Killing process ${pid} that's using port ${port}...`)); // Kill the process const killCommand = process.platform === 'win32' ? `taskkill /F /PID ${pid}` : `kill -9 ${pid}`; execSync(killCommand); console.log(chalk.green(`✅ Process terminated.`)); // Wait a moment for the port to be released execSync('sleep 1'); } else { console.log(chalk.red(`❌ Could not find the process using port ${port}.`)); } } } catch (error) { // If the command fails, it likely means no process is using the port console.log(chalk.green(`✅ Port ${port} is available.`)); } } catch (error) { console.error(chalk.red(`Error checking port ${port}:`, error.message)); } } // Check and clear port 5173 if needed checkPortAndKillProcess(5173); // Start frontend server console.log(chalk.cyan('⚛️ Starting React dev server...')); frontendProcess = spawn('vite', ['--port', '5173', '--host', '0.0.0.0'], { stdio: 'inherit', cwd: __dirname, shell: true }); frontendProcess.on('error', (err) => { console.error(chalk.red('Frontend server error:'), err); }); // Open browser after a delay setTimeout(() => { console.log(chalk.green.bold('\n✨ HANA MCP UI is ready!')); console.log(chalk.gray('Opening browser at http://localhost:5173')); open('http://localhost:5173'); }, 5000); ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/theme.js: -------------------------------------------------------------------------------- ```javascript /** * Design tokens for the HANA MCP UI * This file defines the core design values used throughout the application */ // Color palette export const colors = { // Primary brand colors primary: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b', }, // Secondary accent colors secondary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49', }, // Neutral colors for text, backgrounds gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712', }, // Semantic colors success: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16', }, warning: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03', }, danger: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a', }, }; // Spacing system (in pixels, following 8pt grid) export const spacing = { 0: '0', 1: '0.25rem', // 4px 2: '0.5rem', // 8px 3: '0.75rem', // 12px 4: '1rem', // 16px 5: '1.25rem', // 20px 6: '1.5rem', // 24px 8: '2rem', // 32px 10: '2.5rem', // 40px 12: '3rem', // 48px 16: '4rem', // 64px 20: '5rem', // 80px 24: '6rem', // 96px }; // Typography scale export const typography = { fontFamily: { sans: 'Inter, system-ui, -apple-system, sans-serif', mono: 'ui-monospace, SFMono-Regular, Menlo, monospace', }, fontSize: { xs: '0.75rem', // 12px sm: '0.875rem', // 14px base: '1rem', // 16px lg: '1.125rem', // 18px xl: '1.25rem', // 20px '2xl': '1.5rem', // 24px '3xl': '1.875rem', // 30px '4xl': '2.25rem', // 36px }, fontWeight: { normal: '400', medium: '500', semibold: '600', bold: '700', }, lineHeight: { none: '1', tight: '1.25', snug: '1.375', normal: '1.5', relaxed: '1.625', loose: '2', }, }; // Border radius export const borderRadius = { none: '0', sm: '0.125rem', // 2px DEFAULT: '0.25rem', // 4px md: '0.375rem', // 6px lg: '0.5rem', // 8px xl: '0.75rem', // 12px '2xl': '1rem', // 16px '3xl': '1.5rem', // 24px full: '9999px', }; // Shadows export const shadows = { sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', }; // Z-index scale export const zIndex = { 0: '0', 10: '10', 20: '20', 30: '30', 40: '40', 50: '50', auto: 'auto', modal: '100', tooltip: '110', popover: '90', }; // Transitions export const transitions = { DEFAULT: '150ms cubic-bezier(0.4, 0, 0.2, 1)', fast: '100ms cubic-bezier(0.4, 0, 0.2, 1)', slow: '300ms cubic-bezier(0.4, 0, 0.2, 1)', }; // Export the full theme export const theme = { colors, spacing, typography, borderRadius, shadows, zIndex, transitions, }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/EnhancedServerCard.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { motion } from 'framer-motion'; import { GradientButton, EnvironmentBadge, DatabaseTypeBadge } from './ui'; import { cn } from '../utils/cn'; import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes'; const EnhancedServerCard = ({ name, server, index, activeEnvironment, isSelected = false, onSelect, onEdit, onAddToClaude, onDelete }) => { const environmentCount = Object.keys(server.environments || {}).length; const hasActiveConnection = !!activeEnvironment; // Real connection status const connectionStatus = hasActiveConnection ? 'active' : 'configured'; const lastModified = server.modified ? new Date(server.modified).toLocaleDateString() : 'Unknown'; // Count environments connected to Claude const claudeActiveCount = hasActiveConnection ? 1 : 0; // Detect database type from active environment const activeEnvData = activeEnvironment ? server.environments[activeEnvironment] : {}; const databaseType = detectDatabaseType(activeEnvData); const handleRowClick = () => { if (onSelect) { onSelect(name); } }; const handleRadioChange = (e) => { e.stopPropagation(); if (onSelect) { onSelect(name); } }; const getConnectionStatusColor = () => { switch (connectionStatus) { case 'active': return 'text-green-600'; case 'configured': return 'text-[#86a0ff]'; case 'error': return 'text-red-600'; default: return 'text-gray-400'; } }; return ( <motion.div className={cn( "border-b border-gray-200 transition-colors cursor-pointer", isSelected ? "bg-blue-50 border-blue-200" : "bg-white hover:bg-gray-50" )} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.2, delay: index * 0.02 }} onClick={handleRowClick} > <div className="px-6 py-4"> <div className="grid grid-cols-12 gap-4 items-center"> {/* Selection Radio */} <div className="col-span-1"> <input type="radio" name="database-selection" checked={isSelected} onChange={handleRadioChange} className="w-4 h-4 text-[#86a0ff] border-gray-300 focus:ring-[#86a0ff]" /> </div> {/* Database Info */} <div className="col-span-4"> <div className="flex items-center space-x-3"> <div className="w-8 h-8 bg-blue-50 rounded-lg flex items-center justify-center flex-shrink-0"> <svg className="h-4 w-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> </div> <div className="min-w-0"> <h3 className="text-base font-semibold text-gray-900 truncate">{name}</h3> </div> </div> </div> {/* Active Environment */} <div className="col-span-2"> {hasActiveConnection && activeEnvironment ? ( <EnvironmentBadge environment={activeEnvironment} active size="sm" /> ) : ( <span className="text-sm text-gray-500">None</span> )} </div> {/* Environment Count */} <div className="col-span-2"> <span className="text-sm font-medium text-gray-600">{environmentCount}</span> </div> {/* Description */} <div className="col-span-3"> {server.description && ( <span className="text-sm text-gray-500 truncate block">{server.description}</span> )} </div> </div> </div> </motion.div> ); }; export default EnhancedServerCard; ``` -------------------------------------------------------------------------------- /src/constants/tool-definitions.js: -------------------------------------------------------------------------------- ```javascript /** * Tool definitions for HANA MCP Server * * Note: For tools that accept schema_name as an optional parameter, * the HANA_SCHEMA environment variable will be used if schema_name is not provided. */ const TOOLS = [ { name: "hana_show_config", description: "Show the HANA database configuration", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "hana_test_connection", description: "Test connection to HANA database", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "hana_list_schemas", description: "List all schemas in the HANA database", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "hana_show_env_vars", description: "Show all HANA-related environment variables (for debugging)", inputSchema: { type: "object", properties: {}, required: [] } }, { name: "hana_list_tables", description: "List all tables in a specific schema", inputSchema: { type: "object", properties: { schema_name: { type: "string", description: "Name of the schema to list tables from (optional)" } }, required: [] } }, { name: "hana_describe_table", description: "Describe the structure of a specific table", inputSchema: { type: "object", properties: { schema_name: { type: "string", description: "Name of the schema containing the table (optional)" }, table_name: { type: "string", description: "Name of the table to describe" } }, required: ["table_name"] } }, { name: "hana_list_indexes", description: "List all indexes for a specific table", inputSchema: { type: "object", properties: { schema_name: { type: "string", description: "Name of the schema containing the table (optional)" }, table_name: { type: "string", description: "Name of the table to list indexes for" } }, required: ["table_name"] } }, { name: "hana_describe_index", description: "Describe the structure of a specific index", inputSchema: { type: "object", properties: { schema_name: { type: "string", description: "Name of the schema containing the table (optional)" }, table_name: { type: "string", description: "Name of the table containing the index" }, index_name: { type: "string", description: "Name of the index to describe" } }, required: ["table_name", "index_name"] } }, { name: "hana_execute_query", description: "Execute a custom SQL query against the HANA database", inputSchema: { type: "object", properties: { query: { type: "string", description: "The SQL query to execute" }, parameters: { type: "array", description: "Optional parameters for the query (for prepared statements)", items: { type: "string" } } }, required: ["query"] } } ]; // Tool categories for organization const TOOL_CATEGORIES = { CONFIGURATION: ['hana_show_config', 'hana_test_connection', 'hana_show_env_vars'], SCHEMA: ['hana_list_schemas'], TABLE: ['hana_list_tables', 'hana_describe_table'], INDEX: ['hana_list_indexes', 'hana_describe_index'], QUERY: ['hana_execute_query'] }; // Get tool by name function getTool(name) { return TOOLS.find(tool => tool.name === name); } // Get tools by category function getToolsByCategory(category) { const toolNames = TOOL_CATEGORIES[category] || []; return TOOLS.filter(tool => toolNames.includes(tool.name)); } // Get all tool names function getAllToolNames() { return TOOLS.map(tool => tool.name); } module.exports = { TOOLS, TOOL_CATEGORIES, getTool, getToolsByCategory, getAllToolNames }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/utils/databaseTypes.js: -------------------------------------------------------------------------------- ```javascript /** * Database type detection and display utilities for HANA MCP UI */ export const DATABASE_TYPES = { SINGLE_CONTAINER: 'single_container', MDC_SYSTEM: 'mdc_system', MDC_TENANT: 'mdc_tenant' } /** * Detect database type based on configuration data * @param {Object} data - Configuration data * @returns {string} Database type */ export const detectDatabaseType = (data) => { if (!data) return DATABASE_TYPES.SINGLE_CONTAINER if (data.HANA_INSTANCE_NUMBER && data.HANA_DATABASE_NAME) { return DATABASE_TYPES.MDC_TENANT } else if (data.HANA_INSTANCE_NUMBER && !data.HANA_DATABASE_NAME) { return DATABASE_TYPES.MDC_SYSTEM } else { return DATABASE_TYPES.SINGLE_CONTAINER } } /** * Get display name for database type * @param {string} type - Database type * @returns {string} Display name */ export const getDatabaseTypeDisplayName = (type) => { const displayNames = { [DATABASE_TYPES.SINGLE_CONTAINER]: 'Single-Container Database', [DATABASE_TYPES.MDC_SYSTEM]: 'MDC System Database', [DATABASE_TYPES.MDC_TENANT]: 'MDC Tenant Database' } return displayNames[type] || 'Unknown Database Type' } /** * Get short display name for database type * @param {string} type - Database type * @returns {string} Short display name */ export const getDatabaseTypeShortName = (type) => { const shortNames = { [DATABASE_TYPES.SINGLE_CONTAINER]: 'Single-Container', [DATABASE_TYPES.MDC_SYSTEM]: 'MDC System', [DATABASE_TYPES.MDC_TENANT]: 'MDC Tenant' } return shortNames[type] || 'Unknown' } /** * Get color for database type badge * @param {string} type - Database type * @returns {string} Color class */ export const getDatabaseTypeColor = (type) => { const colors = { [DATABASE_TYPES.SINGLE_CONTAINER]: 'blue', [DATABASE_TYPES.MDC_SYSTEM]: 'amber', [DATABASE_TYPES.MDC_TENANT]: 'green' } return colors[type] || 'gray' } /** * Check if MDC fields should be shown * @param {string} detectedType - Auto-detected type * @param {string} manualType - Manually selected type * @returns {boolean} Should show MDC fields */ export const shouldShowMDCFields = (detectedType, manualType) => { // Show MDC fields only for MDC system or tenant types return manualType === DATABASE_TYPES.MDC_SYSTEM || manualType === DATABASE_TYPES.MDC_TENANT } /** * Get required fields for database type * @param {string} type - Database type * @returns {Array} Required field names */ export const getRequiredFieldsForType = (type) => { const baseFields = ['HANA_HOST', 'HANA_USER', 'HANA_PASSWORD'] switch (type) { case DATABASE_TYPES.MDC_TENANT: return [...baseFields, 'HANA_INSTANCE_NUMBER', 'HANA_DATABASE_NAME'] case DATABASE_TYPES.MDC_SYSTEM: return [...baseFields, 'HANA_INSTANCE_NUMBER'] case DATABASE_TYPES.SINGLE_CONTAINER: default: return baseFields } } /** * Get recommended fields for database type * @param {string} type - Database type * @returns {Array} Recommended field names */ export const getRecommendedFieldsForType = (type) => { switch (type) { case DATABASE_TYPES.SINGLE_CONTAINER: return ['HANA_SCHEMA'] default: return [] } } /** * Validate configuration for specific database type * @param {Object} data - Configuration data * @param {string} type - Database type * @returns {Object} Validation result */ export const validateForDatabaseType = (data, type) => { const errors = {} const requiredFields = getRequiredFieldsForType(type) const recommendedFields = getRecommendedFieldsForType(type) // Check required fields requiredFields.forEach(field => { if (!data[field] || data[field].toString().trim() === '') { errors[field] = `${field.replace('HANA_', '')} is required for ${getDatabaseTypeShortName(type)}` } }) // Check recommended fields recommendedFields.forEach(field => { if (!data[field] || data[field].toString().trim() === '') { errors[field] = `${field.replace('HANA_', '')} is recommended for ${getDatabaseTypeShortName(type)}` } }) return { valid: Object.keys(errors).length === 0, errors, databaseType: type } } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeServerCard.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { EnvironmentBadge, DatabaseTypeBadge } from './ui' import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes' const ClaudeServerCard = ({ server, index, activeEnvironment, onRemove }) => { // Detect database type from server environment data const databaseType = detectDatabaseType(server.env || {}) return ( <motion.div className="bg-gray-50 border border-gray-100 rounded-lg p-3 hover:bg-gray-100 transition-all duration-200" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: index * 0.05 }} whileHover={{ y: -1 }} > {/* Header */} <div className="flex items-start justify-between mb-2"> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2 mb-1"> <div className="w-1.5 h-1.5 bg-green-500 rounded-full"></div> <h4 className="text-sm font-medium text-gray-900 truncate">{server.name}</h4> <DatabaseTypeBadge type={databaseType} size="xs" /> </div> {activeEnvironment && ( <EnvironmentBadge environment={activeEnvironment} active size="xs" /> )} </div> <button onClick={onRemove} className="text-gray-400 hover:text-red-500 transition-colors duration-200 p-1 rounded hover:bg-white/60" title="Remove from Claude" > <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> {/* Connection Info */} <div className="space-y-1"> <div className="flex items-center justify-between text-xs"> <span className="text-gray-500">Host:</span> <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}> {server.env.HANA_HOST} </span> </div> {/* Show MDC-specific info when applicable */} {databaseType === 'mdc_tenant' && server.env.HANA_DATABASE_NAME && ( <div className="flex items-center justify-between text-xs"> <span className="text-gray-500">Database:</span> <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}> {server.env.HANA_DATABASE_NAME} </span> </div> )} {databaseType === 'mdc_system' && server.env.HANA_INSTANCE_NUMBER && ( <div className="flex items-center justify-between text-xs"> <span className="text-gray-500">Instance:</span> <span className="font-mono text-gray-700 bg-white/70 px-1.5 py-0.5 rounded text-xs"> {server.env.HANA_INSTANCE_NUMBER} </span> </div> )} <div className="flex items-center justify-between text-xs"> <span className="text-gray-500">Schema:</span> <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}> {server.env.HANA_SCHEMA} </span> </div> </div> {/* Status */} <div className="mt-2 pt-2 border-t border-gray-200"> <div className="flex items-center justify-between"> <span className="text-xs text-green-700 font-medium flex items-center gap-1"> <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"> <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" /> </svg> Connected </span> <span className="text-xs text-gray-400"> Active </span> </div> </div> </motion.div> ) } export default ClaudeServerCard ``` -------------------------------------------------------------------------------- /src/database/hana-client.js: -------------------------------------------------------------------------------- ```javascript const hana = require('@sap/hana-client'); // Simple logger that doesn't interfere with JSON-RPC const log = (msg) => console.error(`[HANA Client] ${new Date().toISOString()}: ${msg}`); /** * Create and configure a HANA client * @param {Object} config - HANA connection configuration * @returns {Object} HANA client wrapper */ async function createHanaClient(config) { try { // Create connection const connection = hana.createConnection(); // Use connection parameter building if available const connectionParams = config.getConnectionParams ? config.getConnectionParams() : buildLegacyConnectionParams(config); // Log database type information const dbType = config.getHanaDatabaseType ? config.getHanaDatabaseType() : 'single_container'; log(`Connecting to HANA ${dbType} database...`); // Connect to HANA await connect(connection, connectionParams); log(`Successfully connected to HANA ${dbType} database`); // Return client wrapper with utility methods return { /** * Execute a SQL query * @param {string} sql - SQL query to execute * @param {Array} params - Query parameters * @returns {Promise<Array>} Query results */ async query(sql, params = []) { try { const statement = connection.prepare(sql); const results = await executeStatement(statement, params); statement.drop(); return results; } catch (error) { log('Query execution error:', error); throw new Error(`Query execution failed: ${error.message}`); } }, /** * Execute a SQL query that returns a single value * @param {string} sql - SQL query to execute * @param {Array} params - Query parameters * @returns {Promise<any>} Query result */ async queryScalar(sql, params = []) { const results = await this.query(sql, params); if (results.length === 0) return null; const firstRow = results[0]; const keys = Object.keys(firstRow); if (keys.length === 0) return null; return firstRow[keys[0]]; }, /** * Disconnect from HANA database * @returns {Promise<void>} */ async disconnect() { return new Promise((resolve, reject) => { connection.disconnect(err => { if (err) { log('Error disconnecting from HANA:', err); reject(err); } else { log('Disconnected from HANA database'); resolve(); } }); }); } }; } catch (error) { log(`Failed to create HANA client: ${error.message}`); throw error; } } /** * Build legacy connection parameters for backward compatibility */ function buildLegacyConnectionParams(config) { return { serverNode: `${config.host}:${config.port}`, uid: config.user, pwd: config.password, encrypt: config.encrypt !== false, sslValidateCertificate: config.validateCert !== false, ...config.additionalParams }; } /** * Connect to HANA database * @param {Object} connection - HANA connection object * @param {Object} params - Connection parameters * @returns {Promise<void>} */ function connect(connection, params) { return new Promise((resolve, reject) => { connection.connect(params, (err) => { if (err) { reject(new Error(`HANA connection failed: ${err.message}`)); } else { resolve(); } }); }); } /** * Execute a prepared statement * @param {Object} statement - Prepared statement * @param {Array} params - Statement parameters * @returns {Promise<Array>} Query results */ function executeStatement(statement, params) { return new Promise((resolve, reject) => { statement.execQuery(params, (err, results) => { if (err) { reject(err); } else { // Convert results to array of objects const rows = []; while (results.next()) { rows.push(results.getValues()); } resolve(rows); } }); }); } module.exports = { createHanaClient }; ``` -------------------------------------------------------------------------------- /src/database/connection-manager.js: -------------------------------------------------------------------------------- ```javascript /** * HANA Database Connection Manager */ const { logger } = require('../utils/logger'); const { config } = require('../utils/config'); const { createHanaClient } = require('./hana-client'); class ConnectionManager { constructor() { this.client = null; this.isConnecting = false; this.lastConnectionAttempt = null; this.connectionRetries = 0; this.maxRetries = 3; } /** * Get or create HANA client connection */ async getClient() { // Return existing client if available if (this.client) { return this.client; } // Prevent multiple simultaneous connection attempts if (this.isConnecting) { logger.debug('Connection already in progress, waiting...'); while (this.isConnecting) { await new Promise(resolve => setTimeout(resolve, 100)); } return this.client; } // Check if configuration is valid if (!config.isHanaConfigured()) { logger.warn('HANA configuration is incomplete'); return null; } return this.connect(); } /** * Establish connection to HANA database */ async connect() { this.isConnecting = true; this.lastConnectionAttempt = new Date(); try { logger.info('Connecting to HANA database...'); const hanaConfig = config.getHanaConfig(); const dbType = config.getHanaDatabaseType(); logger.info(`Detected HANA database type: ${dbType}`); // Pass the full config object so the client can access the methods this.client = await createHanaClient(config); this.connectionRetries = 0; logger.info(`HANA client connected successfully to ${dbType} database`); return this.client; } catch (error) { this.connectionRetries++; logger.error(`Failed to connect to HANA (attempt ${this.connectionRetries}):`, error.message); if (this.connectionRetries < this.maxRetries) { logger.info(`Retrying connection in 2 seconds...`); await new Promise(resolve => setTimeout(resolve, 2000)); this.isConnecting = false; return this.connect(); } else { logger.error('Max connection retries reached'); this.isConnecting = false; return null; } } } /** * Test the connection */ async testConnection() { const client = await this.getClient(); if (!client) { return { success: false, error: 'No client available' }; } try { const testQuery = 'SELECT 1 as test_value FROM DUMMY'; const result = await client.query(testQuery); if (result && result.length > 0) { return { success: true, result: result[0].TEST_VALUE }; } else { return { success: false, error: 'Connection test returned no results' }; } } catch (error) { logger.error('Connection test failed:', error.message); return { success: false, error: error.message }; } } /** * Check if connection is healthy */ async isHealthy() { const test = await this.testConnection(); return test.success; } /** * Disconnect from HANA database */ async disconnect() { if (this.client) { try { await this.client.disconnect(); logger.info('HANA client disconnected'); } catch (error) { logger.error('Error disconnecting HANA client:', error.message); } finally { this.client = null; this.connectionRetries = 0; } } } /** * Reset connection (disconnect and reconnect) */ async resetConnection() { logger.info('Resetting HANA connection...'); await this.disconnect(); this.connectionRetries = 0; return this.getClient(); } /** * Get connection status */ getStatus() { const dbType = config.getHanaDatabaseType(); return { connected: !!this.client, isConnecting: this.isConnecting, lastConnectionAttempt: this.lastConnectionAttempt, connectionRetries: this.connectionRetries, maxRetries: this.maxRetries, databaseType: dbType }; } } // Create singleton instance const connectionManager = new ConnectionManager(); module.exports = { ConnectionManager, connectionManager }; ``` -------------------------------------------------------------------------------- /src/server/mcp-handler.js: -------------------------------------------------------------------------------- ```javascript /** * MCP Protocol Handler for JSON-RPC 2.0 communication */ const { logger } = require('../utils/logger'); const { METHODS, ERROR_CODES, ERROR_MESSAGES, PROTOCOL_VERSIONS, SERVER_INFO, CAPABILITIES } = require('../constants/mcp-constants'); const ToolRegistry = require('../tools'); class MCPHandler { /** * Handle MCP request */ static async handleRequest(request) { const { id, method, params } = request; logger.method(method); try { switch (method) { case METHODS.INITIALIZE: return this.handleInitialize(id, params); case METHODS.TOOLS_LIST: return this.handleToolsList(id, params); case METHODS.TOOLS_CALL: return this.handleToolsCall(id, params); case METHODS.NOTIFICATIONS_INITIALIZED: return this.handleInitialized(id, params); case METHODS.PROMPTS_LIST: return this.handlePromptsList(id, params); default: return this.createErrorResponse(id, ERROR_CODES.METHOD_NOT_FOUND, `Method not found: ${method}`); } } catch (error) { logger.error(`Error handling request: ${error.message}`); return this.createErrorResponse(id, ERROR_CODES.INTERNAL_ERROR, error.message); } } /** * Handle initialize request */ static handleInitialize(id, params) { logger.info('Initializing server'); return { jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL_VERSIONS.LATEST, capabilities: CAPABILITIES, serverInfo: SERVER_INFO } }; } /** * Handle tools/list request */ static handleToolsList(id, params) { logger.info('Listing tools'); const tools = ToolRegistry.getTools(); return { jsonrpc: '2.0', id, result: { tools } }; } /** * Handle tools/call request */ static async handleToolsCall(id, params) { const { name, arguments: args } = params; logger.tool(name, args); // Validate tool exists if (!ToolRegistry.hasTool(name)) { return this.createErrorResponse(id, ERROR_CODES.TOOL_NOT_FOUND, `Tool not found: ${name}`); } // Validate tool arguments const validation = ToolRegistry.validateToolArgs(name, args); if (!validation.valid) { return this.createErrorResponse(id, ERROR_CODES.INVALID_PARAMS, validation.error); } try { const result = await ToolRegistry.executeTool(name, args); return { jsonrpc: '2.0', id, result }; } catch (error) { logger.error(`Tool execution failed: ${error.message}`); return this.createErrorResponse(id, ERROR_CODES.INTERNAL_ERROR, error.message); } } /** * Handle notifications/initialized */ static handleInitialized(id, params) { logger.info('Server initialized'); return null; // No response for notifications } /** * Handle prompts/list request */ static handlePromptsList(id, params) { logger.info('Listing prompts'); const prompts = [ { name: "hana_query_builder", description: "Build a SQL query for HANA database", template: "I need to build a SQL query for HANA database that {{goal}}." }, { name: "hana_schema_explorer", description: "Explore HANA database schemas and tables", template: "I want to explore the schemas and tables in my HANA database." }, { name: "hana_connection_test", description: "Test HANA database connection", template: "Please test my HANA database connection and show the configuration." } ]; return { jsonrpc: '2.0', id, result: { prompts } }; } /** * Create error response */ static createErrorResponse(id, code, message) { return { jsonrpc: '2.0', id, error: { code, message: message || ERROR_MESSAGES[code] || 'Unknown error' } }; } /** * Validate JSON-RPC request */ static validateRequest(request) { if (!request || typeof request !== 'object') { return { valid: false, error: 'Invalid request: must be an object' }; } if (request.jsonrpc !== '2.0') { return { valid: false, error: 'Invalid JSON-RPC version' }; } if (!request.method) { return { valid: false, error: 'Missing method' }; } if (typeof request.method !== 'string') { return { valid: false, error: 'Method must be a string' }; } return { valid: true }; } } module.exports = MCPHandler; ``` -------------------------------------------------------------------------------- /src/tools/index-tools.js: -------------------------------------------------------------------------------- ```javascript /** * Index management tools for HANA MCP Server */ const { logger } = require('../utils/logger'); const { config } = require('../utils/config'); const QueryExecutor = require('../database/query-executor'); const Validators = require('../utils/validators'); const Formatters = require('../utils/formatters'); class IndexTools { /** * List indexes for a table */ static async listIndexes(args) { logger.tool('hana_list_indexes', args); let { schema_name, table_name } = args || {}; // Use default schema if not provided if (!schema_name) { if (config.hasDefaultSchema()) { schema_name = config.getDefaultSchema(); logger.info(`Using default schema: ${schema_name}`); } else { return Formatters.createErrorResponse( 'Schema name is required', 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' ); } } // Validate required parameters const validation = Validators.validateRequired(args, ['table_name'], 'hana_list_indexes'); if (!validation.valid) { return Formatters.createErrorResponse('Error: table_name parameter is required', validation.error); } // Validate schema and table names const schemaValidation = Validators.validateSchemaName(schema_name); if (!schemaValidation.valid) { return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); } const tableValidation = Validators.validateTableName(table_name); if (!tableValidation.valid) { return Formatters.createErrorResponse('Invalid table name', tableValidation.error); } try { const results = await QueryExecutor.getTableIndexes(schema_name, table_name); if (results.length === 0) { return Formatters.createResponse(`📋 No indexes found for table '${schema_name}.${table_name}'.`); } // Group by index name const indexMap = {}; results.forEach(row => { if (!indexMap[row.INDEX_NAME]) { indexMap[row.INDEX_NAME] = { type: row.INDEX_TYPE, isUnique: row.IS_UNIQUE === 'TRUE', columns: [] }; } indexMap[row.INDEX_NAME].columns.push(row.COLUMN_NAME); }); const formattedIndexes = Formatters.formatIndexList(indexMap, schema_name, table_name); return Formatters.createResponse(formattedIndexes); } catch (error) { logger.error('Error listing indexes:', error.message); return Formatters.createErrorResponse('Error listing indexes', error.message); } } /** * Describe index details */ static async describeIndex(args) { logger.tool('hana_describe_index', args); let { schema_name, table_name, index_name } = args || {}; // Use default schema if not provided if (!schema_name) { if (config.hasDefaultSchema()) { schema_name = config.getDefaultSchema(); logger.info(`Using default schema: ${schema_name}`); } else { return Formatters.createErrorResponse( 'Schema name is required', 'Please provide schema_name parameter or set HANA_SCHEMA environment variable' ); } } // Validate required parameters const validation = Validators.validateRequired(args, ['table_name', 'index_name'], 'hana_describe_index'); if (!validation.valid) { return Formatters.createErrorResponse('Error: table_name and index_name parameters are required', validation.error); } // Validate schema, table, and index names const schemaValidation = Validators.validateSchemaName(schema_name); if (!schemaValidation.valid) { return Formatters.createErrorResponse('Invalid schema name', schemaValidation.error); } const tableValidation = Validators.validateTableName(table_name); if (!tableValidation.valid) { return Formatters.createErrorResponse('Invalid table name', tableValidation.error); } const indexValidation = Validators.validateIndexName(index_name); if (!indexValidation.valid) { return Formatters.createErrorResponse('Invalid index name', indexValidation.error); } try { const results = await QueryExecutor.getIndexDetails(schema_name, table_name, index_name); const formattedDetails = Formatters.formatIndexDetails(results, schema_name, table_name, index_name); return Formatters.createResponse(formattedDetails); } catch (error) { logger.error('Error describing index:', error.message); return Formatters.createErrorResponse('Error describing index', error.message); } } } module.exports = IndexTools; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/EnvironmentSelector.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { useEffect } from 'react' import { XMarkIcon, PlusIcon, CheckCircleIcon } from '@heroicons/react/24/outline' import { EnvironmentBadge } from './ui' const EnvironmentSelector = ({ isOpen, onClose, serverName, environments, activeEnvironment, onDeploy, isLoading }) => { useEffect(() => { if (!isOpen) return const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [isOpen, onClose]) if (!isOpen) return null return ( <motion.div className="fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} > <motion.div className="bg-white rounded-2xl shadow-xl max-w-2xl w-full border border-gray-200 overflow-hidden" initial={{ scale: 0.9, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 20 }} transition={{ type: "spring", stiffness: 300, damping: 25 }} onClick={(e) => e.stopPropagation()} > {/* Header */} <div className="px-6 py-4 border-b border-gray-100"> <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> <div className="p-2 bg-gray-100 rounded-lg"> <PlusIcon className="w-5 h-5 text-gray-600" /> </div> <div> <h2 className="text-xl font-semibold text-gray-900">Add to Claude Config</h2> <p className="text-sm text-gray-500 mt-0.5">Select environment for {serverName}</p> </div> </div> <button onClick={onClose} className="p-2 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors" > <XMarkIcon className="w-5 h-5" /> </button> </div> </div> {/* Body */} <div className="p-6"> <p className="text-gray-600 mb-6 text-sm"> 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. </p> <div className="space-y-3"> {Object.entries(environments).map(([env, config], index) => ( <motion.button key={env} onClick={() => onDeploy(env)} disabled={isLoading} 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" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2, delay: index * 0.05 }} whileHover={!isLoading ? { y: -1 } : {}} > <div className="flex justify-between items-center mb-3"> <div className="flex items-center gap-3"> <h3 className="text-lg font-medium text-gray-900">{env}</h3> <EnvironmentBadge environment={env} size="sm" /> </div> {activeEnvironment === env && ( <div className="flex items-center gap-2 px-2 py-1 bg-green-100 rounded-full"> <CheckCircleIcon className="w-4 h-4 text-green-600" /> <span className="text-green-700 text-xs font-medium">ACTIVE</span> </div> )} </div> <div className="grid grid-cols-2 gap-4 text-sm"> <div> <span className="text-gray-500">Host:</span> <p className="text-gray-700 font-mono text-xs mt-0.5">{config.HANA_HOST}</p> </div> <div> <span className="text-gray-500">Schema:</span> <p className="text-gray-700 font-mono text-xs mt-0.5">{config.HANA_SCHEMA}</p> </div> </div> </motion.button> ))} </div> </div> {/* Footer */} <div className="px-6 py-4 border-t border-gray-100 bg-gray-50 flex justify-end"> <button onClick={onClose} disabled={isLoading} 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" > Cancel </button> </div> </motion.div> </motion.div> ) } export default EnvironmentSelector ``` -------------------------------------------------------------------------------- /src/utils/validators.js: -------------------------------------------------------------------------------- ```javascript /** * Input validation utilities for HANA MCP Server */ const { logger } = require('./logger'); class Validators { /** * Validate required parameters */ static validateRequired(params, requiredFields, toolName) { const missing = []; for (const field of requiredFields) { if (!params || params[field] === undefined || params[field] === null || params[field] === '') { missing.push(field); } } if (missing.length > 0) { const error = `Missing required parameters: ${missing.join(', ')}`; logger.warn(`Validation failed for ${toolName}:`, error); return { valid: false, error }; } return { valid: true }; } /** * Validate schema name */ static validateSchemaName(schemaName) { if (!schemaName || typeof schemaName !== 'string') { return { valid: false, error: 'Schema name must be a non-empty string' }; } if (schemaName.length > 128) { return { valid: false, error: 'Schema name too long (max 128 characters)' }; } // Basic SQL identifier validation if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schemaName)) { return { valid: false, error: 'Invalid schema name format' }; } return { valid: true }; } /** * Validate table name */ static validateTableName(tableName) { if (!tableName || typeof tableName !== 'string') { return { valid: false, error: 'Table name must be a non-empty string' }; } if (tableName.length > 128) { return { valid: false, error: 'Table name too long (max 128 characters)' }; } // Basic SQL identifier validation if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) { return { valid: false, error: 'Invalid table name format' }; } return { valid: true }; } /** * Validate index name */ static validateIndexName(indexName) { if (!indexName || typeof indexName !== 'string') { return { valid: false, error: 'Index name must be a non-empty string' }; } if (indexName.length > 128) { return { valid: false, error: 'Index name too long (max 128 characters)' }; } // Basic SQL identifier validation if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(indexName)) { return { valid: false, error: 'Invalid index name format' }; } return { valid: true }; } /** * Validate SQL query */ static validateQuery(query) { if (!query || typeof query !== 'string') { return { valid: false, error: 'Query must be a non-empty string' }; } if (query.trim().length === 0) { return { valid: false, error: 'Query cannot be empty' }; } // Basic SQL injection prevention - check for suspicious patterns const suspiciousPatterns = [ /;\s*drop\s+table/i, /;\s*delete\s+from/i, /;\s*truncate\s+table/i, /;\s*alter\s+table/i, /;\s*create\s+table/i, /;\s*drop\s+database/i, /;\s*shutdown/i ]; for (const pattern of suspiciousPatterns) { if (pattern.test(query)) { return { valid: false, error: 'Query contains potentially dangerous operations' }; } } return { valid: true }; } /** * Validate query parameters */ static validateParameters(parameters) { if (!parameters) { return { valid: true }; // Parameters are optional } if (!Array.isArray(parameters)) { return { valid: false, error: 'Parameters must be an array' }; } for (let i = 0; i < parameters.length; i++) { const param = parameters[i]; if (param === undefined || param === null) { return { valid: false, error: `Parameter at index ${i} cannot be null or undefined` }; } } return { valid: true }; } /** * Validate tool arguments */ static validateToolArgs(args, toolName) { if (!args || typeof args !== 'object') { return { valid: false, error: 'Arguments must be an object' }; } logger.debug(`Validating arguments for ${toolName}:`, args); return { valid: true }; } /** * Validate configuration for specific database type */ static validateForDatabaseType(config) { const dbType = config.getHanaDatabaseType ? config.getHanaDatabaseType() : 'single_container'; const errors = []; switch (dbType) { case 'mdc_tenant': if (!config.instanceNumber) { errors.push('HANA_INSTANCE_NUMBER is required for MDC Tenant Database'); } if (!config.databaseName) { errors.push('HANA_DATABASE_NAME is required for MDC Tenant Database'); } break; case 'mdc_system': if (!config.instanceNumber) { errors.push('HANA_INSTANCE_NUMBER is required for MDC System Database'); } break; case 'single_container': if (!config.schema) { errors.push('HANA_SCHEMA is recommended for Single-Container Database'); } break; } return { valid: errors.length === 0, errors: errors, databaseType: dbType }; } } module.exports = Validators; ``` -------------------------------------------------------------------------------- /src/database/query-executor.js: -------------------------------------------------------------------------------- ```javascript /** * Query execution utilities for HANA database */ const { logger } = require('../utils/logger'); const { connectionManager } = require('./connection-manager'); const Validators = require('../utils/validators'); class QueryExecutor { /** * Execute a query with parameters */ static async executeQuery(query, parameters = []) { // Validate query const queryValidation = Validators.validateQuery(query); if (!queryValidation.valid) { throw new Error(queryValidation.error); } // Validate parameters const paramValidation = Validators.validateParameters(parameters); if (!paramValidation.valid) { throw new Error(paramValidation.error); } const client = await connectionManager.getClient(); if (!client) { throw new Error('HANA client not connected. Please check your HANA configuration.'); } try { logger.debug(`Executing query: ${query}`, parameters.length > 0 ? `with ${parameters.length} parameters` : ''); const results = await client.query(query, parameters); logger.debug(`Query executed successfully, returned ${results.length} rows`); return results; } catch (error) { logger.error(`Query execution failed: ${error.message}`); throw error; } } /** * Execute a scalar query (returns single value) */ static async executeScalarQuery(query, parameters = []) { const results = await this.executeQuery(query, parameters); if (results.length === 0) { return null; } const firstRow = results[0]; const firstColumn = Object.keys(firstRow)[0]; return firstRow[firstColumn]; } /** * Get all schemas */ static async getSchemas() { const query = `SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME`; const results = await this.executeQuery(query); return results.map(row => row.SCHEMA_NAME); } /** * Get tables in a schema */ static async getTables(schemaName) { const query = ` SELECT TABLE_NAME FROM SYS.TABLES WHERE SCHEMA_NAME = ? ORDER BY TABLE_NAME `; const results = await this.executeQuery(query, [schemaName]); return results.map(row => row.TABLE_NAME); } /** * Get table columns */ static async getTableColumns(schemaName, tableName) { const query = ` SELECT COLUMN_NAME, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE, DEFAULT_VALUE, POSITION, COMMENTS FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION `; return await this.executeQuery(query, [schemaName, tableName]); } /** * Get table indexes */ static async getTableIndexes(schemaName, tableName) { const query = ` SELECT INDEX_NAME, INDEX_TYPE, IS_UNIQUE, COLUMN_NAME FROM SYS.INDEX_COLUMNS ic JOIN SYS.INDEXES i ON ic.INDEX_NAME = i.INDEX_NAME AND ic.SCHEMA_NAME = i.SCHEMA_NAME WHERE ic.SCHEMA_NAME = ? AND ic.TABLE_NAME = ? ORDER BY ic.INDEX_NAME, ic.POSITION `; return await this.executeQuery(query, [schemaName, tableName]); } /** * Get index details */ static async getIndexDetails(schemaName, tableName, indexName) { const query = ` SELECT i.INDEX_NAME, i.INDEX_TYPE, i.IS_UNIQUE, ic.COLUMN_NAME, ic.POSITION, ic.ORDER FROM SYS.INDEXES i JOIN SYS.INDEX_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.SCHEMA_NAME = ic.SCHEMA_NAME WHERE i.SCHEMA_NAME = ? AND i.TABLE_NAME = ? AND i.INDEX_NAME = ? ORDER BY ic.POSITION `; return await this.executeQuery(query, [schemaName, tableName, indexName]); } /** * Test database connection */ static async testConnection() { return await connectionManager.testConnection(); } /** * Get database information */ static async getDatabaseInfo() { try { const versionQuery = 'SELECT * FROM M_DATABASE'; const version = await this.executeQuery(versionQuery); const userQuery = 'SELECT CURRENT_USER, CURRENT_SCHEMA FROM DUMMY'; const user = await this.executeQuery(userQuery); return { version: version.length > 0 ? version[0] : null, currentUser: user.length > 0 ? user[0].CURRENT_USER : null, currentSchema: user.length > 0 ? user[0].CURRENT_SCHEMA : null }; } catch (error) { logger.error('Failed to get database info:', error.message); return { error: error.message }; } } /** * Get table row count */ static async getTableRowCount(schemaName, tableName) { const query = `SELECT COUNT(*) as ROW_COUNT FROM "${schemaName}"."${tableName}"`; const result = await this.executeScalarQuery(query); return result; } /** * Get table sample data */ static async getTableSample(schemaName, tableName, limit = 10) { const query = `SELECT * FROM "${schemaName}"."${tableName}" LIMIT ?`; return await this.executeQuery(query, [limit]); } } module.exports = QueryExecutor; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/StatusBadge.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { cn } from '../../utils/cn' const StatusBadge = ({ status, count, showPulse = true, size = 'md', children, className }) => { const statusConfig = { online: { color: 'bg-gray-700', glow: 'shadow-gray-200/50', text: 'Online', className: 'status-online' }, offline: { color: 'bg-gray-400', glow: 'shadow-gray-200/50', text: 'Offline', className: 'status-offline' }, warning: { color: 'bg-gray-600', glow: 'shadow-gray-200/50', text: 'Warning', className: 'status-warning' }, error: { color: 'bg-gray-800', glow: 'shadow-gray-200/50', text: 'Error', className: 'status-error' } } const sizes = { xs: { dot: 'w-1.5 h-1.5', text: 'text-xs', padding: 'px-1.5 py-0.5' }, sm: { dot: 'w-2 h-2', text: 'text-xs', padding: 'px-2 py-1' }, md: { dot: 'w-3 h-3', text: 'text-sm', padding: 'px-3 py-1' }, lg: { dot: 'w-4 h-4', text: 'text-base', padding: 'px-4 py-2' } } const config = statusConfig[status] || statusConfig.offline const sizeConfig = sizes[size] return ( <motion.div className={cn( 'inline-flex items-center gap-2 rounded-full', sizeConfig.padding, className )} initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ duration: 0.2 }} > <div className={cn('relative flex items-center justify-center rounded-full', sizeConfig.dot)}> <div className={cn('w-full h-full rounded-full shadow-lg', config.color, config.glow)} /> {showPulse && status === 'online' && ( <div className={cn( 'absolute inset-0 rounded-full animate-ping opacity-75', config.color )} /> )} </div> <span className={cn('font-medium', sizeConfig.text)}> {children || config.text} {count !== undefined && ( <span className="ml-1 px-2 py-0.5 bg-gray-200 text-gray-700 rounded-full text-xs"> {count} </span> )} </span> </motion.div> ) } // Environment Badge Component export const EnvironmentBadge = ({ environment, active = false, size = 'sm', className }) => { const envClasses = { Production: 'bg-green-50 border-green-200 text-green-800', Development: 'bg-[#86a0ff] border-[#86a0ff] text-white', Staging: 'bg-amber-50 border-amber-200 text-amber-800', STAGING: 'bg-amber-50 border-amber-200 text-amber-800', Testing: 'bg-purple-50 border-purple-200 text-purple-800', QA: 'bg-indigo-50 border-indigo-200 text-indigo-800' } const sizeClasses = { xs: 'px-1.5 py-0.5 text-xs', sm: 'px-2 py-1 text-xs', md: 'px-3 py-1 text-sm', lg: 'px-4 py-2 text-base' } const activeRingClasses = { Production: 'ring-2 ring-green-300 shadow-sm', Development: 'ring-2 ring-[#86a0ff]/30 shadow-sm', Staging: 'ring-2 ring-amber-300 shadow-sm', STAGING: 'ring-2 ring-amber-300 shadow-sm', Testing: 'ring-2 ring-purple-300 shadow-sm', QA: 'ring-2 ring-indigo-300 shadow-sm' } const dotClasses = { Production: 'bg-green-600', Development: 'bg-[#86a0ff]', Staging: 'bg-amber-600', STAGING: 'bg-amber-600', Testing: 'bg-purple-600', QA: 'bg-indigo-600' } // Enhanced active state styling const activeStateClasses = active ? { Production: 'bg-green-100 border-green-300 text-green-900 shadow-md', Development: 'bg-[#86a0ff]/90 border-[#86a0ff] text-white shadow-md', Staging: 'bg-amber-100 border-amber-300 text-amber-900 shadow-md', STAGING: 'bg-amber-100 border-amber-300 text-amber-900 shadow-md', Testing: 'bg-purple-100 border-purple-300 text-purple-900 shadow-md', QA: 'bg-indigo-100 border-indigo-300 text-indigo-900 shadow-md' } : {} return ( <motion.span className={cn( 'inline-flex items-center rounded-full font-medium', 'border transition-all duration-200', sizeClasses[size], active ? (activeStateClasses[environment] || 'bg-green-100 border-green-300 text-green-900 shadow-md') : (envClasses[environment] || 'bg-gray-50 border-gray-200 text-gray-700'), active && (activeRingClasses[environment] || 'ring-2 ring-green-300 shadow-sm'), className )} whileHover={{ scale: 1.05 }} transition={{ duration: 0.1 }} title={`${environment} environment${active ? ' (active)' : ''}`} > {environment} {active && ( <motion.div className="ml-1.5 flex items-center space-x-1" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.2 }} > <div className={cn("w-2 h-2 rounded-full", dotClasses[environment] || 'bg-green-600')} animate={{ opacity: [1, 0.5, 1] }} transition={{ duration: 1.5, repeat: Infinity }} /> <svg className="w-3 h-3 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </motion.div> )} </motion.span> ) } export default StatusBadge ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/PathSetupModal.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { useEffect } from 'react' import { XMarkIcon, Cog6ToothIcon, ExclamationCircleIcon, InformationCircleIcon } from '@heroicons/react/24/outline' const PathSetupModal = ({ isOpen, onClose, pathInput, setPathInput, onSave, isLoading }) => { useEffect(() => { if (!isOpen) return const onKeyDown = (e) => { if (e.key === 'Escape') onClose() } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [isOpen, onClose]) return ( <motion.div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} > <motion.div className="bg-white rounded-2xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden border border-gray-200 flex flex-col" initial={{ scale: 0.9, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 20 }} transition={{ type: "spring", stiffness: 300, damping: 25 }} onClick={(e) => e.stopPropagation()} > {/* Header */} <div className="px-8 py-6 border-b border-gray-100 bg-white rounded-t-2xl"> <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> <div className="p-3 bg-gray-100 rounded-xl"> <Cog6ToothIcon className="w-5 h-5 text-gray-600" /> </div> <div> <h2 className="text-2xl font-bold text-gray-900 leading-tight"> Setup Claude Desktop Configuration </h2> <p className="text-base text-gray-600 mt-2 font-medium"> First-time setup: Configure Claude Desktop config file path </p> </div> </div> <button onClick={onClose} className="p-3 rounded-xl text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors" > <XMarkIcon className="w-5 h-5" /> </button> </div> </div> {/* Body */} <div className="flex-1 overflow-y-auto p-8"> {/* Info Alert */} <div className="bg-orange-50 border border-orange-200 rounded-xl p-4 mb-6"> <div className="flex items-start gap-3"> <ExclamationCircleIcon className="w-5 h-5 text-orange-500 mt-0.5 flex-shrink-0" /> <div> <h3 className="font-semibold text-orange-900 mb-1">Configuration Required</h3> <p className="text-orange-800 text-sm leading-relaxed"> To add servers to Claude Desktop, we need to know where your Claude configuration file is located. This is typically in your user directory. </p> </div> </div> </div> {/* Form */} <div className="mb-6"> <label className="block text-sm font-medium text-gray-700 mb-3"> Claude Desktop Config Path </label> <input type="text" value={pathInput} onChange={(e) => setPathInput(e.target.value)} placeholder="Enter path to claude_desktop_config.json" className="w-full px-4 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors font-mono" /> {/* Common Paths */} <div className="mt-4 bg-gray-50 border border-gray-200 rounded-xl p-4"> <div className="flex items-center gap-2 mb-3"> <InformationCircleIcon className="w-4 h-4 text-blue-500" /> <h4 className="text-sm font-medium text-gray-700">Common Paths:</h4> </div> <div className="space-y-2 text-sm text-gray-600 font-mono"> <div> <span className="text-gray-500">macOS:</span> <span className="ml-2">~/Library/Application Support/Claude/claude_desktop_config.json</span> </div> <div> <span className="text-gray-500">Windows:</span> <span className="ml-2">%APPDATA%/Claude/claude_desktop_config.json</span> </div> <div> <span className="text-gray-500">Linux:</span> <span className="ml-2">~/.config/claude/claude_desktop_config.json</span> </div> </div> </div> </div> </div> {/* Footer */} <div className="px-8 py-6 border-t border-gray-100 bg-gray-50 rounded-b-2xl flex justify-end gap-4"> <button onClick={onSave} disabled={isLoading || !pathInput.trim()} className="px-8 py-3 text-base font-semibold text-white bg-[#86a0ff] border border-transparent rounded-xl hover:bg-[#7990e6] focus:outline-none focus:ring-2 focus:ring-[#86a0ff] disabled:opacity-50 min-w-[150px] transition-colors shadow-md hover:shadow-lg" > {isLoading ? 'Saving...' : 'Save Configuration'} </button> </div> </motion.div> </motion.div> ) } export default PathSetupModal ``` -------------------------------------------------------------------------------- /tests/manual/manual-test.js: -------------------------------------------------------------------------------- ```javascript const { spawn } = require('child_process'); const readline = require('readline'); console.log('🔍 HANA MCP Server Manual Tester'); console.log('================================\n'); // Spawn the MCP server process const server = spawn('/opt/homebrew/bin/node', ['../../hana-mcp-server.js'], { stdio: ['pipe', 'pipe', 'pipe'], env: { HANA_HOST: "your-hana-host.com", HANA_PORT: "443", HANA_USER: "your-username", HANA_PASSWORD: "your-password", HANA_SCHEMA: "your-schema", HANA_SSL: "true", HANA_ENCRYPT: "true", HANA_VALIDATE_CERT: "true" } }); // Handle server output server.stdout.on('data', (data) => { try { const response = JSON.parse(data.toString().trim()); console.log('\n📤 Response:', JSON.stringify(response, null, 2)); } catch (error) { console.log('🔧 Server Log:', data.toString().trim()); } }); server.stderr.on('data', (data) => { console.log('🔧 Server Log:', data.toString().trim()); }); // Send request function function sendRequest(method, params = {}) { const request = { jsonrpc: '2.0', id: Date.now(), method, params }; console.log(`\n📤 Sending: ${method}`); server.stdin.write(JSON.stringify(request) + '\n'); } // Initialize server async function initializeServer() { console.log('🚀 Initializing server...'); sendRequest('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'manual-test-client', version: '1.0.0' } }); await new Promise(resolve => setTimeout(resolve, 1000)); } // List available tools async function listTools() { console.log('\n📋 Listing tools...'); sendRequest('tools/list', {}); await new Promise(resolve => setTimeout(resolve, 1000)); } // Interactive menu function showMenu() { console.log('\n\n🎯 Available Tests:'); console.log('1. Show HANA Config'); console.log('2. Test Connection'); console.log('3. List Schemas'); console.log('4. List Tables'); console.log('5. Describe Table'); console.log('6. List Indexes'); console.log('7. Execute Query'); console.log('8. Show Environment Variables'); console.log('9. Exit'); console.log('\nEnter your choice (1-9):'); } // Test functions function testShowConfig() { sendRequest('tools/call', { name: "hana_show_config", arguments: {} }); } function testConnection() { sendRequest('tools/call', { name: "hana_test_connection", arguments: {} }); } function testListSchemas() { sendRequest('tools/call', { name: "hana_list_schemas", arguments: {} }); } function testListTables() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Enter schema name (or press Enter for default): ', (schema) => { const args = schema.trim() ? { schema_name: schema.trim() } : {}; sendRequest('tools/call', { name: "hana_list_tables", arguments: args }); rl.close(); }); } function testDescribeTable() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Enter schema name: ', (schema) => { rl.question('Enter table name: ', (table) => { sendRequest('tools/call', { name: "hana_describe_table", arguments: { schema_name: schema.trim(), table_name: table.trim() } }); rl.close(); }); }); } function testListIndexes() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Enter schema name: ', (schema) => { rl.question('Enter table name: ', (table) => { sendRequest('tools/call', { name: "hana_list_indexes", arguments: { schema_name: schema.trim(), table_name: table.trim() } }); rl.close(); }); }); } function testExecuteQuery() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Enter SQL query: ', (query) => { sendRequest('tools/call', { name: "hana_execute_query", arguments: { query: query.trim() } }); rl.close(); }); } function testShowEnvVars() { sendRequest('tools/call', { name: "hana_show_env_vars", arguments: {} }); } // Main interactive loop async function startInteractive() { await initializeServer(); await listTools(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const askQuestion = () => { showMenu(); rl.question('', (answer) => { switch (answer.trim()) { case '1': testShowConfig(); break; case '2': testConnection(); break; case '3': testListSchemas(); break; case '4': testListTables(); break; case '5': testDescribeTable(); break; case '6': testListIndexes(); break; case '7': testExecuteQuery(); break; case '8': testShowEnvVars(); break; case '9': console.log('👋 Goodbye!'); rl.close(); server.kill(); return; default: console.log('❌ Invalid option. Please select 1-9.'); } setTimeout(askQuestion, 2000); }); }; askQuestion(); } // Handle server exit server.on('close', (code) => { console.log(`\n🔚 Server closed with code ${code}`); process.exit(0); }); server.on('error', (error) => { console.error('❌ Server error:', error); process.exit(1); }); // Start interactive testing startInteractive().catch(console.error); ``` -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- ```javascript /** * Configuration management utility for HANA MCP Server */ const { logger } = require('./logger'); class Config { constructor() { this.config = this.loadConfig(); } loadConfig() { return { hana: { host: process.env.HANA_HOST, port: parseInt(process.env.HANA_PORT) || 443, user: process.env.HANA_USER, password: process.env.HANA_PASSWORD, schema: process.env.HANA_SCHEMA, instanceNumber: process.env.HANA_INSTANCE_NUMBER, databaseName: process.env.HANA_DATABASE_NAME, connectionType: process.env.HANA_CONNECTION_TYPE || 'auto', ssl: process.env.HANA_SSL !== 'false', encrypt: process.env.HANA_ENCRYPT !== 'false', validateCert: process.env.HANA_VALIDATE_CERT !== 'false' }, server: { logLevel: process.env.LOG_LEVEL || 'INFO', enableFileLogging: process.env.ENABLE_FILE_LOGGING === 'true', enableConsoleLogging: process.env.ENABLE_CONSOLE_LOGGING !== 'false' } }; } getHanaConfig() { return this.config.hana; } getServerConfig() { return this.config.server; } /** * Determine HANA database type based on configuration */ getHanaDatabaseType() { const hana = this.config.hana; // Use explicit type if set and not 'auto' if (hana.connectionType && hana.connectionType !== 'auto') { return hana.connectionType; } // Auto-detect based on available parameters if (hana.instanceNumber && hana.databaseName) { return 'mdc_tenant'; } else if (hana.instanceNumber && !hana.databaseName) { return 'mdc_system'; } else { return 'single_container'; } } /** * Build connection parameters based on database type */ getConnectionParams() { const hana = this.config.hana; const dbType = this.getHanaDatabaseType(); const baseParams = { uid: hana.user, pwd: hana.password, encrypt: hana.encrypt, sslValidateCertificate: hana.validateCert }; // Build connection string based on database type switch (dbType) { case 'mdc_tenant': baseParams.serverNode = `${hana.host}:${hana.port}`; baseParams.databaseName = hana.databaseName; break; case 'mdc_system': baseParams.serverNode = `${hana.host}:${hana.port}`; break; case 'single_container': default: baseParams.serverNode = `${hana.host}:${hana.port}`; break; } return baseParams; } isHanaConfigured() { const hana = this.config.hana; return !!(hana.host && hana.user && hana.password); } getHanaConnectionString() { const hana = this.config.hana; return `${hana.host}:${hana.port}`; } // Get configuration info for display (hiding sensitive data) getDisplayConfig() { const hana = this.config.hana; const dbType = this.getHanaDatabaseType(); return { databaseType: dbType, connectionType: hana.connectionType, host: hana.host || 'NOT SET', port: hana.port, user: hana.user || 'NOT SET', password: hana.password ? 'SET (hidden)' : 'NOT SET', schema: hana.schema || 'NOT SET', instanceNumber: hana.instanceNumber || 'NOT SET', databaseName: hana.databaseName || 'NOT SET', ssl: hana.ssl, encrypt: hana.encrypt, validateCert: hana.validateCert }; } // Get environment variables for display getEnvironmentVars() { return { HANA_HOST: process.env.HANA_HOST || 'NOT SET', HANA_PORT: process.env.HANA_PORT || 'NOT SET', HANA_USER: process.env.HANA_USER || 'NOT SET', HANA_PASSWORD: process.env.HANA_PASSWORD ? 'SET (hidden)' : 'NOT SET', HANA_SCHEMA: process.env.HANA_SCHEMA || 'NOT SET', HANA_INSTANCE_NUMBER: process.env.HANA_INSTANCE_NUMBER || 'NOT SET', HANA_DATABASE_NAME: process.env.HANA_DATABASE_NAME || 'NOT SET', HANA_CONNECTION_TYPE: process.env.HANA_CONNECTION_TYPE || 'NOT SET', HANA_SSL: process.env.HANA_SSL || 'NOT SET', HANA_ENCRYPT: process.env.HANA_ENCRYPT || 'NOT SET', HANA_VALIDATE_CERT: process.env.HANA_VALIDATE_CERT || 'NOT SET' }; } // Validate configuration validate() { const hana = this.config.hana; const errors = []; const dbType = this.getHanaDatabaseType(); // Common required fields if (!hana.host) errors.push('HANA_HOST is required'); if (!hana.user) errors.push('HANA_USER is required'); if (!hana.password) errors.push('HANA_PASSWORD is required'); // Type-specific validation switch (dbType) { case 'mdc_tenant': if (!hana.instanceNumber) errors.push('HANA_INSTANCE_NUMBER is required for MDC Tenant Database'); if (!hana.databaseName) errors.push('HANA_DATABASE_NAME is required for MDC Tenant Database'); break; case 'mdc_system': if (!hana.instanceNumber) errors.push('HANA_INSTANCE_NUMBER is required for MDC System Database'); break; case 'single_container': if (!hana.schema) errors.push('HANA_SCHEMA is recommended for Single-Container Database'); break; } if (errors.length > 0) { logger.warn('Configuration validation failed:', errors); return false; } logger.info(`Configuration validation passed for ${dbType} database type`); return true; } /** * Get default schema from environment variables */ getDefaultSchema() { return this.config.hana.schema; } /** * Check if default schema is configured */ hasDefaultSchema() { return !!this.config.hana.schema; } } // Create default config instance const config = new Config(); module.exports = { Config, config }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GradientButton.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { cn } from '../../utils/cn' import { colors, transitions } from '../../utils/theme' import { IconComponent } from './index' /** * Button - A versatile button component with multiple variants and styles * * @param {Object} props - Component props * @param {React.ReactNode} props.children - Button content * @param {string} props.variant - Visual variant (primary, secondary, success, warning, danger) * @param {string} props.style - Button style (solid, outline, ghost, link) * @param {string} props.size - Button size (xs, sm, md, lg, xl) * @param {boolean} props.loading - Whether the button is in loading state * @param {React.ElementType} props.icon - Icon component to render * @param {string} props.iconPosition - Position of the icon (left, right) * @param {boolean} props.fullWidth - Whether the button should take full width * @param {string} props.className - Additional CSS classes * @returns {JSX.Element} - Rendered button component */ const Button = ({ children, variant = 'primary', style = 'solid', size = 'md', loading = false, icon, iconPosition = 'left', fullWidth = false, className, ...props }) => { // Style variants (solid, outline, ghost, link) - blue theme to match design const styleVariants = { solid: { primary: 'bg-[#86a0ff] text-white hover:bg-[#7990e6] focus:ring-[#86a0ff]', secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200 focus:ring-blue-500', success: 'bg-[#86a0ff] text-white hover:bg-[#7990e6] focus:ring-[#86a0ff]', warning: 'bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500', danger: 'bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700 focus:ring-red-500 border border-red-200', }, outline: { primary: 'bg-transparent border border-[#86a0ff] text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', secondary: 'bg-transparent border border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-blue-500', success: 'bg-transparent border border-[#86a0ff] text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', warning: 'bg-transparent border border-yellow-500 text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500', danger: 'bg-transparent border border-red-300 text-red-600 hover:bg-red-50 focus:ring-red-500', }, ghost: { primary: 'bg-transparent text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', secondary: 'bg-transparent text-gray-600 hover:bg-gray-50 focus:ring-blue-500', success: 'bg-transparent text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', warning: 'bg-transparent text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500', danger: 'bg-transparent text-red-600 hover:bg-red-50 focus:ring-red-500', }, link: { primary: 'bg-transparent text-[#86a0ff] hover:underline focus:ring-[#86a0ff] p-0 shadow-none', secondary: 'bg-transparent text-gray-600 hover:underline focus:ring-blue-500 p-0 shadow-none', success: 'bg-transparent text-[#86a0ff] hover:underline focus:ring-[#86a0ff] p-0 shadow-none', warning: 'bg-transparent text-yellow-600 hover:underline focus:ring-yellow-500 p-0 shadow-none', danger: 'bg-transparent text-red-600 hover:underline focus:ring-red-500 p-0 shadow-none', } }; // Size variants const sizes = { xs: 'px-2 py-1 text-xs', sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-5 py-2.5 text-lg', xl: 'px-6 py-3 text-xl' }; // Icon sizes based on button size const iconSizes = { xs: 'xs', sm: 'sm', md: 'md', lg: 'lg', xl: 'lg' }; // Get the appropriate variant classes const variantClasses = styleVariants[style][variant]; // Animation settings const animations = { solid: { hover: { scale: 1.02, y: -1 }, tap: { scale: 0.98 } }, outline: { hover: { scale: 1.02 }, tap: { scale: 0.98 } }, ghost: { hover: { scale: 1.02 }, tap: { scale: 0.98 } }, link: { hover: {}, tap: { scale: 0.98 } } }; return ( <motion.button className={cn( // Base styles 'rounded-lg font-medium inline-flex items-center justify-center gap-2 transition-colors', 'focus:outline-none focus:ring-2 focus:ring-offset-1', // Style and size variants variantClasses, sizes[size], // Full width option fullWidth && 'w-full', // Disabled state (loading || props.disabled) && 'opacity-60 cursor-not-allowed', // Custom classes className )} whileHover={!loading && !props.disabled ? animations[style].hover : {}} whileTap={!loading && !props.disabled ? animations[style].tap : {}} transition={{ type: "spring", stiffness: 400, damping: 25 }} disabled={loading || props.disabled} {...props} > {/* Loading spinner */} {loading && ( <svg className="animate-spin h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> )} {/* Left icon */} {icon && iconPosition === 'left' && !loading && ( <IconComponent icon={icon} size={iconSizes[size]} variant={style === 'solid' ? 'white' : variant} /> )} {/* Button text */} {children && <span>{children}</span>} {/* Right icon */} {icon && iconPosition === 'right' && !loading && ( <IconComponent icon={icon} size={iconSizes[size]} variant={style === 'solid' ? 'white' : variant} /> )} </motion.button> ); }; // For backward compatibility, export as GradientButton const GradientButton = Button; export default GradientButton; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript /** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], darkMode: 'class', theme: { extend: { fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], display: ['Inter', 'system-ui', 'sans-serif'], body: ['Inter', 'system-ui', 'sans-serif'], }, fontSize: { 'xs': ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.025em' }], 'sm': ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.025em' }], 'base': ['1rem', { lineHeight: '1.5rem', letterSpacing: '0.025em' }], 'lg': ['1.125rem', { lineHeight: '1.75rem', letterSpacing: '0.025em' }], 'xl': ['1.25rem', { lineHeight: '1.75rem', letterSpacing: '0.025em' }], '2xl': ['1.5rem', { lineHeight: '2rem', letterSpacing: '0.025em' }], '3xl': ['1.875rem', { lineHeight: '2.25rem', letterSpacing: '0.025em' }], '4xl': ['2.25rem', { lineHeight: '2.5rem', letterSpacing: '0.025em' }], '5xl': ['3rem', { lineHeight: '1', letterSpacing: '0.025em' }], '6xl': ['3.75rem', { lineHeight: '1', letterSpacing: '0.025em' }], '7xl': ['4.5rem', { lineHeight: '1', letterSpacing: '0.025em' }], '8xl': ['6rem', { lineHeight: '1', letterSpacing: '0.025em' }], '9xl': ['8rem', { lineHeight: '1', letterSpacing: '0.025em' }], }, fontWeight: { thin: '100', extralight: '200', light: '300', normal: '400', medium: '500', semibold: '600', bold: '700', extrabold: '800', black: '900', }, lineHeight: { 'tight': '1.25', 'snug': '1.375', 'normal': '1.5', 'relaxed': '1.625', 'loose': '2', }, letterSpacing: { 'tighter': '-0.05em', 'tight': '-0.025em', 'normal': '0em', 'wide': '0.025em', 'wider': '0.05em', 'widest': '0.1em', }, colors: { // Professional Light Color System - Matching Image Theme primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554', }, accent: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7c3aed', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764', }, // Professional grays gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712', }, // Status colors with professional styling - Matching Image success: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', }, warning: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', }, danger: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', }, info: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', }, // Button colors matching image theme button: { primary: '#86a0ff', // New custom blue for primary actions secondary: '#f3f4f6', // Light gray for secondary success: '#10b981', // Green for success danger: '#ef4444', // Red for danger warning: '#f59e0b', // Orange for warning light: '#f3f4f6', // Light gray for subtle actions } }, backgroundColor: { 'glass': 'rgba(255, 255, 255, 0.9)', 'glass-dark': 'rgba(248, 250, 252, 0.8)', }, backdropBlur: { 'xs': '2px', 'glass': '20px', }, boxShadow: { 'glass': '0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 6px rgba(0, 0, 0, 0.04)', 'glass-hover': '0 12px 32px rgba(0, 0, 0, 0.08), 0 4px 16px rgba(0, 0, 0, 0.06)', 'glow-blue': '0 0 24px rgba(59, 130, 246, 0.15)', 'glow-purple': '0 0 24px rgba(168, 85, 247, 0.15)', 'glow-green': '0 0 24px rgba(16, 185, 129, 0.15)', 'glow-red': '0 0 24px rgba(239, 68, 68, 0.15)', }, animation: { 'fade-in': 'fadeIn 0.5s ease-in-out', 'slide-up': 'slideUp 0.3s ease-out', 'pulse-glow': 'pulseGlow 2s ease-in-out infinite alternate', 'float': 'float 3s ease-in-out infinite', }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' }, }, slideUp: { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' }, }, pulseGlow: { '0%': { boxShadow: '0 0 5px rgba(59, 130, 246, 0.2)' }, '100%': { boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)' }, }, float: { '0%, 100%': { transform: 'translateY(0px)' }, '50%': { transform: 'translateY(-4px)' }, }, }, borderRadius: { 'xl': '12px', '2xl': '16px', '3xl': '24px', }, }, }, plugins: [ require('@tailwindcss/forms'), ], } ``` -------------------------------------------------------------------------------- /tests/mcpTestingGuide.md: -------------------------------------------------------------------------------- ```markdown # HANA MCP Server Testing Guide This guide shows you how to test your HANA MCP server using different methods, including MCP Inspector. ## 🎯 Testing Methods ### 1. **MCP Inspector (Recommended)** MCP Inspector is the official tool for testing MCP servers. Here's how to use it: #### Installation ```bash # Install MCP Inspector globally npm install -g @modelcontextprotocol/inspector # Or install locally npm install @modelcontextprotocol/inspector ``` #### Usage ```bash # Start MCP Inspector with your server mcp-inspector --config mcp-inspector-config.json # Or run directly with command mcp-inspector --command "/opt/homebrew/bin/node" --args "hana-mcp-server.js" --env-file .env ``` ### 2. **Manual Testing Scripts** We've created custom testing scripts for your convenience: #### Automated Test Suite ```bash # Run all tests automatically /opt/homebrew/bin/node test-mcp-inspector.js ``` #### Interactive Manual Tester ```bash # Interactive menu for testing individual tools /opt/homebrew/bin/node manual-test.js ``` ### 3. **Command Line Testing** Test individual commands manually: ```bash # Test initialization echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /opt/homebrew/bin/node hana-mcp-server.js # Test tools listing echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | /opt/homebrew/bin/node hana-mcp-server.js # Test a specific tool echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hana_show_config","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js ``` ## 🔧 Configuration Files ### MCP Inspector Config (`mcp-inspector-config.json`) ```json { "mcpServers": { "HANA Database": { "command": "/opt/homebrew/bin/node", "args": [ "/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js" ], "env": { "HANA_HOST": "your-hana-host.com", "HANA_PORT": "443", "HANA_USER": "your-username", "HANA_PASSWORD": "your-password", "HANA_SCHEMA": "your-schema", "HANA_SSL": "true", "HANA_ENCRYPT": "true", "HANA_VALIDATE_CERT": "true" } } } } ``` ### Environment File (`.env`) ```bash HANA_HOST=your-hana-host.com HANA_PORT=443 HANA_USER=your-username HANA_PASSWORD=your-password HANA_SCHEMA=your-schema HANA_SSL=true HANA_ENCRYPT=true HANA_VALIDATE_CERT=true ``` ## 🧪 Test Scenarios ### Basic Functionality Tests 1. **Server Initialization** - Verify server starts correctly 2. **Tools Discovery** - Check all 9 tools are available 3. **Configuration Display** - Show HANA connection details 4. **Connection Test** - Verify HANA database connectivity ### Database Operation Tests 1. **Schema Listing** - List all available schemas 2. **Table Discovery** - List tables in a specific schema 3. **Table Structure** - Describe table columns and types 4. **Index Information** - List and describe indexes 5. **Query Execution** - Run custom SQL queries ### Error Handling Tests 1. **Missing Parameters** - Test required parameter validation 2. **Invalid Credentials** - Test connection failure handling 3. **Invalid Queries** - Test SQL error handling 4. **Missing Tables/Schemas** - Test not found scenarios ## 📋 Expected Test Results ### Successful Initialization ```json { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "serverInfo": { "name": "HANA MCP Server", "version": "1.0.0" } } } ``` ### Tools List Response ```json { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "hana_show_config", "description": "Show the HANA database configuration", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, // ... 8 more tools ] } } ``` ### Tool Execution Response ```json { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "📋 Available schemas in HANA database:\n\n- SCHEMA1\n- SCHEMA2\n..." } ] } } ``` ## 🚨 Troubleshooting ### Common Issues 1. **"HANA client not connected"** - Check environment variables are set correctly - Verify HANA credentials are valid - Ensure network connectivity to HANA host 2. **"Tool not found"** - Verify tool name spelling - Check tools/list response includes the tool - Ensure server is properly initialized 3. **"Missing required parameters"** - Check tool documentation for required parameters - Verify parameter names match exactly - Ensure parameters are in correct format 4. **"Parse error"** - Verify JSON-RPC format is correct - Check for extra/missing commas in JSON - Ensure proper escaping of special characters ### Debug Mode Enable debug logging by setting environment variables: ```bash export LOG_LEVEL=debug export ENABLE_FILE_LOGGING=true ``` ## 📊 Performance Testing ### Load Testing ```bash # Test multiple concurrent requests for i in {1..10}; do echo '{"jsonrpc":"2.0","id":'$i',"method":"tools/call","params":{"name":"hana_list_schemas","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js & done wait ``` ### Response Time Testing ```bash # Measure response time time echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hana_list_schemas","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js ``` ## 🔄 Continuous Testing ### Automated Test Script Create a CI/CD pipeline script: ```bash #!/bin/bash set -e echo "🧪 Running HANA MCP Server Tests..." # Start server node hana-mcp-server.js & SERVER_PID=$! # Wait for server to start sleep 2 # Run tests node test-mcp-inspector.js # Cleanup kill $SERVER_PID echo "✅ All tests passed!" ``` ## 📝 Test Checklist - [ ] Server initializes correctly - [ ] All 9 tools are discoverable - [ ] Configuration tool shows correct settings - [ ] Connection test passes - [ ] Schema listing works - [ ] Table listing works - [ ] Table description works - [ ] Index listing works - [ ] Query execution works - [ ] Error handling works correctly - [ ] Performance is acceptable - [ ] No memory leaks detected ## 🎉 Success Criteria Your HANA MCP server is ready for production when: - All tests pass consistently - Response times are under 5 seconds - Error handling is robust - Documentation is complete - Integration with Claude Desktop works seamlessly ``` -------------------------------------------------------------------------------- /src/utils/formatters.js: -------------------------------------------------------------------------------- ```javascript /** * Response formatting utilities for HANA MCP Server */ const { logger } = require('./logger'); class Formatters { /** * Create a standard MCP tool response */ static createResponse(text, type = 'text') { return { content: [ { type, text } ] }; } /** * Create an error response */ static createErrorResponse(message, details = '') { const text = details ? `${message}\n\n${details}` : message; return this.createResponse(`❌ ${text}`); } /** * Create a success response */ static createSuccessResponse(message, details = '') { const text = details ? `${message}\n\n${details}` : message; return this.createResponse(`✅ ${text}`); } /** * Format configuration display */ static formatConfig(config) { const lines = [ 'HANA Configuration:', '', `Host: ${config.host}`, `Port: ${config.port}`, `User: ${config.user}`, `Password: ${config.password}`, `Schema: ${config.schema}`, `SSL: ${config.ssl}`, '' ]; const status = (config.host !== 'NOT SET' && config.user !== 'NOT SET' && config.password !== 'NOT SET') ? 'PROPERLY CONFIGURED' : 'MISSING REQUIRED VALUES'; lines.push(`Status: ${status}`); return lines.join('\n'); } /** * Format environment variables display */ static formatEnvironmentVars(envVars) { const lines = [ '🔧 Environment Variables:', '' ]; for (const [key, value] of Object.entries(envVars)) { lines.push(`${key}: ${value}`); } lines.push(''); lines.push('Mode: Real HANA Connection'); return lines.join('\n'); } /** * Format schema list */ static formatSchemaList(schemas) { const lines = [ '📋 Available schemas in HANA database:', '' ]; schemas.forEach(schema => { lines.push(`- ${schema}`); }); lines.push(''); lines.push(`Total schemas: ${schemas.length}`); return lines.join('\n'); } /** * Format table list */ static formatTableList(tables, schemaName) { const lines = [ `📋 Tables in schema '${schemaName}':`, '' ]; tables.forEach(table => { lines.push(`- ${table}`); }); lines.push(''); lines.push(`Total tables: ${tables.length}`); return lines.join('\n'); } /** * Format table structure */ static formatTableStructure(columns, schemaName, tableName) { const lines = [ `📋 Table structure for '${schemaName}.${tableName}':`, '' ]; if (columns.length === 0) { lines.push('No columns found.'); return lines.join('\n'); } // Create header const header = 'Column Name | Data Type | Length | Nullable | Default | Description'; const separator = '---------------------|--------------|--------|----------|---------|-------------'; lines.push(header); lines.push(separator); // Add columns columns.forEach(col => { const nullable = col.IS_NULLABLE === 'TRUE' ? 'YES' : 'NO'; const defaultValue = col.DEFAULT_VALUE || '-'; const description = col.COMMENTS || '-'; const dataType = col.DATA_TYPE_NAME + (col.LENGTH ? `(${col.LENGTH})` : '') + (col.SCALE ? `,${col.SCALE}` : ''); const line = `${col.COLUMN_NAME.padEnd(20)} | ${dataType.padEnd(12)} | ${(col.LENGTH || '-').toString().padEnd(6)} | ${nullable.padEnd(8)} | ${defaultValue.padEnd(8)} | ${description}`; lines.push(line); }); lines.push(''); lines.push(`Total columns: ${columns.length}`); return lines.join('\n'); } /** * Format index list */ static formatIndexList(indexMap, schemaName, tableName) { const lines = [ `📋 Indexes for table '${schemaName}.${tableName}':`, '' ]; if (Object.keys(indexMap).length === 0) { lines.push('No indexes found.'); return lines.join('\n'); } Object.entries(indexMap).forEach(([indexName, index]) => { const type = index.isUnique ? 'Unique' : index.type; const columns = index.columns.join(', '); lines.push(`- ${indexName} (${type}) - Columns: ${columns}`); }); lines.push(''); lines.push(`Total indexes: ${Object.keys(indexMap).length}`); return lines.join('\n'); } /** * Format index details */ static formatIndexDetails(results, schemaName, tableName, indexName) { if (results.length === 0) { return `❌ Index '${schemaName}.${tableName}.${indexName}' not found.`; } const indexInfo = results[0]; const columns = results.map(row => `${row.COLUMN_NAME} (${row.ORDER || 'ASC'})`).join(', '); const lines = [ `📋 Index details for '${schemaName}.${tableName}.${indexName}':`, '', `Index Name: ${indexInfo.INDEX_NAME}`, `Table: ${schemaName}.${tableName}`, `Type: ${indexInfo.INDEX_TYPE}`, `Unique: ${indexInfo.IS_UNIQUE === 'TRUE' ? 'Yes' : 'No'}`, `Columns: ${columns}`, `Total columns: ${results.length}` ]; return lines.join('\n'); } /** * Format query results as table */ static formatQueryResults(results, query) { const lines = [ '🔍 Query executed successfully:', '', `Query: ${query}`, '' ]; if (results.length === 0) { lines.push('Query executed successfully but returned no results.'); return lines.join('\n'); } // Format as markdown table const columns = Object.keys(results[0]); const header = `| ${columns.join(' | ')} |`; const separator = `| ${columns.map(() => '---').join(' | ')} |`; const rows = results.map(row => `| ${columns.map(col => String(row[col] || '')).join(' | ')} |` ).join('\n'); lines.push(`Results (${results.length} rows):`); lines.push(header); lines.push(separator); lines.push(rows); return lines.join('\n'); } /** * Format connection test result */ static formatConnectionTest(config, success, error = null, testResult = null) { if (!success) { const lines = [ '❌ Connection test failed!', '' ]; if (error) { lines.push(`Error: ${error}`); lines.push(''); } lines.push('Please check your HANA database configuration and ensure the database is accessible.'); lines.push(''); lines.push('Configuration:'); lines.push(`- Host: ${config.host}`); lines.push(`- Port: ${config.port}`); lines.push(`- User: ${config.user}`); lines.push(`- Schema: ${config.schema || 'default'}`); lines.push(`- SSL: ${config.ssl ? 'enabled' : 'disabled'}`); return lines.join('\n'); } const lines = [ '✅ Connection test successful!', '', 'Configuration looks good:', `- Host: ${config.host}`, `- Port: ${config.port}`, `- User: ${config.user}`, `- Schema: ${config.schema || 'default'}`, `- SSL: ${config.ssl ? 'enabled' : 'disabled'}` ]; if (testResult) { lines.push(''); lines.push(`Test query result: ${testResult}`); } return lines.join('\n'); } } module.exports = Formatters; ```