This is page 1 of 8. Use http://codebase.md/sammcj/bybit-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .gitignore ├── client │ ├── .env.example │ ├── .gitignore │ ├── package.json │ ├── pnpm-lock.yaml │ ├── README.md │ ├── src │ │ ├── cli.ts │ │ ├── client.ts │ │ ├── config.ts │ │ ├── env.ts │ │ ├── index.ts │ │ └── launch.ts │ └── tsconfig.json ├── DEV_PLAN.md ├── docs │ └── HTTP_SERVER.md ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── README.md ├── specs │ ├── bybit │ │ ├── bybit-api-v5-openapi.yaml │ │ └── bybit-api-v5-postman-collection.json │ ├── mcp │ │ ├── mcp-schema.json │ │ └── mcp-schema.ts │ └── README.md ├── src │ ├── __tests__ │ │ ├── GetMLRSI.test.ts │ │ ├── integration.test.ts │ │ ├── test-setup.ts │ │ └── tools.test.ts │ ├── constants.ts │ ├── env.ts │ ├── httpServer.ts │ ├── index.ts │ ├── tools │ │ ├── BaseTool.ts │ │ ├── GetInstrumentInfo.ts │ │ ├── GetKline.ts │ │ ├── GetMarketInfo.ts │ │ ├── GetMarketStructure.ts │ │ ├── GetMLRSI.ts │ │ ├── GetOrderBlocks.ts │ │ ├── GetOrderbook.ts │ │ ├── GetOrderHistory.ts │ │ ├── GetPositions.ts │ │ ├── GetTicker.ts │ │ ├── GetTrades.ts │ │ └── GetWalletBalance.ts │ └── utils │ ├── knnAlgorithm.ts │ ├── mathUtils.ts │ ├── toolLoader.ts │ └── volumeAnalysis.ts ├── tsconfig.json └── webui ├── .dockerignore ├── .env.example ├── build-docker.sh ├── docker-compose.yml ├── docker-entrypoint.sh ├── docker-healthcheck.sh ├── DOCKER.md ├── Dockerfile ├── index.html ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public │ ├── favicon.svg │ └── inter.woff2 ├── README.md ├── screenshot.png ├── src │ ├── assets │ │ └── fonts │ │ └── fonts.css │ ├── components │ │ ├── AgentDashboard.ts │ │ ├── chat │ │ │ ├── DataCard.ts │ │ │ └── MessageRenderer.ts │ │ ├── ChatApp.ts │ │ ├── DataVerificationPanel.ts │ │ ├── DebugConsole.ts │ │ └── ToolsManager.ts │ ├── main.ts │ ├── services │ │ ├── agentConfig.ts │ │ ├── agentMemory.ts │ │ ├── aiClient.ts │ │ ├── citationProcessor.ts │ │ ├── citationStore.ts │ │ ├── configService.ts │ │ ├── logService.ts │ │ ├── mcpClient.ts │ │ ├── multiStepAgent.ts │ │ ├── performanceOptimiser.ts │ │ └── systemPrompt.ts │ ├── styles │ │ ├── agent-dashboard.css │ │ ├── base.css │ │ ├── citations.css │ │ ├── components.css │ │ ├── data-cards.css │ │ ├── main.css │ │ ├── processing.css │ │ ├── variables.css │ │ └── verification-panel.css │ ├── types │ │ ├── agent.ts │ │ ├── ai.ts │ │ ├── citation.ts │ │ ├── mcp.ts │ │ └── workflow.ts │ └── utils │ ├── dataDetection.ts │ └── formatters.ts ├── tsconfig.json └── vite.config.ts ``` # Files -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- ``` 1 | # Ollama configuration 2 | OLLAMA_HOST=http://localhost:11434 3 | DEFAULT_MODEL=qwen3-30b-a3b-ud-nothink-128k:q4_k_xl 4 | 5 | # Debug mode 6 | DEBUG=false 7 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Ollama configuration 2 | OLLAMA_HOST=http://localhost:11434 3 | DEFAULT_MODEL=qwen3-30b-a3b-ud-nothink-128k:q4_k_xl 4 | 5 | # Bybit API Configuration 6 | BYBIT_API_KEY=your_api_key_here 7 | BYBIT_API_SECRET=your_api_secret_here 8 | BYBIT_USE_TESTNET=false 9 | 10 | # Debug Mode 11 | DEBUG=false 12 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | .pnpm-store/ 4 | 5 | # Build output 6 | build/ 7 | dist/ 8 | 9 | # Environment variables 10 | .env 11 | .env.* 12 | 13 | # IDE files 14 | .vscode/ 15 | .idea/ 16 | *.swp 17 | *.swo 18 | 19 | # Logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | 26 | # Operating System 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Test coverage 31 | coverage/ 32 | 33 | # TypeScript 34 | *.tsbuildinfo 35 | 36 | !.env.example 37 | 38 | # pinescripts 39 | # DEV_PLAN.md 40 | ``` -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Build output 2 | build/ 3 | dist/ 4 | 5 | # Dependencies 6 | node_modules/ 7 | 8 | # Logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea/ 16 | .vscode/ 17 | *.swp 18 | *.swo 19 | *~ 20 | 21 | # OS generated files 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | ehthumbs.db 28 | Thumbs.db 29 | 30 | # Environment variables 31 | .env 32 | !.env.example 33 | 34 | # Config files 35 | .config/ 36 | 37 | # Cache files 38 | .install-check 39 | ``` -------------------------------------------------------------------------------- /webui/.env.example: -------------------------------------------------------------------------------- ``` 1 | # ============================================================================== 2 | # Bybit MCP WebUI - Environment Variables 3 | # ============================================================================== 4 | # Copy this file to .env and configure for your environment 5 | # ============================================================================== 6 | 7 | # Ollama Configuration 8 | # URL of your Ollama instance (supports custom domains) 9 | OLLAMA_HOST=https://ollama.my.network 10 | 11 | # MCP Server Configuration 12 | # Leave empty to use current origin (recommended for Docker/Traefik) 13 | # Set to specific URL only if MCP server is on different domain 14 | MCP_ENDPOINT= 15 | 16 | # Development overrides (only used in development mode) 17 | # MCP_ENDPOINT=http://localhost:8080 18 | # OLLAMA_HOST=http://localhost:11434 19 | ``` -------------------------------------------------------------------------------- /webui/.dockerignore: -------------------------------------------------------------------------------- ``` 1 | # ============================================================================== 2 | # Docker Ignore File for Bybit MCP WebUI 3 | # ============================================================================== 4 | # This file excludes unnecessary files from the Docker build context 5 | # to reduce build time and final image size. 6 | # ============================================================================== 7 | 8 | # Development files 9 | .env 10 | .env.local 11 | .env.development 12 | .env.test 13 | .env.production 14 | 15 | # Node.js 16 | node_modules/ 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | pnpm-debug.log* 21 | lerna-debug.log* 22 | 23 | # Build outputs (will be copied from builder stage) 24 | dist/ 25 | build/ 26 | .next/ 27 | out/ 28 | 29 | # IDE and editor files 30 | .vscode/ 31 | .idea/ 32 | *.swp 33 | *.swo 34 | *~ 35 | 36 | # OS generated files 37 | .DS_Store 38 | .DS_Store? 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | # Git 46 | .git/ 47 | .gitignore 48 | .gitattributes 49 | 50 | # Documentation 51 | README.md 52 | CHANGELOG.md 53 | LICENSE 54 | *.md 55 | 56 | # Testing 57 | coverage/ 58 | .nyc_output/ 59 | .jest/ 60 | test-results/ 61 | playwright-report/ 62 | 63 | # Logs 64 | logs/ 65 | *.log 66 | 67 | # Runtime data 68 | pids/ 69 | *.pid 70 | *.seed 71 | *.pid.lock 72 | 73 | # Coverage directory used by tools like istanbul 74 | coverage/ 75 | *.lcov 76 | 77 | # ESLint cache 78 | .eslintcache 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Microbundle cache 90 | .rpt2_cache/ 91 | .rts2_cache_cjs/ 92 | .rts2_cache_es/ 93 | .rts2_cache_umd/ 94 | 95 | # Optional REPL history 96 | .node_repl_history 97 | 98 | # Output of 'npm pack' 99 | *.tgz 100 | 101 | # Yarn Integrity file 102 | .yarn-integrity 103 | 104 | # parcel-bundler cache (https://parceljs.org/) 105 | .cache 106 | .parcel-cache 107 | 108 | # Storybook build outputs 109 | .out 110 | .storybook-out 111 | 112 | # Temporary folders 113 | tmp/ 114 | temp/ 115 | 116 | # Docker files (avoid recursive copying) 117 | Dockerfile* 118 | .dockerignore 119 | docker-compose*.yml 120 | 121 | # Keep entrypoint scripts (they're copied explicitly) 122 | # docker-entrypoint.sh 123 | # docker-healthcheck.sh 124 | 125 | # CI/CD 126 | .github/ 127 | .gitlab-ci.yml 128 | .travis.yml 129 | .circleci/ 130 | 131 | # Package manager lock files (will be copied explicitly) 132 | # Commented out as we need these for dependency installation 133 | # package-lock.json 134 | # yarn.lock 135 | # pnpm-lock.yaml 136 | 137 | # Development tools 138 | .prettierrc* 139 | .eslintrc* 140 | tsconfig.json 141 | vite.config.ts 142 | jest.config.* 143 | vitest.config.* 144 | 145 | # Backup files 146 | *.bak 147 | *.backup 148 | *.old 149 | 150 | # Local development 151 | .local/ 152 | .cache/ 153 | 154 | # Webpack 155 | .webpack/ 156 | 157 | # Vite 158 | .vite/ 159 | 160 | # Turbo 161 | .turbo/ 162 | ``` -------------------------------------------------------------------------------- /specs/README.md: -------------------------------------------------------------------------------- ```markdown 1 | NOTE: Files in this directory are used for reference only and not by the client or server. 2 | ``` -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bybit MCP Client 2 | 3 | A TypeScript client for interacting with Ollama LLMs and the bybit-mcp server. This client provides a command-line interface for easy access to both Ollama's language models and bybit-mcp's trading tools. 4 | 5 | ## Quick Start 6 | 7 | 1. Clone the repository and navigate to the client directory: 8 | 9 | ```bash 10 | cd client 11 | ``` 12 | 13 | 2. Copy the example environment file and configure as needed: 14 | 15 | ```bash 16 | cp .env.example .env 17 | ``` 18 | 19 | 3. Start the interactive chat: 20 | 21 | ```bash 22 | pnpm start 23 | ``` 24 | 25 | This will automatically: 26 | 27 | - Check and install dependencies if needed 28 | - Validate the environment configuration 29 | - Verify Ollama connection 30 | - Start the integrated chat interface 31 | 32 | ## Environment Configuration 33 | 34 | The client uses environment variables for configuration. You can set these in your `.env` file: 35 | 36 | ```bash 37 | # Ollama configuration 38 | OLLAMA_HOST=http://localhost:11434 39 | DEFAULT_MODEL=qwen3-30b-a3b-ud-nothink-128k:q4_k_xl 40 | 41 | # Debug mode 42 | DEBUG=false 43 | ``` 44 | 45 | For the bybit-mcp server configuration (when using integrated mode), the following environment variables are required: 46 | 47 | ```bash 48 | # Bybit API Configuration (required for integrated mode) 49 | BYBIT_API_KEY=your_api_key_here 50 | BYBIT_API_SECRET=your_api_secret_here 51 | BYBIT_USE_TESTNET=true # optional, defaults to false 52 | ``` 53 | 54 | Environment variables take precedence over stored configuration. 55 | 56 | ## Installation 57 | 58 | ### Locally 59 | 60 | ```bash 61 | pnpm i 62 | ``` 63 | 64 | ### NPM 65 | 66 | **Note: This will only be available if I decide to publish the package to npm.** 67 | 68 | ```bash 69 | pnpm install @bybit-mcp/client 70 | ``` 71 | 72 | ## Usage 73 | 74 | The client provides several commands for interacting with Ollama models and bybit-mcp tools. It can run in two modes: 75 | 76 | 1. Integrated mode: Automatically manages the bybit-mcp server 77 | 2. Standalone mode: Connects to an externally managed server 78 | 79 | ### Quick Launch 80 | 81 | The fastest way to start chatting: 82 | ```bash 83 | pnpm start # or 84 | pnpm chat # or 85 | node build/launch.js 86 | ``` 87 | 88 | ### Global Options 89 | 90 | These options can be used with any command: 91 | 92 | ```bash 93 | # Run in integrated mode (auto-manages server) 94 | bybit-mcp-client --integrated [command] 95 | 96 | # Enable debug logging 97 | bybit-mcp-client --debug [command] 98 | 99 | # Use testnet (for integrated mode) 100 | bybit-mcp-client --testnet [command] 101 | ``` 102 | 103 | ### List Available Models 104 | 105 | View all available Ollama models: 106 | 107 | ```bash 108 | bybit-mcp-client models 109 | ``` 110 | 111 | ### List Available Tools 112 | 113 | View all available bybit-mcp tools: 114 | 115 | ```bash 116 | # Integrated mode (auto-manages server) 117 | bybit-mcp-client --integrated tools 118 | 119 | # Standalone mode (external server) 120 | bybit-mcp-client tools "node /path/to/bybit-mcp/build/index.js" 121 | ``` 122 | 123 | ### Chat with a Model 124 | 125 | Start an interactive chat session with an Ollama model: 126 | 127 | ```bash 128 | # Use default model 129 | bybit-mcp-client chat 130 | 131 | # Specify a model 132 | bybit-mcp-client chat codellama 133 | 134 | # Add a system message for context 135 | bybit-mcp-client chat qwen3-30b-a3b-ud-nothink-128k:q4_k_xl --system "You are a helpful assistant" 136 | ``` 137 | 138 | ### Call a Tool 139 | 140 | Execute a bybit-mcp tool with arguments: 141 | 142 | ```bash 143 | # Integrated mode 144 | bybit-mcp-client --integrated tool get_ticker symbol=BTCUSDT 145 | 146 | # Standalone mode 147 | bybit-mcp-client tool "node /path/to/bybit-mcp/build/index.js" get_ticker symbol=BTCUSDT 148 | 149 | # With optional category parameter 150 | bybit-mcp-client --integrated tool get_ticker symbol=BTCUSDT category=linear 151 | ``` 152 | 153 | ## API Usage 154 | 155 | You can also use the client programmatically in your TypeScript/JavaScript applications: 156 | 157 | ```typescript 158 | import { BybitMcpClient, Config } from '@bybit-mcp/client'; 159 | 160 | const config = new Config(); 161 | const client = new BybitMcpClient(config); 162 | 163 | // Using integrated mode (auto-manages server) 164 | await client.startIntegratedServer(); 165 | 166 | // Or connect to external server 167 | // await client.connectToServer('node /path/to/bybit-mcp/build/index.js'); 168 | 169 | // Chat with a model 170 | const messages = [ 171 | { role: 'user', content: 'Hello, how are you?' } 172 | ]; 173 | const response = await client.chat('qwen3-30b-a3b-ud-nothink-128k:q4_k_xl', messages); 174 | console.log(response); 175 | 176 | // Call a bybit-mcp tool 177 | const result = await client.callTool('get_ticker', { symbol: 'BTCUSDT' }); 178 | console.log(result); 179 | 180 | // Clean up (this will also stop the integrated server if running) 181 | await client.close(); 182 | ``` 183 | 184 | ## Features 185 | 186 | - Quick start script for instant setup 187 | - Environment-based configuration 188 | - Integrated mode for automatic server management 189 | - Interactive chat with Ollama models 190 | - Streaming chat responses 191 | - Easy access to bybit-mcp trading tools 192 | - Configurable settings with persistent storage 193 | - Debug logging support 194 | - TypeScript support 195 | - Command-line interface 196 | - Programmatic API 197 | 198 | ## Requirements 199 | 200 | - Node.js >= 20 201 | - Ollama running locally or remotely 202 | - bybit-mcp server (automatically managed in integrated mode) 203 | 204 | ## Available Tools 205 | 206 | For a complete list of available trading tools and their parameters, see the [main README](../README.md#tool-documentation). 207 | ``` -------------------------------------------------------------------------------- /webui/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bybit MCP WebUI 2 | 3 | A modern, lightweight web interface for the Bybit MCP (Model Context Protocol) server with AI-powered chat capabilities. 4 | 5 |  6 | 7 | ## Features 8 | 9 | - 🤖 **AI-Powered Chat**: Interactive chat interface with OpenAI-compatible API support (Ollama, etc.) 10 | - 📊 **Real-Time Charts**: Interactive candlestick charts with volume using Lightweight Charts 11 | - 🔧 **MCP Integration**: Direct access to all 12 Bybit MCP tools including advanced technical analysis 12 | - 🧠 **Advanced Analysis**: ML-enhanced RSI, Order Block detection, and Market Structure analysis 13 | - 🎨 **Modern UI**: Clean, responsive design with dark/light theme support 14 | - ⚡ **Fast & Lightweight**: Built with Vite + Vanilla TypeScript for optimal performance 15 | 16 | ## Technology Stack 17 | 18 | - pnpm: Package manager 19 | - **Frontend**: Vite + Vanilla TypeScript 20 | - **Charts**: Lightweight Charts (TradingView) + Chart.js 21 | - **Styling**: CSS3 with CSS Variables 22 | - **AI Integration**: OpenAI-compatible API client with streaming support 23 | - **MCP Integration**: Direct HTTP client with TypeScript types 24 | 25 | ## Quick Start 26 | 27 | ### Prerequisites 28 | 29 | - Node.js 22+ 30 | - Running Bybit MCP server (see main project README) 31 | - AI service (Ollama recommended) running locally or remotely 32 | 33 | ### Installation 34 | 35 | 1. Install dependencies: 36 | 37 | ```bash 38 | pnpm install 39 | ``` 40 | 41 | 2. Start MCP server (in terminal 1): 42 | 43 | ```bash 44 | cd .. && node build/httpServer.js 45 | ``` 46 | 47 | 3. Start WebUI development server (in terminal 2): 48 | 49 | ```bash 50 | pnpm dev 51 | ``` 52 | 53 | 4. Open your browser to `http://localhost:3000` 54 | 55 | **Alternative**: Use the experimental concurrent setup: 56 | ```bash 57 | pnpm dev:full 58 | ``` 59 | 60 | ### Production Build 61 | 62 | ```bash 63 | pnpm build 64 | pnpm preview 65 | ``` 66 | 67 | ## Configuration 68 | 69 | The WebUI can be configured through the settings modal (⚙️ icon) or by modifying the default configuration in the code. 70 | 71 | ### AI Configuration 72 | 73 | - **Endpoint**: URL of your AI service (default: `http://localhost:11434`) 74 | - **Model**: Model name to use (default: `qwen3-30b-a3b-ud-nothink-128k:q4_k_xl`) 75 | - **Temperature**: Response creativity (0.0 - 1.0) 76 | - **Max Tokens**: Maximum response length 77 | 78 | ### MCP Configuration 79 | 80 | - **Endpoint**: URL of your Bybit MCP server (default: `http://localhost:8080`) 81 | - **Timeout**: Request timeout in milliseconds 82 | 83 | ## Available Views 84 | 85 | ### 💬 AI Chat 86 | 87 | - Interactive chat with AI assistant 88 | - Streaming responses 89 | - Example queries for quick start 90 | - Connection status indicators 91 | 92 | ### 📈 Charts 93 | 94 | - Real-time price charts 95 | - Volume indicators 96 | - Multiple timeframes 97 | - Symbol selection 98 | 99 | ### 🔧 MCP Tools 100 | 101 | - Direct access to all MCP tools 102 | - Tool parameter configuration 103 | - Response formatting 104 | 105 | ### 🧠 Analysis 106 | 107 | - ML-enhanced RSI visualisation 108 | - Order block overlays 109 | - Market structure analysis 110 | - Trading recommendations 111 | 112 | ## MCP Tools Integration 113 | 114 | The WebUI provides access to all Bybit MCP server tools: 115 | 116 | ### Market Data 117 | 118 | - `get_ticker` - Real-time price data 119 | - `get_kline` - Candlestick/OHLCV data 120 | - `get_orderbook` - Market depth data 121 | - `get_trades` - Recent trades 122 | - `get_market_info` - Market information 123 | - `get_instrument_info` - Instrument details 124 | 125 | ### Account Data 126 | 127 | - `get_wallet_balance` - Wallet balances 128 | - `get_positions` - Current positions 129 | - `get_order_history` - Order history 130 | 131 | ### Advanced Analysis 132 | 133 | - `get_ml_rsi` - ML-enhanced RSI with adaptive thresholds 134 | - `get_order_blocks` - Institutional order accumulation zones 135 | - `get_market_structure` - Comprehensive market analysis 136 | 137 | ## Keyboard Shortcuts 138 | 139 | - `Ctrl/Cmd + K` - Focus chat input 140 | - `Enter` - Send message 141 | - `Shift + Enter` - New line in chat 142 | - `Escape` - Close modals 143 | 144 | ## Themes 145 | 146 | The WebUI supports three theme modes: 147 | 148 | - **Light**: Clean, bright interface 149 | - **Dark**: Easy on the eyes for extended use 150 | - **Auto**: Follows system preference 151 | 152 | ## Browser Support 153 | 154 | - Chrome 90+ 155 | - Firefox 88+ 156 | - Safari 14+ 157 | - Edge 90+ 158 | 159 | ## Development 160 | 161 | ### Project Structure 162 | 163 | ``` 164 | src/ 165 | ├── components/ # UI components 166 | │ ├── ChatApp.ts # Main chat interface 167 | │ └── ChartComponent.ts # Chart wrapper 168 | ├── services/ # Core services 169 | │ ├── aiClient.ts # AI API integration 170 | │ ├── mcpClient.ts # MCP server client 171 | │ └── configService.ts # Configuration management 172 | ├── styles/ # CSS architecture 173 | │ ├── variables.css # CSS custom properties 174 | │ ├── base.css # Base styles and reset 175 | │ ├── components.css # Component styles 176 | │ └── main.css # Main stylesheet 177 | ├── types/ # TypeScript definitions 178 | │ ├── ai.ts # AI service types 179 | │ ├── mcp.ts # MCP protocol types 180 | │ └── charts.ts # Chart data types 181 | ├── utils/ # Utility functions 182 | │ └── formatters.ts # Data formatting helpers 183 | └── main.ts # Application entry point 184 | ``` 185 | 186 | ### Adding New Features 187 | 188 | 1. **New MCP Tool**: Add types to `src/types/mcp.ts` and update the client 189 | 2. **New Chart Type**: Extend `ChartComponent.ts` with new series types 190 | 3. **New AI Feature**: Update `aiClient.ts` and chat interface 191 | 4. **New Theme**: Modify CSS variables in `src/styles/variables.css` 192 | 193 | ### Code Style 194 | 195 | - Use TypeScript strict mode 196 | - Follow functional programming principles where possible 197 | - Implement comprehensive error handling 198 | - Add JSDoc comments for public APIs 199 | - Use consistent naming conventions 200 | 201 | ## Performance 202 | 203 | The WebUI is optimised for performance: 204 | 205 | - **Minimal Bundle**: Vanilla TypeScript with selective imports 206 | - **Efficient Charts**: Lightweight Charts for optimal rendering 207 | - **Smart Caching**: Configuration and data caching 208 | - **Lazy Loading**: Components loaded on demand 209 | - **Streaming**: Real-time AI responses without blocking 210 | 211 | ## Security 212 | 213 | - **No API Keys in Frontend**: All sensitive data handled by backend services 214 | - **CORS Protection**: Proper cross-origin request handling 215 | - **Input Validation**: Client-side validation for all user inputs 216 | - **Secure Defaults**: Safe configuration defaults 217 | 218 | ## Troubleshooting 219 | 220 | ### Common Issues 221 | 222 | 1. **AI Not Responding**: Check Ollama is running and accessible 223 | 2. **MCP Tools Failing**: Verify Bybit MCP server is running 224 | 3. **Charts Not Loading**: Check browser console for errors 225 | 4. **Theme Not Applying**: Clear browser cache and reload 226 | 227 | ### Debug Mode 228 | 229 | Enable debug logging by opening browser console and running: 230 | ```javascript 231 | localStorage.setItem('debug', 'true'); 232 | location.reload(); 233 | ``` 234 | 235 | ## License 236 | 237 | MIT License - see the main project LICENSE file for details. 238 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bybit MCP Server 2 | 3 | A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that provides read-only access to Bybit's cryptocurrency exchange API. 4 | 5 | **THIS IS ALPHA QUALITY SOFTWARE - USE AT YOUR OWN RISK!** 6 | 7 | Only ever use a read-only API key with this server. I wouldn't trust my code with your "money" and neither should you! 8 | 9 | ## Features 10 | 11 | This MCP server provides the following tools for interacting with Bybit's API: 12 | 13 | - `get_ticker`: Get real-time ticker information for a trading pair 14 | - `get_orderbook`: Get orderbook (market depth) data for a trading pair 15 | - `get_kline`: Get kline/candlestick data for a trading pair 16 | - `get_market_info`: Get detailed market information for trading pairs 17 | - `get_trades`: Get recent trades for a trading pair 18 | - `get_instrument_info`: Get detailed instrument information for a specific trading pair 19 | - `get_wallet_balance`: Get wallet balance information for the authenticated user 20 | - `get_positions`: Get current positions information for the authenticated user 21 | - `get_order_history`: Get order history for the authenticated user 22 | - `get_ml_rsi`: Get machine learning-based RSI (Relative Strength Index) for a trading pair 23 | - `get_market_structure`: Get market structure information for a trading pair 24 | - `get_order_blocks`: Detect institutional order accumulation zones 25 | - `get_order_history`: Get order history for the authenticated user 26 | - `get_orderbook`: Get orderbook (market depth) data for a trading pair 27 | - `get_ticker`: Get real-time ticker information for a trading pair 28 | 29 | There is also a **highly experimental** WebUI, see [WebUI README](webui/README.md) for details. 30 | 31 |  32 | 33 | All code is subject to breaking changes and feature additions / removals as I continue to develop this project. 34 | 35 | ## Requirements & Installation 36 | 37 | 1. Node.js (v22+) 38 | 2. pnpm (`npm i -g pnpm`) 39 | 3. If you want to run the Ollama client as shown in the quick start below, you'll need Ollama installed and running, as well as your model of choice. 40 | 41 | ```bash 42 | pnpm i 43 | ``` 44 | 45 | ## Quick Start 46 | 47 | To install packages build everything and start the interactive client: 48 | ```bash 49 | pnpm i 50 | ``` 51 | 52 | Copy the .env.example file to .env and fill in your details. 53 | 54 | ```bash 55 | cp .env.example .env 56 | code .env 57 | ``` 58 | 59 | ### MCP-Server (Only) 60 | 61 | #### Stdio Transport (Default) 62 | 63 | ```bash 64 | pnpm serve 65 | ``` 66 | 67 | #### HTTP/SSE Transport 68 | 69 | ```bash 70 | pnpm start:http 71 | ``` 72 | 73 | The HTTP server runs on port 8080 by default and provides both modern Streamable HTTP and legacy SSE transports, making it compatible with web applications and various MCP clients. See [HTTP Server Documentation](docs/HTTP_SERVER.md) for detailed information. 74 | 75 | ### MCP-Server and Ollama client 76 | 77 | Install required client packages: 78 | 79 | ```bash 80 | (cd client && pnpm i) 81 | ``` 82 | 83 | Copy the client .env.example file to .env and fill in your details. 84 | 85 | ```bash 86 | cp client/.env.example client/.env 87 | code client/.env 88 | ``` 89 | 90 | Then to start the client and server in one command: 91 | 92 | ```bash 93 | pnpm start 94 | ``` 95 | 96 | ## Configuration 97 | 98 | ### Environment Variables 99 | 100 | The server requires Bybit API credentials to be set as environment variables: 101 | 102 | - `BYBIT_API_KEY`: Your Bybit API key (required) 103 | - `BYBIT_API_SECRET`: Your Bybit API secret (required) - **IMPORTANT - Only ever create a read-only API key!** 104 | - `BYBIT_USE_TESTNET`: Set to "true" to use testnet instead of mainnet (optional, defaults to false) 105 | - `DEBUG`: Set to "true" to enable debug logging (optional, defaults to false) 106 | 107 | Client environment variables (./client/.env): 108 | 109 | - `OLLAMA_HOST`: The host of the Ollama server (defaults to http://localhost:11434) 110 | - `DEFAULT_MODEL`: The default model to use for chat (defaults to qwen3-30b-a3b-ud-nothink-128k:q4_k_xl) 111 | 112 | ### MCP Settings Configuration 113 | 114 | To use this server with MCP clients, you need to add it to your MCP settings configuration file. The file location depends on your client: 115 | 116 | #### MCP Example - Claude Desktop 117 | 118 | Location: `~/Library/Application\ Support/Claude/claude_desktop_config.json` 119 | 120 | ```json 121 | { 122 | "mcpServers": { 123 | "bybit": { 124 | "command": "node", 125 | "args": ["/path/to/bybit-mcp/build/index.js"], 126 | "env": { 127 | "BYBIT_API_KEY": "your-api-key", 128 | "BYBIT_API_SECRET": "your-api-secret", 129 | "BYBIT_USE_TESTNET": "false" 130 | } 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | #### MCP Example - [gomcp](https://github.com/sammcj/gomcp) 137 | 138 | Location: `~/.config/gomcp/config.yaml` 139 | 140 | ```yaml 141 | mcp_servers: 142 | - name: "bybit" 143 | command: "cd /path/to/bybit-mcp && pnpm run serve" 144 | arguments: [] 145 | env: 146 | BYBIT_API_KEY: "" # Add your Bybit API **READ ONLY** key here 147 | BYBIT_API_SECRET: "" # Add your Bybit API **READ ONLY** secret here 148 | BYBIT_USE_TESTNET: "true" # Set to false for production 149 | DEBUG: "false" # Optional: Set to true for debug logging 150 | ``` 151 | 152 | ## Client Integration 153 | 154 | This package includes a TypeScript client that provides a command-line interface for interacting with both Ollama LLMs and the bybit-mcp server. The client supports: 155 | 156 | - Interactive chat with Ollama models 157 | - Direct access to all bybit-mcp trading tools 158 | - Automatic server management 159 | - Environment-based configuration 160 | - Debug logging 161 | 162 | For detailed client documentation, see the [client README](client/README.md). 163 | 164 | ## Running the Server 165 | 166 | ### Production 167 | 168 | 1. Build the server: 169 | ```bash 170 | pnpm build 171 | ``` 172 | 173 | 2. Run the server: 174 | ```bash 175 | node build/index.js 176 | ``` 177 | 178 | ### Development 179 | 180 | For development with automatic TypeScript recompilation: 181 | ```bash 182 | pnpm watch 183 | ``` 184 | 185 | To inspect the MCP server during development: 186 | ```bash 187 | pnpm inspector 188 | ``` 189 | 190 | ## Tool Documentation 191 | 192 | ### Get Ticker Information 193 | 194 | ```typescript 195 | { 196 | "name": "get_ticker", 197 | "arguments": { 198 | "symbol": "BTCUSDT", 199 | "category": "spot" // optional, defaults to "spot" 200 | } 201 | } 202 | ``` 203 | 204 | ### Get Orderbook Data 205 | 206 | ```typescript 207 | { 208 | "name": "get_orderbook", 209 | "arguments": { 210 | "symbol": "BTCUSDT", 211 | "category": "spot", // optional, defaults to "spot" 212 | "limit": 25 // optional, defaults to 25 (available: 1, 25, 50, 100, 200) 213 | } 214 | } 215 | ``` 216 | 217 | ### Get Kline/Candlestick Data 218 | 219 | ```typescript 220 | { 221 | "name": "get_kline", 222 | "arguments": { 223 | "symbol": "BTCUSDT", 224 | "category": "spot", // optional, defaults to "spot" 225 | "interval": "1", // optional, defaults to "1" (available: "1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "M", "W") 226 | "limit": 200 // optional, defaults to 200 (max 1000) 227 | } 228 | } 229 | ``` 230 | 231 | ### Get Market Information 232 | 233 | ```typescript 234 | { 235 | "name": "get_market_info", 236 | "arguments": { 237 | "category": "spot", // optional, defaults to "spot" 238 | "symbol": "BTCUSDT", // optional, if not provided returns info for all symbols in the category 239 | "limit": 200 // optional, defaults to 200 (max 1000) 240 | } 241 | } 242 | ``` 243 | 244 | ### Get Recent Trades 245 | 246 | ```typescript 247 | { 248 | "name": "get_trades", 249 | "arguments": { 250 | "symbol": "BTCUSDT", 251 | "category": "spot", // optional, defaults to "spot" 252 | "limit": 200 // optional, defaults to 200 (max 1000) 253 | } 254 | } 255 | ``` 256 | 257 | ### Get Instrument Information 258 | 259 | ```typescript 260 | { 261 | "name": "get_instrument_info", 262 | "arguments": { 263 | "symbol": "BTCUSDT", // required 264 | "category": "spot" // optional, defaults to "spot" 265 | } 266 | } 267 | ``` 268 | 269 | Returns detailed information about a trading instrument including: 270 | 271 | - Base and quote currencies 272 | - Trading status 273 | - Lot size filters (min/max order quantities) 274 | - Price filters (tick size) 275 | - Leverage settings (for futures) 276 | - Contract details (for futures) 277 | 278 | ### Get Wallet Balance 279 | 280 | ```typescript 281 | { 282 | "name": "get_wallet_balance", 283 | "arguments": { 284 | "accountType": "UNIFIED", // required (available: "UNIFIED", "CONTRACT", "SPOT") 285 | "coin": "BTC" // optional, if not provided returns all coins 286 | } 287 | } 288 | ``` 289 | 290 | ### Get Positions 291 | 292 | ```typescript 293 | { 294 | "name": "get_positions", 295 | "arguments": { 296 | "category": "linear", // required (available: "linear", "inverse") 297 | "symbol": "BTCUSDT", // optional 298 | "baseCoin": "BTC", // optional 299 | "settleCoin": "USDT", // optional 300 | "limit": 200 // optional, defaults to 200 301 | } 302 | } 303 | ``` 304 | 305 | ### Get Order History 306 | 307 | ```typescript 308 | { 309 | "name": "get_order_history", 310 | "arguments": { 311 | "category": "spot", // required (available: "spot", "linear", "inverse") 312 | "symbol": "BTCUSDT", // optional 313 | "baseCoin": "BTC", // optional 314 | "orderId": "1234567890", // optional 315 | "orderLinkId": "myCustomId", // optional 316 | "orderStatus": "Filled", // optional (available: "Created", "New", "Rejected", "PartiallyFilled", "PartiallyFilledCanceled", "Filled", "Cancelled", "Untriggered", "Triggered", "Deactivated") 317 | "orderFilter": "Order", // optional (available: "Order", "StopOrder") 318 | "limit": 200 // optional, defaults to 200 319 | } 320 | } 321 | ``` 322 | 323 | ## Supported Categories 324 | 325 | - `spot`: Spot trading 326 | - `linear`: Linear perpetual contracts 327 | - `inverse`: Inverse perpetual contracts 328 | 329 | ## License 330 | 331 | MIT 332 | ``` -------------------------------------------------------------------------------- /webui/pnpm-workspace.yaml: -------------------------------------------------------------------------------- ```yaml 1 | onlyBuiltDependencies: 2 | - esbuild 3 | ``` -------------------------------------------------------------------------------- /webui/docker-healthcheck.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/sh 2 | # Health check for both MCP server and WebUI 3 | curl -f "http://localhost:${PORT:-8080}/health" || \ 4 | curl -f "http://localhost:${PORT:-8080}/" || \ 5 | exit 1 6 | ``` -------------------------------------------------------------------------------- /client/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Export main client class and types 2 | export { BybitMcpClient } from './client.js' 3 | export type { Message } from './client.js' 4 | 5 | // Export config class 6 | export { Config } from './config.js' 7 | 8 | // Export version 9 | export const version = '0.1.0' 10 | ``` -------------------------------------------------------------------------------- /webui/src/assets/fonts/fonts.css: -------------------------------------------------------------------------------- ```css 1 | /* Local font definitions - uses system fonts for better reliability */ 2 | 3 | /* 4 | * No external font files needed - using system font stack 5 | * This provides excellent performance and no external dependencies 6 | * while maintaining great visual consistency across platforms 7 | */ 8 | ``` -------------------------------------------------------------------------------- /webui/docker-entrypoint.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "🚀 Starting Bybit MCP WebUI..." 5 | echo "📊 Environment: ${NODE_ENV:-production}" 6 | echo "🌐 Host: ${HOST:-0.0.0.0}" 7 | echo "🔌 Port: ${PORT:-8080}" 8 | echo "🔧 MCP Port: ${MCP_PORT:-8080}" 9 | 10 | # Start the MCP HTTP server 11 | echo "🔧 Starting MCP Server..." 12 | exec node build/httpServer.js 13 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "isolatedModules": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "src/__tests__/**/*"] 16 | } 17 | ``` -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const CONSTANTS = { 2 | PROJECT_NAME: "bybit-mcp", 3 | PROJECT_VERSION: "0.1.0", 4 | // Testnet URLs for development/testing 5 | MAINNET_URL: "https://api.bybit.com", 6 | TESTNET_URL: "https://api-testnet.bybit.com", 7 | // Default category for API calls 8 | DEFAULT_CATEGORY: "spot", 9 | // Default interval for kline/candlestick data 10 | DEFAULT_INTERVAL: "1", 11 | // Maximum number of results to return 12 | MAX_RESULTS: 200 13 | } 14 | ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import tseslint from "typescript-eslint"; 4 | import { defineConfig } from "eslint/config"; 5 | 6 | 7 | export default defineConfig([ 8 | { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] }, 9 | { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: {...globals.browser, ...globals.node} } }, 10 | tseslint.configs.recommended, 11 | ]); 12 | ``` -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "build" 21 | ] 22 | } 23 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | extensionsToTreatAsEsm: ['.ts'], 6 | moduleNameMapper: { 7 | '^(\\.{1,2}/.*)\\.js$': '$1', 8 | }, 9 | transform: { 10 | '^.+\\.tsx?$': [ 11 | 'ts-jest', 12 | { 13 | useESM: true, 14 | }, 15 | ], 16 | }, 17 | testTimeout: 30000, 18 | maxWorkers: 1, 19 | forceExit: true, 20 | detectOpenHandles: true, 21 | setupFilesAfterEnv: ['<rootDir>/src/__tests__/test-setup.ts'], 22 | testPathIgnorePatterns: [ 23 | '<rootDir>/build/', 24 | '<rootDir>/node_modules/', 25 | '<rootDir>/src/__tests__/test-setup.ts', 26 | '<rootDir>/src/__tests__/integration.test.ts' 27 | ], 28 | testMatch: [ 29 | '<rootDir>/src/**/*.test.ts' 30 | ], 31 | collectCoverageFrom: [ 32 | 'src/**/*.ts', 33 | '!src/**/*.test.ts', 34 | '!src/__tests__/**', 35 | '!src/index.ts' 36 | ] 37 | }; 38 | ``` -------------------------------------------------------------------------------- /webui/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | /* Path mapping */ 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": ["src/*"], 26 | "@/types/*": ["src/types/*"], 27 | "@/components/*": ["src/components/*"], 28 | "@/services/*": ["src/services/*"], 29 | "@/utils/*": ["src/utils/*"] 30 | } 31 | }, 32 | "include": ["src/**/*", "vite.config.ts"], 33 | "exclude": ["node_modules", "dist"] 34 | } 35 | ``` -------------------------------------------------------------------------------- /webui/src/types/citation.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Types for the citation and data verification system 3 | */ 4 | 5 | export interface CitationData { 6 | referenceId: string; 7 | timestamp: string; 8 | toolName: string; 9 | endpoint?: string; 10 | rawData: any; 11 | extractedMetrics?: ExtractedMetric[]; 12 | } 13 | 14 | export interface ExtractedMetric { 15 | type: 'price' | 'volume' | 'indicator' | 'percentage' | 'other'; 16 | label: string; 17 | value: string | number; 18 | unit?: string; 19 | significance: 'high' | 'medium' | 'low'; 20 | } 21 | 22 | export interface CitationReference { 23 | referenceId: string; 24 | startIndex: number; 25 | endIndex: number; 26 | text: string; 27 | } 28 | 29 | export interface ProcessedMessage { 30 | originalContent: string; 31 | processedContent: string; 32 | citations: CitationReference[]; 33 | } 34 | 35 | export interface CitationTooltipData { 36 | referenceId: string; 37 | toolName: string; 38 | timestamp: string; 39 | endpoint?: string; 40 | keyMetrics: ExtractedMetric[]; 41 | hasFullData: boolean; 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { config } from 'dotenv' 2 | import { join } from 'path' 3 | import { existsSync } from 'fs' 4 | 5 | // Load environment variables from .env file if it exists 6 | const envPath = join(process.cwd(), '.env') 7 | if (existsSync(envPath)) { 8 | config({ path: envPath }) 9 | } 10 | 11 | export interface EnvConfig { 12 | apiKey: string | undefined 13 | apiSecret: string | undefined 14 | useTestnet: boolean 15 | debug: boolean 16 | } 17 | 18 | export function getEnvConfig(): EnvConfig { 19 | return { 20 | apiKey: process.env.BYBIT_API_KEY, 21 | apiSecret: process.env.BYBIT_API_SECRET, 22 | useTestnet: process.env.BYBIT_USE_TESTNET === 'true', 23 | debug: process.env.DEBUG === 'true', 24 | } 25 | } 26 | 27 | // Validate environment variables 28 | export function validateEnv(): void { 29 | const config = getEnvConfig() 30 | 31 | // In development mode, API keys are optional 32 | if (!config.apiKey || !config.apiSecret) { 33 | console.warn('Running in development mode: API keys not provided') 34 | } 35 | 36 | // Additional validations can be added here 37 | } 38 | ``` -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@bybit-mcp/client", 3 | "version": "0.2.0", 4 | "description": "TypeScript client for interacting with Ollama LLMs and bybit-mcp server", 5 | "type": "module", 6 | "bin": { 7 | "bybit-mcp-client": "build/cli.js", 8 | "bybit-mcp-chat": "build/launch.js" 9 | }, 10 | "main": "build/index.js", 11 | "types": "build/index.d.ts", 12 | "files": [ 13 | "build", 14 | "build/**/*" 15 | ], 16 | "scripts": { 17 | "build": "tsc && chmod +x build/cli.js build/launch.js", 18 | "prepare": "npm run build", 19 | "watch": "tsc --watch", 20 | "start": "node build/launch.js", 21 | "chat": "node build/launch.js" 22 | }, 23 | "keywords": [ 24 | "mcp", 25 | "ollama", 26 | "bybit", 27 | "ai", 28 | "llm", 29 | "client" 30 | ], 31 | "dependencies": { 32 | "@modelcontextprotocol/sdk": "1.12.0", 33 | "ollama": "^0.5.15", 34 | "commander": "^14.0.0", 35 | "chalk": "^5.4.1", 36 | "conf": "^13.1.0", 37 | "dotenv": "^16.5.0" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^22.15.21", 41 | "typescript": "^5.8.3" 42 | }, 43 | "engines": { 44 | "node": ">=22" 45 | }, 46 | "exports": { 47 | ".": { 48 | "types": "./build/index.d.ts", 49 | "import": "./build/index.js" 50 | } 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /webui/public/favicon.svg: -------------------------------------------------------------------------------- ``` 1 | <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <!-- Background circle --> 3 | <circle cx="16" cy="16" r="16" fill="#2563eb"/> 4 | 5 | <!-- Chart bars representing trading --> 6 | <rect x="6" y="20" width="2" height="6" fill="white" opacity="0.9"/> 7 | <rect x="10" y="16" width="2" height="10" fill="white" opacity="0.9"/> 8 | <rect x="14" y="12" width="2" height="14" fill="white" opacity="0.9"/> 9 | <rect x="18" y="8" width="2" height="18" fill="white" opacity="0.9"/> 10 | <rect x="22" y="14" width="2" height="12" fill="white" opacity="0.9"/> 11 | <rect x="26" y="18" width="2" height="8" fill="white" opacity="0.9"/> 12 | 13 | <!-- Trend line --> 14 | <path d="M6 22 L10 18 L14 14 L18 10 L22 16 L26 20" stroke="white" stroke-width="1.5" fill="none" opacity="0.8"/> 15 | 16 | <!-- Small dots at data points --> 17 | <circle cx="6" cy="22" r="1" fill="white"/> 18 | <circle cx="10" cy="18" r="1" fill="white"/> 19 | <circle cx="14" cy="14" r="1" fill="white"/> 20 | <circle cx="18" cy="10" r="1" fill="white"/> 21 | <circle cx="22" cy="16" r="1" fill="white"/> 22 | <circle cx="26" cy="20" r="1" fill="white"/> 23 | </svg> 24 | ``` -------------------------------------------------------------------------------- /client/src/env.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { config } from 'dotenv' 2 | import { join } from 'path' 3 | import { existsSync } from 'fs' 4 | 5 | // Load environment variables from .env file if it exists 6 | const envPath = join(process.cwd(), '.env') 7 | if (existsSync(envPath)) { 8 | const result = config({ path: envPath }) 9 | if (result.error) { 10 | console.error('Error loading .env file:', result.error) 11 | } 12 | } 13 | 14 | export interface EnvConfig { 15 | ollamaHost: string 16 | defaultModel: string 17 | debug: boolean 18 | } 19 | 20 | export function getEnvConfig(): EnvConfig { 21 | const ollamaHost = process.env.OLLAMA_HOST || process.env.OLLAMA_API_BASE 22 | if (!ollamaHost) { 23 | throw new Error('OLLAMA_HOST or OLLAMA_API_BASE environment variable must be set') 24 | } 25 | 26 | // Validate the URL format 27 | try { 28 | new URL(ollamaHost) 29 | } catch (error) { 30 | throw new Error(`Invalid OLLAMA_HOST URL format: ${ollamaHost}`) 31 | } 32 | 33 | return { 34 | ollamaHost, 35 | defaultModel: process.env.DEFAULT_MODEL || 'qwen3-30b-a3b-ud-nothink-128k:q4_k_xl', 36 | debug: process.env.DEBUG === 'true', 37 | } 38 | } 39 | 40 | // Validate required environment variables 41 | export function validateEnv(): void { 42 | getEnvConfig() // This will throw if validation fails 43 | } 44 | ``` -------------------------------------------------------------------------------- /webui/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "bybit-mcp-webui", 3 | "version": "1.0.0", 4 | "description": "Modern web interface for Bybit MCP server with AI chat capabilities", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "dev:full": "concurrently \"npm run mcp:start\" \"npm run dev\"", 9 | "mcp:start": "cd .. && node build/httpServer.js", 10 | "mcp:build": "cd .. && pnpm build", 11 | "build": "tsc && vite build", 12 | "preview": "vite preview", 13 | "type-check": "tsc --noEmit", 14 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" 15 | }, 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "1.12.0", 18 | "cors": "2.8.5", 19 | "express": "5.1.0", 20 | "marked": "15.0.12" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^22.15.21", 24 | "@typescript-eslint/eslint-plugin": "^8.32.1", 25 | "@typescript-eslint/parser": "^8.32.1", 26 | "concurrently": "^9.1.2", 27 | "eslint": "^9.27.0", 28 | "typescript": "^5.8.3", 29 | "vite": "^6.3.5" 30 | }, 31 | "engines": { 32 | "node": ">=22.0.0" 33 | }, 34 | "keywords": [ 35 | "bybit", 36 | "mcp", 37 | "trading", 38 | "ai", 39 | "chat", 40 | "webui", 41 | "typescript", 42 | "vite" 43 | ], 44 | "author": "Sam McLeod", 45 | "license": "MIT" 46 | } 47 | ``` -------------------------------------------------------------------------------- /webui/vite.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | 4 | export default defineConfig({ 5 | define: { 6 | // Inject environment variables at build time 7 | '__OLLAMA_HOST__': JSON.stringify(process.env.OLLAMA_HOST || 'http://localhost:11434'), 8 | '__MCP_ENDPOINT__': JSON.stringify(process.env.MCP_ENDPOINT || ''), // Empty means use current origin 9 | }, 10 | resolve: { 11 | alias: { 12 | '@': resolve(__dirname, 'src'), 13 | '@/types': resolve(__dirname, 'src/types'), 14 | '@/components': resolve(__dirname, 'src/components'), 15 | '@/services': resolve(__dirname, 'src/services'), 16 | '@/utils': resolve(__dirname, 'src/utils'), 17 | }, 18 | }, 19 | server: { 20 | port: 3000, 21 | host: true, 22 | proxy: { 23 | // Proxy MCP server requests 24 | '/api/mcp': { 25 | target: 'http://localhost:8080', 26 | changeOrigin: true, 27 | rewrite: (path) => path.replace(/^\/api\/mcp/, ''), 28 | configure: (proxy) => { 29 | proxy.on('error', (err) => { 30 | console.log('Proxy error:', err); 31 | }); 32 | } 33 | }, 34 | }, 35 | }, 36 | build: { 37 | outDir: 'dist', 38 | sourcemap: true, 39 | rollupOptions: { 40 | input: { 41 | main: resolve(__dirname, 'index.html'), 42 | }, 43 | }, 44 | }, 45 | optimizeDeps: { 46 | include: ['marked'], 47 | }, 48 | }) 49 | ``` -------------------------------------------------------------------------------- /webui/docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ============================================================================== 2 | # Docker Compose Configuration for Bybit MCP WebUI 3 | # ============================================================================== 4 | # This file provides easy deployment options for the Bybit MCP WebUI 5 | # ============================================================================== 6 | 7 | services: 8 | # ============================================================================== 9 | # Bybit MCP WebUI Service 10 | # ============================================================================== 11 | bybit-mcp-webui: 12 | build: 13 | context: .. 14 | dockerfile: webui/Dockerfile 15 | target: production 16 | image: bybit-mcp-webui:latest 17 | container_name: bybit-mcp-webui 18 | restart: unless-stopped 19 | stop_grace_period: 2s 20 | 21 | # Port mapping 22 | ports: 23 | - "8080:8080" 24 | 25 | # Environment variables 26 | environment: 27 | - NODE_ENV=production 28 | - PORT=8080 29 | - MCP_PORT=8080 30 | - HOST=0.0.0.0 31 | # Runtime configuration (these override build-time args) 32 | - OLLAMA_HOST=${OLLAMA_HOST:-https://ollama.example.com} 33 | - MCP_ENDPOINT=${MCP_ENDPOINT:-} 34 | # Add your Bybit API credentials here (optional, make sure they're read only!) 35 | # - BYBIT_API_KEY=your_api_key_here 36 | # - BYBIT_API_SECRET=your_api_secret_here 37 | # - BYBIT_TESTNET=true 38 | 39 | # Security 40 | security_opt: 41 | - no-new-privileges:true 42 | read_only: false 43 | user: "1001:1001" 44 | 45 | labels: 46 | - "traefik.enable=true" 47 | # Update this to your actual domain 48 | - "traefik.http.routers.bybit-mcp.rule=Host(`bybit-mcp.example.com`)" 49 | - "traefik.http.services.bybit-mcp.loadbalancer.server.port=8080" 50 | - "com.docker.compose.project=bybit-mcp" 51 | - "com.docker.compose.service=webui" 52 | 53 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "bybit-mcp", 3 | "version": "0.2.0", 4 | "description": "A MCP server to interact with Bybit's API", 5 | "license": "MIT", 6 | "type": "module", 7 | "bin": { 8 | "bybit-mcp": "build/index.js" 9 | }, 10 | "main": "build/index.js", 11 | "files": [ 12 | "build", 13 | "build/**/*" 14 | ], 15 | "scripts": { 16 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 17 | "build:all": "pnpm run build && cd client && pnpm run build", 18 | "prepare": "npm run build", 19 | "watch": "tsc --watch", 20 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 21 | "prepack": "npm run build", 22 | "serve": "node build/index.js", 23 | "serve:http": "node build/httpServer.js", 24 | "start": "pnpm run build:all && cd client && pnpm run start", 25 | "start:http": "pnpm run build && pnpm run serve:http", 26 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 27 | "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", 28 | "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", 29 | "test:api": "NODE_OPTIONS=--experimental-vm-modules pnpm test src/__tests__/integration.test.ts" 30 | }, 31 | "keywords": [ 32 | "mcp", 33 | "claude", 34 | "bybit", 35 | "anthropic", 36 | "ai", 37 | "cryptocurrency", 38 | "trading" 39 | ], 40 | "dependencies": { 41 | "@modelcontextprotocol/sdk": "1.12.0", 42 | "@types/cors": "2.8.18", 43 | "@types/express": "5.0.2", 44 | "@types/node": "^22.10.2", 45 | "bybit-api": "^4.1.8", 46 | "cors": "2.8.5", 47 | "dotenv": "16.5.0", 48 | "express": "5.1.0", 49 | "zod": "^3.25.28" 50 | }, 51 | "devDependencies": { 52 | "@eslint/js": "9.27.0", 53 | "@jest/globals": "29.7.0", 54 | "@types/jest": "29.5.14", 55 | "@types/node": "^22.15.21", 56 | "eslint": "9.27.0", 57 | "globals": "16.2.0", 58 | "jest": "29.7.0", 59 | "ts-jest": "29.3.4", 60 | "typescript": "^5.8.3", 61 | "typescript-eslint": "8.32.1" 62 | }, 63 | "engines": { 64 | "node": ">=22" 65 | } 66 | } 67 | ``` -------------------------------------------------------------------------------- /client/src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import Conf from 'conf' 2 | import { getEnvConfig, type EnvConfig } from './env.js'; 3 | 4 | interface ConfigSchema extends EnvConfig { } 5 | 6 | export class Config { 7 | private conf: Conf<ConfigSchema> 8 | private envConfig: EnvConfig; 9 | 10 | constructor() { 11 | // Get environment configuration 12 | this.envConfig = getEnvConfig(); 13 | 14 | this.conf = new Conf<ConfigSchema>({ 15 | projectName: 'bybit-mcp-client', 16 | schema: { 17 | ollamaHost: { 18 | type: 'string', 19 | default: this.envConfig.ollamaHost 20 | }, 21 | defaultModel: { 22 | type: 'string', 23 | default: this.envConfig.defaultModel 24 | }, 25 | debug: { 26 | type: 'boolean', 27 | default: this.envConfig.debug 28 | } 29 | } 30 | }) 31 | 32 | // Update stored config with any new environment values 33 | this.syncWithEnv() 34 | } 35 | 36 | private syncWithEnv(): void { 37 | // Environment variables take precedence over stored config 38 | if (this.envConfig.ollamaHost) { 39 | this.conf.set('ollamaHost', this.envConfig.ollamaHost) 40 | } 41 | if (this.envConfig.defaultModel) { 42 | this.conf.set('defaultModel', this.envConfig.defaultModel) 43 | } 44 | if (this.envConfig.debug !== undefined) { 45 | this.conf.set('debug', this.envConfig.debug) 46 | } 47 | } 48 | 49 | get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] { 50 | return this.conf.get(key); 51 | } 52 | 53 | set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void { 54 | // Only update if not overridden by environment 55 | if (!(key in this.envConfig) || this.envConfig[key] === undefined) { 56 | this.conf.set(key, value) 57 | } 58 | } 59 | 60 | delete(key: keyof ConfigSchema): void { 61 | // Only delete if not overridden by environment 62 | if (!(key in this.envConfig) || this.envConfig[key] === undefined) { 63 | this.conf.delete(key) 64 | } 65 | } 66 | 67 | clear(): void { 68 | this.conf.clear() 69 | // Restore environment values 70 | this.syncWithEnv(); 71 | } 72 | } 73 | ``` -------------------------------------------------------------------------------- /src/__tests__/test-setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Jest test setup file 3 | * Handles global test configuration and cleanup 4 | */ 5 | 6 | import { jest } from '@jest/globals' 7 | 8 | // Increase timeout for integration tests 9 | jest.setTimeout(30000) 10 | 11 | // Mock console methods to reduce noise during tests 12 | const originalConsoleError = console.error 13 | const originalConsoleWarn = console.warn 14 | const originalConsoleInfo = console.info 15 | 16 | beforeAll(() => { 17 | // Suppress console output during tests unless explicitly needed 18 | console.error = jest.fn() 19 | console.warn = jest.fn() 20 | console.info = jest.fn() 21 | }) 22 | 23 | afterAll(() => { 24 | // Restore original console methods 25 | console.error = originalConsoleError 26 | console.warn = originalConsoleWarn 27 | console.info = originalConsoleInfo 28 | }) 29 | 30 | // Global cleanup after each test 31 | afterEach(() => { 32 | // Clear all mocks 33 | jest.clearAllMocks() 34 | 35 | // Clear any timers 36 | jest.clearAllTimers() 37 | 38 | // Clear any active timeouts from tools 39 | jest.runOnlyPendingTimers() 40 | }) 41 | 42 | // Handle unhandled promise rejections 43 | process.on('unhandledRejection', (reason, promise) => { 44 | console.error('Unhandled Rejection at:', promise, 'reason:', reason) 45 | }) 46 | 47 | // Handle uncaught exceptions 48 | process.on('uncaughtException', (error) => { 49 | console.error('Uncaught Exception:', error) 50 | }) 51 | 52 | // Export common test utilities 53 | export const createMockResponse = (data: any, success: boolean = true) => { 54 | return { 55 | retCode: success ? 0 : 1, 56 | retMsg: success ? 'OK' : 'Error', 57 | result: success ? data : null, 58 | retExtInfo: {}, 59 | time: Date.now() 60 | } 61 | } 62 | 63 | // Mock crypto.randomUUID globally 64 | const mockRandomUUID = jest.fn(() => '123e4567-e89b-12d3-a456-426614174000') 65 | global.crypto = { 66 | ...global.crypto, 67 | randomUUID: mockRandomUUID, 68 | } as Crypto 69 | 70 | // Helper to create mock tool call requests 71 | export const createMockRequest = (name: string, arguments_: any) => { 72 | return { 73 | method: "tools/call" as const, 74 | params: { 75 | name, 76 | arguments: arguments_ 77 | } 78 | } 79 | } 80 | ``` -------------------------------------------------------------------------------- /webui/src/types/agent.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Agent-specific types for integration 3 | */ 4 | 5 | // Simplified agent configuration - removed complex options 6 | export interface AgentConfig { 7 | // Essential settings only 8 | maxIterations: number; // default: 5 9 | toolTimeout: number; // default: 30000ms 10 | 11 | // UI preferences 12 | showWorkflowSteps: boolean; // default: false 13 | showToolCalls: boolean; // default: false 14 | enableDebugMode: boolean; // default: false 15 | streamingEnabled: boolean; // default: true 16 | } 17 | 18 | export interface AgentState { 19 | isProcessing: boolean; 20 | lastQuery?: string; 21 | lastResponse?: string; 22 | queryCount: number; 23 | averageResponseTime: number; 24 | successRate: number; 25 | } 26 | 27 | // Workflow event types 28 | export interface WorkflowEvent { 29 | id: string; 30 | type: 'tool_call' | 'agent_thinking' | 'workflow_step' | 'error' | 'complete'; 31 | timestamp: number; 32 | data: any; 33 | agentName?: string; 34 | } 35 | 36 | export interface ToolCallEvent extends WorkflowEvent { 37 | type: 'tool_call'; 38 | data: { 39 | toolName: string; 40 | parameters: Record<string, any>; 41 | status: 'started' | 'completed' | 'failed'; 42 | duration?: number; 43 | result?: any; 44 | error?: string; 45 | }; 46 | } 47 | 48 | export interface AgentThinkingEvent extends WorkflowEvent { 49 | type: 'agent_thinking'; 50 | data: { 51 | reasoning: string; 52 | nextAction: string; 53 | confidence: number; 54 | }; 55 | } 56 | 57 | export interface WorkflowStepEvent extends WorkflowEvent { 58 | type: 'workflow_step'; 59 | data: { 60 | stepName: string; 61 | stepDescription: string; 62 | progress: number; 63 | totalSteps: number; 64 | }; 65 | } 66 | 67 | // Removed complex multi-agent and workflow preset configurations 68 | // Agent now uses single-agent mode with simplified configuration 69 | 70 | export const DEFAULT_AGENT_CONFIG: AgentConfig = { 71 | // Essential settings with sensible defaults 72 | maxIterations: 5, 73 | toolTimeout: 30000, 74 | 75 | // UI preferences - minimal by default 76 | showWorkflowSteps: false, 77 | showToolCalls: false, 78 | enableDebugMode: false, 79 | streamingEnabled: true, 80 | }; 81 | ``` -------------------------------------------------------------------------------- /webui/src/styles/main.css: -------------------------------------------------------------------------------- ```css 1 | /* Main CSS file - imports all styles */ 2 | 3 | /* Local fonts - no external CDN dependencies */ 4 | @import '../assets/fonts/fonts.css'; 5 | 6 | @import './variables.css'; 7 | @import './base.css'; 8 | @import './components.css'; 9 | @import './citations.css'; 10 | @import './verification-panel.css'; 11 | @import './agent-dashboard.css'; 12 | @import './data-cards.css'; 13 | @import './processing.css'; 14 | 15 | /* Responsive design */ 16 | @media (max-width: 768px) { 17 | .sidebar { 18 | width: var(--sidebar-width-collapsed); 19 | } 20 | 21 | .nav-label { 22 | display: none; 23 | } 24 | 25 | .header-content { 26 | padding: 0 var(--spacing-md); 27 | } 28 | 29 | .logo h1 { 30 | font-size: var(--font-size-lg); 31 | } 32 | 33 | .version { 34 | display: none; 35 | } 36 | 37 | .chat-input-wrapper { 38 | margin: 0 var(--spacing-md); 39 | } 40 | 41 | .chart-grid { 42 | grid-template-columns: 1fr; 43 | } 44 | 45 | .chart-controls { 46 | flex-wrap: wrap; 47 | } 48 | } 49 | 50 | @media (max-width: 480px) { 51 | .sidebar { 52 | position: fixed; 53 | left: -100%; 54 | top: var(--header-height); 55 | height: calc(100vh - var(--header-height)); 56 | z-index: var(--z-fixed); 57 | transition: left var(--transition-normal); 58 | } 59 | 60 | .sidebar.open { 61 | left: 0; 62 | } 63 | 64 | .content-area { 65 | margin-left: 0; 66 | } 67 | 68 | .example-queries { 69 | padding: 0 var(--spacing-md); 70 | } 71 | 72 | .modal-content { 73 | width: 95%; 74 | margin: var(--spacing-md); 75 | } 76 | } 77 | 78 | /* Print styles */ 79 | @media print { 80 | .sidebar, 81 | .header-controls, 82 | .chat-input-container { 83 | display: none; 84 | } 85 | 86 | .main-container { 87 | height: auto; 88 | } 89 | 90 | .content-area { 91 | margin-left: 0; 92 | } 93 | 94 | .chat-messages { 95 | overflow: visible; 96 | height: auto; 97 | } 98 | } 99 | 100 | /* High contrast mode adjustments */ 101 | @media (prefers-contrast: high) { 102 | .nav-item:hover, 103 | .example-query:hover, 104 | .theme-toggle:hover, 105 | .settings-btn:hover { 106 | outline: 2px solid currentColor; 107 | } 108 | } 109 | 110 | /* Reduced motion adjustments */ 111 | @media (prefers-reduced-motion: reduce) { 112 | .view { 113 | transition: none; 114 | } 115 | 116 | .modal, 117 | .modal-content { 118 | animation: none; 119 | } 120 | 121 | .chat-message { 122 | animation: none; 123 | } 124 | } 125 | ``` -------------------------------------------------------------------------------- /src/utils/toolLoader.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseToolImplementation } from "../tools/BaseTool.js" 2 | import { fileURLToPath } from "url" 3 | import { dirname, join } from "path" 4 | import { promises as fs } from "fs" 5 | 6 | async function findToolsPath(): Promise<string> { 7 | const currentFilePath = fileURLToPath(import.meta.url) 8 | const currentDir = dirname(currentFilePath) 9 | return join(currentDir, "..", "tools") 10 | } 11 | 12 | const isToolFile = (file: string): boolean => { 13 | return ( 14 | file.endsWith(".js") && 15 | !file.includes("BaseTool") && 16 | !file.includes("index") && 17 | !file.endsWith(".test.js") && 18 | !file.endsWith(".spec.js") && 19 | !file.endsWith(".d.js") 20 | ) 21 | } 22 | 23 | export async function loadTools(): Promise<BaseToolImplementation[]> { 24 | try { 25 | const toolsPath = await findToolsPath() 26 | const files = await fs.readdir(toolsPath) 27 | const tools: BaseToolImplementation[] = [] 28 | 29 | for (const file of files) { 30 | if (!isToolFile(file)) { 31 | continue 32 | } 33 | 34 | try { 35 | const modulePath = `file://${join(toolsPath, file)}` 36 | const { default: ToolClass } = await import(modulePath) 37 | 38 | if (!ToolClass || typeof ToolClass !== 'function') { 39 | console.warn(JSON.stringify({ 40 | type: "warning", 41 | message: `Invalid tool class in ${file}` 42 | })) 43 | continue 44 | } 45 | 46 | const tool = new ToolClass() 47 | 48 | if ( 49 | tool instanceof BaseToolImplementation && 50 | tool.name && 51 | tool.toolDefinition && 52 | typeof tool.toolCall === "function" 53 | ) { 54 | tools.push(tool) 55 | console.info(JSON.stringify({ 56 | type: "info", 57 | message: `Loaded tool: ${tool.name}` 58 | })) 59 | } else { 60 | console.warn(JSON.stringify({ 61 | type: "warning", 62 | message: `Invalid tool implementation in ${file}` 63 | })) 64 | } 65 | } catch (error) { 66 | console.error(JSON.stringify({ 67 | type: "error", 68 | message: `Error loading tool from ${file}: ${error instanceof Error ? error.message : String(error)}` 69 | })) 70 | } 71 | } 72 | 73 | return tools 74 | } catch (error) { 75 | console.error(JSON.stringify({ 76 | type: "error", 77 | message: `Failed to load tools: ${error instanceof Error ? error.message : String(error)}` 78 | })) 79 | return [] 80 | } 81 | } 82 | 83 | export function createToolsMap(tools: BaseToolImplementation[]): Map<string, BaseToolImplementation> { 84 | return new Map(tools.map((tool) => [tool.name, tool])) 85 | } 86 | ``` -------------------------------------------------------------------------------- /src/tools/GetTrades.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetPublicTradingHistoryParamsV5, 9 | // PublicTradeV5 10 | } from "bybit-api" 11 | 12 | type SupportedCategory = "spot" | "linear" | "inverse" 13 | 14 | class GetTrades extends BaseToolImplementation { 15 | name = "get_trades"; 16 | toolDefinition: Tool = { 17 | name: this.name, 18 | description: "Get recent trades for a trading pair", 19 | inputSchema: { 20 | type: "object", 21 | properties: { 22 | symbol: { 23 | type: "string", 24 | description: "Trading pair symbol (e.g., 'BTCUSDT')", 25 | }, 26 | category: { 27 | type: "string", 28 | description: "Category of the instrument (spot, linear, inverse)", 29 | enum: ["spot", "linear", "inverse"], 30 | }, 31 | limit: { 32 | type: "string", 33 | description: "Limit for the number of trades (max 1000)", 34 | enum: ["1", "10", "50", "100", "200", "500", "1000"], 35 | }, 36 | }, 37 | required: ["symbol"], 38 | }, 39 | }; 40 | 41 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 42 | try { 43 | const args = request.params.arguments as unknown 44 | if (!args || typeof args !== 'object') { 45 | throw new Error("Invalid arguments") 46 | } 47 | 48 | const typedArgs = args as Record<string, unknown> 49 | 50 | if (!typedArgs.symbol || typeof typedArgs.symbol !== 'string') { 51 | throw new Error("Missing or invalid symbol parameter") 52 | } 53 | 54 | const symbol = typedArgs.symbol 55 | const category = ( 56 | typedArgs.category && 57 | typeof typedArgs.category === 'string' && 58 | ["spot", "linear", "inverse"].includes(typedArgs.category) 59 | ) ? typedArgs.category as SupportedCategory 60 | : CONSTANTS.DEFAULT_CATEGORY as SupportedCategory 61 | 62 | const limit = ( 63 | typedArgs.limit && 64 | typeof typedArgs.limit === 'string' && 65 | ["1", "10", "50", "100", "200", "500", "1000"].includes(typedArgs.limit) 66 | ) ? parseInt(typedArgs.limit, 10) : 200 67 | 68 | const params: GetPublicTradingHistoryParamsV5 = { 69 | category, 70 | symbol, 71 | limit, 72 | } 73 | 74 | const response = await this.client.getPublicTradingHistory(params) 75 | 76 | if (response.retCode !== 0) { 77 | throw new Error(`Bybit API error: ${response.retMsg}`) 78 | } 79 | 80 | // Transform the trade data into a more readable format and return as array 81 | const formattedTrades = response.result.list.map(trade => ({ 82 | id: trade.execId, 83 | symbol: trade.symbol, 84 | price: trade.price, 85 | size: trade.size, 86 | side: trade.side, 87 | time: trade.time, 88 | isBlockTrade: trade.isBlockTrade, 89 | })) 90 | 91 | return this.formatResponse(formattedTrades) 92 | } catch (error) { 93 | return this.handleError(error) 94 | } 95 | } 96 | } 97 | 98 | export default GetTrades 99 | ``` -------------------------------------------------------------------------------- /src/tools/GetMarketInfo.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetInstrumentsInfoParamsV5, 9 | } from "bybit-api" 10 | 11 | type SupportedCategory = "spot" | "linear" | "inverse" 12 | 13 | class GetMarketInfo extends BaseToolImplementation { 14 | name = "get_market_info"; 15 | toolDefinition: Tool = { 16 | name: this.name, 17 | description: "Get detailed market information for trading pairs", 18 | inputSchema: { 19 | type: "object", 20 | properties: { 21 | category: { 22 | type: "string", 23 | description: "Category of the instrument (spot, linear, inverse)", 24 | enum: ["spot", "linear", "inverse"], 25 | }, 26 | symbol: { 27 | type: "string", 28 | description: "Optional: Trading pair symbol (e.g., 'BTCUSDT'). If not provided, returns info for all symbols in the category", 29 | }, 30 | limit: { 31 | type: "string", 32 | description: "Limit for the number of results (max 1000)", 33 | enum: ["1", "10", "50", "100", "200", "500", "1000"], 34 | }, 35 | }, 36 | required: [], 37 | }, 38 | }; 39 | 40 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 41 | try { 42 | const args = request.params.arguments as unknown 43 | if (!args || typeof args !== 'object') { 44 | throw new Error("Invalid arguments") 45 | } 46 | 47 | const typedArgs = args as Record<string, unknown> 48 | 49 | // Validate category explicitly 50 | if (typedArgs.category && typeof typedArgs.category === 'string') { 51 | if (!["spot", "linear", "inverse"].includes(typedArgs.category)) { 52 | throw new Error(`Invalid category: ${typedArgs.category}. Must be one of: spot, linear, inverse`) 53 | } 54 | } 55 | 56 | const category = ( 57 | typedArgs.category && 58 | typeof typedArgs.category === 'string' && 59 | ["spot", "linear", "inverse"].includes(typedArgs.category) 60 | ) ? typedArgs.category as SupportedCategory 61 | : CONSTANTS.DEFAULT_CATEGORY as SupportedCategory 62 | 63 | const symbol = typedArgs.symbol && typeof typedArgs.symbol === 'string' 64 | ? typedArgs.symbol 65 | : undefined 66 | 67 | const limit = ( 68 | typedArgs.limit && 69 | typeof typedArgs.limit === 'string' && 70 | ["1", "10", "50", "100", "200", "500", "1000"].includes(typedArgs.limit) 71 | ) ? parseInt(typedArgs.limit, 10) : 200 72 | 73 | const params: GetInstrumentsInfoParamsV5 = { 74 | category, 75 | symbol, 76 | limit, 77 | } 78 | 79 | const response = await this.client.getInstrumentsInfo(params) 80 | 81 | if (response.retCode !== 0) { 82 | throw new Error(`Bybit API error: ${response.retMsg}`) 83 | } 84 | 85 | // Return the list array directly 86 | return this.formatResponse(response.result.list) 87 | } catch (error) { 88 | // Ensure error responses are properly formatted 89 | const errorMessage = error instanceof Error ? error.message : String(error) 90 | return { 91 | content: [{ 92 | type: "text" as const, 93 | text: errorMessage 94 | }], 95 | isError: true 96 | } 97 | } 98 | } 99 | } 100 | 101 | export default GetMarketInfo 102 | ``` -------------------------------------------------------------------------------- /webui/src/styles/processing.css: -------------------------------------------------------------------------------- ```css 1 | /* Processing Indicator Styles */ 2 | 3 | /* Processing message container */ 4 | .processing-message { 5 | margin-bottom: var(--spacing-lg); 6 | animation: fadeIn 0.3s ease-out; 7 | } 8 | 9 | /* Processing indicator container */ 10 | .processing-indicator { 11 | display: flex; 12 | align-items: center; 13 | gap: var(--spacing-md); 14 | padding: var(--spacing-md); 15 | } 16 | 17 | /* Animated dots */ 18 | .processing-dots { 19 | display: flex; 20 | align-items: center; 21 | gap: var(--spacing-xs); 22 | } 23 | 24 | .processing-dots span { 25 | width: 8px; 26 | height: 8px; 27 | border-radius: 50%; 28 | background-color: var(--color-primary); 29 | animation: processingPulse 1.4s ease-in-out infinite both; 30 | } 31 | 32 | .processing-dots span:nth-child(1) { 33 | animation-delay: -0.32s; 34 | } 35 | 36 | .processing-dots span:nth-child(2) { 37 | animation-delay: -0.16s; 38 | } 39 | 40 | .processing-dots span:nth-child(3) { 41 | animation-delay: 0s; 42 | } 43 | 44 | /* Processing text */ 45 | .processing-text { 46 | color: var(--text-secondary); 47 | font-style: italic; 48 | font-size: var(--font-size-sm); 49 | } 50 | 51 | /* Processing content styling */ 52 | .message-content.processing { 53 | background-color: var(--bg-tertiary); 54 | border: 1px dashed var(--border-secondary); 55 | opacity: 0.8; 56 | } 57 | 58 | /* Cursor animation for streaming messages */ 59 | .cursor { 60 | animation: blink 1s infinite; 61 | color: var(--color-primary); 62 | font-weight: bold; 63 | } 64 | 65 | /* Keyframe animations */ 66 | @keyframes processingPulse { 67 | 0%, 80%, 100% { 68 | transform: scale(0); 69 | opacity: 0.5; 70 | } 71 | 40% { 72 | transform: scale(1); 73 | opacity: 1; 74 | } 75 | } 76 | 77 | @keyframes blink { 78 | 0%, 50% { opacity: 1; } 79 | 51%, 100% { opacity: 0; } 80 | } 81 | 82 | @keyframes fadeIn { 83 | from { 84 | opacity: 0; 85 | transform: translateY(10px); 86 | } 87 | to { 88 | opacity: 1; 89 | transform: translateY(0); 90 | } 91 | } 92 | 93 | /* Pulse animation for general loading states */ 94 | @keyframes pulse { 95 | 0%, 100% { 96 | opacity: 1; 97 | } 98 | 50% { 99 | opacity: 0.5; 100 | } 101 | } 102 | 103 | /* Spin animation for loading spinners */ 104 | @keyframes spin { 105 | from { 106 | transform: rotate(0deg); 107 | } 108 | to { 109 | transform: rotate(360deg); 110 | } 111 | } 112 | 113 | /* Enhanced processing states */ 114 | .processing-message .message-avatar { 115 | animation: pulse 2s ease-in-out infinite; 116 | } 117 | 118 | .processing-message .message-time { 119 | color: var(--color-primary); 120 | font-weight: var(--font-weight-medium); 121 | } 122 | 123 | /* Retry-specific styling */ 124 | .processing-text:has-text("Retrying") { 125 | color: var(--color-warning); 126 | } 127 | 128 | .processing-message[data-retry="true"] .processing-dots span { 129 | background-color: var(--color-warning); 130 | } 131 | 132 | .processing-message[data-retry="true"] .message-avatar { 133 | background-color: var(--color-warning); 134 | } 135 | 136 | /* Dark mode adjustments */ 137 | @media (prefers-color-scheme: dark) { 138 | .processing-dots span { 139 | background-color: var(--color-primary-light); 140 | } 141 | 142 | .message-content.processing { 143 | background-color: rgba(255, 255, 255, 0.02); 144 | border-color: rgba(255, 255, 255, 0.1); 145 | } 146 | } 147 | 148 | /* Reduced motion support */ 149 | @media (prefers-reduced-motion: reduce) { 150 | .processing-dots span { 151 | animation: none; 152 | opacity: 0.7; 153 | } 154 | 155 | .processing-message .message-avatar { 156 | animation: none; 157 | } 158 | 159 | .cursor { 160 | animation: none; 161 | } 162 | 163 | .processing-indicator { 164 | animation: none; 165 | } 166 | } 167 | 168 | /* High contrast mode */ 169 | @media (prefers-contrast: high) { 170 | .processing-dots span { 171 | background-color: currentColor; 172 | } 173 | 174 | .message-content.processing { 175 | border-width: 2px; 176 | border-style: solid; 177 | } 178 | } 179 | ``` -------------------------------------------------------------------------------- /src/tools/GetWalletBalance.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { 6 | AccountTypeV5, 7 | APIResponseV3WithTime, 8 | WalletBalanceV5 9 | } from "bybit-api" 10 | 11 | // Zod schema for input validation 12 | const inputSchema = z.object({ 13 | accountType: z.enum(["UNIFIED", "CONTRACT", "SPOT"]), 14 | coin: z.string().optional() 15 | }) 16 | 17 | type ToolArguments = z.infer<typeof inputSchema> 18 | 19 | // Type for the formatted response 20 | interface FormattedWalletResponse { 21 | accountType: AccountTypeV5 22 | coin?: string 23 | data: { 24 | list: WalletBalanceV5[] 25 | } 26 | timestamp: string 27 | meta: { 28 | requestId: string 29 | } 30 | } 31 | 32 | export class GetWalletBalance extends BaseToolImplementation { 33 | name = "get_wallet_balance" 34 | 35 | toolDefinition: Tool = { 36 | name: this.name, 37 | description: "Get wallet balance information for the authenticated user", 38 | inputSchema: { 39 | type: "object", 40 | properties: { 41 | accountType: { 42 | type: "string", 43 | description: "Account type", 44 | enum: ["UNIFIED", "CONTRACT", "SPOT"], 45 | }, 46 | coin: { 47 | type: "string", 48 | description: "Cryptocurrency symbol, e.g., BTC, ETH, USDT. If not specified, returns all coins.", 49 | }, 50 | }, 51 | required: ["accountType"], 52 | }, 53 | } 54 | 55 | private async getWalletData( 56 | accountType: AccountTypeV5, 57 | coin?: string 58 | ): Promise<APIResponseV3WithTime<{ list: WalletBalanceV5[] }>> { 59 | this.logInfo(`Fetching wallet balance for account type: ${accountType}${coin ? `, coin: ${coin}` : ''}`) 60 | return await this.client.getWalletBalance({ 61 | accountType, 62 | coin, 63 | }) 64 | } 65 | 66 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 67 | try { 68 | this.logInfo("Starting get_wallet_balance tool call") 69 | 70 | if (this.isDevMode) { 71 | throw new Error("Cannot get wallet balance in development mode - API credentials required") 72 | } 73 | 74 | // Parse and validate input 75 | const validationResult = inputSchema.safeParse(request.params.arguments) 76 | if (!validationResult.success) { 77 | const errorDetails = validationResult.error.errors.map(err => ({ 78 | field: err.path.join('.'), 79 | message: err.message, 80 | code: err.code 81 | })) 82 | throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`) 83 | } 84 | 85 | const { accountType, coin } = validationResult.data 86 | this.logInfo(`Validated arguments - accountType: ${accountType}${coin ? `, coin: ${coin}` : ''}`) 87 | 88 | // Execute API request with rate limiting and retry logic 89 | const response = await this.executeRequest(async () => { 90 | return await this.getWalletData(accountType, coin) 91 | }) 92 | 93 | // Format response 94 | const result: FormattedWalletResponse = { 95 | accountType, 96 | coin, 97 | data: { 98 | list: response.list 99 | }, 100 | timestamp: new Date().toISOString(), 101 | meta: { 102 | requestId: crypto.randomUUID() 103 | } 104 | } 105 | 106 | this.logInfo(`Successfully retrieved wallet balance for ${accountType}${coin ? ` (${coin})` : ''}`) 107 | return this.formatResponse(result) 108 | } catch (error) { 109 | this.logInfo(`Error in get_wallet_balance: ${error instanceof Error ? error.message : String(error)}`) 110 | return this.handleError(error) 111 | } 112 | } 113 | } 114 | 115 | export default GetWalletBalance 116 | ``` -------------------------------------------------------------------------------- /webui/src/styles/base.css: -------------------------------------------------------------------------------- ```css 1 | /* Base styles and reset */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | html { 10 | font-size: 16px; 11 | scroll-behavior: smooth; 12 | } 13 | 14 | body { 15 | font-family: var(--font-family-sans); 16 | font-size: var(--font-size-base); 17 | font-weight: var(--font-weight-normal); 18 | line-height: var(--line-height-normal); 19 | color: var(--text-primary); 20 | background-color: var(--bg-primary); 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | overflow-x: hidden; 24 | } 25 | 26 | /* Focus styles */ 27 | *:focus { 28 | outline: 2px solid var(--border-focus); 29 | outline-offset: 2px; 30 | } 31 | 32 | *:focus:not(:focus-visible) { 33 | outline: none; 34 | } 35 | 36 | /* Selection styles */ 37 | ::selection { 38 | background-color: var(--color-primary); 39 | color: var(--text-inverse); 40 | } 41 | 42 | /* Scrollbar styles */ 43 | ::-webkit-scrollbar { 44 | width: 8px; 45 | height: 8px; 46 | } 47 | 48 | ::-webkit-scrollbar-track { 49 | background: var(--bg-secondary); 50 | } 51 | 52 | ::-webkit-scrollbar-thumb { 53 | background: var(--border-secondary); 54 | border-radius: var(--radius-full); 55 | } 56 | 57 | ::-webkit-scrollbar-thumb:hover { 58 | background: var(--text-tertiary); 59 | } 60 | 61 | /* Typography */ 62 | h1, h2, h3, h4, h5, h6 { 63 | font-weight: var(--font-weight-semibold); 64 | line-height: var(--line-height-tight); 65 | color: var(--text-primary); 66 | } 67 | 68 | h1 { 69 | font-size: var(--font-size-3xl); 70 | } 71 | 72 | h2 { 73 | font-size: var(--font-size-2xl); 74 | } 75 | 76 | h3 { 77 | font-size: var(--font-size-xl); 78 | } 79 | 80 | h4 { 81 | font-size: var(--font-size-lg); 82 | } 83 | 84 | h5, h6 { 85 | font-size: var(--font-size-base); 86 | } 87 | 88 | p { 89 | margin-bottom: var(--spacing-md); 90 | } 91 | 92 | a { 93 | color: var(--color-primary); 94 | text-decoration: none; 95 | transition: color var(--transition-fast); 96 | } 97 | 98 | a:hover { 99 | color: var(--color-primary-hover); 100 | text-decoration: underline; 101 | } 102 | 103 | /* Form elements */ 104 | input, textarea, select, button { 105 | font-family: inherit; 106 | font-size: inherit; 107 | } 108 | 109 | button { 110 | cursor: pointer; 111 | border: none; 112 | background: none; 113 | padding: 0; 114 | } 115 | 116 | button:disabled { 117 | cursor: not-allowed; 118 | opacity: 0.6; 119 | } 120 | 121 | /* Utility classes */ 122 | .hidden { 123 | display: none !important; 124 | } 125 | 126 | .sr-only { 127 | position: absolute; 128 | width: 1px; 129 | height: 1px; 130 | padding: 0; 131 | margin: -1px; 132 | overflow: hidden; 133 | clip: rect(0, 0, 0, 0); 134 | white-space: nowrap; 135 | border: 0; 136 | } 137 | 138 | .text-center { 139 | text-align: center; 140 | } 141 | 142 | .text-left { 143 | text-align: left; 144 | } 145 | 146 | .text-right { 147 | text-align: right; 148 | } 149 | 150 | .font-mono { 151 | font-family: var(--font-family-mono); 152 | } 153 | 154 | .font-medium { 155 | font-weight: var(--font-weight-medium); 156 | } 157 | 158 | .font-semibold { 159 | font-weight: var(--font-weight-semibold); 160 | } 161 | 162 | .font-bold { 163 | font-weight: var(--font-weight-bold); 164 | } 165 | 166 | .text-xs { 167 | font-size: var(--font-size-xs); 168 | } 169 | 170 | .text-sm { 171 | font-size: var(--font-size-sm); 172 | } 173 | 174 | .text-lg { 175 | font-size: var(--font-size-lg); 176 | } 177 | 178 | .text-xl { 179 | font-size: var(--font-size-xl); 180 | } 181 | 182 | .text-primary { 183 | color: var(--text-primary); 184 | } 185 | 186 | .text-secondary { 187 | color: var(--text-secondary); 188 | } 189 | 190 | .text-tertiary { 191 | color: var(--text-tertiary); 192 | } 193 | 194 | .text-success { 195 | color: var(--color-success); 196 | } 197 | 198 | .text-warning { 199 | color: var(--color-warning); 200 | } 201 | 202 | .text-danger { 203 | color: var(--color-danger); 204 | } 205 | 206 | /* Loading animation */ 207 | @keyframes spin { 208 | to { 209 | transform: rotate(360deg); 210 | } 211 | } 212 | 213 | @keyframes pulse { 214 | 0%, 100% { 215 | opacity: 1; 216 | } 217 | 50% { 218 | opacity: 0.5; 219 | } 220 | } 221 | 222 | @keyframes fadeIn { 223 | from { 224 | opacity: 0; 225 | transform: translateY(10px); 226 | } 227 | to { 228 | opacity: 1; 229 | transform: translateY(0); 230 | } 231 | } 232 | 233 | @keyframes slideInRight { 234 | from { 235 | opacity: 0; 236 | transform: translateX(20px); 237 | } 238 | to { 239 | opacity: 1; 240 | transform: translateX(0); 241 | } 242 | } 243 | 244 | .animate-spin { 245 | animation: spin 1s linear infinite; 246 | } 247 | 248 | .animate-pulse { 249 | animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 250 | } 251 | 252 | .animate-fadeIn { 253 | animation: fadeIn 0.3s ease-out; 254 | } 255 | 256 | .animate-slideInRight { 257 | animation: slideInRight 0.3s ease-out; 258 | } 259 | ``` -------------------------------------------------------------------------------- /webui/src/styles/variables.css: -------------------------------------------------------------------------------- ```css 1 | /* CSS Custom Properties for theming and consistency */ 2 | 3 | :root { 4 | /* Colors - Light Theme */ 5 | --color-primary: #2563eb; 6 | --color-primary-hover: #1d4ed8; 7 | --color-secondary: #64748b; 8 | --color-accent: #10b981; 9 | --color-accent-hover: #059669; 10 | --color-danger: #ef4444; 11 | --color-warning: #f59e0b; 12 | --color-success: #10b981; 13 | 14 | /* Background Colors */ 15 | --bg-primary: #ffffff; 16 | --bg-secondary: #f8fafc; 17 | --bg-tertiary: #f1f5f9; 18 | --bg-elevated: #ffffff; 19 | --bg-overlay: rgba(0, 0, 0, 0.5); 20 | 21 | /* Text Colors */ 22 | --text-primary: #0f172a; 23 | --text-secondary: #475569; 24 | --text-tertiary: #94a3b8; 25 | --text-inverse: #ffffff; 26 | 27 | /* Border Colors */ 28 | --border-primary: #e2e8f0; 29 | --border-secondary: #cbd5e1; 30 | --border-focus: #2563eb; 31 | 32 | /* Shadows */ 33 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 34 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 35 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 36 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 37 | 38 | /* Spacing */ 39 | --spacing-xs: 0.25rem; 40 | --spacing-sm: 0.5rem; 41 | --spacing-md: 1rem; 42 | --spacing-lg: 1.5rem; 43 | --spacing-xl: 2rem; 44 | --spacing-2xl: 3rem; 45 | 46 | /* Typography - Modern system font stack */ 47 | --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif; 48 | --font-family-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; 49 | 50 | --font-size-xs: 0.75rem; 51 | --font-size-sm: 0.875rem; 52 | --font-size-base: 1rem; 53 | --font-size-lg: 1.125rem; 54 | --font-size-xl: 1.25rem; 55 | --font-size-2xl: 1.5rem; 56 | --font-size-3xl: 1.875rem; 57 | 58 | --font-weight-normal: 400; 59 | --font-weight-medium: 500; 60 | --font-weight-semibold: 600; 61 | --font-weight-bold: 700; 62 | 63 | --line-height-tight: 1.25; 64 | --line-height-normal: 1.5; 65 | --line-height-relaxed: 1.75; 66 | 67 | /* Border Radius */ 68 | --radius-sm: 0.25rem; 69 | --radius-md: 0.375rem; 70 | --radius-lg: 0.5rem; 71 | --radius-xl: 0.75rem; 72 | --radius-2xl: 1rem; 73 | --radius-full: 9999px; 74 | 75 | /* Layout */ 76 | --header-height: 4rem; 77 | --sidebar-width: 16rem; 78 | --sidebar-width-collapsed: 4rem; 79 | 80 | /* Transitions */ 81 | --transition-fast: 150ms ease-in-out; 82 | --transition-normal: 250ms ease-in-out; 83 | --transition-slow: 350ms ease-in-out; 84 | 85 | /* Z-index */ 86 | --z-dropdown: 1000; 87 | --z-sticky: 1020; 88 | --z-fixed: 1030; 89 | --z-modal-backdrop: 1040; 90 | --z-modal: 1050; 91 | --z-popover: 1060; 92 | --z-tooltip: 1070; 93 | } 94 | 95 | /* Dark Theme */ 96 | [data-theme="dark"] { 97 | /* Background Colors */ 98 | --bg-primary: #0f172a; 99 | --bg-secondary: #1e293b; 100 | --bg-tertiary: #334155; 101 | --bg-elevated: #1e293b; 102 | 103 | /* Text Colors */ 104 | --text-primary: #f8fafc; 105 | --text-secondary: #cbd5e1; 106 | --text-tertiary: #64748b; 107 | 108 | /* Border Colors */ 109 | --border-primary: #334155; 110 | --border-secondary: #475569; 111 | 112 | /* Shadows (adjusted for dark theme) */ 113 | --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); 114 | --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); 115 | --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); 116 | --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2); 117 | } 118 | 119 | /* High contrast mode */ 120 | @media (prefers-contrast: high) { 121 | :root { 122 | --border-primary: #000000; 123 | --border-secondary: #000000; 124 | } 125 | 126 | [data-theme="dark"] { 127 | --border-primary: #ffffff; 128 | --border-secondary: #ffffff; 129 | } 130 | } 131 | 132 | /* Reduced motion */ 133 | @media (prefers-reduced-motion: reduce) { 134 | :root { 135 | --transition-fast: 0ms; 136 | --transition-normal: 0ms; 137 | --transition-slow: 0ms; 138 | } 139 | } 140 | 141 | /* Chart Colors */ 142 | :root { 143 | --chart-bullish: #10b981; 144 | --chart-bearish: #ef4444; 145 | --chart-volume: #6366f1; 146 | --chart-grid: #e5e7eb; 147 | --chart-text: var(--text-secondary); 148 | --chart-background: var(--bg-primary); 149 | } 150 | 151 | [data-theme="dark"] { 152 | --chart-grid: #374151; 153 | } 154 | ``` -------------------------------------------------------------------------------- /src/tools/GetPositions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { 6 | // CategoryV5, 7 | PositionInfoParamsV5, 8 | APIResponseV3WithTime, 9 | PositionV5 10 | } from "bybit-api" 11 | 12 | // Zod schema for input validation 13 | const inputSchema = z.object({ 14 | category: z.enum(["linear", "inverse"]), 15 | symbol: z.string().optional(), 16 | baseCoin: z.string().optional(), 17 | settleCoin: z.string().optional(), 18 | limit: z.enum(["1", "10", "50", "100", "200"]).optional() 19 | }) 20 | 21 | type ToolArguments = z.infer<typeof inputSchema> 22 | 23 | // Type for the formatted response 24 | interface FormattedPositionsResponse { 25 | category: "linear" | "inverse" 26 | symbol?: string 27 | baseCoin?: string 28 | settleCoin?: string 29 | limit: number 30 | data: PositionV5[] 31 | timestamp: string 32 | meta: { 33 | requestId: string 34 | } 35 | } 36 | 37 | class GetPositions extends BaseToolImplementation { 38 | name = "get_positions" 39 | toolDefinition: Tool = { 40 | name: this.name, 41 | description: "Get positions information for the authenticated user", 42 | inputSchema: { 43 | type: "object", 44 | properties: { 45 | category: { 46 | type: "string", 47 | description: "Product type", 48 | enum: ["linear", "inverse"], 49 | }, 50 | symbol: { 51 | type: "string", 52 | description: "Trading symbol, e.g., BTCUSDT", 53 | }, 54 | baseCoin: { 55 | type: "string", 56 | description: "Base coin. Used to get all symbols with this base coin", 57 | }, 58 | settleCoin: { 59 | type: "string", 60 | description: "Settle coin. Used to get all symbols with this settle coin", 61 | }, 62 | limit: { 63 | type: "string", 64 | description: "Maximum number of results (default: 200)", 65 | enum: ["1", "10", "50", "100", "200"], 66 | }, 67 | }, 68 | required: ["category"], 69 | }, 70 | } 71 | 72 | private async getPositionsData( 73 | params: PositionInfoParamsV5 74 | ): Promise<APIResponseV3WithTime<{ list: PositionV5[] }>> { 75 | this.logInfo(`Fetching positions with params: ${JSON.stringify(params)}`) 76 | return await this.client.getPositionInfo(params) 77 | } 78 | 79 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 80 | try { 81 | this.logInfo("Starting get_positions tool call") 82 | 83 | // Parse and validate input 84 | const validationResult = inputSchema.safeParse(request.params.arguments) 85 | if (!validationResult.success) { 86 | throw new Error(`Invalid input: ${validationResult.error.message}`) 87 | } 88 | 89 | const { 90 | category, 91 | symbol, 92 | baseCoin, 93 | settleCoin, 94 | limit = "200" 95 | } = validationResult.data 96 | 97 | this.logInfo(`Validated arguments - category: ${category}, symbol: ${symbol}, limit: ${limit}`) 98 | 99 | // Prepare request parameters 100 | const params: PositionInfoParamsV5 = { 101 | category, 102 | symbol, 103 | baseCoin, 104 | settleCoin, 105 | limit: parseInt(limit, 10) 106 | } 107 | 108 | // Execute API request with rate limiting and retry logic 109 | const response = await this.executeRequest(async () => { 110 | return await this.getPositionsData(params) 111 | }) 112 | 113 | // Format response 114 | const result: FormattedPositionsResponse = { 115 | category, 116 | symbol, 117 | baseCoin, 118 | settleCoin, 119 | limit: parseInt(limit, 10), 120 | data: response.list, 121 | timestamp: new Date().toISOString(), 122 | meta: { 123 | requestId: crypto.randomUUID() 124 | } 125 | } 126 | 127 | this.logInfo(`Successfully retrieved positions data${symbol ? ` for ${symbol}` : ''}`) 128 | return this.formatResponse(result) 129 | } catch (error) { 130 | this.logInfo(`Error in get_positions: ${error instanceof Error ? error.message : String(error)}`) 131 | return this.handleError(error) 132 | } 133 | } 134 | } 135 | 136 | export default GetPositions 137 | ``` -------------------------------------------------------------------------------- /webui/src/types/ai.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * TypeScript types for AI integration (OpenAI-compatible API) 3 | */ 4 | 5 | // Tool calling types (for function calling) 6 | export interface ToolCall { 7 | id: string; 8 | type: 'function'; 9 | function: { 10 | name: string; 11 | arguments: string; 12 | }; 13 | } 14 | 15 | export interface Tool { 16 | type: 'function'; 17 | function: { 18 | name: string; 19 | description: string; 20 | parameters: any; 21 | }; 22 | } 23 | 24 | // Chat message types 25 | export interface ChatMessage { 26 | role: 'system' | 'user' | 'assistant' | 'tool'; 27 | content: string | null; 28 | timestamp?: number; 29 | id?: string; 30 | tool_calls?: ToolCall[]; 31 | tool_call_id?: string; 32 | name?: string; 33 | } 34 | 35 | export interface ChatCompletionRequest { 36 | model: string; 37 | messages: ChatMessage[]; 38 | temperature?: number; 39 | max_tokens?: number; 40 | top_p?: number; 41 | frequency_penalty?: number; 42 | presence_penalty?: number; 43 | stream?: boolean; 44 | stop?: string | string[]; 45 | tools?: Tool[]; 46 | tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } }; 47 | } 48 | 49 | export interface ChatCompletionResponse { 50 | id: string; 51 | object: 'chat.completion'; 52 | created: number; 53 | model: string; 54 | choices: Array<{ 55 | index: number; 56 | message: ChatMessage; 57 | finish_reason: 'stop' | 'length' | 'content_filter' | null; 58 | }>; 59 | usage: { 60 | prompt_tokens: number; 61 | completion_tokens: number; 62 | total_tokens: number; 63 | }; 64 | } 65 | 66 | export interface ChatCompletionStreamResponse { 67 | id: string; 68 | object: 'chat.completion.chunk'; 69 | created: number; 70 | model: string; 71 | choices: Array<{ 72 | index: number; 73 | delta: { 74 | role?: 'assistant'; 75 | content?: string; 76 | }; 77 | finish_reason: 'stop' | 'length' | 'content_filter' | null; 78 | }>; 79 | } 80 | 81 | // AI configuration 82 | export interface AIConfig { 83 | endpoint: string; 84 | model: string; 85 | temperature: number; 86 | maxTokens: number; 87 | systemPrompt: string; 88 | } 89 | 90 | // Chat UI types 91 | export interface ChatUIMessage extends ChatMessage { 92 | id: string; 93 | timestamp: number; 94 | isStreaming?: boolean; 95 | error?: string; 96 | } 97 | 98 | export interface ChatState { 99 | messages: ChatUIMessage[]; 100 | isLoading: boolean; 101 | isConnected: boolean; 102 | currentStreamingId?: string; 103 | } 104 | 105 | 106 | 107 | // AI service interface 108 | export interface AIService { 109 | chat(messages: ChatMessage[], options?: Partial<ChatCompletionRequest>): Promise<ChatCompletionResponse>; 110 | streamChat( 111 | messages: ChatMessage[], 112 | onChunk: (chunk: ChatCompletionStreamResponse) => void, 113 | options?: Partial<ChatCompletionRequest> 114 | ): Promise<void>; 115 | isConnected(): Promise<boolean>; 116 | } 117 | 118 | // Error types 119 | export interface AIError { 120 | code: string; 121 | message: string; 122 | details?: unknown; 123 | } 124 | 125 | // Model information 126 | export interface ModelInfo { 127 | id: string; 128 | name: string; 129 | description?: string; 130 | contextLength?: number; 131 | capabilities?: string[]; 132 | } 133 | 134 | // Conversation types 135 | export interface Conversation { 136 | id: string; 137 | title: string; 138 | messages: ChatUIMessage[]; 139 | createdAt: number; 140 | updatedAt: number; 141 | } 142 | 143 | export interface ConversationSummary { 144 | id: string; 145 | title: string; 146 | lastMessage?: string; 147 | messageCount: number; 148 | createdAt: number; 149 | updatedAt: number; 150 | } 151 | 152 | // Settings types 153 | export interface ChatSettings { 154 | ai: AIConfig; 155 | mcp: { 156 | endpoint: string; 157 | timeout: number; 158 | }; 159 | ui: { 160 | theme: 'light' | 'dark' | 'auto'; 161 | fontSize: 'small' | 'medium' | 'large'; 162 | showTimestamps: boolean; 163 | enableSounds: boolean; 164 | }; 165 | } 166 | 167 | // Event types for real-time updates 168 | export type ChatEvent = 169 | | { type: 'message_start'; messageId: string } 170 | | { type: 'message_chunk'; messageId: string; content: string } 171 | | { type: 'message_complete'; messageId: string } 172 | | { type: 'message_error'; messageId: string; error: string } 173 | | { type: 'connection_status'; connected: boolean } 174 | | { type: 'typing_start' } 175 | | { type: 'typing_stop' }; 176 | 177 | // Utility types 178 | export type MessageRole = ChatMessage['role']; 179 | export type StreamingState = 'idle' | 'connecting' | 'streaming' | 'complete' | 'error'; 180 | ``` -------------------------------------------------------------------------------- /client/src/launch.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { existsSync } from 'fs' 3 | import { join } from 'path' 4 | import { execSync, spawn } from 'child_process' 5 | import { fileURLToPath } from 'url' 6 | import { dirname } from 'path' 7 | import { validateEnv, getEnvConfig } from './env.js' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = dirname(__filename) 11 | 12 | // Cache file to store last check timestamp 13 | const CACHE_FILE = join(__dirname, '..', '.install-check') 14 | const CHECK_INTERVAL = 1000 * 60 * 60 // 1 hour 15 | 16 | function shouldCheckDependencies(): boolean { 17 | try { 18 | if (existsSync(CACHE_FILE)) { 19 | const stat = execSync(`stat -f %m "${CACHE_FILE}"`).toString().trim() 20 | const lastCheck = parseInt(stat, 10) * 1000 // Convert to milliseconds 21 | return Date.now() - lastCheck > CHECK_INTERVAL 22 | } 23 | return true 24 | } catch { 25 | return true 26 | } 27 | } 28 | 29 | function updateCheckTimestamp(): void { 30 | try { 31 | execSync(`touch "${CACHE_FILE}"`) 32 | } catch (error) { 33 | console.warn('Warning: Could not update dependency check timestamp') 34 | } 35 | } 36 | 37 | interface OllamaModel { 38 | name: string 39 | } 40 | 41 | interface OllamaListResponse { 42 | models: OllamaModel[] 43 | } 44 | 45 | function checkOllama(): boolean { 46 | const config = getEnvConfig() 47 | try { 48 | // First check if we can connect to Ollama 49 | const tagsResponse = execSync(`curl -s ${config.ollamaHost}/api/tags`).toString() 50 | 51 | // Parse the response to check if the required model exists 52 | const models = JSON.parse(tagsResponse) as OllamaListResponse 53 | const modelExists = models.models.some(model => model.name === config.defaultModel) 54 | 55 | if (!modelExists) { 56 | console.error(`Error: Model "${config.defaultModel}" not found on Ollama server at ${config.ollamaHost}`) 57 | console.log('Available models:', models.models.map(m => m.name).join(', ')) 58 | console.log(`\nTo pull the required model, run:\ncurl -X POST ${config.ollamaHost}/api/pull -d '{"name": "${config.defaultModel}"}'`) 59 | return false 60 | } 61 | 62 | return true 63 | } catch (error) { 64 | console.error('Error checking Ollama:', error) 65 | return false 66 | } 67 | } 68 | 69 | function quickDependencyCheck(): boolean { 70 | const nodeModulesPath = join(__dirname, '..', 'node_modules') 71 | const buildPath = join(__dirname, '..', 'build') 72 | 73 | if (!existsSync(nodeModulesPath) || !existsSync(buildPath)) { 74 | console.log('Installing and building...') 75 | try { 76 | execSync('pnpm install && pnpm run build', { 77 | stdio: 'inherit', 78 | cwd: join(__dirname, '..') 79 | }) 80 | } catch (error) { 81 | console.error('Failed to install dependencies and build') 82 | return false 83 | } 84 | } 85 | 86 | return true 87 | } 88 | 89 | async function main() { 90 | try { 91 | // Validate environment configuration first 92 | validateEnv() 93 | const config = getEnvConfig() 94 | 95 | // Check Ollama connection and model availability 96 | if (!checkOllama()) { 97 | process.exit(1) 98 | } 99 | 100 | // Only check dependencies if needed 101 | if (shouldCheckDependencies()) { 102 | if (!quickDependencyCheck()) { 103 | process.exit(1) 104 | } 105 | updateCheckTimestamp() 106 | } 107 | 108 | // Enable debug mode for better error reporting 109 | process.env.DEBUG = 'true' 110 | 111 | // Start the chat interface in integrated mode with debug enabled 112 | spawn('node', ['build/cli.js', '--integrated', '--debug', 'chat'], { 113 | stdio: 'inherit', 114 | cwd: join(__dirname, '..'), 115 | env: { 116 | ...process.env, 117 | OLLAMA_HOST: config.ollamaHost, 118 | DEFAULT_MODEL: config.defaultModel 119 | } 120 | }) 121 | 122 | } catch (error) { 123 | if (error instanceof Error) { 124 | console.error('Error:', error.message) 125 | if (process.env.DEBUG === 'true') { 126 | console.error('Stack trace:', error.stack) 127 | } 128 | } else { 129 | console.error('Unknown error occurred') 130 | } 131 | process.exit(1) 132 | } 133 | } 134 | 135 | main().catch(error => { 136 | console.error('Error:', error) 137 | if (process.env.DEBUG === 'true') { 138 | console.error('Stack trace:', error.stack) 139 | } 140 | process.exit(1) 141 | }) 142 | ``` -------------------------------------------------------------------------------- /src/tools/GetOrderbook.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetOrderbookParamsV5, 9 | APIResponseV3WithTime 10 | } from "bybit-api" 11 | 12 | // Zod schema for input validation 13 | const inputSchema = z.object({ 14 | symbol: z.string() 15 | .min(1, "Symbol is required") 16 | .regex(/^[A-Z0-9]+$/, "Symbol must contain only uppercase letters and numbers"), 17 | category: z.enum(["spot", "linear", "inverse"]).optional(), 18 | limit: z.union([ 19 | z.enum(["1", "25", "50", "100", "200"]), 20 | z.number().transform(n => { 21 | const validLimits = [1, 25, 50, 100, 200] 22 | const closest = validLimits.reduce((prev, curr) => 23 | Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev 24 | ) 25 | return String(closest) 26 | }) 27 | ]).optional() 28 | }) 29 | 30 | type SupportedCategory = z.infer<typeof inputSchema>["category"] 31 | type ToolArguments = z.infer<typeof inputSchema> 32 | 33 | // Type for Bybit orderbook response 34 | interface OrderbookData { 35 | s: string // Symbol 36 | b: [string, string][] // Bids [price, size] 37 | a: [string, string][] // Asks [price, size] 38 | ts: number // Timestamp 39 | u: number // Update ID 40 | } 41 | 42 | class GetOrderbook extends BaseToolImplementation { 43 | name = "get_orderbook" 44 | toolDefinition: Tool = { 45 | name: this.name, 46 | description: "Get orderbook (market depth) data for a trading pair", 47 | inputSchema: { 48 | type: "object", 49 | properties: { 50 | symbol: { 51 | type: "string", 52 | description: "Trading pair symbol (e.g., 'BTCUSDT')", 53 | pattern: "^[A-Z0-9]+$" 54 | }, 55 | category: { 56 | type: "string", 57 | description: "Category of the instrument (spot, linear, inverse)", 58 | enum: ["spot", "linear", "inverse"], 59 | }, 60 | limit: { 61 | type: "string", 62 | description: "Limit for the number of bids and asks (1, 25, 50, 100, 200)", 63 | enum: ["1", "25", "50", "100", "200"], 64 | }, 65 | }, 66 | required: ["symbol"], 67 | }, 68 | } 69 | 70 | private async getOrderbookData( 71 | symbol: string, 72 | category: "spot" | "linear" | "inverse", 73 | limit: string 74 | ): Promise<APIResponseV3WithTime<OrderbookData>> { 75 | const params: GetOrderbookParamsV5 = { 76 | category, 77 | symbol, 78 | limit: parseInt(limit, 10), 79 | } 80 | this.logInfo(`Fetching orderbook with params: ${JSON.stringify(params)}`) 81 | return await this.client.getOrderbook(params) 82 | } 83 | 84 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 85 | try { 86 | this.logInfo("Starting get_orderbook tool call") 87 | 88 | // Parse and validate input 89 | const validationResult = inputSchema.safeParse(request.params.arguments) 90 | if (!validationResult.success) { 91 | throw new Error(`Invalid input: ${JSON.stringify(validationResult.error.errors)}`) 92 | } 93 | 94 | const { 95 | symbol, 96 | category = CONSTANTS.DEFAULT_CATEGORY as "spot" | "linear" | "inverse", 97 | limit = "25" 98 | } = validationResult.data 99 | 100 | this.logInfo(`Validated arguments - symbol: ${symbol}, category: ${category}, limit: ${limit}`) 101 | 102 | // Execute API request with rate limiting and retry logic 103 | const response = await this.executeRequest(async () => { 104 | const data = await this.getOrderbookData(symbol, category, limit) 105 | return data 106 | }) 107 | 108 | // Format response 109 | const result = { 110 | symbol, 111 | category, 112 | limit: parseInt(limit, 10), 113 | asks: response.a, 114 | bids: response.b, 115 | timestamp: response.ts, 116 | updateId: response.u, 117 | meta: { 118 | requestId: crypto.randomUUID() 119 | } 120 | } 121 | 122 | this.logInfo(`Successfully retrieved orderbook data for ${symbol}`) 123 | return this.formatResponse(result) 124 | } catch (error) { 125 | this.logInfo(`Error in get_orderbook: ${error instanceof Error ? error.message : String(error)}`) 126 | return this.handleError(error) 127 | } 128 | } 129 | } 130 | 131 | export default GetOrderbook 132 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js" 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | LoggingLevel, 9 | // SetLevelRequest 10 | } from "@modelcontextprotocol/sdk/types.js" 11 | import { z } from "zod" 12 | import { CONSTANTS } from "./constants.js" 13 | import { loadTools, createToolsMap } from "./utils/toolLoader.js" 14 | import { validateEnv } from "./env.js" 15 | 16 | // Standard JSON-RPC error codes 17 | const PARSE_ERROR = -32700 18 | const INVALID_REQUEST = -32600 19 | const METHOD_NOT_FOUND = -32601 20 | const INVALID_PARAMS = -32602 21 | const INTERNAL_ERROR = -32603 22 | 23 | const { PROJECT_NAME, PROJECT_VERSION } = CONSTANTS 24 | 25 | let toolsMap: Map<string, any> 26 | 27 | // Create schema for logging request 28 | const LoggingRequestSchema = z.object({ 29 | method: z.literal("logging/setLevel"), 30 | params: z.object({ 31 | level: z.enum([ 32 | "debug", 33 | "info", 34 | "notice", 35 | "warning", 36 | "error", 37 | "critical", 38 | "alert", 39 | "emergency" 40 | ] as const) 41 | }) 42 | }) 43 | 44 | const server = new Server( 45 | { 46 | name: PROJECT_NAME, 47 | version: PROJECT_VERSION, 48 | }, 49 | { 50 | capabilities: { 51 | tools: { 52 | listChanged: true 53 | }, 54 | resources: { 55 | subscribe: true, 56 | listChanged: true 57 | }, 58 | prompts: { 59 | listChanged: true 60 | }, 61 | logging: {} 62 | }, 63 | } 64 | ) 65 | 66 | // Set up logging handler 67 | server.setRequestHandler(LoggingRequestSchema, async (request) => { 68 | const level = request.params.level 69 | // Configure logging level 70 | return {} 71 | }) 72 | 73 | server.setRequestHandler(ListToolsRequestSchema, async () => { 74 | if (!toolsMap || toolsMap.size === 0) { 75 | return { tools: [] } 76 | } 77 | return { 78 | tools: Array.from(toolsMap.values()).map((tool) => tool.toolDefinition), 79 | } 80 | }) 81 | 82 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 83 | try { 84 | if (!toolsMap) { 85 | throw new Error("Tools not initialized") 86 | } 87 | 88 | const tool = toolsMap.get(request.params.name) 89 | if (!tool) { 90 | throw { 91 | code: METHOD_NOT_FOUND, 92 | message: `Unknown tool: ${request.params.name}. Available tools: ${Array.from( 93 | toolsMap.keys() 94 | ).join(", ")}` 95 | } 96 | } 97 | 98 | if (!request.params.arguments || typeof request.params.arguments !== 'object') { 99 | throw { 100 | code: INVALID_PARAMS, 101 | message: "Invalid or missing arguments" 102 | } 103 | } 104 | 105 | return tool.toolCall(request) 106 | } catch (error: any) { 107 | if (error.code) { 108 | throw error 109 | } 110 | throw { 111 | code: INTERNAL_ERROR, 112 | message: error instanceof Error ? error.message : String(error) 113 | } 114 | } 115 | }) 116 | 117 | function formatJsonRpcMessage(level: LoggingLevel, message: string) { 118 | return { 119 | jsonrpc: "2.0", 120 | method: "notifications/message", 121 | params: { 122 | level, 123 | message, 124 | logger: "bybit-mcp" 125 | }, 126 | } 127 | } 128 | 129 | async function main() { 130 | try { 131 | // Validate environment configuration 132 | validateEnv() 133 | 134 | const tools = await loadTools() 135 | toolsMap = createToolsMap(tools) 136 | 137 | if (tools.length === 0) { 138 | console.log(JSON.stringify(formatJsonRpcMessage( 139 | "warning", 140 | "No tools were loaded. Server will start but may have limited functionality." 141 | ))) 142 | } else { 143 | console.log(JSON.stringify(formatJsonRpcMessage( 144 | "info", 145 | `Loaded ${tools.length} tools: ${tools.map(t => t.name).join(", ")}` 146 | ))) 147 | } 148 | 149 | const transport = new StdioServerTransport() 150 | await server.connect(transport) 151 | 152 | console.log(JSON.stringify(formatJsonRpcMessage( 153 | "info", 154 | "Server started successfully" 155 | ))) 156 | } catch (error) { 157 | console.error(JSON.stringify(formatJsonRpcMessage( 158 | "error", 159 | error instanceof Error ? error.message : String(error) 160 | ))) 161 | process.exit(1) 162 | } 163 | } 164 | 165 | process.on("unhandledRejection", (error) => { 166 | console.error(JSON.stringify(formatJsonRpcMessage( 167 | "error", 168 | error instanceof Error ? error.message : String(error) 169 | ))) 170 | }) 171 | 172 | main().catch((error) => { 173 | console.error(JSON.stringify(formatJsonRpcMessage( 174 | "error", 175 | error instanceof Error ? error.message : String(error) 176 | ))) 177 | process.exit(1) 178 | }) 179 | ``` -------------------------------------------------------------------------------- /src/tools/GetKline.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetKlineParamsV5, 9 | } from "bybit-api" 10 | 11 | type SupportedCategory = "spot" | "linear" | "inverse" 12 | type Interval = "1" | "3" | "5" | "15" | "30" | "60" | "120" | "240" | "360" | "720" | "D" | "M" | "W" 13 | 14 | // Zod schema for input validation 15 | const inputSchema = z.object({ 16 | symbol: z.string().min(1, "Symbol is required"), 17 | category: z.enum(["spot", "linear", "inverse"]).optional(), 18 | interval: z.enum(["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "M", "W"]).optional(), 19 | limit: z.number().min(1).max(1000).optional().default(200), 20 | includeReferenceId: z.boolean().optional().default(false) 21 | }) 22 | 23 | type ToolArguments = z.infer<typeof inputSchema> 24 | 25 | class GetKline extends BaseToolImplementation { 26 | name = "get_kline"; 27 | toolDefinition: Tool = { 28 | name: this.name, 29 | description: "Get kline/candlestick data for a trading pair. Supports optional reference ID for data verification.", 30 | inputSchema: { 31 | type: "object", 32 | properties: { 33 | symbol: { 34 | type: "string", 35 | description: "Trading pair symbol (e.g., 'BTCUSDT')", 36 | }, 37 | category: { 38 | type: "string", 39 | description: "Category of the instrument (spot, linear, inverse)", 40 | enum: ["spot", "linear", "inverse"], 41 | }, 42 | interval: { 43 | type: "string", 44 | description: "Kline interval", 45 | enum: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "M", "W"], 46 | }, 47 | limit: { 48 | type: "number", 49 | description: "Limit for the number of candles (max 1000)", 50 | minimum: 1, 51 | maximum: 1000, 52 | }, 53 | includeReferenceId: { 54 | type: "boolean", 55 | description: "Include reference ID and metadata for data verification (default: false)", 56 | } 57 | }, 58 | required: ["symbol"], 59 | }, 60 | }; 61 | 62 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 63 | try { 64 | this.logInfo("Starting get_kline tool call") 65 | 66 | // Parse and validate input 67 | const validationResult = inputSchema.safeParse(request.params.arguments) 68 | if (!validationResult.success) { 69 | const errorDetails = validationResult.error.errors.map(err => ({ 70 | field: err.path.join('.'), 71 | message: err.message, 72 | code: err.code 73 | })) 74 | throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`) 75 | } 76 | 77 | const { 78 | symbol, 79 | category = CONSTANTS.DEFAULT_CATEGORY as SupportedCategory, 80 | interval = CONSTANTS.DEFAULT_INTERVAL as Interval, 81 | limit, 82 | includeReferenceId 83 | } = validationResult.data 84 | 85 | this.logInfo(`Validated arguments - symbol: ${symbol}, category: ${category}, interval: ${interval}, limit: ${limit}, includeReferenceId: ${includeReferenceId}`) 86 | 87 | const params: GetKlineParamsV5 = { 88 | category, 89 | symbol, 90 | interval, 91 | limit, 92 | } 93 | 94 | // Execute API request with rate limiting and retry logic 95 | const response = await this.executeRequest(async () => { 96 | return await this.client.getKline(params) 97 | }) 98 | 99 | // Transform the kline data into a more readable format 100 | const formattedKlines = response.list.map(kline => ({ 101 | timestamp: kline[0], 102 | open: kline[1], 103 | high: kline[2], 104 | low: kline[3], 105 | close: kline[4], 106 | volume: kline[5], 107 | turnover: kline[6] 108 | })) 109 | 110 | const result = { 111 | symbol, 112 | category, 113 | interval, 114 | limit, 115 | data: formattedKlines 116 | } 117 | 118 | // Add reference metadata if requested 119 | const resultWithMetadata = this.addReferenceMetadata( 120 | result, 121 | includeReferenceId, 122 | this.name, 123 | `/v5/market/kline` 124 | ) 125 | 126 | this.logInfo(`Successfully retrieved kline data for ${symbol}`) 127 | return this.formatResponse(resultWithMetadata) 128 | } catch (error) { 129 | this.logInfo(`Error in get_kline: ${error instanceof Error ? error.message : String(error)}`) 130 | return this.handleError(error) 131 | } 132 | } 133 | } 134 | 135 | export default GetKline 136 | ``` -------------------------------------------------------------------------------- /src/tools/GetInstrumentInfo.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetInstrumentsInfoParamsV5, 9 | SpotInstrumentInfoV5, 10 | LinearInverseInstrumentInfoV5 11 | } from "bybit-api" 12 | 13 | type SupportedCategory = "spot" | "linear" | "inverse" 14 | 15 | class GetInstrumentInfo extends BaseToolImplementation { 16 | name = "get_instrument_info"; 17 | toolDefinition: Tool = { 18 | name: this.name, 19 | description: "Get detailed instrument information for a specific trading pair", 20 | inputSchema: { 21 | type: "object", 22 | properties: { 23 | symbol: { 24 | type: "string", 25 | description: "Trading pair symbol (e.g., 'BTCUSDT')", 26 | }, 27 | category: { 28 | type: "string", 29 | description: "Category of the instrument (spot, linear, inverse)", 30 | enum: ["spot", "linear", "inverse"], 31 | }, 32 | }, 33 | required: ["symbol"], 34 | }, 35 | }; 36 | 37 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 38 | try { 39 | const args = request.params.arguments as unknown 40 | if (!args || typeof args !== 'object') { 41 | throw new Error("Invalid arguments") 42 | } 43 | 44 | const typedArgs = args as Record<string, unknown> 45 | 46 | if (!typedArgs.symbol || typeof typedArgs.symbol !== 'string') { 47 | throw new Error("Missing or invalid symbol parameter") 48 | } 49 | 50 | const symbol = typedArgs.symbol 51 | const category = ( 52 | typedArgs.category && 53 | typeof typedArgs.category === 'string' && 54 | ["spot", "linear", "inverse"].includes(typedArgs.category) 55 | ) ? typedArgs.category as SupportedCategory 56 | : CONSTANTS.DEFAULT_CATEGORY as SupportedCategory 57 | 58 | const params: GetInstrumentsInfoParamsV5 = { 59 | category, 60 | symbol, 61 | } 62 | 63 | const response = await this.client.getInstrumentsInfo(params) 64 | 65 | if (response.retCode !== 0) { 66 | throw new Error(`Bybit API error: ${response.retMsg}`) 67 | } 68 | 69 | if (!response.result.list || response.result.list.length === 0) { 70 | throw new Error(`No instrument info found for symbol: ${symbol}`) 71 | } 72 | 73 | const info = response.result.list[0] 74 | let formattedInfo: any 75 | 76 | if (category === 'spot') { 77 | const spotInfo = info as SpotInstrumentInfoV5 78 | formattedInfo = { 79 | symbol: spotInfo.symbol, 80 | status: spotInfo.status, 81 | baseCoin: spotInfo.baseCoin, 82 | quoteCoin: spotInfo.quoteCoin, 83 | innovation: spotInfo.innovation === "1", 84 | marginTrading: spotInfo.marginTrading, 85 | lotSizeFilter: { 86 | basePrecision: spotInfo.lotSizeFilter.basePrecision, 87 | quotePrecision: spotInfo.lotSizeFilter.quotePrecision, 88 | minOrderQty: spotInfo.lotSizeFilter.minOrderQty, 89 | maxOrderQty: spotInfo.lotSizeFilter.maxOrderQty, 90 | minOrderAmt: spotInfo.lotSizeFilter.minOrderAmt, 91 | maxOrderAmt: spotInfo.lotSizeFilter.maxOrderAmt, 92 | }, 93 | priceFilter: { 94 | tickSize: spotInfo.priceFilter.tickSize, 95 | }, 96 | } 97 | } else { 98 | const futuresInfo = info as LinearInverseInstrumentInfoV5 99 | formattedInfo = { 100 | symbol: futuresInfo.symbol, 101 | status: futuresInfo.status, 102 | baseCoin: futuresInfo.baseCoin, 103 | quoteCoin: futuresInfo.quoteCoin, 104 | settleCoin: futuresInfo.settleCoin, 105 | contractType: futuresInfo.contractType, 106 | launchTime: futuresInfo.launchTime, 107 | deliveryTime: futuresInfo.deliveryTime, 108 | deliveryFeeRate: futuresInfo.deliveryFeeRate, 109 | priceFilter: { 110 | tickSize: futuresInfo.priceFilter.tickSize, 111 | }, 112 | lotSizeFilter: { 113 | qtyStep: futuresInfo.lotSizeFilter.qtyStep, 114 | minOrderQty: futuresInfo.lotSizeFilter.minOrderQty, 115 | maxOrderQty: futuresInfo.lotSizeFilter.maxOrderQty, 116 | }, 117 | leverageFilter: { 118 | minLeverage: futuresInfo.leverageFilter.minLeverage, 119 | maxLeverage: futuresInfo.leverageFilter.maxLeverage, 120 | leverageStep: futuresInfo.leverageFilter.leverageStep, 121 | }, 122 | fundingInterval: futuresInfo.fundingInterval, 123 | } 124 | } 125 | 126 | // Add category and timestamp to the root level 127 | formattedInfo.category = category 128 | formattedInfo.retrievedAt = new Date().toISOString() 129 | 130 | return this.formatResponse(formattedInfo) 131 | } catch (error) { 132 | return this.handleError(error) 133 | } 134 | } 135 | } 136 | 137 | export default GetInstrumentInfo 138 | ``` -------------------------------------------------------------------------------- /webui/Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # ============================================================================== 2 | # Bybit MCP WebUI - Two-stage Docker Build 3 | # ============================================================================== 4 | # This Dockerfile creates an optimized container that includes both: 5 | # - The Bybit MCP Server (backend API) 6 | # - The WebUI (frontend interface) 7 | # 8 | # Built with Node.js 22 and following Docker best practices for: 9 | # - Two-stage build for smaller final image 10 | # - Layer caching optimization 11 | # - Security hardening 12 | # - Production-ready configuration 13 | # ============================================================================== 14 | 15 | # ============================================================================== 16 | # Stage 1: Build Stage 17 | # ============================================================================== 18 | FROM node:22-alpine AS builder 19 | 20 | # Install system dependencies 21 | RUN apk update && \ 22 | apk upgrade && \ 23 | apk add --no-cache \ 24 | curl \ 25 | ca-certificates && \ 26 | rm -rf /var/cache/apk/* 27 | 28 | # Enable pnpm 29 | ENV PNPM_HOME="/pnpm" 30 | ENV PATH="$PNPM_HOME:$PATH" 31 | RUN corepack enable 32 | 33 | # Set working directory 34 | WORKDIR /app 35 | 36 | # Copy package files for dependency installation 37 | COPY package.json pnpm-lock.yaml* ./ 38 | COPY webui/package.json webui/pnpm-lock.yaml* ./webui/ 39 | 40 | # Install all dependencies (including dev dependencies for building) 41 | # Skip prepare scripts during dependency installation 42 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ 43 | pnpm install --frozen-lockfile --prefer-offline --ignore-scripts 44 | 45 | # Install WebUI dependencies 46 | WORKDIR /app/webui 47 | RUN --mount=type=cache,id=pnpm-webui,target=/pnpm/store \ 48 | pnpm install --frozen-lockfile --prefer-offline 49 | 50 | # Copy source code 51 | WORKDIR /app 52 | COPY . . 53 | 54 | # Build MCP Server (run tsc manually to avoid prepare script issues) 55 | RUN npx tsc && node -e "require('fs').chmodSync('build/index.js', '755')" 56 | 57 | # Build WebUI with environment variables 58 | WORKDIR /app/webui 59 | ARG OLLAMA_HOST 60 | ARG MCP_ENDPOINT 61 | ENV OLLAMA_HOST=${OLLAMA_HOST} 62 | ENV MCP_ENDPOINT=${MCP_ENDPOINT} 63 | RUN pnpm build 64 | 65 | # Install only production dependencies for final stage 66 | WORKDIR /app 67 | RUN --mount=type=cache,id=pnpm-prod,target=/pnpm/store \ 68 | pnpm install --frozen-lockfile --prod --prefer-offline --ignore-scripts 69 | 70 | # ============================================================================== 71 | # Stage 2: Production Image 72 | # ============================================================================== 73 | FROM node:22-alpine AS production 74 | 75 | # Install system dependencies and security updates 76 | RUN apk update && \ 77 | apk upgrade && \ 78 | apk add --no-cache \ 79 | tini \ 80 | curl \ 81 | ca-certificates && \ 82 | rm -rf /var/cache/apk/* 83 | 84 | # Create non-root user for security 85 | RUN addgroup -g 1001 -S nodejs && \ 86 | adduser -S bybit -u 1001 -G nodejs 87 | 88 | # Set production environment 89 | ENV NODE_ENV=production 90 | ENV PORT=8080 91 | ENV MCP_PORT=8080 92 | ENV MCP_HTTP_PORT=8080 93 | ENV MCP_HTTP_HOST=0.0.0.0 94 | ENV HOST=0.0.0.0 95 | 96 | # Enable pnpm 97 | ENV PNPM_HOME="/pnpm" 98 | ENV PATH="$PNPM_HOME:$PATH" 99 | RUN corepack enable 100 | 101 | # Set working directory 102 | WORKDIR /app 103 | 104 | # Copy production dependencies 105 | COPY --from=builder --chown=bybit:nodejs /app/node_modules ./node_modules 106 | 107 | # Copy built applications 108 | COPY --from=builder --chown=bybit:nodejs /app/build ./build 109 | COPY --from=builder --chown=bybit:nodejs /app/webui/dist ./webui/dist 110 | 111 | # Copy necessary configuration files 112 | COPY --chown=bybit:nodejs package.json ./ 113 | COPY --chown=bybit:nodejs webui/package.json ./webui/ 114 | 115 | # Copy entrypoint and health check scripts 116 | COPY --chown=bybit:nodejs webui/docker-entrypoint.sh ./docker-entrypoint.sh 117 | COPY --chown=bybit:nodejs webui/docker-healthcheck.sh ./docker-healthcheck.sh 118 | 119 | # Make scripts executable 120 | RUN chmod +x ./docker-entrypoint.sh ./docker-healthcheck.sh 121 | 122 | # Switch to non-root user 123 | USER bybit:nodejs 124 | 125 | # Expose port 126 | EXPOSE 8080 127 | 128 | # Add health check 129 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 130 | CMD ./docker-healthcheck.sh 131 | 132 | # Add labels for better container management 133 | LABEL maintainer="Sam McLeod" \ 134 | description="Bybit MCP WebUI - Trading interface with AI chat capabilities" \ 135 | version="1.0.0" \ 136 | org.opencontainers.image.title="Bybit MCP WebUI" \ 137 | org.opencontainers.image.description="Modern web interface for Bybit MCP server with AI chat capabilities" \ 138 | org.opencontainers.image.vendor="Sam McLeod" \ 139 | org.opencontainers.image.version="1.0.0" \ 140 | org.opencontainers.image.source="https://github.com/sammcj/bybit-mcp" \ 141 | org.opencontainers.image.licenses="MIT" 142 | 143 | # Use tini as init system for proper signal handling 144 | ENTRYPOINT ["/sbin/tini", "--"] 145 | 146 | # Start the application 147 | CMD ["./docker-entrypoint.sh"] 148 | ``` -------------------------------------------------------------------------------- /src/tools/GetOrderHistory.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { 6 | // CategoryV5, 7 | GetAccountHistoricOrdersParamsV5, 8 | } from "bybit-api" 9 | 10 | type SupportedCategory = "spot" | "linear" | "inverse" 11 | type OrderStatus = "Created" | "New" | "Rejected" | "PartiallyFilled" | "PartiallyFilledCanceled" | "Filled" | "Cancelled" | "Untriggered" | "Triggered" | "Deactivated" 12 | type OrderFilter = "Order" | "StopOrder" 13 | 14 | class GetOrderHistory extends BaseToolImplementation { 15 | name = "get_order_history"; 16 | toolDefinition: Tool = { 17 | name: this.name, 18 | description: "Get order history for the authenticated user", 19 | inputSchema: { 20 | type: "object", 21 | properties: { 22 | category: { 23 | type: "string", 24 | description: "Product type", 25 | enum: ["spot", "linear", "inverse"], 26 | default: "spot", 27 | }, 28 | symbol: { 29 | type: "string", 30 | description: "Trading symbol, e.g., BTCUSDT", 31 | }, 32 | baseCoin: { 33 | type: "string", 34 | description: "Base coin. Used to get all symbols with this base coin", 35 | }, 36 | orderId: { 37 | type: "string", 38 | description: "Order ID", 39 | }, 40 | orderLinkId: { 41 | type: "string", 42 | description: "User customised order ID", 43 | }, 44 | orderStatus: { 45 | type: "string", 46 | description: "Order status", 47 | enum: ["Created", "New", "Rejected", "PartiallyFilled", "PartiallyFilledCanceled", "Filled", "Cancelled", "Untriggered", "Triggered", "Deactivated"], 48 | }, 49 | orderFilter: { 50 | type: "string", 51 | description: "Order filter", 52 | enum: ["Order", "StopOrder"], 53 | }, 54 | limit: { 55 | type: "string", 56 | description: "Maximum number of results (default: 200)", 57 | enum: ["1", "10", "50", "100", "200"], 58 | }, 59 | }, 60 | required: ["category"], 61 | }, 62 | }; 63 | 64 | async toolCall(request: z.infer<typeof CallToolRequestSchema>) { 65 | try { 66 | const args = request.params.arguments as unknown 67 | if (!args || typeof args !== 'object') { 68 | throw new Error("Invalid arguments") 69 | } 70 | 71 | const typedArgs = args as Record<string, unknown> 72 | 73 | if (!typedArgs.category || typeof typedArgs.category !== 'string' || !["spot", "linear", "inverse"].includes(typedArgs.category)) { 74 | throw new Error("Missing or invalid category parameter") 75 | } 76 | 77 | const category = typedArgs.category as SupportedCategory 78 | const symbol = typedArgs.symbol && typeof typedArgs.symbol === 'string' 79 | ? typedArgs.symbol 80 | : undefined 81 | const baseCoin = typedArgs.baseCoin && typeof typedArgs.baseCoin === 'string' 82 | ? typedArgs.baseCoin 83 | : undefined 84 | const orderId = typedArgs.orderId && typeof typedArgs.orderId === 'string' 85 | ? typedArgs.orderId 86 | : undefined 87 | const orderLinkId = typedArgs.orderLinkId && typeof typedArgs.orderLinkId === 'string' 88 | ? typedArgs.orderLinkId 89 | : undefined 90 | const orderStatus = ( 91 | typedArgs.orderStatus && 92 | typeof typedArgs.orderStatus === 'string' && 93 | ["Created", "New", "Rejected", "PartiallyFilled", "PartiallyFilledCanceled", "Filled", "Cancelled", "Untriggered", "Triggered", "Deactivated"].includes(typedArgs.orderStatus) 94 | ) ? typedArgs.orderStatus as OrderStatus 95 | : undefined 96 | const orderFilter = ( 97 | typedArgs.orderFilter && 98 | typeof typedArgs.orderFilter === 'string' && 99 | ["Order", "StopOrder"].includes(typedArgs.orderFilter) 100 | ) ? typedArgs.orderFilter as OrderFilter 101 | : undefined 102 | const limit = ( 103 | typedArgs.limit && 104 | typeof typedArgs.limit === 'string' && 105 | ["1", "10", "50", "100", "200"].includes(typedArgs.limit) 106 | ) ? parseInt(typedArgs.limit, 10) : 200 107 | 108 | const params: GetAccountHistoricOrdersParamsV5 = { 109 | category, 110 | symbol, 111 | baseCoin, 112 | orderId, 113 | orderLinkId, 114 | orderStatus, 115 | orderFilter, 116 | limit, 117 | } 118 | 119 | const response = await this.client.getHistoricOrders(params) 120 | 121 | if (response.retCode !== 0) { 122 | throw new Error(`Bybit API error: ${response.retMsg}`) 123 | } 124 | 125 | return this.formatResponse({ 126 | category, 127 | symbol, 128 | baseCoin, 129 | orderId, 130 | orderLinkId, 131 | orderStatus, 132 | orderFilter, 133 | limit, 134 | data: response.result.list, 135 | retrievedAt: new Date().toISOString(), 136 | }) 137 | } catch (error) { 138 | return this.handleError(error) 139 | } 140 | } 141 | } 142 | 143 | export default GetOrderHistory 144 | ``` -------------------------------------------------------------------------------- /src/tools/GetTicker.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js" 2 | import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js" 3 | import { z } from "zod" 4 | import { BaseToolImplementation } from "./BaseTool.js" 5 | import { CONSTANTS } from "../constants.js" 6 | import { 7 | // CategoryV5, 8 | GetTickersParamsV5, 9 | TickerSpotV5, 10 | TickerLinearInverseV5, 11 | APIResponseV3WithTime, 12 | CategoryListV5 13 | } from "bybit-api" 14 | 15 | // Zod schema for input validation 16 | const inputSchema = z.object({ 17 | symbol: z.string() 18 | .min(1, "Symbol is required") 19 | .regex(/^[A-Z0-9]+$/, "Symbol must contain only uppercase letters and numbers"), 20 | category: z.enum(["spot", "linear", "inverse"]).optional(), 21 | includeReferenceId: z.boolean().optional().default(false) 22 | }) 23 | 24 | type SupportedCategory = z.infer<typeof inputSchema>["category"] 25 | type ToolArguments = z.infer<typeof inputSchema> 26 | 27 | class GetTicker extends BaseToolImplementation { 28 | name = "get_ticker" 29 | toolDefinition: Tool = { 30 | name: this.name, 31 | description: "Get real-time ticker information for a trading pair. Supports optional reference ID for data verification.", 32 | inputSchema: { 33 | type: "object", 34 | properties: { 35 | symbol: { 36 | type: "string", 37 | description: "Trading pair symbol (e.g., 'BTCUSDT')", 38 | pattern: "^[A-Z0-9]+$", 39 | annotations: { 40 | priority: 1 // Required parameter 41 | } 42 | }, 43 | category: { 44 | type: "string", 45 | description: "Category of the instrument (spot, linear, inverse)", 46 | enum: ["spot", "linear", "inverse"], 47 | annotations: { 48 | priority: 0 // Optional parameter 49 | } 50 | }, 51 | includeReferenceId: { 52 | type: "boolean", 53 | description: "Include reference ID and metadata for data verification (default: false)", 54 | annotations: { 55 | priority: 0 // Optional parameter 56 | } 57 | } 58 | }, 59 | required: ["symbol"] 60 | } 61 | } 62 | 63 | private async getTickerData( 64 | symbol: string, 65 | category: "spot" | "linear" | "inverse" 66 | ): Promise<APIResponseV3WithTime<CategoryListV5<TickerSpotV5[] | TickerLinearInverseV5[], typeof category>>> { 67 | if (category === "spot") { 68 | const params: GetTickersParamsV5<"spot"> = { 69 | category: "spot", 70 | symbol 71 | } 72 | return await this.client.getTickers(params) 73 | } else { 74 | const params: GetTickersParamsV5<"linear" | "inverse"> = { 75 | category: category, 76 | symbol 77 | } 78 | return await this.client.getTickers(params) 79 | } 80 | } 81 | 82 | async toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult> { 83 | try { 84 | this.logInfo("Starting get_ticker tool call") 85 | 86 | // Parse and validate input 87 | const validationResult = inputSchema.safeParse(request.params.arguments) 88 | if (!validationResult.success) { 89 | const errorDetails = validationResult.error.errors.map(err => ({ 90 | field: err.path.join('.'), 91 | message: err.message, 92 | code: err.code 93 | })) 94 | throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`) 95 | } 96 | 97 | const { symbol, category = CONSTANTS.DEFAULT_CATEGORY as "spot" | "linear" | "inverse", includeReferenceId } = validationResult.data 98 | this.logInfo(`Validated arguments - symbol: ${symbol}, category: ${category}, includeReferenceId: ${includeReferenceId}`) 99 | 100 | // Execute API request with rate limiting and retry logic 101 | const response = await this.executeRequest(async () => { 102 | return await this.getTickerData(symbol, category) 103 | }) 104 | 105 | // Extract the first ticker from the list 106 | const ticker = response.list[0] 107 | if (!ticker) { 108 | throw new Error(`No ticker data found for ${symbol}`) 109 | } 110 | 111 | // Format response with lastPrice at root level 112 | const baseResult = { 113 | timestamp: new Date().toISOString(), 114 | meta: { 115 | requestId: crypto.randomUUID() 116 | }, 117 | symbol, 118 | category, 119 | lastPrice: ticker.lastPrice, 120 | price24hPcnt: ticker.price24hPcnt, 121 | highPrice24h: ticker.highPrice24h, 122 | lowPrice24h: ticker.lowPrice24h, 123 | prevPrice24h: ticker.prevPrice24h, 124 | volume24h: ticker.volume24h, 125 | turnover24h: ticker.turnover24h, 126 | bid1Price: ticker.bid1Price, 127 | bid1Size: ticker.bid1Size, 128 | ask1Price: ticker.ask1Price, 129 | ask1Size: ticker.ask1Size 130 | } 131 | 132 | // Add reference metadata if requested 133 | const resultWithMetadata = this.addReferenceMetadata( 134 | baseResult, 135 | includeReferenceId, 136 | this.name, 137 | `/v5/market/tickers` 138 | ) 139 | 140 | this.logInfo(`Successfully retrieved ticker data for ${symbol}`) 141 | return this.formatResponse(resultWithMetadata) 142 | } catch (error) { 143 | this.logInfo(`Error in get_ticker: ${error instanceof Error ? error.message : String(error)}`) 144 | return this.handleError(error) 145 | } 146 | } 147 | } 148 | 149 | export default GetTicker 150 | ``` -------------------------------------------------------------------------------- /webui/src/styles/data-cards.css: -------------------------------------------------------------------------------- ```css 1 | /* Data Cards - Expandable cards for visualising tool response data */ 2 | 3 | .data-card { 4 | background: var(--bg-elevated); 5 | border: 1px solid var(--border-primary); 6 | border-radius: var(--radius-lg); 7 | margin: var(--spacing-sm) 0; 8 | overflow: hidden; 9 | transition: all var(--transition-normal); 10 | box-shadow: var(--shadow-sm); 11 | } 12 | 13 | .data-card:hover { 14 | border-color: var(--border-secondary); 15 | box-shadow: var(--shadow-md); 16 | } 17 | 18 | .data-card.expanded { 19 | border-color: var(--color-primary); 20 | } 21 | 22 | /* Card Header */ 23 | .data-card-header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | padding: var(--spacing-md); 28 | cursor: pointer; 29 | user-select: none; 30 | transition: background-color var(--transition-fast); 31 | } 32 | 33 | .data-card-header:hover { 34 | background: var(--bg-secondary); 35 | } 36 | 37 | .data-card-header:focus { 38 | outline: 2px solid var(--color-primary); 39 | outline-offset: -2px; 40 | } 41 | 42 | .data-card-title { 43 | display: flex; 44 | align-items: center; 45 | gap: var(--spacing-sm); 46 | flex: 1; 47 | } 48 | 49 | .data-card-icon { 50 | font-size: 1.2em; 51 | opacity: 0.8; 52 | } 53 | 54 | .data-card-title h4 { 55 | margin: 0; 56 | font-size: var(--font-size-base); 57 | font-weight: var(--font-weight-semibold); 58 | color: var(--text-primary); 59 | } 60 | 61 | .data-card-controls { 62 | display: flex; 63 | align-items: center; 64 | gap: var(--spacing-md); 65 | } 66 | 67 | .data-card-summary { 68 | font-size: var(--font-size-sm); 69 | color: var(--text-secondary); 70 | font-family: var(--font-family-mono); 71 | background: var(--bg-tertiary); 72 | padding: var(--spacing-xs) var(--spacing-sm); 73 | border-radius: var(--radius-sm); 74 | } 75 | 76 | .expand-toggle { 77 | background: none; 78 | border: none; 79 | cursor: pointer; 80 | padding: var(--spacing-xs); 81 | border-radius: var(--radius-sm); 82 | transition: all var(--transition-fast); 83 | display: flex; 84 | align-items: center; 85 | justify-content: center; 86 | min-width: 24px; 87 | min-height: 24px; 88 | } 89 | 90 | .expand-toggle:hover { 91 | background: var(--bg-tertiary); 92 | } 93 | 94 | .expand-toggle:focus { 95 | outline: 2px solid var(--color-primary); 96 | outline-offset: 2px; 97 | } 98 | 99 | .expand-icon { 100 | font-size: var(--font-size-sm); 101 | color: var(--text-secondary); 102 | transition: transform var(--transition-fast); 103 | } 104 | 105 | .data-card.expanded .expand-icon { 106 | transform: rotate(0deg); 107 | } 108 | 109 | .data-card.collapsed .expand-icon { 110 | transform: rotate(-90deg); 111 | } 112 | 113 | /* Card Content */ 114 | .data-card-content { 115 | border-top: 1px solid var(--border-primary); 116 | animation: slideDown var(--transition-normal) ease-out; 117 | } 118 | 119 | .data-card.collapsed .data-card-content { 120 | animation: slideUp var(--transition-normal) ease-out; 121 | } 122 | 123 | .data-card-details { 124 | padding: var(--spacing-md); 125 | } 126 | 127 | .data-preview { 128 | background: var(--bg-secondary); 129 | border: 1px solid var(--border-primary); 130 | border-radius: var(--radius-sm); 131 | padding: var(--spacing-sm); 132 | font-family: var(--font-family-mono); 133 | font-size: var(--font-size-sm); 134 | color: var(--text-secondary); 135 | max-height: 200px; 136 | overflow-y: auto; 137 | white-space: pre-wrap; 138 | word-break: break-word; 139 | } 140 | 141 | .data-card-chart { 142 | padding: var(--spacing-md); 143 | border-top: 1px solid var(--border-primary); 144 | background: var(--bg-secondary); 145 | } 146 | 147 | .chart-placeholder { 148 | text-align: center; 149 | padding: var(--spacing-xl); 150 | color: var(--text-tertiary); 151 | border: 2px dashed var(--border-secondary); 152 | border-radius: var(--radius-md); 153 | background: var(--bg-tertiary); 154 | } 155 | 156 | .chart-placeholder p { 157 | margin: 0 0 var(--spacing-xs) 0; 158 | font-weight: var(--font-weight-medium); 159 | } 160 | 161 | .chart-placeholder small { 162 | font-size: var(--font-size-xs); 163 | opacity: 0.7; 164 | } 165 | 166 | /* Data Type Specific Styling */ 167 | .data-card[data-type="kline"] { 168 | border-left: 4px solid #10b981; 169 | } 170 | 171 | .data-card[data-type="rsi"] { 172 | border-left: 4px solid #6366f1; 173 | } 174 | 175 | .data-card[data-type="orderBlocks"] { 176 | border-left: 4px solid #f59e0b; 177 | } 178 | 179 | .data-card[data-type="price"] { 180 | border-left: 4px solid #ef4444; 181 | } 182 | 183 | .data-card[data-type="volume"] { 184 | border-left: 4px solid #8b5cf6; 185 | } 186 | 187 | /* Animations */ 188 | @keyframes slideDown { 189 | from { 190 | opacity: 0; 191 | max-height: 0; 192 | transform: translateY(-10px); 193 | } 194 | to { 195 | opacity: 1; 196 | max-height: 500px; 197 | transform: translateY(0); 198 | } 199 | } 200 | 201 | @keyframes slideUp { 202 | from { 203 | opacity: 1; 204 | max-height: 500px; 205 | transform: translateY(0); 206 | } 207 | to { 208 | opacity: 0; 209 | max-height: 0; 210 | transform: translateY(-10px); 211 | } 212 | } 213 | 214 | /* Mobile Responsiveness */ 215 | @media (max-width: 768px) { 216 | .data-card-header { 217 | padding: var(--spacing-sm); 218 | } 219 | 220 | .data-card-controls { 221 | gap: var(--spacing-sm); 222 | } 223 | 224 | .data-card-summary { 225 | display: none; /* Hide summary on mobile to save space */ 226 | } 227 | 228 | .data-card-details, 229 | .data-card-chart { 230 | padding: var(--spacing-sm); 231 | } 232 | 233 | .data-preview { 234 | font-size: var(--font-size-xs); 235 | max-height: 150px; 236 | } 237 | } 238 | 239 | /* Accessibility */ 240 | @media (prefers-reduced-motion: reduce) { 241 | .data-card, 242 | .data-card-content, 243 | .expand-toggle, 244 | .expand-icon { 245 | transition: none; 246 | } 247 | 248 | .data-card-content { 249 | animation: none; 250 | } 251 | } 252 | 253 | /* High contrast mode */ 254 | @media (prefers-contrast: high) { 255 | .data-card { 256 | border-width: 2px; 257 | } 258 | 259 | .data-card-header:focus { 260 | outline-width: 3px; 261 | } 262 | 263 | .expand-toggle:focus { 264 | outline-width: 3px; 265 | } 266 | } 267 | ``` -------------------------------------------------------------------------------- /DEV_PLAN.md: -------------------------------------------------------------------------------- ```markdown 1 | # Bybit MCP WebUI Development Plan 2 | 3 | ## 📋 Project Overview 4 | 5 | A modern web interface for the Bybit MCP (Model Context Protocol) server that provides real-time cryptocurrency market data and advanced technical analysis tools through AI-powered chat interactions. 6 | 7 | ### 🎯 Core Concept 8 | 9 | - **AI Chat Interface**: Users ask questions about crypto markets in natural language 10 | - **MCP Integration**: AI automatically calls MCP tools to fetch real-time data from Bybit 11 | - **Data Visualization**: Charts and analysis are displayed alongside chat responses 12 | - **Technical Analysis**: ML-enhanced RSI, Order Blocks, Market Structure analysis 13 | 14 | ## 🚧 What's Remaining 15 | 16 | ### 4. **Enhanced Chat Features** 17 | 18 | **Tasks**: 19 | 20 | - [ ] **Chat history**: Add persisted chat history, we want to keep this very lightweight, simple and fast / low latency, we also don't want it getting too big, so we will limit it to the last 20 messages for up to 20 chats, it shouldn't rely on any external services. 21 | - [ ] Add in-memory caching of tool responses with a default TTL of 3 minutes (configurable) 22 | - [ ] **Chart Integration**: Auto-generate charts when AI mentions price data 23 | - [ ] **Analysis Widgets**: Embed analysis results directly in chat 24 | - [ ] **Message Actions**: Copy, share, regenerate responses 25 | - [ ] **Chat History**: Persistent conversation storage 26 | - [ ] **Quick Actions**: Predefined queries for common tasks 27 | 28 | ### 5. **Data Visualisation Improvements** 29 | 30 | **Tasks**: 31 | 32 | - [ ] **Real-time Updates**: WebSocket or polling for live data 33 | - [ ] **Multiple Symbols**: Support for comparing different cryptocurrencies 34 | - [ ] **Portfolio View**: Track multiple positions and P&L 35 | - [ ] **Alert System**: Price and indicator-based notifications 36 | 37 | ## 🔧 Technical Architecture 38 | 39 | ### **Frontend Stack** 40 | 41 | - **Framework**: Vanilla TypeScript with Vite 42 | - **Styling**: CSS with CSS variables for theming 43 | - **MCP Integration**: Official `@modelcontextprotocol/sdk` with HTTP fallback 44 | - **AI Integration**: OpenAI-compatible API (Ollama) 45 | 46 | ### **Backend Integration** 47 | 48 | - **MCP Server**: Node.js with Express HTTP server 49 | - **API Endpoints**: 50 | - `GET /tools` - List available tools 51 | - `POST /call-tool` - Execute tools 52 | - `GET /health` - Health check 53 | - **Data Source**: Bybit REST API 54 | 55 | ### **Development Setup** 56 | 57 | ```bash 58 | # Terminal 1: Start MCP Server 59 | pnpm run serve:http 60 | 61 | # Terminal 2: Start WebUI 62 | cd webui && pnpm dev 63 | ``` 64 | 65 | ## 🎯 Priority Tasks (Next Sprint) 66 | 67 | ### **High Priority** ✅ **COMPLETED** 68 | 69 | 1. **Charts Implementation** - Core value proposition ✅ 70 | 2. **MCP Tools Tab** - Essential for debugging and exploration ✅ 71 | 3. **Analysis Tab** - Showcase advanced features ✅ 72 | 73 | ### **Medium Priority** 74 | 75 | 1. **Chat Enhancements** - Improve user experience 76 | 2. **Real-time Updates** - Add live data streaming 77 | 78 | ### **Low Priority** 79 | 80 | 1. **Portfolio Features** - Advanced functionality 81 | 2. **Alert System** - Nice-to-have features 82 | 83 | ## 📚 Key Learnings 84 | 85 | ### **MCP Integration Challenges** 86 | 87 | - **Complex Protocol**: Official MCP SDK requires proper session management 88 | - **Solution**: Custom HTTP endpoints provide simpler integration path 89 | - **Hybrid Approach**: Use HTTP for tools, keep MCP protocol for future features 90 | 91 | ### **AI Tool Calling** 92 | 93 | - **Model Compatibility**: Not all models support function calling properly 94 | - **Fallback Strategy**: Text parsing works when native tool calls fail 95 | - **Format Issues**: Ollama expects `arguments` as string, not object 96 | 97 | ### **Development Workflow** 98 | 99 | - **Debug Console**: Essential for troubleshooting complex integrations 100 | - **Real-time Logging**: Dramatically improves development speed 101 | - **Incremental Testing**: Build and test each component separately 102 | 103 | ### **CORS and Proxying** 104 | 105 | - **Development**: Vite proxy handles CORS issues elegantly 106 | - **Production**: Direct API calls work with proper CORS headers 107 | 108 | ## 🚀 Success Metrics 109 | 110 | ### **Functional Goals** 111 | 112 | - [x] AI can fetch real-time crypto prices 113 | - [x] Tool calling works reliably 114 | - [x] Debug information is accessible 115 | - [ ] Charts display live market data 116 | - [ ] Analysis tools provide actionable insights 117 | 118 | ### **User Experience Goals** 119 | 120 | - [x] Intuitive chat interface 121 | - [x] Responsive design 122 | - [x] Error handling and recovery 123 | - [ ] Fast chart rendering 124 | - [ ] Seamless data updates 125 | 126 | ## 📝 Notes for Next Developer 127 | 128 | ### **Immediate Focus** 129 | 130 | Start with the **Charts Tab** as it provides the most user value. The `get_kline` tool is working and returns OHLCV data ready for charting. 131 | 132 | ### **Code Structure** 133 | 134 | - **Services**: Well-organized in `src/services/` 135 | - **Components**: Modular design in `src/components/` 136 | - **Types**: Comprehensive TypeScript definitions 137 | - **Debugging**: Use the debug console (`Ctrl+``) extensively 138 | 139 | ### **Testing Strategy** 140 | 141 | - Use debug console to verify tool calls 142 | - Test with different AI models (current: qwen3-30b) 143 | - Verify MCP server endpoints manually if needed 144 | 145 | ### **Known Working Examples** 146 | 147 | - Ask: "What's the current BTC price?" → Gets real data 148 | - Tool: `get_ticker(symbol="BTCUSDT", category="spot")` → Returns price data 149 | - All 12 MCP tools are loaded and accessible 150 | 151 | The foundation is solid - now it's time to build the visualization layer! 🎨 152 | ``` -------------------------------------------------------------------------------- /webui/src/services/logService.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Log Service - Captures and streams application logs for debugging 3 | */ 4 | 5 | export interface LogEntry { 6 | id: string; 7 | timestamp: number; 8 | level: 'log' | 'info' | 'warn' | 'error' | 'debug'; 9 | message: string; 10 | data?: any; 11 | source?: string; 12 | } 13 | 14 | export class LogService { 15 | private logs: LogEntry[] = []; 16 | private listeners: Set<(logs: LogEntry[]) => void> = new Set(); 17 | private maxLogs = 1000; // Keep last 1000 logs 18 | private originalConsole: { 19 | log: typeof console.log; 20 | info: typeof console.info; 21 | warn: typeof console.warn; 22 | error: typeof console.error; 23 | debug: typeof console.debug; 24 | }; 25 | 26 | constructor() { 27 | // Store original console methods 28 | this.originalConsole = { 29 | log: console.log.bind(console), 30 | info: console.info.bind(console), 31 | warn: console.warn.bind(console), 32 | error: console.error.bind(console), 33 | debug: console.debug.bind(console), 34 | }; 35 | 36 | // Intercept console methods 37 | this.interceptConsole(); 38 | } 39 | 40 | private interceptConsole(): void { 41 | const self = this; 42 | 43 | console.log = function(...args: any[]) { 44 | self.addLog('log', self.formatMessage(args), args.length > 1 ? args.slice(1) : undefined); 45 | self.originalConsole.log(...args); 46 | }; 47 | 48 | console.info = function(...args: any[]) { 49 | self.addLog('info', self.formatMessage(args), args.length > 1 ? args.slice(1) : undefined); 50 | self.originalConsole.info(...args); 51 | }; 52 | 53 | console.warn = function(...args: any[]) { 54 | self.addLog('warn', self.formatMessage(args), args.length > 1 ? args.slice(1) : undefined); 55 | self.originalConsole.warn(...args); 56 | }; 57 | 58 | console.error = function(...args: any[]) { 59 | self.addLog('error', self.formatMessage(args), args.length > 1 ? args.slice(1) : undefined); 60 | self.originalConsole.error(...args); 61 | }; 62 | 63 | console.debug = function(...args: any[]) { 64 | self.addLog('debug', self.formatMessage(args), args.length > 1 ? args.slice(1) : undefined); 65 | self.originalConsole.debug(...args); 66 | }; 67 | } 68 | 69 | private formatMessage(args: any[]): string { 70 | return args.map(arg => { 71 | if (typeof arg === 'string') { 72 | return arg; 73 | } else if (typeof arg === 'object') { 74 | try { 75 | return JSON.stringify(arg, null, 2); 76 | } catch { 77 | return String(arg); 78 | } 79 | } else { 80 | return String(arg); 81 | } 82 | }).join(' '); 83 | } 84 | 85 | private addLog(level: LogEntry['level'], message: string, data?: any): void { 86 | const entry: LogEntry = { 87 | id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, 88 | timestamp: Date.now(), 89 | level, 90 | message, 91 | data, 92 | source: this.getSource() 93 | }; 94 | 95 | this.logs.push(entry); 96 | 97 | // Keep only the last maxLogs entries 98 | if (this.logs.length > this.maxLogs) { 99 | this.logs = this.logs.slice(-this.maxLogs); 100 | } 101 | 102 | // Notify listeners 103 | this.notifyListeners(); 104 | } 105 | 106 | private getSource(): string { 107 | try { 108 | const stack = new Error().stack; 109 | if (stack) { 110 | const lines = stack.split('\n'); 111 | // Find the first line that's not from this service 112 | for (let i = 3; i < lines.length; i++) { 113 | const line = lines[i]; 114 | if (line && !line.includes('logService.ts') && !line.includes('console.')) { 115 | const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/); 116 | if (match) { 117 | const [, , file, lineNum] = match; 118 | const fileName = file.split('/').pop() || file; 119 | return `${fileName}:${lineNum}`; 120 | } 121 | } 122 | } 123 | } 124 | } catch { 125 | // Ignore errors in source detection 126 | } 127 | return 'unknown'; 128 | } 129 | 130 | /** 131 | * Get all logs 132 | */ 133 | getLogs(): LogEntry[] { 134 | return [...this.logs]; 135 | } 136 | 137 | /** 138 | * Get logs filtered by level 139 | */ 140 | getLogsByLevel(levels: LogEntry['level'][]): LogEntry[] { 141 | return this.logs.filter(log => levels.includes(log.level)); 142 | } 143 | 144 | /** 145 | * Clear all logs 146 | */ 147 | clearLogs(): void { 148 | this.logs = []; 149 | this.notifyListeners(); 150 | } 151 | 152 | /** 153 | * Subscribe to log updates 154 | */ 155 | subscribe(listener: (logs: LogEntry[]) => void): () => void { 156 | this.listeners.add(listener); 157 | return () => this.listeners.delete(listener); 158 | } 159 | 160 | /** 161 | * Export logs as text 162 | */ 163 | exportLogs(): string { 164 | return this.logs.map(log => { 165 | const time = new Date(log.timestamp).toISOString(); 166 | const level = log.level.toUpperCase().padEnd(5); 167 | const source = log.source ? ` [${log.source}]` : ''; 168 | return `${time} ${level}${source} ${log.message}`; 169 | }).join('\n'); 170 | } 171 | 172 | /** 173 | * Add a custom log entry 174 | */ 175 | addCustomLog(level: LogEntry['level'], message: string, data?: any): void { 176 | this.addLog(level, message, data); 177 | } 178 | 179 | private notifyListeners(): void { 180 | this.listeners.forEach(listener => { 181 | try { 182 | listener([...this.logs]); 183 | } catch (error) { 184 | this.originalConsole.error('Error in log listener:', error); 185 | } 186 | }); 187 | } 188 | 189 | /** 190 | * Restore original console methods 191 | */ 192 | restore(): void { 193 | console.log = this.originalConsole.log; 194 | console.info = this.originalConsole.info; 195 | console.warn = this.originalConsole.warn; 196 | console.error = this.originalConsole.error; 197 | console.debug = this.originalConsole.debug; 198 | } 199 | } 200 | 201 | // Create singleton instance 202 | export const logService = new LogService(); 203 | 204 | // Cleanup on page unload 205 | if (typeof window !== 'undefined') { 206 | window.addEventListener('beforeunload', () => { 207 | logService.restore(); 208 | }); 209 | } 210 | ```