#
tokens: 48573/50000 54/101 files (page 1/8)
lines: on (toggle) GitHub
raw markdown copy reset
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 | ![screenshot](screenshot.png)
  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 | ![WebUI Screenshot](webui/screenshot.png)
 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 | 
```
Page 1/8FirstPrevNextLast