#
tokens: 43526/50000 12/101 files (page 3/6)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 6. Use http://codebase.md/sammcj/bybit-mcp?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

--------------------------------------------------------------------------------
/webui/src/services/performanceOptimiser.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Performance Optimiser Service - Handles parallel tool execution and workflow optimisation
 */

import type { ToolCall } from '@/types/ai';
import { mcpClient } from './mcpClient';

export interface ToolExecutionPlan {
  parallel: ToolCall[][];  // Groups of tools that can run in parallel
  sequential: ToolCall[];  // Tools that must run sequentially
  dependencies: Map<string, string[]>;  // Tool dependencies
}

export interface ToolExecutionResult {
  toolCall: ToolCall;
  result?: any;
  error?: string;
  duration: number;
  startTime: number;
  endTime: number;
}

export interface ExecutionMetrics {
  totalDuration: number;
  parallelSavings: number;  // Time saved by parallel execution
  toolCount: number;
  successRate: number;
  averageToolTime: number;
}

export class PerformanceOptimiserService {
  private static readonly TOOL_TIMEOUT = 30000; // 30 seconds
  private static readonly MAX_PARALLEL_TOOLS = 5;

  private toolDependencies: Map<string, string[]> = new Map();
  private toolPerformanceCache: Map<string, number> = new Map(); // Average execution times
  private executionHistory: ToolExecutionResult[] = [];

  constructor() {
    this.initializeToolDependencies();
  }

  /**
   * Execute tools with optimal parallelisation
   */
  async executeToolsOptimised(toolCalls: ToolCall[]): Promise<ToolExecutionResult[]> {
    if (toolCalls.length === 0) return [];

    const startTime = Date.now();
    console.log(`🚀 Optimising execution of ${toolCalls.length} tools...`);

    // Create execution plan
    const plan = this.createExecutionPlan(toolCalls);
    console.log(`📋 Execution plan: ${plan.parallel.length} parallel groups, ${plan.sequential.length} sequential tools`);

    const results: ToolExecutionResult[] = [];

    try {
      // Execute parallel groups first
      for (let i = 0; i < plan.parallel.length; i++) {
        const group = plan.parallel[i];
        console.log(`⚡ Executing parallel group ${i + 1}/${plan.parallel.length} with ${group.length} tools`);

        const groupResults = await this.executeToolsInParallel(group);
        results.push(...groupResults);
      }

      // Execute sequential tools
      if (plan.sequential.length > 0) {
        console.log(`🔄 Executing ${plan.sequential.length} sequential tools`);
        for (const toolCall of plan.sequential) {
          const result = await this.executeSingleTool(toolCall);
          results.push(result);
        }
      }

      // Update performance metrics
      this.updatePerformanceMetrics(results);

      const totalDuration = Date.now() - startTime;
      console.log(`✅ Optimised execution completed in ${totalDuration}ms`);

      return results;

    } catch (error) {
      console.error('❌ Optimised execution failed:', error);
      throw error;
    }
  }

  /**
   * Create execution plan based on tool dependencies and characteristics
   */
  private createExecutionPlan(toolCalls: ToolCall[]): ToolExecutionPlan {
    const plan: ToolExecutionPlan = {
      parallel: [],
      sequential: [],
      dependencies: new Map()
    };

    // Analyse tool dependencies
    const dependencyGraph = this.buildDependencyGraph(toolCalls);

    // Group tools by dependency levels
    const processed = new Set<string>();
    const remaining = [...toolCalls];

    while (remaining.length > 0) {
      // Find tools with no unprocessed dependencies
      const readyTools = remaining.filter(tool => {
        const deps = dependencyGraph.get(tool.function.name) || [];
        return deps.every(dep => processed.has(dep));
      });

      if (readyTools.length === 0) {
        // No tools ready - add remaining to sequential (fallback)
        plan.sequential.push(...remaining);
        break;
      }

      // Group ready tools for parallel execution
      const parallelGroup = this.groupForParallelExecution(readyTools);

      if (parallelGroup.length > 1) {
        plan.parallel.push(parallelGroup);
      } else {
        plan.sequential.push(...parallelGroup);
      }

      // Mark tools as processed
      parallelGroup.forEach(tool => {
        processed.add(tool.function.name);
        const index = remaining.findIndex(t => t.id === tool.id);
        if (index >= 0) remaining.splice(index, 1);
      });
    }

    return plan;
  }

  /**
   * Build dependency graph for tools
   */
  private buildDependencyGraph(toolCalls: ToolCall[]): Map<string, string[]> {
    const graph = new Map<string, string[]>();

    for (const toolCall of toolCalls) {
      const toolName = toolCall.function.name;
      const dependencies = this.toolDependencies.get(toolName) || [];

      // Filter dependencies to only include tools in current execution
      const relevantDeps = dependencies.filter(dep =>
        toolCalls.some(tc => tc.function.name === dep)
      );

      graph.set(toolName, relevantDeps);
    }

    return graph;
  }

  /**
   * Group tools for optimal parallel execution
   */
  private groupForParallelExecution(tools: ToolCall[]): ToolCall[] {
    // Prioritise by estimated execution time (faster tools first)
    const sortedTools = tools.sort((a, b) => {
      const timeA = this.getEstimatedExecutionTime(a.function.name);
      const timeB = this.getEstimatedExecutionTime(b.function.name);
      return timeA - timeB;
    });

    // Limit parallel execution to avoid overwhelming the system
    return sortedTools.slice(0, PerformanceOptimiserService.MAX_PARALLEL_TOOLS);
  }

  /**
   * Execute multiple tools in parallel
   */
  private async executeToolsInParallel(toolCalls: ToolCall[]): Promise<ToolExecutionResult[]> {
    const promises = toolCalls.map(toolCall => this.executeSingleTool(toolCall));

    try {
      const results = await Promise.allSettled(promises);

      return results.map((result, index) => {
        if (result.status === 'fulfilled') {
          return result.value;
        } else {
          // Handle rejected promise
          const toolCall = toolCalls[index];
          return {
            toolCall,
            error: result.reason?.message || 'Unknown error',
            duration: 0,
            startTime: Date.now(),
            endTime: Date.now()
          };
        }
      });

    } catch (error) {
      console.error('Parallel execution error:', error);
      throw error;
    }
  }

  /**
   * Execute a single tool with timing
   */
  private async executeSingleTool(toolCall: ToolCall): Promise<ToolExecutionResult> {
    const startTime = Date.now();

    try {
      console.log(`🔧 Executing tool: ${toolCall.function.name}`);

      // Create timeout promise
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Tool execution timeout')), PerformanceOptimiserService.TOOL_TIMEOUT);
      });

      // Execute tool with timeout
      const resultPromise = mcpClient.callTool(toolCall.function.name as any, toolCall.function.arguments as any);
      const result = await Promise.race([resultPromise, timeoutPromise]);

      const endTime = Date.now();
      const duration = endTime - startTime;

      console.log(`✅ Tool ${toolCall.function.name} completed in ${duration}ms`);

      const executionResult: ToolExecutionResult = {
        toolCall,
        result,
        duration,
        startTime,
        endTime
      };

      // Cache performance data
      this.updateToolPerformanceCache(toolCall.function.name, duration);
      this.executionHistory.push(executionResult);

      return executionResult;

    } catch (error) {
      const endTime = Date.now();
      const duration = endTime - startTime;

      console.error(`❌ Tool ${toolCall.function.name} failed after ${duration}ms:`, error);

      const executionResult: ToolExecutionResult = {
        toolCall,
        error: error instanceof Error ? error.message : 'Unknown error',
        duration,
        startTime,
        endTime
      };

      this.executionHistory.push(executionResult);

      return executionResult;
    }
  }

  /**
   * Get estimated execution time for a tool
   */
  private getEstimatedExecutionTime(toolName: string): number {
    // Return cached average or default estimate
    return this.toolPerformanceCache.get(toolName) || 5000; // Default 5 seconds
  }

  /**
   * Update tool performance cache
   */
  private updateToolPerformanceCache(toolName: string, duration: number): void {
    const existing = this.toolPerformanceCache.get(toolName);
    if (existing) {
      // Calculate moving average (weight recent executions more)
      const newAverage = (existing * 0.7) + (duration * 0.3);
      this.toolPerformanceCache.set(toolName, newAverage);
    } else {
      this.toolPerformanceCache.set(toolName, duration);
    }
  }

  /**
   * Update performance metrics
   */
  private updatePerformanceMetrics(results: ToolExecutionResult[]): void {
    // Keep only recent history (last 100 executions)
    this.executionHistory = this.executionHistory.slice(-100);

    // Log performance summary
    const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
    const successCount = results.filter(r => !r.error).length;
    const successRate = results.length > 0 ? successCount / results.length : 0;

    console.log(`📊 Execution metrics: ${totalDuration}ms total, ${(successRate * 100).toFixed(1)}% success rate`);
  }

  /**
   * Initialize tool dependencies based on domain knowledge
   */
  private initializeToolDependencies(): void {
    // Define tool dependencies for Bybit tools
    this.toolDependencies.set('get_kline', []); // Independent
    this.toolDependencies.set('get_ticker', []); // Independent
    this.toolDependencies.set('get_orderbook', []); // Independent
    this.toolDependencies.set('get_recent_trades', []); // Independent
    this.toolDependencies.set('get_funding_rate', []); // Independent
    this.toolDependencies.set('get_open_interest', []); // Independent

    // Technical analysis tools might depend on price data
    this.toolDependencies.set('calculate_rsi', ['get_kline']); // Needs price data
    this.toolDependencies.set('calculate_macd', ['get_kline']); // Needs price data
    this.toolDependencies.set('detect_order_blocks', ['get_kline']); // Needs price data

    // Risk analysis might depend on multiple data sources
    this.toolDependencies.set('calculate_position_size', ['get_ticker', 'get_funding_rate']);
  }

  /**
   * Get performance statistics
   */
  getPerformanceStats(): ExecutionMetrics {
    if (this.executionHistory.length === 0) {
      return {
        totalDuration: 0,
        parallelSavings: 0,
        toolCount: 0,
        successRate: 0,
        averageToolTime: 0
      };
    }

    const recentResults = this.executionHistory.slice(-50); // Last 50 executions
    const totalDuration = recentResults.reduce((sum, r) => sum + r.duration, 0);
    const successCount = recentResults.filter(r => !r.error).length;
    const successRate = successCount / recentResults.length;
    const averageToolTime = totalDuration / recentResults.length;

    // Estimate parallel savings (rough calculation)
    const sequentialTime = recentResults.reduce((sum, r) => sum + r.duration, 0);
    const actualTime = Math.max(...recentResults.map(r => r.endTime)) - Math.min(...recentResults.map(r => r.startTime));
    const parallelSavings = Math.max(0, sequentialTime - actualTime);

    return {
      totalDuration,
      parallelSavings,
      toolCount: recentResults.length,
      successRate,
      averageToolTime
    };
  }

  /**
   * Clear performance cache and history
   */
  clearPerformanceData(): void {
    this.toolPerformanceCache.clear();
    this.executionHistory = [];
    console.log('🧹 Performance data cleared');
  }

  /**
   * Get tool performance summary
   */
  getToolPerformanceSummary(): Array<{tool: string, avgTime: number, executions: number}> {
    const toolStats = new Map<string, {totalTime: number, count: number}>();

    for (const result of this.executionHistory) {
      const toolName = result.toolCall.function.name;
      const existing = toolStats.get(toolName) || {totalTime: 0, count: 0};

      toolStats.set(toolName, {
        totalTime: existing.totalTime + result.duration,
        count: existing.count + 1
      });
    }

    return Array.from(toolStats.entries()).map(([tool, stats]) => ({
      tool,
      avgTime: stats.totalTime / stats.count,
      executions: stats.count
    })).sort((a, b) => b.executions - a.executions);
  }
}

// Singleton instance
export const performanceOptimiser = new PerformanceOptimiserService();

```

--------------------------------------------------------------------------------
/src/httpServer.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * HTTP/SSE server for Bybit MCP server
 * Provides both modern Streamable HTTP and legacy SSE transport support
 */

import express from "express";
import cors from "cors";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { CONSTANTS } from "./constants.js";
import { loadTools, createToolsMap } from "./utils/toolLoader.js";
import { validateEnv } from "./env.js";

const { PROJECT_NAME, PROJECT_VERSION } = CONSTANTS;

// Server configuration
const PORT = process.env.MCP_HTTP_PORT ? parseInt(process.env.MCP_HTTP_PORT) : 8080;
const HOST = process.env.MCP_HTTP_HOST || "0.0.0.0";

// Store transports for each session type
const transports = {
  streamable: {} as Record<string, StreamableHTTPServerTransport>,
  sse: {} as Record<string, SSEServerTransport>
};

// Create Express app
const app = express();

// Middleware
app.use(cors({
  origin: process.env.CORS_ORIGIN || "*",
  methods: ["GET", "POST", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "mcp-session-id", "Authorization"],
  credentials: true
}));

app.use(express.json({ limit: "10mb" }));

// Serve WebUI static files
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const webuiPath = path.join(__dirname, "..", "webui", "dist");

// Serve static files from WebUI dist directory
app.use(express.static(webuiPath));

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    name: PROJECT_NAME,
    version: PROJECT_VERSION,
    timestamp: new Date().toISOString(),
    transports: {
      streamable: Object.keys(transports.streamable).length,
      sse: Object.keys(transports.sse).length
    }
  });
});

// Simple HTTP endpoints for WebUI integration
app.get("/tools", async (req, res) => {
  try {
    const tools = await loadTools();
    const toolsList = tools.map(tool => ({
      name: tool.name,
      description: tool.toolDefinition.description,
      inputSchema: tool.toolDefinition.inputSchema
    }));

    res.json(toolsList);
  } catch (error) {
    console.error("Error loading tools:", error);
    res.status(500).json({
      error: "Failed to load tools",
      message: error instanceof Error ? error.message : String(error)
    });
  }
});

app.post("/call-tool", async (req, res) => {
  try {
    const { name, arguments: args } = req.body;

    if (!name) {
      res.status(400).json({ error: "Tool name is required" });
      return;
    }

    // Load tools and find the requested tool
    const tools = await loadTools();
    const toolsMap = createToolsMap(tools);
    const tool = toolsMap.get(name);

    if (!tool) {
      res.status(404).json({ error: `Tool '${name}' not found` });
      return;
    }

    // Call the tool
    const mcpRequest = {
      method: "tools/call" as const,
      params: {
        name,
        arguments: args || {}
      }
    };

    const result = await tool.toolCall(mcpRequest);
    res.json(result);
  } catch (error) {
    console.error(`Error calling tool:`, error);
    res.status(500).json({
      error: "Tool execution failed",
      message: error instanceof Error ? error.message : String(error)
    });
  }
});

// Create MCP server instance
function createMcpServer(toolsMap: Map<string, any>): McpServer {
  const server = new McpServer({
    name: PROJECT_NAME,
    version: PROJECT_VERSION,
  });

  // Set up tools from the loaded tools map
  if (toolsMap && toolsMap.size > 0) {
    for (const [name, tool] of toolsMap) {
      // Register each tool with the server using the tool definition
      const toolDef = tool.toolDefinition;
      const inputSchema = toolDef.inputSchema;

      // Convert JSON schema to Zod schema (simplified approach)
      const zodSchema: any = {};
      if (inputSchema.properties) {
        for (const [propName, propDef] of Object.entries(inputSchema.properties as any)) {
          const prop = propDef as any;
          let zodType;

          switch (prop.type) {
            case 'string':
              zodType = z.string();
              break;
            case 'number':
              zodType = z.number();
              break;
            case 'boolean':
              zodType = z.boolean();
              break;
            case 'array':
              zodType = z.array(z.any());
              break;
            case 'object':
              zodType = z.object({});
              break;
            default:
              zodType = z.any();
          }

          // Make optional if not required
          const isRequired = inputSchema.required?.includes(propName);
          zodSchema[propName] = isRequired ? zodType : zodType.optional();
        }
      }

      // Register the tool
      server.tool(
        name,
        zodSchema,
        async (params: any) => {
          // Call the original tool with MCP request format
          const mcpRequest = {
            params: {
              name,
              arguments: params
            }
          };
          const result = await tool.toolCall(mcpRequest);
          return result;
        }
      );
    }
  }

  return server;
}

// Modern Streamable HTTP endpoint (preferred)
app.all('/mcp', async (req, res) => {
  try {
    const sessionId = req.headers['mcp-session-id'] as string | undefined;
    let transport: StreamableHTTPServerTransport;

    if (sessionId && transports.streamable[sessionId]) {
      // Reuse existing transport
      transport = transports.streamable[sessionId];
    } else if (!sessionId && isInitializeRequest(req.body)) {
      // New initialization request
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => randomUUID(),
        onsessioninitialized: (sessionId) => {
          transports.streamable[sessionId] = transport;
          console.log(`[HTTP] New session initialized: ${sessionId}`);
        }
      });

      // Clean up transport when closed
      transport.onclose = () => {
        if (transport.sessionId) {
          delete transports.streamable[transport.sessionId];
          console.log(`[HTTP] Session closed: ${transport.sessionId}`);
        }
      };

      // Load tools and create server
      const tools = await loadTools();
      const toolsMap = createToolsMap(tools);
      const server = createMcpServer(toolsMap);

      // Connect to the MCP server
      await server.connect(transport);
    } else {
      // Invalid request
      res.status(400).json({
        jsonrpc: '2.0',
        error: {
          code: -32000,
          message: 'Bad Request: No valid session ID provided or not an initialize request',
        },
        id: null,
      });
      return;
    }

    // Handle the request
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("[HTTP] Error handling request:", error);
    res.status(500).json({
      jsonrpc: '2.0',
      error: {
        code: -32603,
        message: 'Internal server error',
        data: error instanceof Error ? error.message : String(error)
      },
      id: null,
    });
  }
});

// Legacy SSE endpoint for backwards compatibility
app.get('/sse', async (req, res) => {
  try {
    console.log("[SSE] New SSE connection request");

    // Create SSE transport for legacy clients
    const transport = new SSEServerTransport('/messages', res);
    transports.sse[transport.sessionId] = transport;

    console.log(`[SSE] Session created: ${transport.sessionId}`);

    // Clean up on connection close
    res.on("close", () => {
      delete transports.sse[transport.sessionId];
      console.log(`[SSE] Session closed: ${transport.sessionId}`);
    });

    // Load tools and create server
    const tools = await loadTools();
    const toolsMap = createToolsMap(tools);
    const server = createMcpServer(toolsMap);

    // Connect to the MCP server
    await server.connect(transport);
  } catch (error) {
    console.error("[SSE] Error setting up SSE connection:", error);
    res.status(500).send("Internal Server Error");
  }
});

// Legacy message endpoint for SSE clients
app.post('/messages', async (req, res) => {
  try {
    const sessionId = req.query.sessionId as string;

    if (!sessionId) {
      res.status(400).json({
        error: "Missing sessionId query parameter"
      });
      return;
    }

    const transport = transports.sse[sessionId];
    if (!transport) {
      res.status(404).json({
        error: `No SSE transport found for sessionId: ${sessionId}`
      });
      return;
    }

    await transport.handlePostMessage(req, res, req.body);
  } catch (error) {
    console.error("[SSE] Error handling message:", error);
    res.status(500).json({
      error: "Internal server error",
      details: error instanceof Error ? error.message : String(error)
    });
  }
});

// Reusable handler for GET and DELETE requests on /mcp
const handleSessionRequest = async (req: express.Request, res: express.Response) => {
  const sessionId = req.headers['mcp-session-id'] as string | undefined;

  if (!sessionId || !transports.streamable[sessionId]) {
    res.status(400).json({
      error: 'Invalid or missing session ID'
    });
    return;
  }

  const transport = transports.streamable[sessionId];
  await transport.handleRequest(req, res);
};

// Handle GET requests for server-to-client notifications via SSE
app.get('/mcp', handleSessionRequest);

// Handle DELETE requests for session termination
app.delete('/mcp', handleSessionRequest);

// Error handling middleware
app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error("Express error:", error);
  res.status(500).json({
    error: "Internal server error",
    message: error.message
  });
});

// 404 handler for API routes
app.use((req, res, next) => {
  // Check if this is an API route
  if (req.path.startsWith('/health') ||
      req.path.startsWith('/tools') ||
      req.path.startsWith('/call-tool') ||
      req.path.startsWith('/mcp') ||
      req.path.startsWith('/sse') ||
      req.path.startsWith('/messages')) {
    // This is an API route that wasn't handled, return 404
    res.status(404).json({
      error: "Not found",
      message: `Endpoint ${req.method} ${req.path} not found`,
      availableEndpoints: [
        "GET /health - Health check",
        "GET /tools - List available tools",
        "POST /call-tool - Execute a tool",
        "POST /mcp - Modern Streamable HTTP transport",
        "GET /mcp - Server-to-client notifications",
        "DELETE /mcp - Session termination",
        "GET /sse - Legacy SSE transport",
        "POST /messages - Legacy SSE messages"
      ]
    });
  } else {
    // This is not an API route, serve the SPA
    res.sendFile(path.join(webuiPath, 'index.html'));
  }
});



async function startHttpServer() {
  try {
    // Validate environment configuration
    validateEnv();

    // Test tool loading
    const tools = await loadTools();
    console.log(`✅ Loaded ${tools.length} tools: ${tools.map(t => t.name).join(", ")}`);

    // Start the server
    const server = app.listen(PORT, HOST, () => {
      console.log(`🚀 Bybit MCP HTTP Server started`);
      console.log(`📍 Server: http://${HOST}:${PORT}`);
      console.log(`🔗 Modern HTTP: http://${HOST}:${PORT}/mcp`);
      console.log(`🔗 Legacy SSE: http://${HOST}:${PORT}/sse`);
      console.log(`❤️  Health check: http://${HOST}:${PORT}/health`);
      console.log(`📊 Project: ${PROJECT_NAME} v${PROJECT_VERSION}`);
    });

    // Graceful shutdown
    process.on('SIGTERM', () => {
      console.log('🛑 SIGTERM received, shutting down gracefully');
      server.close(() => {
        console.log('✅ HTTP server closed');
        process.exit(0);
      });
    });

    process.on('SIGINT', () => {
      console.log('🛑 SIGINT received, shutting down gracefully');
      server.close(() => {
        console.log('✅ HTTP server closed');
        process.exit(0);
      });
    });

  } catch (error) {
    console.error("❌ Failed to start HTTP server:", error);
    process.exit(1);
  }
}

// Handle unhandled rejections
process.on("unhandledRejection", (error) => {
  console.error("❌ Unhandled rejection:", error);
});

// Start the server if this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
  startHttpServer().catch((error) => {
    console.error("❌ Failed to start server:", error);
    process.exit(1);
  });
}

export { startHttpServer, app };

```

--------------------------------------------------------------------------------
/webui/src/components/DataVerificationPanel.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Data Verification Panel - Shows recent tool calls and extracted metrics
 */

import type { CitationData } from '@/types/citation';
import { citationStore } from '@/services/citationStore';
import { citationProcessor } from '@/services/citationProcessor';

export class DataVerificationPanel {
  private container: HTMLElement;
  private isVisible: boolean = false;
  private refreshInterval: NodeJS.Timeout | null = null;
  private currentFilter: string = 'all';

  constructor(containerId: string) {
    this.container = document.getElementById(containerId)!;
    if (!this.container) {
      throw new Error(`Container element with id "${containerId}" not found`);
    }

    this.initialize();
  }

  private initialize(): void {
    this.createPanelStructure();
    this.setupEventListeners();
    this.startAutoRefresh();
  }

  private createPanelStructure(): void {
    this.container.innerHTML = `
      <div class="verification-panel ${this.isVisible ? 'visible' : 'hidden'}">
        <div class="panel-header">
          <h3>Data Verification</h3>
          <div class="panel-controls">
            <select class="filter-select" id="verification-filter">
              <option value="all">All Data</option>
              <option value="price">Prices</option>
              <option value="volume">Volume</option>
              <option value="indicator">Indicators</option>
              <option value="percentage">Percentages</option>
            </select>
            <button class="toggle-btn" id="verification-toggle" aria-label="Toggle verification panel">
              <span class="toggle-icon">📊</span>
            </button>
          </div>
        </div>

        <div class="panel-content">
          <div class="citations-summary">
            <div class="summary-item">
              <span class="label">Total Citations:</span>
              <span class="value" id="total-citations">0</span>
            </div>
            <div class="summary-item">
              <span class="label">Recent Tools:</span>
              <span class="value" id="recent-tools">0</span>
            </div>
          </div>

          <div class="citations-list" id="citations-list">
            <div class="empty-state">
              <p>No tool calls yet. Start a conversation to see data verification.</p>
            </div>
          </div>
        </div>
      </div>
    `;
  }

  private setupEventListeners(): void {
    // Toggle panel visibility
    const toggleBtn = this.container.querySelector('#verification-toggle') as HTMLButtonElement;
    toggleBtn?.addEventListener('click', () => {
      this.toggleVisibility();
    });

    // Filter change
    const filterSelect = this.container.querySelector('#verification-filter') as HTMLSelectElement;
    filterSelect?.addEventListener('change', (e) => {
      this.currentFilter = (e.target as HTMLSelectElement).value;
      this.refreshCitationsList();
    });

    // Keyboard shortcut to toggle panel (Ctrl/Cmd + D)
    document.addEventListener('keydown', (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
        e.preventDefault();
        this.toggleVisibility();
      }
    });
  }

  private startAutoRefresh(): void {
    // Refresh every 2 seconds when panel is visible
    this.refreshInterval = setInterval(() => {
      if (this.isVisible) {
        this.refreshCitationsList();
      }
    }, 2000);
  }

  public toggleVisibility(): void {
    this.isVisible = !this.isVisible;
    const panel = this.container.querySelector('.verification-panel');

    if (this.isVisible) {
      panel?.classList.remove('hidden');
      panel?.classList.add('visible');
      this.refreshCitationsList();
    } else {
      panel?.classList.remove('visible');
      panel?.classList.add('hidden');
    }
  }

  public show(): void {
    if (!this.isVisible) {
      this.toggleVisibility();
    }
  }

  public hide(): void {
    if (this.isVisible) {
      this.toggleVisibility();
    }
  }

  private refreshCitationsList(): void {
    const citations = citationStore.getAllCitations();
    const filteredCitations = this.filterCitations(citations);

    this.updateSummary(citations);
    this.renderCitationsList(filteredCitations);
  }

  private filterCitations(citations: CitationData[]): CitationData[] {
    if (this.currentFilter === 'all') {
      return citations;
    }

    return citations.filter(citation => {
      if (!citation.extractedMetrics) return false;

      return citation.extractedMetrics.some(metric =>
        metric.type === this.currentFilter
      );
    });
  }

  private updateSummary(citations: CitationData[]): void {
    const totalCitationsEl = this.container.querySelector('#total-citations');
    const recentToolsEl = this.container.querySelector('#recent-tools');

    if (totalCitationsEl) {
      totalCitationsEl.textContent = citations.length.toString();
    }

    if (recentToolsEl) {
      const uniqueTools = new Set(citations.map(c => c.toolName));
      recentToolsEl.textContent = uniqueTools.size.toString();
    }
  }

  private renderCitationsList(citations: CitationData[]): void {
    const listContainer = this.container.querySelector('#citations-list');
    if (!listContainer) return;

    if (citations.length === 0) {
      listContainer.innerHTML = `
        <div class="empty-state">
          <p>No citations found for the selected filter.</p>
        </div>
      `;
      return;
    }

    const citationsHtml = citations.map(citation => this.renderCitationItem(citation)).join('');
    listContainer.innerHTML = citationsHtml;

    // Add event listeners for citation items
    this.addCitationItemListeners(listContainer);
  }

  private renderCitationItem(citation: CitationData): string {
    const timeAgo = this.getTimeAgo(citation.timestamp);
    const keyMetrics = citation.extractedMetrics?.slice(0, 3) || [];

    return `
      <div class="citation-item" data-reference-id="${citation.referenceId}">
        <div class="citation-header">
          <span class="reference-id">${citation.referenceId}</span>
          <span class="tool-name">${citation.toolName}</span>
          <span class="timestamp">${timeAgo}</span>
        </div>

        ${keyMetrics.length > 0 ? `
          <div class="key-metrics">
            ${keyMetrics.map(metric => `
              <div class="metric-item metric-${metric.significance}">
                <span class="metric-label">${metric.label}:</span>
                <span class="metric-value">${metric.value}${metric.unit ? ' ' + metric.unit : ''}</span>
              </div>
            `).join('')}
          </div>
        ` : ''}

        <div class="citation-actions">
          <button class="btn-view-details" data-reference-id="${citation.referenceId}">
            View Details
          </button>
          <button class="btn-copy-data" data-reference-id="${citation.referenceId}">
            Copy Data
          </button>
        </div>
      </div>
    `;
  }

  private addCitationItemListeners(container: Element): void {
    // View details buttons
    container.querySelectorAll('.btn-view-details').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const referenceId = (e.target as HTMLElement).dataset.referenceId;
        if (referenceId) {
          this.showCitationDetails(referenceId);
        }
      });
    });

    // Copy data buttons
    container.querySelectorAll('.btn-copy-data').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const referenceId = (e.target as HTMLElement).dataset.referenceId;
        if (referenceId) {
          this.copyCitationData(referenceId);
        }
      });
    });
  }

  private showCitationDetails(referenceId: string): void {
    const citation = citationStore.getCitation(referenceId);
    if (!citation) {
      console.warn(`Citation ${referenceId} not found`);
      return;
    }

    this.showDetailModal(citation);
  }

  private showDetailModal(citation: CitationData): void {
    const overlay = document.createElement('div');
    overlay.className = 'verification-modal-overlay';

    const modal = document.createElement('div');
    modal.className = 'verification-modal';

    modal.innerHTML = `
      <div class="modal-header">
        <h3>Citation Details: ${citation.referenceId}</h3>
        <button class="modal-close" aria-label="Close">&times;</button>
      </div>
      <div class="modal-content">
        <div class="citation-metadata">
          <div class="metadata-item">
            <strong>Tool:</strong> ${citation.toolName}
          </div>
          <div class="metadata-item">
            <strong>Timestamp:</strong> ${citationProcessor.formatTimestamp(citation.timestamp)}
          </div>
          ${citation.endpoint ? `
            <div class="metadata-item">
              <strong>Endpoint:</strong> ${citation.endpoint}
            </div>
          ` : ''}
        </div>

        ${citation.extractedMetrics && citation.extractedMetrics.length > 0 ? `
          <div class="extracted-metrics">
            <h4>Extracted Metrics</h4>
            <div class="metrics-grid">
              ${citation.extractedMetrics.map(metric => `
                <div class="metric-card metric-${metric.significance}">
                  <div class="metric-type">${metric.type}</div>
                  <div class="metric-label">${metric.label}</div>
                  <div class="metric-value">${metric.value}${metric.unit ? ' ' + metric.unit : ''}</div>
                </div>
              `).join('')}
            </div>
          </div>
        ` : ''}

        <div class="raw-data-section">
          <div class="section-header">
            <h4>Raw Data</h4>
            <button class="btn-copy-json" data-json='${JSON.stringify(citation.rawData)}'>
              Copy JSON
            </button>
          </div>
          <pre class="json-viewer"><code>${this.formatJSON(citation.rawData)}</code></pre>
        </div>
      </div>
    `;

    overlay.appendChild(modal);
    document.body.appendChild(overlay);

    // Event listeners
    const closeBtn = modal.querySelector('.modal-close');
    const copyBtn = modal.querySelector('.btn-copy-json');

    const closeModal = () => overlay.remove();

    closeBtn?.addEventListener('click', closeModal);
    overlay.addEventListener('click', (e) => {
      if (e.target === overlay) closeModal();
    });

    copyBtn?.addEventListener('click', (e) => {
      const jsonData = (e.target as HTMLElement).dataset.json;
      if (jsonData) {
        this.copyToClipboard(jsonData);
      }
    });

    // Close on Escape
    const handleKeydown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        closeModal();
        document.removeEventListener('keydown', handleKeydown);
      }
    };
    document.addEventListener('keydown', handleKeydown);
  }

  private copyCitationData(referenceId: string): void {
    const citation = citationStore.getCitation(referenceId);
    if (!citation) {
      console.warn(`Citation ${referenceId} not found`);
      return;
    }

    const dataToExport = {
      referenceId: citation.referenceId,
      timestamp: citation.timestamp,
      toolName: citation.toolName,
      endpoint: citation.endpoint,
      extractedMetrics: citation.extractedMetrics,
      rawData: citation.rawData
    };

    this.copyToClipboard(JSON.stringify(dataToExport, null, 2));
  }

  private copyToClipboard(text: string): void {
    navigator.clipboard.writeText(text).then(() => {
      this.showToast('Data copied to clipboard!');
    }).catch(err => {
      console.error('Failed to copy to clipboard:', err);
      this.showToast('Failed to copy data', 'error');
    });
  }

  private showToast(message: string, type: 'success' | 'error' = 'success'): void {
    const toast = document.createElement('div');
    toast.className = `verification-toast toast-${type}`;
    toast.textContent = message;

    document.body.appendChild(toast);

    // Animate in
    setTimeout(() => toast.classList.add('show'), 10);

    // Remove after 3 seconds
    setTimeout(() => {
      toast.classList.remove('show');
      setTimeout(() => toast.remove(), 300);
    }, 3000);
  }

  private formatJSON(data: any): string {
    return JSON.stringify(data, null, 2);
  }

  private getTimeAgo(timestamp: string): string {
    const now = Date.now();
    const time = new Date(timestamp).getTime();
    const diff = now - time;

    const seconds = Math.floor(diff / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);

    if (seconds < 60) return `${seconds}s ago`;
    if (minutes < 60) return `${minutes}m ago`;
    if (hours < 24) return `${hours}h ago`;

    return new Date(timestamp).toLocaleDateString();
  }

  public destroy(): void {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
      this.refreshInterval = null;
    }
  }
}

```

--------------------------------------------------------------------------------
/src/tools/BaseTool.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool, TextContent, CallToolResult } from "@modelcontextprotocol/sdk/types.js"
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { z } from "zod"
import { RestClientV5, APIResponseV3WithTime } from "bybit-api"
import { getEnvConfig } from "../env.js"

// Error categories for better error handling
export enum ErrorCategory {
  VALIDATION = "VALIDATION",
  API_ERROR = "API_ERROR",
  RATE_LIMIT = "RATE_LIMIT",
  NETWORK = "NETWORK",
  AUTHENTICATION = "AUTHENTICATION",
  PERMISSION = "PERMISSION",
  INTERNAL = "INTERNAL"
}

// Structured error interface
export interface ToolError {
  category: ErrorCategory
  code?: string | number
  message: string
  details?: any
  timestamp: string
  tool: string
}

// Standard error codes
export const ERROR_CODES = {
  INVALID_INPUT: "INVALID_INPUT",
  MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
  INVALID_SYMBOL: "INVALID_SYMBOL",
  INVALID_CATEGORY: "INVALID_CATEGORY",
  API_KEY_REQUIRED: "API_KEY_REQUIRED",
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
  BYBIT_API_ERROR: "BYBIT_API_ERROR",
  NETWORK_ERROR: "NETWORK_ERROR",
  TIMEOUT: "TIMEOUT",
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
} as const

// Rate limit configuration (as per Bybit docs)
const RATE_LIMIT = {
  maxRequestsPerSecond: 10,
  maxRequestsPerMinute: 120,
  retryAfter: 2000, // ms
  maxRetries: 3
}

interface QueuedRequest {
  execute: () => Promise<any>
  resolve: (value: any) => void
  reject: (error: any) => void
}

export abstract class BaseToolImplementation {
  abstract name: string
  abstract toolDefinition: Tool
  abstract toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult>

  protected client: RestClientV5
  protected isDevMode: boolean
  protected isTestMode: boolean = false
  private requestQueue: QueuedRequest[] = []
  private processingQueue = false
  private requestCount = 0
  private lastRequestTime = 0
  private requestHistory: number[] = [] // Timestamps of requests within the last minute
  private initialized = false
  private activeTimeouts: NodeJS.Timeout[] = []

  constructor(mockClient?: RestClientV5) {
    if (mockClient) {
      // Use provided mock client for testing
      this.client = mockClient
      this.isDevMode = true
      this.isTestMode = true
    } else {
      // Normal production/development initialization
      const config = getEnvConfig()
      this.isDevMode = !config.apiKey || !config.apiSecret

      if (this.isDevMode) {
        this.client = new RestClientV5({
          testnet: true,
        })
      } else {
        this.client = new RestClientV5({
          key: config.apiKey,
          secret: config.apiSecret,
          testnet: config.useTestnet,
          recv_window: 5000, // 5 second receive window
        })
      }
    }
  }

  protected ensureInitialized() {
    if (!this.initialized) {
      if (this.isDevMode) {
        this.logWarning("Running in development mode with limited functionality")
      }
      this.initialized = true
    }
  }

  /**
   * Enqueues a request with rate limiting and retry logic
   */
  protected async executeRequest<T>(
    operation: () => Promise<APIResponseV3WithTime<T>>,
    retryCount = 0
  ): Promise<T> {
    this.ensureInitialized()
    return new Promise((resolve, reject) => {
      this.requestQueue.push({
        execute: async () => {
          try {
            // Check rate limits
            if (!this.canMakeRequest() && !this.isTestMode) {
              const waitTime = this.getWaitTime()
              this.logInfo(`Rate limit reached. Waiting ${waitTime}ms`)
              await new Promise(resolve => setTimeout(resolve, waitTime))
            }

            // Execute request with timeout
            let response: APIResponseV3WithTime<T>

            if (this.isTestMode) {
              // In test mode, don't create timeout promises to avoid open handles
              response = await operation() as APIResponseV3WithTime<T>
            } else {
              // In production mode, use timeout for real API calls
              response = await Promise.race([
                operation(),
                new Promise<never>((_, reject) =>
                  setTimeout(() => reject(new Error("Request timeout")), 10000)
                )
              ]) as APIResponseV3WithTime<T>
            }

            // Update rate limit tracking
            this.updateRequestHistory()

            // Handle Bybit API errors
            if (response.retCode !== 0) {
              throw this.createBybitError(response.retCode, response.retMsg)
            }

            return response.result
          } catch (error) {
            // Retry logic for specific errors
            if (
              retryCount < RATE_LIMIT.maxRetries &&
              this.shouldRetry(error)
            ) {
              this.logWarning(`Retrying request (attempt ${retryCount + 1})`)
              if (!this.isTestMode) {
                await new Promise(resolve =>
                  setTimeout(resolve, RATE_LIMIT.retryAfter)
                )
              }
              return this.executeRequest(operation, retryCount + 1)
            }
            throw error
          }
        },
        resolve,
        reject
      })

      if (!this.processingQueue) {
        this.processQueue()
      }
    })
  }

  private async processQueue() {
    if (this.requestQueue.length === 0) {
      this.processingQueue = false
      return
    }

    this.processingQueue = true
    const request = this.requestQueue.shift()

    if (request) {
      try {
        const result = await request.execute()
        request.resolve(result)
      } catch (error) {
        request.reject(error)
      }
    }

    // Process next request
    setImmediate(() => this.processQueue())
  }

  private canMakeRequest(): boolean {
    const now = Date.now()
    // Clean up old requests
    this.requestHistory = this.requestHistory.filter(
      time => now - time < 60000
    )

    return (
      this.requestHistory.length < RATE_LIMIT.maxRequestsPerMinute &&
      now - this.lastRequestTime >= (1000 / RATE_LIMIT.maxRequestsPerSecond)
    )
  }

  private getWaitTime(): number {
    const now = Date.now()
    const timeToWaitForSecondLimit = Math.max(
      0,
      this.lastRequestTime + (1000 / RATE_LIMIT.maxRequestsPerSecond) - now
    )

    if (this.requestHistory.length >= RATE_LIMIT.maxRequestsPerMinute) {
      const timeToWaitForMinuteLimit = Math.max(
        0,
        this.requestHistory[0] + 60000 - now
      )
      return Math.max(timeToWaitForSecondLimit, timeToWaitForMinuteLimit)
    }

    return timeToWaitForSecondLimit
  }

  private updateRequestHistory() {
    const now = Date.now()
    this.requestHistory.push(now)
    this.lastRequestTime = now
  }

  private shouldRetry(error: any): boolean {
    // Retry on network errors or specific Bybit error codes
    return (
      error.name === "NetworkError" ||
      error.code === 10002 || // Rate limit
      error.code === 10006 || // System busy
      error.code === -1      // Unknown error
    )
  }

  private createBybitError(code: number, message: string): Error {
    const errorMap: Record<number, string> = {
      10001: "Parameter error",
      10002: "Rate limit exceeded",
      10003: "Invalid API key",
      10004: "Invalid sign",
      10005: "Permission denied",
      10006: "System busy",
      10009: "Order not found",
      10010: "Insufficient balance",
    }

    const errorMessage = errorMap[code] || message
    const error = new Error(`Bybit API Error ${code}: ${errorMessage}`)
    ; (error as any).code = code
    ; (error as any).bybitCode = code
    ; (error as any).category = this.categoriseBybitError(code)
    return error
  }

  /**
   * Creates a standardised ToolError object
   */
  protected createToolError(
    category: ErrorCategory,
    message: string,
    code?: string | number,
    details?: any
  ): ToolError {
    return {
      category,
      code,
      message,
      details,
      timestamp: new Date().toISOString(),
      tool: this.name
    }
  }

  /**
   * Creates a validation error for invalid input
   */
  protected createValidationError(message: string, details?: any): ToolError {
    return this.createToolError(
      ErrorCategory.VALIDATION,
      message,
      ERROR_CODES.INVALID_INPUT,
      details
    )
  }

  /**
   * Creates an API error from Bybit response
   */
  protected createApiError(code: number, message: string): ToolError {
    const category = this.categoriseBybitError(code)
    return this.createToolError(
      category,
      `Bybit API Error ${code}: ${message}`,
      code
    )
  }

  /**
   * Categorises Bybit API errors
   */
  private categoriseBybitError(code: number): ErrorCategory {
    switch (code) {
      case 10002:
        return ErrorCategory.RATE_LIMIT
      case 10003:
      case 10004:
        return ErrorCategory.AUTHENTICATION
      case 10005:
        return ErrorCategory.PERMISSION
      case 10001:
        return ErrorCategory.VALIDATION
      default:
        return ErrorCategory.API_ERROR
    }
  }

  /**
   * Handles errors and returns MCP-compliant CallToolResult
   */
  protected handleError(error: any): CallToolResult {
    let toolError: ToolError

    if (error instanceof Error) {
      // Check if it's a Bybit API error (has bybitCode property)
      if ((error as any).bybitCode) {
        toolError = this.createApiError((error as any).bybitCode, error.message)
      }
      // Check if it's a validation error (from Zod)
      else if (error.message.includes("Invalid input")) {
        toolError = this.createValidationError(error.message)
      }
      // Check for specific error patterns
      else if (error.message.includes("API credentials required") || error.message.includes("development mode")) {
        toolError = this.createToolError(
          ErrorCategory.AUTHENTICATION,
          error.message,
          ERROR_CODES.API_KEY_REQUIRED
        )
      } else if (error.message.includes("Rate limit")) {
        toolError = this.createToolError(
          ErrorCategory.RATE_LIMIT,
          error.message,
          ERROR_CODES.RATE_LIMIT_EXCEEDED
        )
      } else if (error.message.includes("timeout") || error.message.includes("Request timeout")) {
        toolError = this.createToolError(
          ErrorCategory.NETWORK,
          error.message,
          ERROR_CODES.TIMEOUT
        )
      } else if (error.name === "NetworkError") {
        toolError = this.createToolError(
          ErrorCategory.NETWORK,
          error.message,
          ERROR_CODES.NETWORK_ERROR
        )
      } else {
        toolError = this.createToolError(
          ErrorCategory.INTERNAL,
          error.message,
          ERROR_CODES.UNKNOWN_ERROR
        )
      }
    } else {
      toolError = this.createToolError(
        ErrorCategory.INTERNAL,
        String(error),
        ERROR_CODES.UNKNOWN_ERROR
      )
    }

    // Log the error
    console.error(JSON.stringify({
      jsonrpc: "2.0",
      method: "notify",
      params: {
        level: "error",
        message: `${this.name} tool error: ${toolError.message}`
      }
    }))

    // Create MCP-compliant error response
    const content: TextContent = {
      type: "text",
      text: JSON.stringify(toolError, null, 2),
      annotations: {
        audience: ["assistant", "user"],
        priority: 1
      }
    }

    return {
      content: [content],
      isError: true
    }
  }

  // Reference ID counter for generating unique IDs
  private static referenceIdCounter = 0

  /**
   * Generate a unique reference ID
   */
  protected generateReferenceId(): string {
    BaseToolImplementation.referenceIdCounter += 1
    return `REF${String(BaseToolImplementation.referenceIdCounter).padStart(3, '0')}`
  }

  /**
   * Add reference ID metadata to response if requested
   */
  protected addReferenceMetadata(data: any, includeReferenceId: boolean, toolName: string, endpoint?: string): any {
    if (!includeReferenceId) {
      return data
    }

    return {
      ...data,
      _referenceId: this.generateReferenceId(),
      _timestamp: new Date().toISOString(),
      _toolName: toolName,
      _endpoint: endpoint
    }
  }

  protected formatResponse(data: any): CallToolResult {
    this.ensureInitialized()
    const content: TextContent = {
      type: "text",
      text: JSON.stringify(data, null, 2),
      annotations: {
        audience: ["assistant", "user"],
        priority: 1
      }
    }

    return {
      content: [content]
    }
  }

  protected logInfo(message: string) {
    console.info(JSON.stringify({
      jsonrpc: "2.0",
      method: "notify",
      params: {
        level: "info",
        message: `${this.name}: ${message}`
      }
    }))
  }

  protected logWarning(message: string) {
    console.warn(JSON.stringify({
      jsonrpc: "2.0",
      method: "notify",
      params: {
        level: "warning",
        message: `${this.name}: ${message}`
      }
    }))
  }

  /**
   * Cleanup method for tests to clear any remaining timeouts
   */
  public cleanup() {
    this.activeTimeouts.forEach(timeout => clearTimeout(timeout))
    this.activeTimeouts = []
    this.requestQueue = []
    this.processingQueue = false
  }
}

```

--------------------------------------------------------------------------------
/webui/src/services/mcpClient.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * MCP (Model Context Protocol) client for communicating with the Bybit MCP server
 * Uses the official MCP SDK with StreamableHTTPClientTransport for browser compatibility
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { citationStore } from './citationStore';
import type {
  MCPTool,
  MCPToolCall,
  MCPToolResult,
  MCPToolName,
  MCPToolParams,
  MCPToolResponse,
} from '@/types/mcp';

export class MCPClient {
  private baseUrl: string;
  private timeout: number;
  private client: Client | null = null;
  private transport: StreamableHTTPClientTransport | null = null;
  private tools: MCPTool[] = [];
  private connected: boolean = false;

  constructor(baseUrl: string = '', timeout: number = 30000) {
    // Determine the correct base URL based on environment
    if (typeof window !== 'undefined') {
      if (window.location.hostname === 'localhost' && window.location.port === '3000') {
        // Development mode with Vite dev server
        this.baseUrl = '/api/mcp'; // Use Vite proxy in development
      } else if (baseUrl && baseUrl !== '' && baseUrl !== 'auto') {
        // Explicit base URL provided (not empty or 'auto')
        this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
      } else {
        // Production mode or Docker - use current origin
        this.baseUrl = window.location.origin;
      }
    } else {
      // Server-side or fallback
      this.baseUrl = baseUrl || 'http://localhost:8080';
      this.baseUrl = this.baseUrl.replace(/\/$/, ''); // Remove trailing slash
    }
    this.timeout = timeout;

    console.log('🔧 MCP Client initialised with baseUrl:', this.baseUrl);
    console.log('🔧 Environment check:', {
      hostname: typeof window !== 'undefined' ? window.location.hostname : 'server-side',
      port: typeof window !== 'undefined' ? window.location.port : 'server-side',
      origin: typeof window !== 'undefined' ? window.location.origin : 'server-side',
      providedBaseUrl: baseUrl,
      finalBaseUrl: this.baseUrl
    });
  }

  /**
   * Initialize the client and connect to the MCP server
   */
  async initialize(): Promise<void> {
    try {
      console.log('🔌 Initialising MCP client...');
      console.log('🔗 MCP endpoint:', this.baseUrl);

      // For now, skip the complex MCP client setup and just load tools
      // This allows the WebUI to work while we debug the MCP protocol issues
      console.log('🔄 Loading tools via HTTP...');
      await this.listTools();

      // Mark as connected if we successfully loaded tools
      this.connected = this.tools.length > 0;

      if (this.connected) {
        console.log('✅ MCP client initialised via HTTP');
      } else {
        console.warn('⚠️ No tools loaded, but continuing...');
      }
    } catch (error) {
      console.error('❌ Failed to initialise MCP client:', error);
      console.error('❌ MCP Error details:', {
        name: error instanceof Error ? error.name : 'Unknown',
        message: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined
      });
      this.connected = false;
      // Don't throw error, allow WebUI to continue
      console.log('💡 Continuing without MCP tools...');
    }
  }

  /**
   * Check if the MCP server is reachable
   */
  async isConnected(): Promise<boolean> {
    try {
      // Simple health check to the HTTP server
      const response = await fetch(`${this.baseUrl}/health`);
      return response.ok;
    } catch (error) {
      console.warn('🔍 MCP health check failed:', error);
      return false;
    }
  }

  /**
   * List all available tools from the MCP server using direct HTTP
   */
  async listTools(): Promise<MCPTool[]> {
    try {
      // Use direct HTTP request to get tools
      const response = await fetch(`${this.baseUrl}/tools`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const data = await response.json();

      // Handle different response formats
      let tools = [];
      if (Array.isArray(data)) {
        tools = data;
      } else if (data.tools && Array.isArray(data.tools)) {
        tools = data.tools;
      } else {
        console.warn('Unexpected tools response format:', data);
        return [];
      }

      this.tools = tools.map((tool: any) => ({
        name: tool.name,
        description: tool.description || '',
        inputSchema: tool.inputSchema || { type: 'object', properties: {} },
      }));

      console.log('🔧 Loaded tools via HTTP:', this.tools.length);
      return this.tools;
    } catch (error) {
      console.error('Failed to list tools via HTTP:', error);
      // Fallback: return empty array instead of throwing
      this.tools = [];
      return this.tools;
    }
  }

  /**
   * Get information about a specific tool
   */
  getTool(name: string): MCPTool | undefined {
    return this.tools.find(tool => tool.name === name);
  }

  /**
   * Get all available tools
   */
  getTools(): MCPTool[] {
    return [...this.tools];
  }

  /**
   * Validate and convert parameters based on tool schema
   */
  private validateAndConvertParams(toolName: string, params: Record<string, any>): Record<string, any> {
    const tool = this.getTool(toolName);
    if (!tool || !tool.inputSchema || !tool.inputSchema.properties) {
      return params;
    }

    const convertedParams: Record<string, any> = {};
    const schema = tool.inputSchema.properties;

    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) {
        continue;
      }

      const propertySchema = schema[key] as any;
      if (!propertySchema) {
        convertedParams[key] = value;
        continue;
      }

      // Convert based on schema type
      if (propertySchema.type === 'number') {
        const numValue = typeof value === 'string' ? parseFloat(value) : value;
        if (!isNaN(numValue)) {
          convertedParams[key] = numValue;
        } else {
          console.warn(`⚠️ Invalid number value for ${key}: ${value}`);
          convertedParams[key] = value; // Keep original value
        }
      } else if (propertySchema.type === 'integer') {
        const intValue = typeof value === 'string' ? parseInt(value, 10) : value;
        if (!isNaN(intValue)) {
          convertedParams[key] = intValue;
        } else {
          console.warn(`⚠️ Invalid integer value for ${key}: ${value}`);
          convertedParams[key] = value; // Keep original value
        }
      } else if (propertySchema.type === 'boolean') {
        if (typeof value === 'string') {
          convertedParams[key] = value.toLowerCase() === 'true';
        } else {
          convertedParams[key] = Boolean(value);
        }
      } else {
        // String or other types - keep as is
        convertedParams[key] = value;
      }
    }

    return convertedParams;
  }

  /**
   * Call a specific MCP tool using HTTP
   */
  async callTool<T extends MCPToolName>(
    name: T,
    params: MCPToolParams<T>
  ): Promise<MCPToolResponse<T>> {
    try {
      console.log(`🔧 Calling tool ${name} with params:`, params);

      // Validate and convert parameters
      const convertedParams = this.validateAndConvertParams(name as string, params as Record<string, any>);
      console.log(`🔧 Converted params:`, convertedParams);

      const response = await fetch(`${this.baseUrl}/call-tool`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: name as string,
          arguments: convertedParams,
        }),
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`HTTP ${response.status}: ${errorText}`);
      }

      const result = await response.json();
      console.log(`✅ Tool ${name} result:`, result);

      // Process tool response for citation storage
      console.log(`🔍 About to process tool response for citations...`);
      citationStore.processToolResponse(result);

      return result as MCPToolResponse<T>;
    } catch (error) {
      console.error(`❌ Failed to call tool ${name}:`, error);
      throw error;
    }
  }

  /**
   * Call multiple tools in sequence
   */
  async callTools(toolCalls: MCPToolCall[]): Promise<MCPToolResult[]> {
    const results: MCPToolResult[] = [];

    for (const toolCall of toolCalls) {
      try {
        const result = await this.callTool(
          toolCall.name as MCPToolName,
          toolCall.arguments as any
        );

        results.push({
          content: [{
            type: 'text',
            text: JSON.stringify(result, null, 2),
          }],
          isError: false,
        });
      } catch (error) {
        results.push({
          content: [{
            type: 'text',
            text: `Error calling ${toolCall.name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
          }],
          isError: true,
        });
      }
    }

    return results;
  }

  /**
   * Disconnect from the MCP server
   */
  async disconnect(): Promise<void> {
    if (this.client && this.transport) {
      try {
        await this.client.close();
      } catch (error) {
        console.error('Error disconnecting from MCP server:', error);
      }
    }

    this.client = null;
    this.transport = null;
    this.connected = false;
    this.tools = [];
  }

  /**
   * Update the base URL for the MCP server
   */
  setBaseUrl(url: string): void {
    // Handle empty string or 'auto' to use current origin
    if (!url || url === '' || url === 'auto') {
      if (typeof window !== 'undefined') {
        this.baseUrl = window.location.origin;
        console.log('🔧 setBaseUrl: Using current origin:', this.baseUrl);
      } else {
        this.baseUrl = 'http://localhost:8080';
        console.log('🔧 setBaseUrl: Using server-side fallback:', this.baseUrl);
      }
    } else {
      this.baseUrl = url.replace(/\/$/, '');
      console.log('🔧 setBaseUrl: Using explicit URL:', this.baseUrl);
    }

    // If connected, disconnect and reconnect with new URL
    if (this.connected) {
      this.disconnect().then(() => {
        this.initialize().catch(console.error);
      });
    }
  }

  /**
   * Update the request timeout
   */
  setTimeout(timeout: number): void {
    this.timeout = timeout;
  }

  /**
   * Get current configuration
   */
  getConfig(): { baseUrl: string; timeout: number; isConnected: boolean } {
    return {
      baseUrl: this.baseUrl,
      timeout: this.timeout,
      isConnected: this.connected,
    };
  }
}

// Create a singleton instance with environment-aware defaults
const getDefaultMCPUrl = (): string => {
  // Check for build-time injected environment variable
  const envEndpoint = (typeof window !== 'undefined' && (window as any).__MCP_ENDPOINT__) || '';

  console.log('🔧 MCP URL Detection:', {
    envEndpoint,
    isWindow: typeof window !== 'undefined',
    windowMcpEndpoint: typeof window !== 'undefined' ? (window as any).__MCP_ENDPOINT__ : 'N/A',
    hostname: typeof window !== 'undefined' ? window.location.hostname : 'N/A',
    origin: typeof window !== 'undefined' ? window.location.origin : 'N/A'
  });

  // If we have an explicit endpoint from build-time injection, use it
  if (envEndpoint && envEndpoint !== '' && envEndpoint !== 'auto') {
    console.log('🔧 Using explicit MCP endpoint:', envEndpoint);
    return envEndpoint;
  }

  // In browser, always use empty string to trigger current origin logic
  if (typeof window !== 'undefined') {
    console.log('🔧 Using current origin for MCP endpoint (empty string)');
    return ''; // Empty string means use current origin
  }

  // Server-side fallback
  console.log('🔧 Using server-side fallback for MCP endpoint');
  return 'http://localhost:8080';
};

export const mcpClient = new MCPClient(getDefaultMCPUrl());

// Convenience functions for common operations
export async function getTicker(symbol: string, category?: 'spot' | 'linear' | 'inverse' | 'option') {
  return mcpClient.callTool('get_ticker', { symbol, category });
}

export async function getKlineData(symbol: string, interval?: string, limit?: number) {
  return mcpClient.callTool('get_kline', { symbol, interval, limit });
}

export async function getOrderbook(symbol: string, category?: 'spot' | 'linear' | 'inverse' | 'option', limit?: number) {
  return mcpClient.callTool('get_orderbook', { symbol, category, limit });
}

export async function getMLRSI(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_ml_rsi'>>) {
  return mcpClient.callTool('get_ml_rsi', { symbol, category, interval, ...options });
}

export async function getOrderBlocks(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_order_blocks'>>) {
  return mcpClient.callTool('get_order_blocks', { symbol, category, interval, ...options });
}

export async function getMarketStructure(symbol: string, category: 'spot' | 'linear' | 'inverse' | 'option', interval: string, options?: Partial<MCPToolParams<'get_market_structure'>>) {
  return mcpClient.callTool('get_market_structure', { symbol, category, interval, ...options });
}

```

--------------------------------------------------------------------------------
/webui/src/services/multiStepAgent.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Multi-step agent service for enhanced agentic capabilities
 * Implements multi-step tool calling and workflow orchestration
 */

import type { AgentConfig, AgentState } from '@/types/agent';
import type { WorkflowEvent } from '@/types/workflow';
import { WorkflowEventEmitter, createWorkflowEvent } from '@/types/workflow';
import { agentConfigService } from './agentConfig';
import { aiClient } from './aiClient';
import { mcpClient } from './mcpClient';
import { agentMemory } from './agentMemory';
import { performanceOptimiser } from './performanceOptimiser';
import { systemPromptService } from './systemPrompt';
import type { ChatMessage } from '@/types/ai';

export class MultiStepAgentService {
  private availableTools: any[] = [];
  private isInitialized = false;
  private eventEmitter: WorkflowEventEmitter;
  private currentConfig: AgentConfig;
  private conversationHistory: ChatMessage[] = [];
  private currentConversationId?: string;

  constructor() {
    this.eventEmitter = new WorkflowEventEmitter();
    this.currentConfig = agentConfigService.getConfig();

    // Subscribe to config changes
    agentConfigService.subscribe((config) => {
      this.currentConfig = config;
      this.reinitializeAgents();
    });
  }

  /**
   * Initialize the agent service
   */
  async initialize(): Promise<void> {
    if (this.isInitialized) return;

    try {
      console.log('🤖 Initializing Multi-Step Agent Service...');

      // Load MCP tools
      await this.loadMCPTools();

      // Initialize agents based on configuration
      await this.initializeAgents();

      // Update state
      agentConfigService.updateState({
        isProcessing: false
      });

      this.isInitialized = true;
      console.log('✅ Multi-Step Agent Service initialized successfully');

    } catch (error) {
      console.error('❌ Failed to initialize Multi-Step Agent Service:', error);

      agentConfigService.updateState({
        isProcessing: false
      });

      throw error;
    }
  }

  /**
   * Load MCP tools from the server
   */
  private async loadMCPTools(): Promise<void> {
    try {
      console.log('🔧 Loading MCP tools...');

      // Get available tools from MCP client
      const tools = await mcpClient.listTools();
      this.availableTools = tools;

      console.log(`🔧 Loaded ${this.availableTools.length} MCP tools:`, this.availableTools.map(t => t.name));

    } catch (error) {
      console.error('❌ Failed to load MCP tools:', error);
      // Continue with empty tools array for now
      this.availableTools = [];
    }
  }

  /**
   * Initialize agent system
   */
  private async initializeAgents(): Promise<void> {
    console.log('🤖 Initializing multi-step agent system...');

    // Agent system is ready - we'll use the existing AI client with multi-step logic
    console.log('✅ Multi-step agent system initialized');
  }

  /**
   * Build system prompt based on configuration and memory context
   */
  private async buildSystemPrompt(symbol?: string): Promise<string> {
    // Get base system prompt from centralized service
    const basePrompt = await systemPromptService.generateSystemPrompt({
      includeTimestamp: true,
      includeTools: true,
      includeMemoryContext: false
    });

    // Add memory context if available
    const memoryContext = agentMemory.buildContextSummary(symbol);
    const finalPrompt = basePrompt + memoryContext;

    return finalPrompt;
  }

  /**
   * Process a chat message with the agent using multi-step reasoning
   */
  async chat(message: string): Promise<string> {
    if (!this.isInitialized) {
      await this.initialize();
    }

    const startTime = Date.now();
    const toolCallsCount = 0;

    try {
      agentConfigService.updateState({ isProcessing: true });

      console.log('💬 Processing chat message with multi-step agent...');

      // Start new conversation if needed
      if (!this.currentConversationId) {
        this.currentConversationId = agentMemory.startConversation();
      }

      // Add user message to conversation history
      const userMessage: ChatMessage = {
        role: 'user',
        content: message
      };

      this.conversationHistory.push(userMessage);
      agentMemory.addMessage(this.currentConversationId, userMessage);

      // Extract symbol from message for context
      const symbolMatch = message.match(/\b([A-Z]{2,5})(?:USD|USDT)?\b/);
      const symbol = symbolMatch ? symbolMatch[1] : undefined;

      // Run multi-step agent loop
      const result = await this.runAgentLoop(symbol);

      // Add assistant response to memory
      if (this.currentConversationId) {
        const assistantMessage: ChatMessage = {
          role: 'assistant',
          content: result
        };
        agentMemory.addMessage(this.currentConversationId, assistantMessage);
      }

      // Record analysis in memory
      if (symbol) {
        const duration = Date.now() - startTime;
        agentMemory.recordAnalysis({
          symbol,
          analysisType: this.determineAnalysisType(),
          query: message,
          response: result,
          toolsUsed: [], // Will be populated by runAgentLoop
          duration
        });
      }

      // Record successful query
      const duration = Date.now() - startTime;
      agentConfigService.recordQuery(duration, toolCallsCount);

      return result;

    } catch (error) {
      console.error('❌ Multi-step agent chat failed:', error);
      agentConfigService.recordFailure();

      agentConfigService.updateState({
        isProcessing: false
      });

      throw error;

    } finally {
      agentConfigService.updateState({ isProcessing: false });
    }
  }

  /**
   * Run the multi-step agent reasoning loop
   */
  private async runAgentLoop(symbol?: string): Promise<string> {
    const maxIterations = this.currentConfig.maxIterations;
    let iteration = 0;

    // Build system prompt once and cache it for this conversation
    const systemPrompt = await this.buildSystemPrompt(symbol);
    console.log('🎯 System prompt generated once for conversation');

    const messages: ChatMessage[] = [
      { role: 'system', content: systemPrompt },
      ...this.conversationHistory
    ];

    while (iteration < maxIterations) {
      iteration++;
      console.log(`🔄 Agent iteration ${iteration}/${maxIterations}`);

      // Emit workflow step event
      this.emitEvent(createWorkflowEvent('workflow_step', {
        stepName: `Iteration ${iteration}`,
        stepDescription: 'Agent reasoning and tool execution',
        progress: iteration,
        totalSteps: maxIterations
      }));

      // Get AI response with tool calling
      const response = await aiClient.chatWithTools(messages);

      // Find the latest assistant message
      const assistantMessages = response.filter(msg => msg.role === 'assistant');
      const latestAssistant = assistantMessages[assistantMessages.length - 1];

      if (!latestAssistant) {
        throw new Error('No assistant response received');
      }

      // Check if there are tool calls
      if (latestAssistant.tool_calls && latestAssistant.tool_calls.length > 0) {
        console.log(`🔧 Processing ${latestAssistant.tool_calls.length} tool calls`);

        // Update conversation history with the complete response
        this.conversationHistory = response.slice(1); // Remove system message

        // Continue the loop for next iteration - rebuild messages with cached system prompt
        messages.length = 1; // Keep only system message (already cached)
        messages.push(...this.conversationHistory);

        continue;
      }

      // No more tool calls - we have the final response
      if (latestAssistant.content) {
        // Check if content is meaningful (not just placeholder text)
        const trimmedContent = latestAssistant.content.trim();
        const isPlaceholder = trimmedContent === '...' ||
                             trimmedContent === '' ||
                             trimmedContent.length < 3;

        if (!isPlaceholder) {
          // Add final response to conversation history
          this.conversationHistory.push({
            role: 'assistant',
            content: latestAssistant.content
          });

          console.log(`✅ Multi-step agent completed in ${iteration} iterations`);
          return latestAssistant.content;
        } else {
          console.log(`⚠️ Received placeholder content: "${trimmedContent}", continuing iteration...`);
          // Continue to next iteration - treat as if no meaningful response
        }
      }

      // If we get here and it's not the last iteration, continue
      if (iteration < maxIterations) {
        console.log(`🔄 No meaningful response in iteration ${iteration}, continuing...`);
        continue;
      }

      // If we get here on the last iteration, something went wrong
      throw new Error('Assistant response has no meaningful content and no tool calls');
    }

    // Max iterations reached
    const fallbackResponse = 'I apologise, but I reached the maximum number of reasoning steps. Let me provide what I can based on the analysis so far.';

    this.conversationHistory.push({
      role: 'assistant',
      content: fallbackResponse
    });

    return fallbackResponse;
  }

  /**
   * Stream chat with real-time events
   */
  async streamChat(
    message: string,
    onChunk: (chunk: string) => void,
    onEvent?: (event: WorkflowEvent) => void
  ): Promise<void> {
    if (!this.isInitialized) {
      await this.initialize();
    }

    const startTime = Date.now();
    const toolCallsCount = 0;

    try {
      agentConfigService.updateState({ isProcessing: true });

      console.log('💬 Streaming chat with multi-step agent...');

      // Subscribe to events if callback provided
      let unsubscribe: (() => void) | undefined;
      if (onEvent) {
        unsubscribe = this.onEvent(onEvent);
      }

      // Add user message to conversation history
      this.conversationHistory.push({
        role: 'user',
        content: message
      });

      // Run multi-step agent loop and stream the final response
      const result = await this.runAgentLoop();

      // Stream the final result
      const words = result.split(' ');
      for (let i = 0; i < words.length; i++) {
        const chunk = (i === 0 ? '' : ' ') + words[i];
        onChunk(chunk);

        // Small delay for streaming effect
        await new Promise(resolve => setTimeout(resolve, 30));
      }

      // Clean up event subscription
      if (unsubscribe) {
        unsubscribe();
      }

      // Record successful query
      const duration = Date.now() - startTime;
      agentConfigService.recordQuery(duration, toolCallsCount);

    } catch (error) {
      console.error('❌ Multi-step agent stream chat failed:', error);
      agentConfigService.recordFailure();

      agentConfigService.updateState({
        isProcessing: false
      });

      throw error;

    } finally {
      agentConfigService.updateState({ isProcessing: false });
    }
  }

  /**
   * Emit a workflow event
   */
  private emitEvent(event: WorkflowEvent): void {
    this.eventEmitter.emit(event);
  }

  /**
   * Check if the service is connected and ready
   */
  async isConnected(): Promise<boolean> {
    return this.isInitialized && this.availableTools.length > 0;
  }

  /**
   * Get current agent state
   */
  getState(): AgentState {
    return agentConfigService.getState();
  }

  /**
   * Subscribe to workflow events
   */
  onEvent(listener: (event: WorkflowEvent) => void): () => void {
    this.eventEmitter.on('all', listener);
    return () => this.eventEmitter.off('all', listener);
  }

  /**
   * Determine analysis type based on current configuration
   */
  private determineAnalysisType(): 'quick' | 'standard' | 'comprehensive' {
    const maxIterations = this.currentConfig.maxIterations;
    if (maxIterations <= 2) return 'quick';
    if (maxIterations <= 5) return 'standard';
    return 'comprehensive';
  }

  // Note: Market context updating will be implemented in future iterations
  // when tool response interception is added to the agent loop

  /**
   * Reinitialise agents when configuration changes
   */
  private async reinitializeAgents(): Promise<void> {
    if (!this.isInitialized) return;

    console.log('🔄 Reinitialising multi-step agents due to configuration change...');

    try {
      await this.initializeAgents();
      console.log('✅ Multi-step agents reinitialised successfully');
    } catch (error) {
      console.error('❌ Failed to reinitialise multi-step agents:', error);
    }
  }

  /**
   * Get memory statistics
   */
  getMemoryStats() {
    return agentMemory.getMemoryStats();
  }

  /**
   * Get performance statistics
   */
  getPerformanceStats() {
    return performanceOptimiser.getPerformanceStats();
  }

  /**
   * Get conversation history for a symbol
   */
  getSymbolHistory(symbol: string, limit: number = 5) {
    return agentMemory.getSymbolContext(symbol, limit);
  }

  /**
   * Get recent analysis history
   */
  getAnalysisHistory(symbol?: string, limit: number = 10) {
    if (symbol) {
      return agentMemory.getSymbolAnalysisHistory(symbol, limit);
    }
    return agentMemory.getRecentAnalysisHistory(limit);
  }

  /**
   * Clear all memory data
   */
  clearMemory(): void {
    agentMemory.clearAllMemory();
    this.conversationHistory = [];
    this.currentConversationId = undefined;
    console.log('🧹 Multi-step agent memory cleared');
  }

  /**
   * Start a new conversation session
   */
  startNewConversation(): void {
    this.conversationHistory = [];
    this.currentConversationId = undefined;
    console.log('🆕 New conversation session started');
  }
}

// Singleton instance
export const multiStepAgent = new MultiStepAgentService();

```

--------------------------------------------------------------------------------
/webui/src/services/aiClient.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * AI client for OpenAI-compatible API integration (Ollama, etc.)
 */

import type {
  AIService,
  ChatMessage,
  ChatCompletionRequest,
  ChatCompletionResponse,
  ChatCompletionStreamResponse,
  AIConfig,
  AIError,
  ModelInfo,
} from '@/types/ai';
import { mcpClient } from './mcpClient';
import { systemPromptService } from './systemPrompt';

export class AIClient implements AIService {
  private config: AIConfig;
  private controller?: AbortController;

  constructor(config: AIConfig) {
    this.config = { ...config };
  }

  /**
   * Send a chat completion request with tool calling support
   */
  async chat(
    messages: ChatMessage[],
    options?: Partial<ChatCompletionRequest>
  ): Promise<ChatCompletionResponse> {
    // First, try to get available tools from MCP
    let tools: any[] = [];
    try {
      const mcpTools = await mcpClient.getTools();
      tools = mcpTools.map(tool => ({
        type: 'function',
        function: {
          name: tool.name,
          description: tool.description,
          parameters: tool.inputSchema,
        },
      }));
      console.log('🔧 Available MCP tools:', tools.length);
      console.log('🔧 Tool definitions:', tools);
    } catch (error) {
      console.warn('Failed to get MCP tools:', error);
    }

    const request: ChatCompletionRequest = {
      model: this.config.model,
      messages,
      temperature: this.config.temperature,
      max_tokens: this.config.maxTokens,
      stream: false,
      tools: tools.length > 0 ? tools : undefined,
      tool_choice: tools.length > 0 ? 'auto' : undefined,
      ...options,
    };

    console.log('🚀 Sending request to AI:', {
      model: request.model,
      toolsCount: tools.length,
      hasTools: !!request.tools,
      toolChoice: request.tool_choice
    });

    try {
      console.log('🌐 Making request to:', `${this.config.endpoint}/v1/chat/completions`);
      console.log('📤 Request body:', JSON.stringify(request, null, 2));

      const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(request),
      });

      console.log('📡 Response status:', response.status, response.statusText);

      if (!response.ok) {
        const errorText = await response.text();
        console.error('❌ Error response body:', errorText);

        let errorData;
        try {
          errorData = JSON.parse(errorText);
        } catch {
          errorData = { message: errorText };
        }

        throw this.createError(
          'API_ERROR',
          `HTTP ${response.status}: ${response.statusText}`,
          errorData
        );
      }

      const data = await response.json();
      console.log('📥 AI Response:', {
        choices: data.choices?.length,
        hasToolCalls: !!data.choices?.[0]?.message?.tool_calls,
        toolCallsCount: data.choices?.[0]?.message?.tool_calls?.length || 0,
        content: data.choices?.[0]?.message?.content?.substring(0, 100) + '...'
      });
      return data as ChatCompletionResponse;
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        throw this.createError('REQUEST_CANCELLED', 'Request was cancelled');
      }

      if (error instanceof Error) {
        throw error;
      }

      throw this.createError('UNKNOWN_ERROR', 'An unknown error occurred');
    }
  }

  /**
   * Execute tool calls and return results
   */
  async executeToolCalls(toolCalls: any[]): Promise<any[]> {
    console.log('🔧 Executing tool calls:', toolCalls.length);
    const results = [];

    for (const toolCall of toolCalls) {
      try {
        console.log('🔧 Processing tool call:', toolCall);
        const { function: func } = toolCall;

        // Parse arguments if they're a string (from Ollama format)
        const args = typeof func.arguments === 'string'
          ? JSON.parse(func.arguments)
          : func.arguments;

        console.log(`🔧 Calling tool ${func.name} with args:`, args);
        const result = await mcpClient.callTool(func.name, args);
        console.log(`✅ Tool ${func.name} result:`, result);

        // Extract reference ID from the result to include in AI context
        let referenceId: string | null = null;
        let actualData: any = result;

        // Check if response has content array (MCP format)
        if ((result as any).content && Array.isArray((result as any).content) && (result as any).content.length > 0) {
          const contentItem = (result as any).content[0];
          if (contentItem.type === 'text' && contentItem.text) {
            try {
              actualData = JSON.parse(contentItem.text);
              referenceId = actualData._referenceId;
            } catch (e) {
              // If parsing fails, just use the original result
            }
          }
        } else if ((result as any)._referenceId) {
          referenceId = (result as any)._referenceId;
        }

        // Prepare content for AI with reference ID hint
        let toolContent = JSON.stringify(result, null, 2);
        if (referenceId) {
          toolContent += `\n\n📋 Reference ID: ${referenceId}\n🔗 When responding to the user, please include this reference ID in square brackets like [${referenceId}] to enable data verification and interactive features.`;
        }

        results.push({
          tool_call_id: toolCall.id,
          role: 'tool',
          content: toolContent,
        });
      } catch (error) {
        console.error(`❌ Tool execution failed for ${toolCall.function?.name}:`, error);
        results.push({
          tool_call_id: toolCall.id,
          role: 'tool',
          content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
        });
      }
    }

    console.log('🔧 Tool execution results:', results);
    return results;
  }

  /**
   * Parse tool calls from text content (fallback for models that don't support function calling)
   */
  private parseToolCallsFromText(content: string): { toolCalls: any[], cleanContent: string } {
    // Match both single and triple backticks
    const toolCallPattern = /(`{1,3})tool_code\s*\n?([^`]+)\1/g;
    const toolCalls: any[] = [];
    let cleanContent = content;

    let match;
    while ((match = toolCallPattern.exec(content)) !== null) {
      const toolCallText = match[2].trim(); // match[2] is the content, match[1] is the backticks

      // Parse function call like: get_ticker(symbol="BTCUSDT")
      const functionCallPattern = /(\w+)\s*\(\s*([^)]*)\s*\)/;
      const funcMatch = functionCallPattern.exec(toolCallText);

      if (funcMatch) {
        const functionName = funcMatch[1];
        const argsString = funcMatch[2];

        // Parse arguments (simple key=value parsing)
        const args: Record<string, any> = {};
        if (argsString) {
          const argPattern = /(\w+)\s*=\s*"([^"]+)"/g;
          let argMatch;
          while ((argMatch = argPattern.exec(argsString)) !== null) {
            args[argMatch[1]] = argMatch[2];
          }
        }

        toolCalls.push({
          id: `call_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
          type: 'function',
          function: {
            name: functionName,
            arguments: JSON.stringify(args)
          }
        });

        // Remove the tool call from content
        cleanContent = cleanContent.replace(match[0], '').trim();
      }
    }

    return { toolCalls, cleanContent };
  }

  /**
   * Send a chat completion with automatic tool calling
   */
  async chatWithTools(messages: ChatMessage[]): Promise<ChatMessage[]> {
    const conversationMessages = [...messages];
    let response = await this.chat(conversationMessages);

    // Check if the response contains tool calls
    const choice = response.choices[0];
    let toolCalls = choice?.message?.tool_calls;
    let content = choice?.message?.content || '';

    // If no native tool calls, try to parse from text content
    if (!toolCalls && content) {
      const parsed = this.parseToolCallsFromText(content);
      if (parsed.toolCalls.length > 0) {
        toolCalls = parsed.toolCalls;
        content = parsed.cleanContent;
        console.log('🔍 Parsed tool calls from text:', toolCalls);
      }
    }

    if (toolCalls && toolCalls.length > 0) {
      // Add the assistant's message with tool calls
      conversationMessages.push({
        role: 'assistant',
        content: content,
        tool_calls: toolCalls,
      });

      // Execute tool calls
      const toolResults = await this.executeToolCalls(toolCalls);

      // Add tool results to conversation
      conversationMessages.push(...toolResults);

      // Get final response with tool results - reuse the same conversation context
      console.log('🔄 Getting final response with tool results (system prompt already included)');
      response = await this.chat(conversationMessages);
    }

    return conversationMessages.concat({
      role: 'assistant',
      content: response.choices[0]?.message?.content || '',
    });
  }

  /**
   * Send a streaming chat completion request
   */
  async streamChat(
    messages: ChatMessage[],
    onChunk: (chunk: ChatCompletionStreamResponse) => void,
    options?: Partial<ChatCompletionRequest>
  ): Promise<void> {
    // Cancel any existing stream
    this.cancelStream();

    this.controller = new AbortController();

    const request: ChatCompletionRequest = {
      model: this.config.model,
      messages,
      temperature: this.config.temperature,
      max_tokens: this.config.maxTokens,
      stream: true,
      ...options,
    };

    try {
      const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(request),
        signal: this.controller.signal,
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw this.createError(
          'API_ERROR',
          `HTTP ${response.status}: ${response.statusText}`,
          errorData
        );
      }

      if (!response.body) {
        throw this.createError('STREAM_ERROR', 'No response body received');
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      try {
        while (true) {
          const { done, value } = await reader.read();

          if (done) break;

          const chunk = decoder.decode(value, { stream: true });
          const lines = chunk.split('\n').filter(line => line.trim());

          for (const line of lines) {
            if (line.startsWith('data: ')) {
              const data = line.slice(6);

              if (data === '[DONE]') {
                return;
              }

              try {
                const parsed = JSON.parse(data) as ChatCompletionStreamResponse;
                onChunk(parsed);
              } catch (parseError) {
                console.warn('Failed to parse streaming chunk:', parseError);
              }
            }
          }
        }
      } finally {
        reader.releaseLock();
      }
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        throw this.createError('REQUEST_CANCELLED', 'Stream was cancelled');
      }

      if (error instanceof Error) {
        throw error;
      }

      throw this.createError('STREAM_ERROR', 'Streaming failed');
    } finally {
      this.controller = undefined;
    }
  }

  /**
   * Cancel the current streaming request
   */
  cancelStream(): void {
    if (this.controller) {
      this.controller.abort();
      this.controller = undefined;
    }
  }

  /**
   * Check if the AI service is connected and available
   */
  async isConnected(): Promise<boolean> {
    try {
      const response = await fetch(`${this.config.endpoint}/v1/models`, {
        method: 'GET',
        signal: AbortSignal.timeout(5000), // 5 second timeout
      });

      return response.ok;
    } catch {
      return false;
    }
  }

  /**
   * Get available models from the AI service
   */
  async getModels(): Promise<ModelInfo[]> {
    try {
      const response = await fetch(`${this.config.endpoint}/v1/models`);

      if (!response.ok) {
        throw this.createError('API_ERROR', 'Failed to fetch models');
      }

      const data = await response.json();

      if (data.data && Array.isArray(data.data)) {
        return data.data.map((model: any) => ({
          id: model.id,
          name: model.id,
          description: model.description,
          contextLength: model.context_length,
          capabilities: model.capabilities,
        }));
      }

      return [];
    } catch (error) {
      console.error('Failed to fetch models:', error);
      return [];
    }
  }

  /**
   * Update the AI configuration
   */
  updateConfig(newConfig: Partial<AIConfig>): void {
    this.config = { ...this.config, ...newConfig };
  }

  /**
   * Get current configuration
   */
  getConfig(): AIConfig {
    return { ...this.config };
  }



  /**
   * Create a standardised error object
   */
  private createError(code: string, message: string, details?: unknown): AIError {
    const error = new Error(message) as Error & AIError;
    error.code = code;
    error.message = message;
    error.details = details;
    return error;
  }
}

// Function to generate system prompt with current timestamp
export function generateSystemPrompt(): string {
  // Use the centralized system prompt service for legacy compatibility
  return systemPromptService.generateLegacySystemPrompt();
}

// Async function to generate system prompt with dynamic tools
export async function generateDynamicSystemPrompt(): Promise<string> {
  return await systemPromptService.generateSystemPrompt({
    includeTimestamp: true,
    includeTools: true,
    includeMemoryContext: false
  });
}

// Default system prompt for Bybit MCP integration (for backward compatibility)
export const DEFAULT_SYSTEM_PROMPT = generateSystemPrompt();

// Create default AI client instance
export function createAIClient(config?: Partial<AIConfig>): AIClient {
  const defaultConfig: AIConfig = {
    endpoint: 'http://localhost:11434',
    model: 'qwen3-30b-a3b-ud-nothink-128k:q4_k_xl',
    temperature: 0.7,
    maxTokens: 2048,
    systemPrompt: DEFAULT_SYSTEM_PROMPT,
    ...config,
  };

  return new AIClient(defaultConfig);
}

// Singleton instance
export const aiClient = createAIClient();

```

--------------------------------------------------------------------------------
/webui/src/components/AgentDashboard.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Agent Dashboard - Shows memory, performance, and analysis statistics
 */

import { multiStepAgent } from '@/services/multiStepAgent';
import type { ChatApp } from './ChatApp';

// Interface for memory statistics
interface MemoryStats {
  conversations: number;
  marketContexts: number;
  analysisHistory: number;
  totalSymbols: number;
}

export class AgentDashboard {
  private container: HTMLElement;
  private isVisible: boolean = false;
  private refreshInterval: NodeJS.Timeout | null = null;
  private chatApp?: ChatApp;

  constructor(containerId: string, chatApp?: ChatApp) {
    this.container = document.getElementById(containerId)!;
    if (!this.container) {
      throw new Error(`Container element with id "${containerId}" not found`);
    }

    this.chatApp = chatApp;
    this.initialize();
  }

  private initialize(): void {
    this.createDashboardStructure();
    this.setupEventListeners();
    this.startAutoRefresh();
  }

  private createDashboardStructure(): void {
    this.container.innerHTML = `
      <div class="agent-dashboard ${this.isVisible ? 'visible' : 'hidden'}">
        <div class="dashboard-header">
          <h3>Agent Dashboard</h3>
          <div class="dashboard-controls">
            <button class="refresh-btn" id="dashboard-refresh" aria-label="Refresh dashboard">
              🔄
            </button>
            <button class="toggle-btn" id="dashboard-toggle" aria-label="Toggle dashboard">
              📊
            </button>
          </div>
        </div>

        <div class="dashboard-content">
          <!-- Empty State Notice -->
          <div class="dashboard-notice" id="dashboard-notice" style="display: none;">
            <div class="notice-content">
              <h4>🤖 Agent Dashboard</h4>
              <p>This dashboard will show agent performance metrics, memory usage, and analysis history once you start using the agent mode.</p>
              <p><strong>To get started:</strong></p>
              <ol>
                <li>Enable Agent Mode in Settings (⚙️)</li>
                <li>Ask questions about cryptocurrency markets</li>
                <li>Watch the dashboard populate with data!</li>
              </ol>
            </div>
          </div>

          <!-- Memory Statistics -->
          <div class="dashboard-section">
            <h4>Memory Statistics</h4>
            <div class="stats-grid" id="memory-stats">
              <div class="stat-item">
                <span class="stat-label">Conversations:</span>
                <span class="stat-value" id="memory-conversations">0</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Market Contexts:</span>
                <span class="stat-value" id="memory-contexts">0</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Analysis History:</span>
                <span class="stat-value" id="memory-analyses">0</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Tracked Symbols:</span>
                <span class="stat-value" id="memory-symbols">0</span>
              </div>
            </div>
          </div>

          <!-- Performance Statistics -->
          <div class="dashboard-section">
            <h4>Performance Statistics</h4>
            <div class="stats-grid" id="performance-stats">
              <div class="stat-item">
                <span class="stat-label">Success Rate:</span>
                <span class="stat-value" id="perf-success-rate">0%</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Avg Tool Time:</span>
                <span class="stat-value" id="perf-avg-time">0ms</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Parallel Savings:</span>
                <span class="stat-value" id="perf-savings">0ms</span>
              </div>
              <div class="stat-item">
                <span class="stat-label">Total Tools:</span>
                <span class="stat-value" id="perf-tool-count">0</span>
              </div>
            </div>
          </div>

          <!-- Recent Analysis -->
          <div class="dashboard-section">
            <h4>Recent Analysis</h4>
            <div class="analysis-list" id="recent-analysis">
              <div class="empty-state">
                <p>No recent analysis available.</p>
              </div>
            </div>
          </div>

          <!-- Actions -->
          <div class="dashboard-section">
            <h4>Actions</h4>
            <div class="action-buttons">
              <button class="action-btn" id="clear-memory">Clear Memory</button>
              <button class="action-btn" id="new-conversation">New Conversation</button>
              <button class="action-btn" id="export-data">Export Data</button>
            </div>
          </div>
        </div>
      </div>
    `;
  }

  private setupEventListeners(): void {
    // Toggle dashboard visibility
    const toggleBtn = this.container.querySelector('#dashboard-toggle') as HTMLButtonElement;
    toggleBtn?.addEventListener('click', () => {
      this.toggleVisibility();
    });

    // Refresh dashboard
    const refreshBtn = this.container.querySelector('#dashboard-refresh') as HTMLButtonElement;
    refreshBtn?.addEventListener('click', () => {
      this.refreshDashboard();
    });

    // Clear memory
    const clearMemoryBtn = this.container.querySelector('#clear-memory') as HTMLButtonElement;
    clearMemoryBtn?.addEventListener('click', () => {
      this.clearMemory();
    });

    // New conversation
    const newConversationBtn = this.container.querySelector('#new-conversation') as HTMLButtonElement;
    newConversationBtn?.addEventListener('click', () => {
      this.startNewConversation();
    });

    // Export data
    const exportDataBtn = this.container.querySelector('#export-data') as HTMLButtonElement;
    exportDataBtn?.addEventListener('click', () => {
      this.exportData();
    });

    // Keyboard shortcut to toggle dashboard (Ctrl/Cmd + M)
    document.addEventListener('keydown', (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
        e.preventDefault();
        this.toggleVisibility();
      }
    });
  }

  private startAutoRefresh(): void {
    // Refresh every 5 seconds when dashboard is visible
    this.refreshInterval = setInterval(() => {
      if (this.isVisible) {
        this.refreshDashboard();
      }
    }, 5000);
  }

  public toggleVisibility(): void {
    this.isVisible = !this.isVisible;
    const dashboard = this.container.querySelector('.agent-dashboard');

    if (this.isVisible) {
      dashboard?.classList.remove('hidden');
      dashboard?.classList.add('visible');
      this.refreshDashboard();
    } else {
      dashboard?.classList.remove('visible');
      dashboard?.classList.add('hidden');
    }
  }

  public show(): void {
    if (!this.isVisible) {
      this.toggleVisibility();
    }
  }

  public hide(): void {
    if (this.isVisible) {
      this.toggleVisibility();
    }
  }

  public get visible(): boolean {
    return this.isVisible;
  }

  private refreshDashboard(): void {
    this.updateMemoryStats();
    this.updatePerformanceStats();
    this.updateRecentAnalysis();
  }

  private updateMemoryStats(): void {
    try {
      const memoryStats = multiStepAgent.getMemoryStats();

      const conversationsEl = this.container.querySelector('#memory-conversations');
      const contextsEl = this.container.querySelector('#memory-contexts');
      const analysesEl = this.container.querySelector('#memory-analyses');
      const symbolsEl = this.container.querySelector('#memory-symbols');

      if (conversationsEl) conversationsEl.textContent = memoryStats.conversations.toString();
      if (contextsEl) contextsEl.textContent = memoryStats.marketContexts.toString();
      if (analysesEl) analysesEl.textContent = memoryStats.analysisHistory.toString();
      if (symbolsEl) symbolsEl.textContent = memoryStats.totalSymbols.toString();

      // Show helpful message if no data yet
      this.updateEmptyStateMessage(memoryStats);

    } catch (error) {
      console.warn('Failed to update memory stats:', error);
    }
  }

  private updatePerformanceStats(): void {
    try {
      const perfStats = multiStepAgent.getPerformanceStats();

      const successRateEl = this.container.querySelector('#perf-success-rate');
      const avgTimeEl = this.container.querySelector('#perf-avg-time');
      const savingsEl = this.container.querySelector('#perf-savings');
      const toolCountEl = this.container.querySelector('#perf-tool-count');

      if (successRateEl) {
        successRateEl.textContent = `${(perfStats.successRate * 100).toFixed(1)}%`;
      }
      if (avgTimeEl) {
        avgTimeEl.textContent = `${Math.round(perfStats.averageToolTime)}ms`;
      }
      if (savingsEl) {
        savingsEl.textContent = `${Math.round(perfStats.parallelSavings)}ms`;
      }
      if (toolCountEl) {
        toolCountEl.textContent = perfStats.toolCount.toString();
      }

    } catch (error) {
      console.warn('Failed to update performance stats:', error);
    }
  }

  private updateRecentAnalysis(): void {
    try {
      const recentAnalysis = multiStepAgent.getAnalysisHistory(undefined, 5);
      const listContainer = this.container.querySelector('#recent-analysis');

      if (!listContainer) return;

      if (recentAnalysis.length === 0) {
        listContainer.innerHTML = `
          <div class="empty-state">
            <p>No recent analysis available.</p>
          </div>
        `;
        return;
      }

      const analysisHtml = recentAnalysis.map(analysis => `
        <div class="analysis-item">
          <div class="analysis-header">
            <span class="analysis-symbol">${analysis.symbol}</span>
            <span class="analysis-type">${analysis.analysisType}</span>
            <span class="analysis-time">${this.getTimeAgo(analysis.timestamp)}</span>
          </div>
          <div class="analysis-query">${this.truncateText(analysis.query, 60)}</div>
          <div class="analysis-metrics">
            <span class="metric">Duration: ${analysis.duration}ms</span>
            <span class="metric">Tools: ${analysis.toolsUsed.length}</span>
            ${analysis.accuracy ? `<span class="metric">Accuracy: ${(analysis.accuracy * 100).toFixed(0)}%</span>` : ''}
          </div>
        </div>
      `).join('');

      listContainer.innerHTML = analysisHtml;

    } catch (error) {
      console.warn('Failed to update recent analysis:', error);
    }
  }

  private clearMemory(): void {
    if (confirm('Are you sure you want to clear all agent memory? This action cannot be undone.')) {
      multiStepAgent.clearMemory();
      this.refreshDashboard();
      this.showToast('Memory cleared successfully!');
    }
  }

  private startNewConversation(): void {
    // Clear agent memory
    multiStepAgent.startNewConversation();

    // Clear chat UI if available
    if (this.chatApp) {
      this.chatApp.clearMessages();
    }

    this.showToast('New conversation started!');
  }

  private exportData(): void {
    try {
      const data = {
        memoryStats: multiStepAgent.getMemoryStats(),
        performanceStats: multiStepAgent.getPerformanceStats(),
        recentAnalysis: multiStepAgent.getAnalysisHistory(undefined, 20),
        exportedAt: new Date().toISOString()
      };

      const dataStr = JSON.stringify(data, null, 2);
      const blob = new Blob([dataStr], { type: 'application/json' });
      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');
      a.href = url;
      a.download = `agent-data-${new Date().toISOString().split('T')[0]}.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);

      this.showToast('Data exported successfully!');

    } catch (error) {
      console.error('Failed to export data:', error);
      this.showToast('Failed to export data', 'error');
    }
  }

  private showToast(message: string, type: 'success' | 'error' = 'success'): void {
    const toast = document.createElement('div');
    toast.className = `dashboard-toast toast-${type}`;
    toast.textContent = message;

    document.body.appendChild(toast);

    // Animate in
    setTimeout(() => toast.classList.add('show'), 10);

    // Remove after 3 seconds
    setTimeout(() => {
      toast.classList.remove('show');
      setTimeout(() => toast.remove(), 300);
    }, 3000);
  }

  private getTimeAgo(timestamp: number): string {
    const now = Date.now();
    const diff = now - timestamp;

    const seconds = Math.floor(diff / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);

    if (seconds < 60) return `${seconds}s ago`;
    if (minutes < 60) return `${minutes}m ago`;
    if (hours < 24) return `${hours}h ago`;

    return new Date(timestamp).toLocaleDateString();
  }

  private truncateText(text: string, maxLength: number): string {
    if (text.length <= maxLength) return text;
    return text.substring(0, maxLength) + '...';
  }

  private updateEmptyStateMessage(memoryStats: MemoryStats): void {
    const hasData = memoryStats.conversations > 0 || memoryStats.analysisHistory > 0;

    if (!hasData) {
      // Add notice to dashboard if no data
      const dashboardContent = this.container.querySelector('.dashboard-content');
      if (dashboardContent && !dashboardContent.querySelector('.dashboard-notice')) {
        const notice = document.createElement('div');
        notice.className = 'dashboard-notice';
        notice.innerHTML = `
          <div class="notice-content">
            <h4>🤖 Agent Dashboard</h4>
            <p>This dashboard will show agent performance metrics, memory usage, and analysis history once you start using the agent mode.</p>
            <p><strong>To get started:</strong></p>
            <ol>
              <li>Enable Agent Mode in Settings (⚙️)</li>
              <li>Ask questions about cryptocurrency markets</li>
              <li>Watch the dashboard populate with data!</li>
            </ol>
          </div>
        `;
        dashboardContent.insertBefore(notice, dashboardContent.firstChild);
      }
    } else {
      // Remove notice if data exists
      const notice = this.container.querySelector('.dashboard-notice');
      if (notice) {
        notice.remove();
      }
    }
  }

  public destroy(): void {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
      this.refreshInterval = null;
    }
  }
}

```

--------------------------------------------------------------------------------
/webui/src/components/chat/DataCard.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * DataCard Component - Expandable cards for visualising tool response data
 *
 * Provides a clean, collapsible interface for displaying structured data
 * with embedded visualisations when expanded.
 */

export interface DataCardConfig {
  title: string;
  summary: string;
  data: any;
  dataType: 'kline' | 'rsi' | 'orderBlocks' | 'price' | 'volume' | 'unknown';
  expanded?: boolean;
  showChart?: boolean;
}

export class DataCard {
  private container: HTMLElement;
  private config: DataCardConfig;
  private isExpanded: boolean = false;
  private chartContainer?: HTMLElement;

  constructor(container: HTMLElement, config: DataCardConfig) {
    this.container = container;
    this.config = config;
    this.isExpanded = config.expanded || false;

    this.render();
    this.setupEventListeners();

    // If expanded by default, render chart after DOM is ready
    if (this.isExpanded) {
      setTimeout(() => {
        this.renderChart();
      }, 150);
    }
  }

  /**
   * Render the data card structure
   */
  private render(): void {
    this.container.innerHTML = `
      <div class="data-card ${this.isExpanded ? 'expanded' : 'collapsed'}" data-type="${this.config.dataType}">
        <div class="data-card-header" role="button" tabindex="0" aria-expanded="${this.isExpanded}">
          <div class="data-card-title">
            <span class="data-card-icon">${this.getDataTypeIcon()}</span>
            <h4>${this.config.title}</h4>
          </div>
          <div class="data-card-controls">
            <span class="data-card-summary">${this.config.summary}</span>
            <button class="expand-toggle" aria-label="${this.isExpanded ? 'Collapse' : 'Expand'} data card">
              <span class="expand-icon">${this.isExpanded ? '▼' : '▶'}</span>
            </button>
          </div>
        </div>
        <div class="data-card-content" ${this.isExpanded ? '' : 'style="display: none;"'}>
          <div class="data-card-details">
            ${this.renderDataSummary()}
          </div>
          ${this.config.showChart !== false ? '<div class="data-card-chart" id="chart-' + this.generateId() + '"></div>' : ''}
        </div>
      </div>
    `;

    // Store reference to chart container if it exists
    const chartElement = this.container.querySelector('.data-card-chart') as HTMLElement;
    if (chartElement) {
      this.chartContainer = chartElement;
    }
  }

  /**
   * Set up event listeners for card interactions
   */
  private setupEventListeners(): void {
    const header = this.container.querySelector('.data-card-header') as HTMLElement;
    const toggleButton = this.container.querySelector('.expand-toggle') as HTMLElement;

    if (header) {
      header.addEventListener('click', () => this.toggle());
      header.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          this.toggle();
        }
      });
    }

    if (toggleButton) {
      toggleButton.addEventListener('click', (e) => {
        e.stopPropagation();
        this.toggle();
      });
    }
  }

  /**
   * Toggle card expanded/collapsed state
   */
  public toggle(): void {
    this.isExpanded = !this.isExpanded;
    this.updateExpandedState();
  }

  /**
   * Expand the card
   */
  public expand(): void {
    if (!this.isExpanded) {
      this.isExpanded = true;
      this.updateExpandedState();
    }
  }

  /**
   * Collapse the card
   */
  public collapse(): void {
    if (this.isExpanded) {
      this.isExpanded = false;
      this.updateExpandedState();
    }
  }

  /**
   * Update the visual state when expanded/collapsed
   */
  private updateExpandedState(): void {
    const card = this.container.querySelector('.data-card') as HTMLElement;
    const content = this.container.querySelector('.data-card-content') as HTMLElement;
    const header = this.container.querySelector('.data-card-header') as HTMLElement;
    const expandIcon = this.container.querySelector('.expand-icon') as HTMLElement;

    if (card && content && header && expandIcon) {
      // Update classes
      card.classList.toggle('expanded', this.isExpanded);
      card.classList.toggle('collapsed', !this.isExpanded);

      // Update ARIA attributes
      header.setAttribute('aria-expanded', this.isExpanded.toString());

      // Update expand icon
      expandIcon.textContent = this.isExpanded ? '▼' : '▶';

      // Update button label
      const toggleButton = this.container.querySelector('.expand-toggle') as HTMLElement;
      if (toggleButton) {
        toggleButton.setAttribute('aria-label', `${this.isExpanded ? 'Collapse' : 'Expand'} data card`);
      }

      // Animate content visibility
      if (this.isExpanded) {
        content.style.display = 'block';
        // Trigger chart rendering with a small delay to ensure DOM is ready
        setTimeout(() => {
          this.renderChart();
        }, 100);
      } else {
        // Add a small delay to allow animation
        setTimeout(() => {
          if (!this.isExpanded) {
            content.style.display = 'none';
          }
        }, 250);
      }
    }
  }

  /**
   * Get appropriate icon for data type
   */
  private getDataTypeIcon(): string {
    switch (this.config.dataType) {
      case 'kline': return '📈';
      case 'rsi': return '📊';
      case 'orderBlocks': return '🧱';
      case 'price': return '💰';
      case 'volume': return '📊';
      default: return '📋';
    }
  }

  /**
   * Render data summary in the expanded view
   */
  private renderDataSummary(): string {
    // This will be enhanced based on data type
    if (typeof this.config.data === 'object') {
      return `<pre class="data-preview">${JSON.stringify(this.config.data, null, 2)}</pre>`;
    }
    return `<div class="data-preview">${this.config.data}</div>`;
  }

  /**
   * Render chart when card is expanded
   */
  private renderChart(): void {
    if (!this.chartContainer || this.config.showChart === false) {
      return;
    }

    // Render different chart types based on data type
    switch (this.config.dataType) {
      case 'kline':
        this.renderCandlestickChart();
        break;
      case 'rsi':
        this.renderLineChart();
        break;
      case 'price':
        this.renderPriceChart();
        break;
      default:
        this.renderPlaceholder();
        break;
    }
  }

  /**
   * Format timestamp for X-axis labels
   */
  private formatTimestamp(timestamp: number, interval: string): string {
    const date = new Date(timestamp);

    // For different intervals, show different levels of detail
    switch (interval) {
      case '1':  // 1 minute
      case '5':  // 5 minutes
      case '15': // 15 minutes
      case '30': // 30 minutes
        return date.toLocaleTimeString('en-US', {
          hour: '2-digit',
          minute: '2-digit',
          hour12: false
        });
      case '60':  // 1 hour
      case '240': // 4 hours
        return date.toLocaleDateString('en-US', {
          month: 'short',
          day: 'numeric',
          hour: '2-digit'
        });
      case 'D':   // Daily
      case 'W':   // Weekly
      default:
        return date.toLocaleDateString('en-US', {
          month: 'short',
          day: 'numeric'
        });
    }
  }

  /**
   * Render candlestick chart for kline data
   */
  private renderCandlestickChart(): void {
    if (!this.chartContainer) return;

    // Extract kline data
    let klineData = this.config.data;
    if (this.config.data.data && Array.isArray(this.config.data.data)) {
      klineData = this.config.data.data;
    }

    if (!Array.isArray(klineData) || klineData.length === 0) {
      this.renderPlaceholder();
      return;
    }

    // Create canvas element with responsive sizing
    const canvas = document.createElement('canvas');
    const maxWidth = Math.min(this.chartContainer.clientWidth || 600, 800); // Cap at 800px
    const containerWidth = Math.max(maxWidth - 40, 400); // Ensure minimum 400px with padding
    canvas.width = containerWidth;
    canvas.height = 350; // Increased height for X-axis labels
    canvas.style.width = '100%';
    canvas.style.maxWidth = `${containerWidth}px`;
    canvas.style.height = '350px';
    canvas.style.border = '1px solid #ddd';
    canvas.style.display = 'block';
    canvas.style.margin = '0 auto';

    this.chartContainer.innerHTML = '';
    this.chartContainer.appendChild(canvas);

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // Parse and normalize data
    const candles = klineData.slice(-50).map((item: any) => {
      if (Array.isArray(item)) {
        return {
          timestamp: parseInt(item[0]),
          open: parseFloat(item[1]),
          high: parseFloat(item[2]),
          low: parseFloat(item[3]),
          close: parseFloat(item[4]),
          volume: parseFloat(item[5] || 0)
        };
      } else if (typeof item === 'object') {
        return {
          timestamp: parseInt(item.timestamp || item.time || item.openTime || 0),
          open: parseFloat(item.open || 0),
          high: parseFloat(item.high || 0),
          low: parseFloat(item.low || 0),
          close: parseFloat(item.close || 0),
          volume: parseFloat(item.volume || 0)
        };
      }
      return null;
    }).filter(Boolean);

    if (candles.length === 0) {
      this.renderPlaceholder();
      return;
    }

    // Calculate price range
    const prices = candles.flatMap(c => c ? [c.high, c.low] : []);
    const minPrice = Math.min(...prices);
    const maxPrice = Math.max(...prices);
    const priceRange = maxPrice - minPrice;
    const padding = priceRange * 0.1;

    // Chart dimensions - adjusted for X-axis labels
    const chartWidth = canvas.width - 80;
    const chartHeight = canvas.height - 90; // More space for X-axis
    const chartX = 60;
    const chartY = 20;

    // Clear canvas
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Draw background grid
    ctx.strokeStyle = '#f0f0f0';
    ctx.lineWidth = 1;
    for (let i = 0; i <= 5; i++) {
      const y = chartY + (chartHeight * i) / 5;
      ctx.beginPath();
      ctx.moveTo(chartX, y);
      ctx.lineTo(chartX + chartWidth, y);
      ctx.stroke();
    }

    // Draw vertical grid lines for time
    const timeSteps = Math.min(candles.length, 6);
    for (let i = 0; i <= timeSteps; i++) {
      const x = chartX + (chartWidth * i) / timeSteps;
      ctx.beginPath();
      ctx.moveTo(x, chartY);
      ctx.lineTo(x, chartY + chartHeight);
      ctx.stroke();
    }

    // Draw price labels (Y-axis)
    ctx.fillStyle = '#666';
    ctx.font = '12px Arial';
    ctx.textAlign = 'right';
    for (let i = 0; i <= 5; i++) {
      const price = maxPrice + padding - ((maxPrice + padding - (minPrice - padding)) * i) / 5;
      const y = chartY + (chartHeight * i) / 5;
      ctx.fillText(price.toFixed(4), chartX - 10, y + 4);
    }

    // Draw time labels (X-axis)
    ctx.textAlign = 'center';
    const interval = this.config.data.interval || 'D';
    for (let i = 0; i <= timeSteps; i++) {
      const candleIndex = Math.floor((candles.length - 1) * i / timeSteps);
      if (candles[candleIndex]) {
        const x = chartX + (chartWidth * i) / timeSteps;
        const timeLabel = this.formatTimestamp(candles[candleIndex].timestamp, interval);
        ctx.fillText(timeLabel, x, chartY + chartHeight + 20);
      }
    }

    // Draw candlesticks
    const candleWidth = Math.max(2, chartWidth / candles.length - 2);
    candles.forEach((candle, index) => {
      if (!candle) return;

      const x = chartX + (index * chartWidth) / candles.length + (chartWidth / candles.length - candleWidth) / 2;

      // Calculate y positions
      const highY = chartY + ((maxPrice + padding - candle.high) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
      const lowY = chartY + ((maxPrice + padding - candle.low) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
      const openY = chartY + ((maxPrice + padding - candle.open) / (maxPrice + padding - (minPrice - padding))) * chartHeight;
      const closeY = chartY + ((maxPrice + padding - candle.close) / (maxPrice + padding - (minPrice - padding))) * chartHeight;

      // Determine candle color
      const isGreen = candle.close >= candle.open;
      const bodyColor = isGreen ? '#22c55e' : '#ef4444';
      const wickColor = '#666';

      // Draw wick (high-low line)
      ctx.strokeStyle = wickColor;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(x + candleWidth / 2, highY);
      ctx.lineTo(x + candleWidth / 2, lowY);
      ctx.stroke();

      // Draw candle body
      ctx.fillStyle = bodyColor;
      const bodyTop = Math.min(openY, closeY);
      const bodyHeight = Math.abs(closeY - openY) || 1;
      ctx.fillRect(x, bodyTop, candleWidth, bodyHeight);
    });

    // Add title
    ctx.fillStyle = '#333';
    ctx.font = 'bold 14px Arial';
    ctx.textAlign = 'left';
    const symbol = this.config.data.symbol || 'Symbol';
    const intervalLabel = this.config.data.interval || '';
    ctx.fillText(`${symbol} ${intervalLabel} Candlestick Chart`, chartX, 15);

    // Add current price info
    const lastCandle = candles[candles.length - 1];
    if (lastCandle) {
      ctx.font = '12px Arial';
      ctx.fillStyle = lastCandle.close >= lastCandle.open ? '#22c55e' : '#ef4444';
      ctx.textAlign = 'right';
      ctx.fillText(`Last: $${lastCandle.close.toFixed(4)}`, canvas.width - 10, 15);
    }
  }

  /**
   * Render line chart for RSI and other indicators
   */
  private renderLineChart(): void {
    if (!this.chartContainer) return;

    this.chartContainer.innerHTML = `
      <div class="chart-placeholder">
        <p>📊 Line chart for ${this.config.dataType} data</p>
        <small>Line chart implementation coming soon</small>
      </div>
    `;
  }

  /**
   * Render simple price chart
   */
  private renderPriceChart(): void {
    if (!this.chartContainer) return;

    this.chartContainer.innerHTML = `
      <div class="chart-placeholder">
        <p>💰 Price chart rendering</p>
        <small>Price chart implementation coming soon</small>
      </div>
    `;
  }

  /**
   * Render placeholder for unsupported chart types
   */
  private renderPlaceholder(): void {
    if (!this.chartContainer) return;

    this.chartContainer.innerHTML = `
      <div class="chart-placeholder">
        <p>Chart rendering for ${this.config.dataType} data</p>
        <small>Chart component will be implemented next</small>
      </div>
    `;
  }

  /**
   * Generate unique ID for chart container
   */
  private generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }

  /**
   * Update card configuration
   */
  public updateConfig(newConfig: Partial<DataCardConfig>): void {
    this.config = { ...this.config, ...newConfig };
    this.render();
    this.setupEventListeners();
  }

  /**
   * Get current card state
   */
  public getState(): { expanded: boolean; dataType: string } {
    return {
      expanded: this.isExpanded,
      dataType: this.config.dataType
    };
  }

  /**
   * Destroy the card and clean up event listeners
   */
  public destroy(): void {
    // Event listeners will be automatically removed when innerHTML is cleared
    this.container.innerHTML = '';
  }
}

```

--------------------------------------------------------------------------------
/src/tools/GetMarketStructure.ts:
--------------------------------------------------------------------------------

```typescript
import { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js"
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"
import { z } from "zod"
import { BaseToolImplementation } from "./BaseTool.js"
import { KlineData, calculateRSI, calculateVolatility } from "../utils/mathUtils.js"
import { 
  detectOrderBlocks,
  getActiveLevels,
  calculateOrderBlockStats,
  VolumeAnalysisConfig
} from "../utils/volumeAnalysis.js"
import { 
  applyKNNToRSI, 
  KNNConfig 
} from "../utils/knnAlgorithm.js"
import { GetKlineParamsV5, KlineIntervalV3 } from "bybit-api"

// Zod schema for input validation
const inputSchema = z.object({
  symbol: z.string()
    .min(1, "Symbol is required")
    .regex(/^[A-Z0-9]+$/, "Symbol must contain only uppercase letters and numbers"),
  category: z.enum(["spot", "linear", "inverse"]),
  interval: z.enum(["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]),
  analysisDepth: z.number().min(100).max(500).optional().default(200),
  includeOrderBlocks: z.boolean().optional().default(true),
  includeMLRSI: z.boolean().optional().default(true),
  includeLiquidityZones: z.boolean().optional().default(true)
})

type ToolArguments = z.infer<typeof inputSchema>

interface LiquidityZone {
  price: number;
  strength: number;
  type: "support" | "resistance";
}

interface MarketStructureResponse {
  symbol: string;
  interval: string;
  marketRegime: "trending_up" | "trending_down" | "ranging" | "volatile";
  trendStrength: number;
  volatilityLevel: "low" | "medium" | "high";
  keyLevels: {
    support: number[];
    resistance: number[];
    liquidityZones: LiquidityZone[];
  };
  orderBlocks?: {
    bullishBlocks: any[];
    bearishBlocks: any[];
    activeBullishBlocks: number;
    activeBearishBlocks: number;
  };
  mlRsi?: {
    currentRsi: number;
    mlRsi: number;
    adaptiveOverbought: number;
    adaptiveOversold: number;
    trend: string;
    confidence: number;
  };
  recommendations: string[];
  metadata: {
    analysisDepth: number;
    calculationTime: number;
    confidence: number;
    dataQuality: "excellent" | "good" | "fair" | "poor";
  };
}

class GetMarketStructure extends BaseToolImplementation {
  name = "get_market_structure"
  toolDefinition: Tool = {
    name: this.name,
    description: "Advanced market structure analysis combining ML-RSI, order blocks, and liquidity zones. Provides comprehensive market regime detection and trading recommendations.",
    inputSchema: {
      type: "object",
      properties: {
        symbol: {
          type: "string",
          description: "Trading pair symbol (e.g., 'BTCUSDT')",
          pattern: "^[A-Z0-9]+$"
        },
        category: {
          type: "string",
          description: "Category of the instrument",
          enum: ["spot", "linear", "inverse"]
        },
        interval: {
          type: "string",
          description: "Kline interval",
          enum: ["1", "3", "5", "15", "30", "60", "120", "240", "360", "720", "D", "W", "M"]
        },
        analysisDepth: {
          type: "number",
          description: "How far back to analyse (default: 200)",
          minimum: 100,
          maximum: 500
        },
        includeOrderBlocks: {
          type: "boolean",
          description: "Include order block analysis (default: true)"
        },
        includeMLRSI: {
          type: "boolean",
          description: "Include ML-RSI analysis (default: true)"
        },
        includeLiquidityZones: {
          type: "boolean",
          description: "Include liquidity analysis (default: true)"
        }
      },
      required: ["symbol", "category", "interval"]
    }
  }

  async toolCall(request: z.infer<typeof CallToolRequestSchema>): Promise<CallToolResult> {
    const startTime = Date.now()
    
    try {
      this.logInfo("Starting get_market_structure tool call")

      // Parse and validate input
      const validationResult = inputSchema.safeParse(request.params.arguments)
      if (!validationResult.success) {
        const errorDetails = validationResult.error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
          code: err.code
        }))
        throw new Error(`Invalid input: ${JSON.stringify(errorDetails)}`)
      }

      const args = validationResult.data

      // Fetch kline data
      const klineData = await this.fetchKlineData(args)
      
      if (klineData.length < 50) {
        throw new Error(`Insufficient data. Need at least 50 data points, got ${klineData.length}`)
      }

      // Analyze market structure
      const analysis = await this.analyzeMarketStructure(klineData, args)

      const calculationTime = Date.now() - startTime

      const response: MarketStructureResponse = {
        symbol: args.symbol,
        interval: args.interval,
        marketRegime: analysis.marketRegime,
        trendStrength: analysis.trendStrength,
        volatilityLevel: analysis.volatilityLevel,
        keyLevels: analysis.keyLevels,
        orderBlocks: args.includeOrderBlocks ? analysis.orderBlocks : undefined,
        mlRsi: args.includeMLRSI ? analysis.mlRsi : undefined,
        recommendations: analysis.recommendations,
        metadata: {
          analysisDepth: args.analysisDepth,
          calculationTime,
          confidence: analysis.confidence,
          dataQuality: analysis.dataQuality
        }
      }

      this.logInfo(`Market structure analysis completed in ${calculationTime}ms. Regime: ${analysis.marketRegime}, Confidence: ${analysis.confidence}%`)
      return this.formatResponse(response)

    } catch (error) {
      this.logInfo(`Market structure analysis failed: ${error instanceof Error ? error.message : String(error)}`)
      return this.handleError(error)
    }
  }

  private async fetchKlineData(args: ToolArguments): Promise<KlineData[]> {
    const params: GetKlineParamsV5 = {
      category: args.category,
      symbol: args.symbol,
      interval: args.interval as KlineIntervalV3,
      limit: args.analysisDepth
    }

    const response = await this.executeRequest(() => this.client.getKline(params))
    
    if (!response.list || response.list.length === 0) {
      throw new Error("No kline data received from API")
    }

    // Convert API response to KlineData format
    return response.list.map(kline => ({
      timestamp: parseInt(kline[0]),
      open: parseFloat(kline[1]),
      high: parseFloat(kline[2]),
      low: parseFloat(kline[3]),
      close: parseFloat(kline[4]),
      volume: parseFloat(kline[5])
    })).reverse() // Reverse to get chronological order
  }

  private async analyzeMarketStructure(klineData: KlineData[], args: ToolArguments) {
    const closePrices = klineData.map(k => k.close)
    const highs = klineData.map(k => k.high)
    const lows = klineData.map(k => k.low)
    
    // Calculate basic indicators
    const rsiValues = calculateRSI(closePrices, 14)
    const volatility = calculateVolatility(closePrices, 20)
    
    // Determine market regime
    const marketRegime = this.determineMarketRegime(klineData, rsiValues, volatility)
    
    // Calculate trend strength
    const trendStrength = this.calculateTrendStrength(closePrices)
    
    // Determine volatility level
    const volatilityLevel = this.determineVolatilityLevel(volatility)
    
    // Analyze order blocks if requested
    let orderBlocks: any = undefined
    if (args.includeOrderBlocks) {
      const config: VolumeAnalysisConfig = {
        volumePivotLength: 3, // Reduced for better detection
        bullishBlocks: 5,
        bearishBlocks: 5,
        mitigationMethod: 'wick'
      }
      
      const { bullishBlocks, bearishBlocks } = detectOrderBlocks(klineData, config)
      const stats = calculateOrderBlockStats(bullishBlocks, bearishBlocks)
      
      orderBlocks = {
        bullishBlocks,
        bearishBlocks,
        activeBullishBlocks: stats.activeBullishBlocks,
        activeBearishBlocks: stats.activeBearishBlocks
      }
    }
    
    // Analyze ML-RSI if requested
    let mlRsi: any = undefined
    if (args.includeMLRSI && rsiValues.length > 0) {
      const currentRsi = rsiValues[rsiValues.length - 1]
      // Simplified ML-RSI for market structure analysis
      mlRsi = {
        currentRsi,
        mlRsi: currentRsi, // Simplified for now
        adaptiveOverbought: 70,
        adaptiveOversold: 30,
        trend: currentRsi > 50 ? "bullish" : "bearish",
        confidence: 75
      }
    }
    
    // Identify key levels
    const keyLevels = this.identifyKeyLevels(klineData, args.includeLiquidityZones)
    
    // Generate recommendations
    const recommendations = this.generateRecommendations(marketRegime, trendStrength, volatilityLevel, mlRsi, orderBlocks)
    
    // Calculate overall confidence
    const confidence = this.calculateConfidence(klineData.length, volatility)
    
    // Assess data quality
    const dataQuality = this.assessDataQuality(klineData)
    
    return {
      marketRegime,
      trendStrength,
      volatilityLevel,
      keyLevels,
      orderBlocks,
      mlRsi,
      recommendations,
      confidence,
      dataQuality
    }
  }

  private determineMarketRegime(klineData: KlineData[], rsiValues: number[], volatility: number[]): "trending_up" | "trending_down" | "ranging" | "volatile" {
    const closePrices = klineData.map(k => k.close)
    const recentPrices = closePrices.slice(-20) // Last 20 periods
    
    if (recentPrices.length < 10) return "ranging"
    
    const firstPrice = recentPrices[0]
    const lastPrice = recentPrices[recentPrices.length - 1]
    const priceChange = (lastPrice - firstPrice) / firstPrice
    
    const avgVolatility = volatility.length > 0 
      ? volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
      : 0
    
    const avgRsi = rsiValues.length > 0
      ? rsiValues.slice(-10).reduce((sum, r) => sum + r, 0) / Math.min(10, rsiValues.length)
      : 50
    
    // High volatility threshold
    const highVolatilityThreshold = lastPrice * 0.02 // 2% of price
    
    if (avgVolatility > highVolatilityThreshold) {
      return "volatile"
    }
    
    if (priceChange > 0.03 && avgRsi > 45) { // 3% up move
      return "trending_up"
    } else if (priceChange < -0.03 && avgRsi < 55) { // 3% down move
      return "trending_down"
    } else {
      return "ranging"
    }
  }

  private calculateTrendStrength(prices: number[]): number {
    if (prices.length < 20) return 50
    
    const recent = prices.slice(-20)
    const slope = this.calculateLinearRegressionSlope(recent)
    const correlation = this.calculateCorrelation(recent)
    
    // Normalize slope and correlation to 0-100 scale
    const normalizedSlope = Math.min(100, Math.max(0, (Math.abs(slope) * 1000) + 50))
    const normalizedCorrelation = Math.abs(correlation) * 100
    
    return Math.round((normalizedSlope + normalizedCorrelation) / 2)
  }

  private calculateLinearRegressionSlope(values: number[]): number {
    const n = values.length
    const x = Array.from({ length: n }, (_, i) => i)
    
    const sumX = x.reduce((sum, val) => sum + val, 0)
    const sumY = values.reduce((sum, val) => sum + val, 0)
    const sumXY = x.reduce((sum, val, idx) => sum + val * values[idx], 0)
    const sumXX = x.reduce((sum, val) => sum + val * val, 0)
    
    return (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
  }

  private calculateCorrelation(values: number[]): number {
    const n = values.length
    const x = Array.from({ length: n }, (_, i) => i)
    
    const meanX = x.reduce((sum, val) => sum + val, 0) / n
    const meanY = values.reduce((sum, val) => sum + val, 0) / n
    
    const numerator = x.reduce((sum, val, idx) => sum + (val - meanX) * (values[idx] - meanY), 0)
    const denomX = Math.sqrt(x.reduce((sum, val) => sum + Math.pow(val - meanX, 2), 0))
    const denomY = Math.sqrt(values.reduce((sum, val) => sum + Math.pow(val - meanY, 2), 0))
    
    return denomX * denomY !== 0 ? numerator / (denomX * denomY) : 0
  }

  private determineVolatilityLevel(volatility: number[]): "low" | "medium" | "high" {
    if (volatility.length === 0) return "medium"
    
    const avgVolatility = volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
    const maxVolatility = Math.max(...volatility.slice(-20))
    
    const relativeVolatility = avgVolatility / maxVolatility
    
    if (relativeVolatility < 0.3) return "low"
    if (relativeVolatility > 0.7) return "high"
    return "medium"
  }

  private identifyKeyLevels(klineData: KlineData[], includeLiquidityZones: boolean) {
    const highs = klineData.map(k => k.high)
    const lows = klineData.map(k => k.low)
    
    // Find significant highs and lows
    const resistance = this.findSignificantLevels(highs, 'high').slice(0, 5)
    const support = this.findSignificantLevels(lows, 'low').slice(0, 5)
    
    const liquidityZones: LiquidityZone[] = []
    
    if (includeLiquidityZones) {
      // Create liquidity zones around key levels
      resistance.forEach(level => {
        liquidityZones.push({
          price: level,
          strength: 75,
          type: "resistance"
        })
      })
      
      support.forEach(level => {
        liquidityZones.push({
          price: level,
          strength: 75,
          type: "support"
        })
      })
    }
    
    return {
      support: support.sort((a, b) => b - a), // Descending
      resistance: resistance.sort((a, b) => a - b), // Ascending
      liquidityZones
    }
  }

  private findSignificantLevels(values: number[], type: 'high' | 'low'): number[] {
    const levels: number[] = []
    const lookback = 5
    
    for (let i = lookback; i < values.length - lookback; i++) {
      const current = values[i]
      let isSignificant = true
      
      // Check if current value is a local extreme
      for (let j = i - lookback; j <= i + lookback; j++) {
        if (j !== i) {
          if (type === 'high' && values[j] >= current) {
            isSignificant = false
            break
          } else if (type === 'low' && values[j] <= current) {
            isSignificant = false
            break
          }
        }
      }
      
      if (isSignificant) {
        levels.push(current)
      }
    }
    
    return levels
  }

  private generateRecommendations(
    marketRegime: string,
    trendStrength: number,
    volatilityLevel: string,
    mlRsi: any,
    orderBlocks: any
  ): string[] {
    const recommendations: string[] = []
    
    // Market regime recommendations
    switch (marketRegime) {
      case "trending_up":
        recommendations.push("Market is in an uptrend - consider long positions on pullbacks")
        if (trendStrength > 70) {
          recommendations.push("Strong uptrend detected - momentum strategies may be effective")
        }
        break
      case "trending_down":
        recommendations.push("Market is in a downtrend - consider short positions on rallies")
        if (trendStrength > 70) {
          recommendations.push("Strong downtrend detected - avoid catching falling knives")
        }
        break
      case "ranging":
        recommendations.push("Market is ranging - consider mean reversion strategies")
        recommendations.push("Look for support and resistance bounces")
        break
      case "volatile":
        recommendations.push("High volatility detected - use smaller position sizes")
        recommendations.push("Consider volatility-based strategies or wait for calmer conditions")
        break
    }
    
    // Volatility recommendations
    if (volatilityLevel === "high") {
      recommendations.push("High volatility - use wider stops and smaller positions")
    } else if (volatilityLevel === "low") {
      recommendations.push("Low volatility - potential for breakout moves")
    }
    
    // RSI recommendations
    if (mlRsi) {
      if (mlRsi.currentRsi > 70) {
        recommendations.push("RSI indicates overbought conditions - watch for potential reversal")
      } else if (mlRsi.currentRsi < 30) {
        recommendations.push("RSI indicates oversold conditions - potential buying opportunity")
      }
    }
    
    // Order block recommendations
    if (orderBlocks && (orderBlocks.activeBullishBlocks > 0 || orderBlocks.activeBearishBlocks > 0)) {
      recommendations.push("Active order blocks detected - watch for reactions at these levels")
    }
    
    return recommendations
  }

  private calculateConfidence(dataPoints: number, volatility: number[]): number {
    let confidence = 50 // Base confidence
    
    // More data points = higher confidence
    if (dataPoints > 150) confidence += 20
    else if (dataPoints > 100) confidence += 10
    
    // Lower volatility = higher confidence in analysis
    if (volatility.length > 0) {
      const avgVolatility = volatility.slice(-10).reduce((sum, v) => sum + v, 0) / Math.min(10, volatility.length)
      const maxVolatility = Math.max(...volatility)
      const relativeVolatility = avgVolatility / maxVolatility
      
      if (relativeVolatility < 0.3) confidence += 15
      else if (relativeVolatility > 0.7) confidence -= 10
    }
    
    return Math.min(100, Math.max(0, confidence))
  }

  private assessDataQuality(klineData: KlineData[]): "excellent" | "good" | "fair" | "poor" {
    if (klineData.length > 200) return "excellent"
    if (klineData.length > 150) return "good"
    if (klineData.length > 100) return "fair"
    return "poor"
  }
}

export default GetMarketStructure

```

--------------------------------------------------------------------------------
/webui/src/components/ToolsManager.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tools Manager - Handles the MCP Tools tab functionality
 * Displays available tools, allows manual testing, and shows execution history
 */

import { mcpClient } from '@/services/mcpClient';
import type { MCPTool } from '@/types/mcp';
import { DataCard, type DataCardConfig } from './chat/DataCard';
import { detectDataType } from '../utils/dataDetection';

export class ToolsManager {
  private tools: MCPTool[] = [];
  private isInitialized = false;
  private executionHistory: Array<{
    id: string;
    tool: string;
    params: any;
    result: any;
    timestamp: number;
    success: boolean;
  }> = [];
  private dataCards: Map<string, DataCard> = new Map(); // Track DataCards by tool name

  constructor() {}

  /**
   * Initialize the tools manager
   */
  async initialize(): Promise<void> {
    if (this.isInitialized) return;

    try {
      console.log('🔧 Initializing Tools Manager...');

      // Load available tools
      await this.loadTools();

      // Render tools interface
      this.renderToolsInterface();

      this.isInitialized = true;
      console.log('✅ Tools Manager initialized');
    } catch (error) {
      console.error('❌ Failed to initialize Tools Manager:', error);
      this.showError('Failed to initialize tools');
    }
  }

  /**
   * Load available tools from MCP server
   */
  private async loadTools(): Promise<void> {
    try {
      this.tools = await mcpClient.listTools();
      console.log(`🔧 Loaded ${this.tools.length} tools`);
    } catch (error) {
      console.error('Failed to load tools:', error);
      this.tools = [];
    }
  }

  /**
   * Render the tools interface
   */
  private renderToolsInterface(): void {
    const container = document.getElementById('tools-grid');
    if (!container) return;

    if (this.tools.length === 0) {
      container.innerHTML = `
        <div class="tools-empty">
          <h3>No Tools Available</h3>
          <p>Unable to load MCP tools. Please check your connection.</p>
          <button onclick="location.reload()" class="retry-btn">Retry</button>
        </div>
      `;
      return;
    }

    // Create tools grid
    container.innerHTML = `
      <div class="tools-header">
        <h3>Available MCP Tools (${this.tools.length})</h3>
        <div class="tools-actions">
          <button id="refresh-tools" class="refresh-btn">Refresh</button>
          <button id="clear-history" class="clear-btn">Clear History</button>
        </div>
      </div>
      <div class="tools-list">
        ${this.tools.map(tool => this.renderToolCard(tool)).join('')}
      </div>
      <div class="execution-history">
        <h3>Execution History</h3>
        <div id="history-list" class="history-list">
          ${this.renderExecutionHistory()}
        </div>
      </div>
    `;

    // Set up event listeners
    this.setupEventListeners();
  }

  /**
   * Render a single tool card
   */
  private renderToolCard(tool: MCPTool): string {
    const requiredParams = tool.inputSchema?.required || [];
    const properties = tool.inputSchema?.properties || {};

    const html = `
      <div class="tool-card" data-tool="${tool.name}">
        <div class="tool-header">
          <h4>${tool.name}</h4>
          <button class="test-tool-btn" data-tool="${tool.name}">Test</button>
        </div>
        <p class="tool-description">${tool.description}</p>
        <div class="tool-params">
          <h5>Parameters:</h5>
          ${Object.entries(properties).map(([key, param]: [string, any]) => {
            // Determine default value
            let defaultValue = '';
            if (key === 'symbol') {
              defaultValue = 'XRPUSDT';
            } else if (key === 'category') {
              defaultValue = 'spot';
            } else if (key === 'interval') {
              defaultValue = '15';
            } else if (key === 'limit') {
              defaultValue = '100';
            }

            // Check if this field has enum options
            if (param.enum && Array.isArray(param.enum)) {
              // Use dropdown for enum fields
              return `
                <div class="param-item">
                  <label for="${tool.name}-${key}">
                    ${key}${requiredParams.includes(key) ? ' *' : ''}
                  </label>
                  <select id="${tool.name}-${key}" class="param-select">
                    ${param.enum.map((value: string) => `
                      <option value="${value}" ${value === defaultValue ? 'selected' : ''}>
                        ${value}
                      </option>
                    `).join('')}
                  </select>
                  ${param.description ? `<small class="param-description">${param.description}</small>` : ''}
                </div>
              `;
            } else {
              // Use input for non-enum fields
              return `
                <div class="param-item">
                  <label for="${tool.name}-${key}">
                    ${key}${requiredParams.includes(key) ? ' *' : ''}
                  </label>
                  <input
                    type="text"
                    id="${tool.name}-${key}"
                    placeholder="${param.description || ''}"
                    value="${defaultValue}"
                    class="param-input"
                  />
                  ${param.description ? `<small class="param-description">${param.description}</small>` : ''}
                </div>
              `;
            }
          }).join('')}
        </div>
        <div class="tool-result" id="result-${tool.name}" style="display: none;">
          <div class="result-header">
            <h5>Result</h5>
            <button class="result-close" data-tool="${tool.name}" title="Close result">&times;</button>
          </div>
          <div class="result-content" id="result-content-${tool.name}"></div>
        </div>
      </div>
    `;

    return html;
  }

  /**
   * Render execution history
   */
  private renderExecutionHistory(): string {
    if (this.executionHistory.length === 0) {
      return '<p class="history-empty">No executions yet</p>';
    }

    return this.executionHistory
      .slice(-10) // Show last 10 executions
      .reverse()
      .map(execution => `
        <div class="history-item ${execution.success ? 'success' : 'error'}">
          <div class="history-header">
            <span class="tool-name">${execution.tool}</span>
            <span class="timestamp">${new Date(execution.timestamp).toLocaleTimeString()}</span>
          </div>
          <div class="history-params">
            <strong>Params:</strong> ${JSON.stringify(execution.params, null, 2)}
          </div>
          <div class="history-result">
            <strong>Result:</strong>
            <pre>${JSON.stringify(execution.result, null, 2)}</pre>
          </div>
        </div>
      `).join('');
  }

  /**
   * Set up event listeners
   */
  private setupEventListeners(): void {
    // Refresh tools button
    const refreshBtn = document.getElementById('refresh-tools');
    if (refreshBtn) {
      refreshBtn.addEventListener('click', () => {
        this.refreshTools();
      });
    }

    // Clear history button
    const clearBtn = document.getElementById('clear-history');
    if (clearBtn) {
      clearBtn.addEventListener('click', () => {
        this.clearHistory();
      });
    }

    // Test tool buttons
    document.querySelectorAll('.test-tool-btn').forEach(btn => {
      btn.addEventListener('click', (event) => {
        const target = event.target as HTMLElement;
        const toolName = target.dataset.tool;
        if (toolName) {
          this.testTool(toolName);
        }
      });
    });

    // Result close buttons
    document.querySelectorAll('.result-close').forEach(btn => {
      btn.addEventListener('click', (event) => {
        const target = event.target as HTMLElement;
        const toolName = target.dataset.tool;
        if (toolName) {
          this.hideToolResult(toolName);
        }
      });
    });
  }

  /**
   * Test a specific tool
   */
  private async testTool(toolName: string): Promise<void> {
    const tool = this.tools.find(t => t.name === toolName);
    if (!tool) return;

    // Collect parameters from form
    const params: any = {};
    const properties = tool.inputSchema?.properties || {};

    for (const [key] of Object.entries(properties)) {
      const element = document.getElementById(`${toolName}-${key}`) as HTMLInputElement | HTMLSelectElement;
      if (element && element.value) {
        params[key] = element.value;
      }
    }

    try {
      console.log(`🔧 Testing tool ${toolName} with params:`, params);

      // Show loading state
      this.showToolLoading(toolName);

      // Execute tool
      const result = await mcpClient.callTool(toolName as any, params);

      // Record execution
      this.recordExecution(toolName, params, result, true);

      // Show result in tool card
      this.showToolResult(toolName, result, true);

      // Update UI
      this.hideToolLoading(toolName);
      this.updateHistoryDisplay();

      console.log(`✅ Tool ${toolName} executed successfully:`, result);

    } catch (error) {
      console.error(`❌ Tool ${toolName} execution failed:`, error);

      // Record failed execution
      this.recordExecution(toolName, params, error, false);

      // Show error in tool card
      this.showToolResult(toolName, error, false);

      this.hideToolLoading(toolName);
      this.updateHistoryDisplay();
    }
  }

  /**
   * Record tool execution
   */
  private recordExecution(tool: string, params: any, result: any, success: boolean): void {
    this.executionHistory.push({
      id: Date.now().toString(),
      tool,
      params,
      result,
      timestamp: Date.now(),
      success,
    });

    // Keep only last 50 executions
    if (this.executionHistory.length > 50) {
      this.executionHistory = this.executionHistory.slice(-50);
    }
  }

  /**
   * Update history display
   */
  private updateHistoryDisplay(): void {
    const historyContainer = document.getElementById('history-list');
    if (historyContainer) {
      historyContainer.innerHTML = this.renderExecutionHistory();
    }
  }

  /**
   * Show tool loading state
   */
  private showToolLoading(toolName: string): void {
    const btn = document.querySelector(`.test-tool-btn[data-tool="${toolName}"]`) as HTMLElement;
    if (btn) {
      btn.textContent = 'Testing...';
      btn.setAttribute('disabled', 'true');
    }
  }

  /**
   * Hide tool loading state
   */
  private hideToolLoading(toolName: string): void {
    const btn = document.querySelector(`.test-tool-btn[data-tool="${toolName}"]`) as HTMLElement;
    if (btn) {
      btn.textContent = 'Test';
      btn.removeAttribute('disabled');
    }
  }

  /**
   * Show tool result in the tool card
   */
  private showToolResult(toolName: string, result: any, success: boolean): void {
    const resultContainer = document.getElementById(`result-${toolName}`);
    const resultContent = document.getElementById(`result-content-${toolName}`);

    if (!resultContainer || !resultContent) {
      console.error(`❌ Could not find result DOM elements for ${toolName}`);
      return;
    }

    this.displayResult(resultContainer, resultContent, result, success);
  }

  private displayResult(resultContainer: HTMLElement, resultContent: HTMLElement, result: any, success: boolean): void {
    if (!success) {
      // Handle error case with existing logic
      this.displayErrorResult(resultContent, result);
      resultContainer.style.display = 'block';
      return;
    }

    // Extract actual data from MCP content structure
    const actualData = this.extractActualData(result);

    // Try to create a DataCard for visualisable data
    const dataCardCreated = this.tryCreateDataCard(resultContainer, resultContent, actualData);

    if (!dataCardCreated) {
      // Fall back to traditional JSON display
      this.displayTraditionalResult(resultContent, actualData);
    }

    // Show the result container
    resultContainer.style.display = 'block';

    // Scroll result into view
    resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  }

  /**
   * Extract actual data from MCP content structure
   */
  private extractActualData(result: any): any {
    let actualData = result;

    // Check if this is an MCP content response
    if (result && result.content && Array.isArray(result.content) && result.content.length > 0) {
      const firstContent = result.content[0];
      if (firstContent.type === 'text' && firstContent.text) {
        try {
          // Try to parse the text as JSON
          actualData = JSON.parse(firstContent.text);
        } catch {
          // If parsing fails, use the text as-is
          actualData = firstContent.text;
        }
      }
    }

    return actualData;
  }

  /**
   * Try to create a DataCard for visualisable data
   */
  private tryCreateDataCard(resultContainer: HTMLElement, resultContent: HTMLElement, actualData: any): boolean {
    try {
      // Detect if the data is visualisable
      const detection = detectDataType(actualData);

      if (!detection.visualisable || detection.confidence < 0.6) {
        return false;
      }

      // Get tool name from container
      const toolName = this.getToolNameFromContainer(resultContainer);
      if (!toolName) {
        return false;
      }

      // Clean up any existing DataCard for this tool
      const existingCard = this.dataCards.get(toolName);
      if (existingCard) {
        existingCard.destroy();
        this.dataCards.delete(toolName);
      }

      // Create DataCard configuration
      const cardConfig: DataCardConfig = {
        title: this.generateToolCardTitle(toolName, detection),
        summary: detection.summary,
        data: actualData,
        dataType: detection.dataType,
        expanded: true, // Start expanded in tools tab for immediate visibility
        showChart: true
      };

      // Create container for DataCard
      const cardContainer = document.createElement('div');
      cardContainer.className = 'tool-result-datacard';

      // Create and store the DataCard
      const dataCard = new DataCard(cardContainer, cardConfig);
      this.dataCards.set(toolName, dataCard);

      // Add status and actions above the card
      resultContent.innerHTML = `
        <div class="result-status result-success">
          ✅ Success - Data Visualisation Available
        </div>
        <div class="result-actions">
          <button class="copy-result-btn" data-result="${encodeURIComponent(JSON.stringify(actualData, null, 2))}">
            📋 Copy Raw Data
          </button>
          <button class="toggle-raw-btn">
            📊 Show Raw JSON
          </button>
        </div>
      `;

      // Append the DataCard
      resultContent.appendChild(cardContainer);

      // Add toggle functionality for raw data
      this.setupDataCardActions(resultContent, actualData);

      return true;
    } catch (error) {
      console.warn('Failed to create DataCard for tool result:', error);
      return false;
    }
  }

  /**
   * Set up actions for DataCard (copy, toggle raw data)
   */
  private setupDataCardActions(resultContent: HTMLElement, actualData: any): void {
    const copyBtn = resultContent.querySelector('.copy-result-btn') as HTMLElement;
    const toggleBtn = resultContent.querySelector('.toggle-raw-btn') as HTMLElement;

    if (copyBtn) {
      copyBtn.addEventListener('click', () => {
        const resultData = decodeURIComponent(copyBtn.dataset.result || '');
        navigator.clipboard.writeText(resultData).then(() => {
          copyBtn.textContent = '✅ Copied!';
          setTimeout(() => {
            copyBtn.textContent = '📋 Copy Raw Data';
          }, 2000);
        }).catch(() => {
          copyBtn.textContent = '❌ Failed';
          setTimeout(() => {
            copyBtn.textContent = '📋 Copy Raw Data';
          }, 2000);
        });
      });
    }

    if (toggleBtn) {
      let showingRaw = false;
      toggleBtn.addEventListener('click', () => {
        const cardContainer = resultContent.querySelector('.tool-result-datacard') as HTMLElement;
        if (!cardContainer) return;

        if (showingRaw) {
          // Show DataCard
          cardContainer.style.display = 'block';
          const rawDataDiv = resultContent.querySelector('.raw-data-display');
          if (rawDataDiv) rawDataDiv.remove();
          toggleBtn.textContent = '📊 Show Raw JSON';
          showingRaw = false;
        } else {
          // Show raw JSON
          cardContainer.style.display = 'none';
          const rawDataDiv = document.createElement('div');
          rawDataDiv.className = 'raw-data-display';
          rawDataDiv.innerHTML = `<pre class="result-data">${JSON.stringify(actualData, null, 2)}</pre>`;
          resultContent.appendChild(rawDataDiv);
          toggleBtn.textContent = '🎴 Show DataCard';
          showingRaw = true;
        }
      });
    }
  }

  /**
   * Display traditional JSON result (fallback)
   */
  private displayTraditionalResult(resultContent: HTMLElement, actualData: any): void {
    let formattedResult: string;

    if (typeof actualData === 'object') {
      formattedResult = JSON.stringify(actualData, null, 2);
    } else {
      formattedResult = String(actualData);
    }

    resultContent.innerHTML = `
      <div class="result-status result-success">
        ✅ Success
      </div>
      <pre class="result-data">${formattedResult}</pre>
      <div class="result-actions">
        <button class="copy-result-btn" data-result="${encodeURIComponent(formattedResult)}">
          📋 Copy
        </button>
      </div>
    `;

    // Add copy functionality
    const copyBtn = resultContent.querySelector('.copy-result-btn') as HTMLElement;
    if (copyBtn) {
      copyBtn.addEventListener('click', () => {
        const resultData = decodeURIComponent(copyBtn.dataset.result || '');
        navigator.clipboard.writeText(resultData).then(() => {
          copyBtn.textContent = '✅ Copied!';
          setTimeout(() => {
            copyBtn.textContent = '📋 Copy';
          }, 2000);
        }).catch(() => {
          copyBtn.textContent = '❌ Failed';
          setTimeout(() => {
            copyBtn.textContent = '📋 Copy';
          }, 2000);
        });
      });
    }
  }

  /**
   * Display error result
   */
  private displayErrorResult(resultContent: HTMLElement, result: any): void {
    let formattedResult: string;

    if (result instanceof Error) {
      formattedResult = `Error: ${result.message}`;
    } else {
      formattedResult = `Error: ${String(result)}`;
    }

    resultContent.innerHTML = `
      <div class="result-status result-error">
        ❌ Error
      </div>
      <pre class="result-data">${formattedResult}</pre>
    `;
  }

  /**
   * Get tool name from result container
   */
  private getToolNameFromContainer(resultContainer: HTMLElement): string | null {
    const id = resultContainer.id;
    if (id && id.startsWith('result-')) {
      return id.substring(7); // Remove 'result-' prefix
    }
    return null;
  }

  /**
   * Generate appropriate title for tool DataCard
   */
  private generateToolCardTitle(toolName: string, _detection: any): string {
    const toolDisplayNames: Record<string, string> = {
      'get_ticker': 'Ticker Data',
      'get_kline_data': 'Kline Data',
      'get_ml_rsi': 'ML-RSI Analysis',
      'get_order_blocks': 'Order Blocks',
      'get_market_structure': 'Market Structure'
    };

    return toolDisplayNames[toolName] || toolName.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
  }

  /**
   * Hide tool result
   */
  private hideToolResult(toolName: string): void {
    const resultContainer = document.getElementById(`result-${toolName}`);
    if (resultContainer) {
      resultContainer.style.display = 'none';
    }

    // Clean up associated DataCard
    const dataCard = this.dataCards.get(toolName);
    if (dataCard) {
      dataCard.destroy();
      this.dataCards.delete(toolName);
    }
  }

  /**
   * Refresh tools
   */
  private async refreshTools(): Promise<void> {
    console.log('🔄 Refreshing tools...');
    await this.loadTools();
    this.renderToolsInterface();
  }

  /**
   * Clear execution history
   */
  private clearHistory(): void {
    this.executionHistory = [];
    this.updateHistoryDisplay();
  }

  /**
   * Show error message
   */
  private showError(message: string): void {
    const container = document.getElementById('tools-grid');
    if (container) {
      container.innerHTML = `
        <div class="tools-error">
          <h3>❌ Error</h3>
          <p>${message}</p>
          <button onclick="location.reload()">Retry</button>
        </div>
      `;
    }
  }

  /**
   * Get current state
   */
  getState(): { tools: MCPTool[]; history: any[] } {
    return {
      tools: [...this.tools],
      history: [...this.executionHistory],
    };
  }

  /**
   * Destroy tools manager
   */
  destroy(): void {
    // Clean up all DataCards
    this.dataCards.forEach(card => card.destroy());
    this.dataCards.clear();

    this.isInitialized = false;
    console.log('🗑️ Tools Manager destroyed');
  }
}

// Create singleton instance
export const toolsManager = new ToolsManager();

```

--------------------------------------------------------------------------------
/webui/src/main.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Main application entry point
 */

console.log('🚀 Main.ts loading...');

import './styles/main.css';

import { ChatApp } from './components/ChatApp';
import { DebugConsole } from './components/DebugConsole';
import { DataVerificationPanel } from './components/DataVerificationPanel';
import { AgentDashboard } from './components/AgentDashboard';
import { toolsManager } from './components/ToolsManager';
import { configService } from './services/configService';
import { agentConfigService } from './services/agentConfig';
import { mcpClient } from './services/mcpClient';
import { aiClient } from './services/aiClient';
import { multiStepAgent } from './services/multiStepAgent';
// Import logService to initialize console interception
import './services/logService';

class App {
  private chatApp?: ChatApp;
  private debugConsole?: DebugConsole;
  private verificationPanel?: DataVerificationPanel;
  private agentDashboard?: AgentDashboard;
  private isInitialized = false;
  private toolsInitialized = false;

  async initialize(): Promise<void> {
    if (this.isInitialized) return;

    try {
      // Show loading state
      this.showLoading();

      // Initialize services
      await this.initializeServices();

      // Initialize UI components
      this.initializeUI();

      // Initialize debug console
      this.initializeDebugConsole();

      // Initialize data verification panel
      this.initializeVerificationPanel();

      // Initialize agent dashboard
      this.initializeAgentDashboard();

      // Hide loading and show main app
      this.hideLoading();

      this.isInitialized = true;
      console.log('✅ Bybit MCP WebUI initialized successfully');
    } catch (error) {
      console.error('❌ Failed to initialize application:', error);
      this.showError('Failed to initialize application. Please check your configuration.');
    }
  }

  private async initializeServices(): Promise<void> {
    console.log('🚀 Initializing services...');

    // Get current configuration
    const aiConfig = configService.getAIConfig();
    const mcpConfig = configService.getMCPConfig();

    console.log('⚙️ AI Config:', {
      endpoint: aiConfig.endpoint,
      model: aiConfig.model,
      temperature: aiConfig.temperature,
      maxTokens: aiConfig.maxTokens
    });
    console.log('⚙️ MCP Config:', mcpConfig);

    // Note: MCP server should be started automatically with 'pnpm dev:full'
    console.log('💡 If MCP server is not running, use "pnpm dev:full" to start both services');

    // Update clients with current config
    aiClient.updateConfig(aiConfig);
    mcpClient.setBaseUrl(mcpConfig.endpoint);
    mcpClient.setTimeout(mcpConfig.timeout);

    // Test connections
    console.log('🔄 Testing connections...');
    const [aiConnected, mcpConnected] = await Promise.allSettled([
      aiClient.isConnected(),
      mcpClient.isConnected(),
    ]);

    console.log('📊 Connection results:', {
      ai: aiConnected.status === 'fulfilled' ? aiConnected.value : aiConnected.reason,
      mcp: mcpConnected.status === 'fulfilled' ? mcpConnected.value : mcpConnected.reason
    });

    // Initialize MCP client (fetch available tools)
    if (mcpConnected.status === 'fulfilled' && mcpConnected.value) {
      try {
        await mcpClient.initialize();
        console.log('✅ MCP client initialized');
      } catch (error) {
        console.warn('⚠️ MCP client initialization failed:', error);
      }
    } else {
      console.warn('⚠️ MCP server not reachable');
    }

    // Log connection status
    if (aiConnected.status === 'fulfilled' && aiConnected.value) {
      console.log('✅ AI service connected');
    } else {
      console.warn('⚠️ AI service not reachable');
    }

    // Initialize multi-step agent
    try {
      console.log('🤖 Initializing multi-step agent...');
      await multiStepAgent.initialize();
      console.log('✅ Multi-step agent initialized');
    } catch (error) {
      console.warn('⚠️ Multi-step agent initialization failed:', error);
      console.log('💡 Falling back to legacy AI client');
    }

    console.log('✅ Service initialization complete');
  }

  private initializeUI(): void {
    // Initialize chat application
    this.chatApp = new ChatApp();

    // Set up global event listeners
    this.setupGlobalEventListeners();

    // Set up theme toggle
    this.setupThemeToggle();

    // Set up settings modal
    this.setupSettingsModal();
  }

  private setupGlobalEventListeners(): void {
    // Handle keyboard shortcuts
    document.addEventListener('keydown', (event) => {
      // Ctrl/Cmd + K to focus chat input
      if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
        event.preventDefault();
        const chatInput = document.getElementById('chat-input') as HTMLTextAreaElement;
        if (chatInput) {
          chatInput.focus();
        }
      }

      // Escape to close modals
      if (event.key === 'Escape') {
        this.closeAllModals();
      }
    });

    // Handle navigation
    document.querySelectorAll('.nav-item').forEach(item => {
      item.addEventListener('click', (event) => {
        const target = event.currentTarget as HTMLElement;
        const view = target.dataset.view;
        if (view) {
          this.switchView(view);
        }
      });
    });

    // Handle example queries
    document.querySelectorAll('.example-query').forEach(button => {
      button.addEventListener('click', (event) => {
        const target = event.currentTarget as HTMLElement;
        const query = target.textContent?.trim();
        if (query && this.chatApp) {
          this.chatApp.sendMessage(query);
        }
      });
    });

    // Agent settings button removed - now integrated into main settings modal

    // Handle agent mode toggle
    const agentToggleBtn = document.getElementById('agent-toggle-btn');
    if (agentToggleBtn && this.chatApp) {
      agentToggleBtn.addEventListener('click', () => {
        const isUsingAgent = this.chatApp!.isUsingAgent();
        this.chatApp!.toggleAgentMode(!isUsingAgent);
        agentToggleBtn.textContent = !isUsingAgent ? '🤖 Agent Mode' : '🔄 Legacy Mode';
      });
    }
  }

  private setupThemeToggle(): void {
    const themeToggle = document.getElementById('theme-toggle');
    if (themeToggle) {
      themeToggle.addEventListener('click', () => {
        const settings = configService.getSettings();
        const currentTheme = settings.ui.theme;

        let newTheme: 'light' | 'dark' | 'auto';
        let icon: string;

        if (currentTheme === 'light') {
          newTheme = 'dark';
          icon = '☀️';
        } else if (currentTheme === 'dark') {
          newTheme = 'auto';
          icon = '🌓';
        } else {
          newTheme = 'light';
          icon = '🌙';
        }

        configService.updateSettings({
          ui: { ...settings.ui, theme: newTheme },
        });

        // Update icon
        const iconElement = themeToggle.querySelector('.theme-icon');
        if (iconElement) {
          iconElement.textContent = icon;
        }
      });
    }
  }

  private setupAgentDashboardButton(): void {
    const agentDashboardBtn = document.getElementById('agent-dashboard-btn');
    if (agentDashboardBtn && this.agentDashboard) {
      agentDashboardBtn.addEventListener('click', () => {
        this.agentDashboard!.toggleVisibility();

        // Update button appearance based on dashboard visibility
        const isVisible = this.agentDashboard!.visible;
        if (isVisible) {
          agentDashboardBtn.classList.add('active');
        } else {
          agentDashboardBtn.classList.remove('active');
        }
      });
    }
  }

  private setupSettingsModal(): void {
    const settingsBtn = document.getElementById('settings-btn');
    const settingsModal = document.getElementById('settings-modal');
    const closeSettings = document.getElementById('close-settings');
    const saveSettings = document.getElementById('save-settings');

    if (settingsBtn && settingsModal) {
      settingsBtn.addEventListener('click', () => {
        this.openSettingsModal();
      });
    }

    if (closeSettings && settingsModal) {
      closeSettings.addEventListener('click', () => {
        settingsModal.classList.add('hidden');
        settingsModal.classList.remove('active');
      });
    }

    if (saveSettings) {
      saveSettings.addEventListener('click', () => {
        this.saveSettingsFromModal();
      });
    }

    // Close modal when clicking backdrop
    if (settingsModal) {
      settingsModal.addEventListener('click', (event) => {
        if (event.target === settingsModal) {
          settingsModal.classList.add('hidden');
          settingsModal.classList.remove('active');
        }
      });
    }
  }

  private initializeDebugConsole(): void {
    // Create debug console container
    const debugContainer = document.createElement('div');
    debugContainer.id = 'debug-console-container';
    document.body.appendChild(debugContainer);

    // Initialize debug console
    this.debugConsole = new DebugConsole(debugContainer);

    // Add keyboard shortcut to toggle debug console (Ctrl+` or Cmd+`)
    document.addEventListener('keydown', (e) => {
      if ((e.ctrlKey || e.metaKey) && e.key === '`') {
        e.preventDefault();
        this.debugConsole?.toggle();
      }
    });

    console.log('🔍 Debug console initialized (Ctrl+` to toggle)');
  }

  private initializeVerificationPanel(): void {
    try {
      // Initialize data verification panel
      this.verificationPanel = new DataVerificationPanel('verification-panel-container');
      console.log('📊 Data verification panel initialized (Ctrl+D to toggle)');

      // Make panel accessible for debugging
      (window as any).verificationPanel = this.verificationPanel;
    } catch (error) {
      console.warn('⚠️ Failed to initialize verification panel:', error);
    }
  }

  private initializeAgentDashboard(): void {
    try {
      // Initialize agent dashboard with ChatApp reference
      this.agentDashboard = new AgentDashboard('agent-dashboard-container', this.chatApp);
      console.log('🤖 Agent dashboard initialized (Ctrl+M to toggle)');

      // Set up agent dashboard button now that dashboard is initialized
      this.setupAgentDashboardButton();

      // Make dashboard accessible for debugging
      (window as any).agentDashboard = this.agentDashboard;
    } catch (error) {
      console.warn('⚠️ Failed to initialize agent dashboard:', error);
    }
  }

  private openSettingsModal(): void {
    const modal = document.getElementById('settings-modal');
    if (!modal) return;

    // Populate current settings
    const settings = configService.getSettings();
    const agentConfig = agentConfigService.getConfig();
    console.log('🔧 Opening settings modal with current settings:', settings, agentConfig);

    // AI Configuration
    const aiEndpoint = document.getElementById('ai-endpoint') as HTMLInputElement;
    const aiModel = document.getElementById('ai-model') as HTMLInputElement;
    const mcpEndpoint = document.getElementById('mcp-endpoint') as HTMLInputElement;

    if (aiEndpoint) {
      aiEndpoint.value = settings.ai.endpoint;
      console.log('📝 Set AI endpoint field to:', settings.ai.endpoint);
    }
    if (aiModel) {
      aiModel.value = settings.ai.model;
      console.log('📝 Set AI model field to:', settings.ai.model);
    }
    if (mcpEndpoint) {
      mcpEndpoint.value = settings.mcp.endpoint;
      console.log('📝 Set MCP endpoint field to:', settings.mcp.endpoint);
    }

    // Agent Configuration
    const agentModeEnabled = document.getElementById('agent-mode-enabled') as HTMLInputElement;
    const maxIterations = document.getElementById('max-iterations') as HTMLInputElement;
    const toolTimeout = document.getElementById('tool-timeout') as HTMLInputElement;
    const showWorkflowSteps = document.getElementById('show-workflow-steps') as HTMLInputElement;
    const showToolCalls = document.getElementById('show-tool-calls') as HTMLInputElement;
    const enableDebugMode = document.getElementById('enable-debug-mode') as HTMLInputElement;

    if (agentModeEnabled) {
      agentModeEnabled.checked = this.chatApp?.isAgentModeEnabled() || false;
    }
    if (maxIterations) {
      maxIterations.value = agentConfig.maxIterations.toString();
    }
    if (toolTimeout) {
      toolTimeout.value = agentConfig.toolTimeout.toString();
    }
    if (showWorkflowSteps) {
      showWorkflowSteps.checked = agentConfig.showWorkflowSteps;
    }
    if (showToolCalls) {
      showToolCalls.checked = agentConfig.showToolCalls;
    }
    if (enableDebugMode) {
      enableDebugMode.checked = agentConfig.enableDebugMode;
    }

    modal.classList.remove('hidden');
    modal.classList.add('active');
  }

  private saveSettingsFromModal(): void {
    const aiEndpoint = document.getElementById('ai-endpoint') as HTMLInputElement;
    const aiModel = document.getElementById('ai-model') as HTMLInputElement;
    const mcpEndpoint = document.getElementById('mcp-endpoint') as HTMLInputElement;

    // Agent Configuration elements
    const agentModeEnabled = document.getElementById('agent-mode-enabled') as HTMLInputElement;
    const maxIterations = document.getElementById('max-iterations') as HTMLInputElement;
    const toolTimeout = document.getElementById('tool-timeout') as HTMLInputElement;
    const showWorkflowSteps = document.getElementById('show-workflow-steps') as HTMLInputElement;
    const showToolCalls = document.getElementById('show-tool-calls') as HTMLInputElement;
    const enableDebugMode = document.getElementById('enable-debug-mode') as HTMLInputElement;

    console.log('💾 Saving settings from modal...');
    console.log('AI Endpoint:', aiEndpoint?.value);
    console.log('AI Model:', aiModel?.value);
    console.log('MCP Endpoint:', mcpEndpoint?.value);
    console.log('Agent Mode:', agentModeEnabled?.checked);

    const currentSettings = configService.getSettings();
    const updates: Partial<typeof currentSettings> = {};

    // Build AI config updates
    const aiUpdates: Partial<typeof currentSettings.ai> = {};
    let hasAIUpdates = false;

    if (aiEndpoint?.value && aiEndpoint.value.trim() !== '') {
      aiUpdates.endpoint = aiEndpoint.value.trim();
      hasAIUpdates = true;
    }

    if (aiModel?.value && aiModel.value.trim() !== '') {
      aiUpdates.model = aiModel.value.trim();
      hasAIUpdates = true;
    }

    if (hasAIUpdates) {
      updates.ai = { ...currentSettings.ai, ...aiUpdates };
    }

    // Build MCP config updates
    if (mcpEndpoint?.value && mcpEndpoint.value.trim() !== '') {
      updates.mcp = { ...currentSettings.mcp, endpoint: mcpEndpoint.value.trim() };
    }

    console.log('📝 Settings updates:', updates);

    if (Object.keys(updates).length > 0) {
      configService.updateSettings(updates);
      console.log('✅ Settings saved successfully');

      // Reinitialize services with new config
      this.initializeServices().catch(console.error);
    } else {
      console.log('ℹ️ No settings changes to save');
    }

    // Save agent configuration
    const agentConfig = {
      maxIterations: parseInt(maxIterations?.value || '5'),
      toolTimeout: parseInt(toolTimeout?.value || '30000'),
      showWorkflowSteps: showWorkflowSteps?.checked || false,
      showToolCalls: showToolCalls?.checked || false,
      enableDebugMode: enableDebugMode?.checked || false,
      streamingEnabled: true // Always enabled
    };

    console.log('🤖 Saving agent config:', agentConfig);
    agentConfigService.updateConfig(agentConfig);

    // Update agent mode in chat app
    if (this.chatApp && agentModeEnabled) {
      this.chatApp.toggleAgentMode(agentModeEnabled.checked);
    }

    // Close modal
    const modal = document.getElementById('settings-modal');
    if (modal) {
      modal.classList.add('hidden');
      modal.classList.remove('active');
    }
  }

  private switchView(viewName: string): void {
    // Update navigation
    document.querySelectorAll('.nav-item').forEach(item => {
      item.classList.remove('active');
    });

    const activeNavItem = document.querySelector(`[data-view="${viewName}"]`);
    if (activeNavItem) {
      activeNavItem.classList.add('active');
    }

    // Update views
    document.querySelectorAll('.view').forEach(view => {
      view.classList.remove('active');
    });

    const activeView = document.getElementById(`${viewName}-view`);
    if (activeView) {
      activeView.classList.add('active');
    }

    // Initialize components when their views are accessed
    if (viewName === 'tools' && !this.toolsInitialized) {
      this.initializeTools();
    }

    // Handle dashboard view - embed agent dashboard into the tab
    if (viewName === 'dashboard' && this.agentDashboard) {
      this.embedDashboardInTab();
    }
  }

  /**
   * Initialize tools when tools tab is first accessed
   */
  private async initializeTools(): Promise<void> {
    if (this.toolsInitialized) return;

    try {
      console.log('🔧 Initializing tools...');
      await toolsManager.initialize();
      this.toolsInitialized = true;
      console.log('✅ Tools initialized successfully');
    } catch (error) {
      console.error('❌ Failed to initialize tools:', error);
    }
  }

  /**
   * Embed agent dashboard into the dashboard tab view
   */
  private embedDashboardInTab(): void {
    if (!this.agentDashboard) return;

    const dashboardWrapper = document.getElementById('dashboard-content-wrapper');
    const agentDashboardContainer = document.getElementById('agent-dashboard-container');

    if (dashboardWrapper && agentDashboardContainer) {
      // Check if dashboard content already exists in the tab
      if (dashboardWrapper.querySelector('.agent-dashboard')) {
        return; // Already embedded
      }

      // Get the dashboard content from the original container
      const dashboardContent = agentDashboardContainer.querySelector('.agent-dashboard');

      if (dashboardContent) {
        // Clone the dashboard content for the tab view
        const clonedContent = dashboardContent.cloneNode(true) as HTMLElement;

        // Remove overlay-specific classes and styles
        clonedContent.classList.remove('hidden');
        clonedContent.classList.add('visible');
        clonedContent.style.position = 'static';
        clonedContent.style.zIndex = 'auto';
        clonedContent.style.background = 'transparent';
        clonedContent.style.boxShadow = 'none';
        clonedContent.style.border = 'none';
        clonedContent.style.borderRadius = '0';
        clonedContent.style.width = '100%';
        clonedContent.style.height = '100%';
        clonedContent.style.maxWidth = 'none';
        clonedContent.style.maxHeight = 'none';
        clonedContent.style.transform = 'none';
        clonedContent.style.top = 'auto';
        clonedContent.style.left = 'auto';
        clonedContent.style.right = 'auto';
        clonedContent.style.bottom = 'auto';

        // Add the cloned content to the tab view
        dashboardWrapper.innerHTML = '';
        dashboardWrapper.appendChild(clonedContent);

        // Set up event listeners for the cloned content
        this.setupTabDashboardEventListeners(clonedContent);

        // Debug: Check what data is available
        console.log('🔍 Dashboard data check:');
        console.log('Memory stats:', multiStepAgent.getMemoryStats());
        console.log('Performance stats:', multiStepAgent.getPerformanceStats());
        console.log('Analysis history:', multiStepAgent.getAnalysisHistory(undefined, 5));

        // Refresh the dashboard data
        this.agentDashboard.show(); // This will trigger a refresh
        this.agentDashboard.hide(); // Hide the overlay version
      }
    }
  }

  /**
   * Set up event listeners for the dashboard in tab view
   */
  private setupTabDashboardEventListeners(dashboardElement: HTMLElement): void {
    // Refresh button
    const refreshBtn = dashboardElement.querySelector('#dashboard-refresh') as HTMLButtonElement;
    refreshBtn?.addEventListener('click', () => {
      if (this.agentDashboard) {
        // Trigger refresh and then update the tab view
        this.agentDashboard.show();
        this.agentDashboard.hide();
        setTimeout(() => this.embedDashboardInTab(), 100);
      }
    });

    // Clear memory button
    const clearMemoryBtn = dashboardElement.querySelector('#clear-memory') as HTMLButtonElement;
    clearMemoryBtn?.addEventListener('click', () => {
      if (confirm('Are you sure you want to clear all agent memory? This action cannot be undone.')) {
        multiStepAgent.clearMemory();
        // Refresh the tab view
        setTimeout(() => this.embedDashboardInTab(), 100);
        this.showToast('Memory cleared successfully!');
      }
    });

    // New conversation button
    const newConversationBtn = dashboardElement.querySelector('#new-conversation') as HTMLButtonElement;
    newConversationBtn?.addEventListener('click', () => {
      // Clear agent memory
      multiStepAgent.startNewConversation();

      // Clear chat UI if available
      if (this.chatApp) {
        this.chatApp.clearMessages();
      }

      this.showToast('New conversation started!');
    });

    // Export data button
    const exportDataBtn = dashboardElement.querySelector('#export-data') as HTMLButtonElement;
    exportDataBtn?.addEventListener('click', () => {
      try {
        const data = {
          memoryStats: multiStepAgent.getMemoryStats(),
          performanceStats: multiStepAgent.getPerformanceStats(),
          recentAnalysis: multiStepAgent.getAnalysisHistory(undefined, 20),
          exportedAt: new Date().toISOString()
        };

        const dataStr = JSON.stringify(data, null, 2);
        const blob = new Blob([dataStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = `agent-data-${new Date().toISOString().split('T')[0]}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        this.showToast('Data exported successfully!');

      } catch (error) {
        console.error('Failed to export data:', error);
        this.showToast('Failed to export data', 'error');
      }
    });
  }

  private showToast(message: string, type: 'success' | 'error' = 'success'): void {
    const toast = document.createElement('div');
    toast.className = `dashboard-toast toast-${type}`;
    toast.textContent = message;

    document.body.appendChild(toast);

    // Animate in
    setTimeout(() => toast.classList.add('show'), 10);

    // Remove after 3 seconds
    setTimeout(() => {
      toast.classList.remove('show');
      setTimeout(() => toast.remove(), 300);
    }, 3000);
  }

  private closeAllModals(): void {
    document.querySelectorAll('.modal').forEach(modal => {
      modal.classList.add('hidden');
      modal.classList.remove('active');
    });
  }

  private showLoading(): void {
    const loading = document.getElementById('loading');
    const mainContainer = document.getElementById('main-container');

    if (loading) loading.classList.remove('hidden');
    if (mainContainer) mainContainer.classList.add('hidden');
  }

  private hideLoading(): void {
    const loading = document.getElementById('loading');
    const mainContainer = document.getElementById('main-container');

    if (loading) loading.classList.add('hidden');
    if (mainContainer) mainContainer.classList.remove('hidden');
  }

  private showError(message: string): void {
    const loading = document.getElementById('loading');
    if (loading) {
      loading.innerHTML = `
        <div class="loading-container">
          <div style="color: var(--color-danger); text-align: center;">
            <h2>❌ Error</h2>
            <p>${message}</p>
            <button onclick="location.reload()" style="
              margin-top: 1rem;
              padding: 0.5rem 1rem;
              background: var(--color-primary);
              color: white;
              border: none;
              border-radius: 0.5rem;
              cursor: pointer;
            ">Reload Page</button>
          </div>
        </div>
      `;
    }
  }
}

// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  const app = new App();
  app.initialize().catch(console.error);
});

// Handle unhandled errors
window.addEventListener('error', (event) => {
  console.error('Unhandled error:', event.error);
});

window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
});

```
Page 3/6FirstPrevNextLast