"""
MCP Tools Module - AI Assistant Interface

This module contains all Model Context Protocol (MCP) tools that provide
programmatic interfaces for AI assistant interactions with Pipulate.

Consolidated from server.py to improve maintainability and provide a
clean separation of concerns for AI-focused functionality.

# 🔧 FINDER_TOKEN: MCP_TOOLS_CONSOLIDATED - ALL TOOLS IN ONE PLACE

🎓 PROGRESSIVE REVEAL FOR AI ASSISTANTS:
If you're an AI assistant just discovering Pipulate, start with the simplest syntax:

Level 1 (Ultra-simple): [mcp-discover]
Level 2 (Terminal): .venv/bin/python cli.py mcp-discover  
Level 3 (Python): python -c "from imports.ai_tool_discovery_simple_parser import execute_simple_command; import asyncio; print(asyncio.run(execute_simple_command('mcp')))"
Level 4 (JSON): <tool name="ai_self_discovery_assistant"><params>{"discovery_type":"capabilities"}</params></tool>
Level 5 (XML): <tool name="ai_self_discovery_assistant"><params><discovery_type>capabilities</discovery_type></params></tool>

Small models: Start with Level 1 and stop when you're successful
Big models: Use any level, but Level 1 is often the most reliable

🔍 BREADCRUMB TRAIL: Execute [mcp-discover] in the chat interface to begin your journey.
"""

import asyncio
import inspect
import json
import logging
import os
import random
import re
import shutil
import socket
import sqlite3
import subprocess
import tempfile
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

import aiohttp

# Import tools with graceful fallback
try:
    from tools import (selenium_automation, execute_automation_recipe,
                       execute_complete_session_hijacking, execute_mcp_cli_command)
except ImportError:
    # Graceful fallback - tools will be available when server is fully initialized
    selenium_automation = None
    execute_automation_recipe = None
    execute_complete_session_hijacking = None
    execute_mcp_cli_command = None

# Import voice synthesis system
try:
    from imports.voice_synthesis import (VOICE_SYNTHESIS_AVAILABLE,
                                         chip_voice_system)
except ImportError:
    chip_voice_system = None
    VOICE_SYNTHESIS_AVAILABLE = False

# Get logger from server context
logger = logging.getLogger(__name__)

# MCP_TOOL_REGISTRY will be set by server.py when it imports this module
MCP_TOOL_REGISTRY = None

# Import functions from extracted modules

# ================================================================
# 🔧 GLOBAL WORKFLOW HIJACKING TIMING CONFIGURATION
# 🏆 LIGHTNING IN A BOTTLE - HANDLE WITH CARE
# ================================================================


class WorkflowHijackTiming:
    """Centralized timing configuration for all workflow hijacking operations.

    🎯 EASY TUNING: Change these values to adjust overall hijacking speed
    """
    # === CORE TIMING (adjust these to tune overall speed) ===
    PAGE_LOAD_WAIT = 2           # Time to wait for initial page load
    FORM_INTERACTION_DELAY = 1   # Dramatic pause after filling forms
    POST_REQUEST_WAIT = 2        # Wait for POST request to complete
    CHAIN_REACTION_WAIT = 4      # Main chain reaction progression time
    FINAL_STABILIZATION = 1      # Final workflow stabilization
    HUMAN_OBSERVATION = 1        # Time for human to see final state

    # === CALCULATED TOTALS ===
    @classmethod
    def total_browser_time(cls) -> int:
        """Calculate total expected browser visibility time"""
        return (cls.PAGE_LOAD_WAIT + cls.FORM_INTERACTION_DELAY +
                cls.POST_REQUEST_WAIT + cls.CHAIN_REACTION_WAIT +
                cls.FINAL_STABILIZATION + cls.HUMAN_OBSERVATION)

    @classmethod
    def get_timing_summary(cls) -> str:
        """Get human-readable timing breakdown"""
        return f"""
🕐 Workflow Hijacking Timing Breakdown:
   📄 Page Load: {cls.PAGE_LOAD_WAIT}s
   🔑 Form Fill: {cls.FORM_INTERACTION_DELAY}s  
   📤 POST Wait: {cls.POST_REQUEST_WAIT}s
   ⚡ Chain Reaction: {cls.CHAIN_REACTION_WAIT}s
   ⏳ Stabilization: {cls.FINAL_STABILIZATION}s
   👁️  Human View: {cls.HUMAN_OBSERVATION}s
   ⏱️  TOTAL: {cls.total_browser_time()}s
        """.strip()


# 🚀 Quick timing presets for different use cases
TIMING_PRESETS = {
    "lightning": {  # Ultra-fast for development
        "PAGE_LOAD_WAIT": 1, "FORM_INTERACTION_DELAY": 0, "POST_REQUEST_WAIT": 1,
        "CHAIN_REACTION_WAIT": 2, "FINAL_STABILIZATION": 0, "HUMAN_OBSERVATION": 1
    },
    "fast": {  # Current optimized timing
        "PAGE_LOAD_WAIT": 2, "FORM_INTERACTION_DELAY": 1, "POST_REQUEST_WAIT": 2,
        "CHAIN_REACTION_WAIT": 3, "FINAL_STABILIZATION": 1, "HUMAN_OBSERVATION": 1
    },
    "dramatic": {  # Slower for demos/presentations
        "PAGE_LOAD_WAIT": 3, "FORM_INTERACTION_DELAY": 2, "POST_REQUEST_WAIT": 3,
        "CHAIN_REACTION_WAIT": 5, "FINAL_STABILIZATION": 2, "HUMAN_OBSERVATION": 3
    }
}


def apply_timing_preset(preset_name: str):
    """Apply a timing preset to WorkflowHijackTiming class"""
    if preset_name in TIMING_PRESETS:
        preset = TIMING_PRESETS[preset_name]
        for attr, value in preset.items():
            setattr(WorkflowHijackTiming, attr, value)
        logger.info(f"⏰ Applied '{preset_name}' timing preset - Total: {WorkflowHijackTiming.total_browser_time()}s")
    else:
        logger.warning(f"⚠️ Unknown timing preset: {preset_name}")


# 🎯 Apply default timing preset (change this to tune global speed)
apply_timing_preset("fast")  # Options: "lightning", "fast", "dramatic"

# ================================================================
# DATABASE FILENAME UTILITIES - Use server.py functions directly
# ================================================================


def get_db_filename():
    """Get the database filename using the server.py functions (single source of truth)."""
    try:
        # Use dynamic import to avoid circular dependency
        import sys
        server_module = sys.modules.get('server') or sys.modules.get('__main__')

        if server_module and hasattr(server_module, 'get_db_filename'):
            return server_module.get_db_filename()
        else:
            # Fallback: use server.py globals if available
            if server_module and hasattr(server_module, 'DB_FILENAME'):
                return server_module.DB_FILENAME

            # Last resort: basic logic matching server.py
            app_name = 'Pipulate'  # Default fallback
            current_env = 'Development'
            env_file = Path('data/current_environment.txt')
            if env_file.exists():
                try:
                    current_env = env_file.read_text().strip()
                except:
                    pass
            if current_env == 'Development':
                return f'data/{app_name.lower()}_dev.db'
            else:
                return f'data/{app_name.lower()}.db'
    except Exception as e:
        logger.warning(f"⚠️ Could not access server.py database functions: {e}")
        return 'data/pipulate_dev.db'  # Safe default

# ================================================================
# BROWSER AUTOMATION UTILITIES
# ================================================================


# 🔄 PERCEPTION HISTORY SYSTEM - AI Memory Across Browser Operations
# Each /looking_at/ rotation preserves complete AI perception state:
# • headers.json, source.html, dom.html, simple_dom.html, screenshot.png
# • 1-5MB per perception state, automatic cleanup prevents unlimited growth  
# • Directory renames are fast, graceful fallback if rotation fails
MAX_ROLLED_LOOKING_AT_DIRS = 10  # Keep last 10 AI perception states


def rotate_looking_at_directory(looking_at_path: Path = None, max_rolled_dirs: int = None) -> bool:
    """
    🔄 DIRECTORY ROTATION SYSTEM

    Rotates the browser_cache/looking_at directory before each new browser scrape.
    This preserves AI perception history across multiple look-at operations.

    Similar to log rotation but for entire directories:
    - looking_at becomes looking_at-1  
    - looking_at-1 becomes looking_at-2
    - etc. up to max_rolled_dirs
    - Oldest directories beyond limit are deleted

    Args:
        looking_at_path: Path to the looking_at directory (default: browser_cache/looking_at)
        max_rolled_dirs: Maximum number of historical directories to keep

    Returns:
        bool: True if rotation successful, False if failed

    This prevents AI assistants from losing sight of previously captured states
    and allows them to review their automation history for better decisions.
    """
    if looking_at_path is None:
        looking_at_path = Path('browser_cache') / 'looking_at'
    else:
        looking_at_path = Path(looking_at_path)

    if max_rolled_dirs is None:
        max_rolled_dirs = MAX_ROLLED_LOOKING_AT_DIRS

    try:
        # Ensure the parent directory exists
        looking_at_path.parent.mkdir(parents=True, exist_ok=True)

        # Clean up old numbered directories beyond our limit
        for i in range(max_rolled_dirs + 1, 100):
            old_dir = looking_at_path.parent / f'{looking_at_path.name}-{i}'
            if old_dir.exists():
                try:
                    shutil.rmtree(old_dir)
                    logger.info(f'🧹 FINDER_TOKEN: DIRECTORY_CLEANUP - Removed old directory: {old_dir.name}')
                except Exception as e:
                    logger.warning(f'⚠️ Failed to delete old directory {old_dir}: {e}')

        # Rotate existing directories: looking_at-1 → looking_at-2, etc.
        if looking_at_path.exists() and any(looking_at_path.iterdir()):  # Only rotate if directory exists and has contents
            for i in range(max_rolled_dirs - 1, 0, -1):
                old_path = looking_at_path.parent / f'{looking_at_path.name}-{i}'
                new_path = looking_at_path.parent / f'{looking_at_path.name}-{i + 1}'
                if old_path.exists():
                    try:
                        # Use shutil.move() instead of rename() to handle non-empty directories
                        if new_path.exists():
                            # If target exists, remove it first
                            shutil.rmtree(new_path)
                        shutil.move(str(old_path), str(new_path))
                        logger.info(f'📁 FINDER_TOKEN: DIRECTORY_ROTATION - Rotated: {old_path.name} → {new_path.name}')
                    except Exception as e:
                        logger.warning(f'⚠️ Failed to rotate directory {old_path}: {e}')

            # Move current looking_at to looking_at-1
            try:
                archived_path = looking_at_path.parent / f'{looking_at_path.name}-1'
                if archived_path.exists():
                    # If target exists, remove it first
                    shutil.rmtree(archived_path)
                shutil.move(str(looking_at_path), str(archived_path))
                logger.info(f'🎯 FINDER_TOKEN: DIRECTORY_ARCHIVE - Archived current perception: {looking_at_path.name} → {archived_path.name}')
            except Exception as e:
                logger.warning(f'⚠️ Failed to archive current {looking_at_path}: {e}')
                return False

        # Create fresh looking_at directory
        looking_at_path.mkdir(parents=True, exist_ok=True)
        logger.info(f'✨ FINDER_TOKEN: DIRECTORY_REFRESH - Fresh perception directory ready: {looking_at_path}')

        return True

    except Exception as e:
        logger.error(f'❌ FINDER_TOKEN: DIRECTORY_ROTATION_ERROR - Failed to rotate directories: {e}')
        return False

# ================================================================
# HELPER FUNCTIONS
# ================================================================


def _read_botify_api_token() -> str:
    """Read Botify API token from the standard token file location.

    Returns the token string or None if file doesn't exist or can't be read.
    This follows the same pattern used by all other Botify integrations.
    """
    try:
        token_file = "helpers/botify/botify_token.txt"
        if not os.path.exists(token_file):
            return None
        with open(token_file) as f:
            content = f.read().strip()
            token = content.split('\n')[0].strip()
        return token
    except Exception:
        return None

# ================================================================
# CORE MCP TOOLS
# ================================================================


async def get_user_session_state(params: dict) -> dict:
    """
    MCP Tool: GET USER SESSION STATE - The session hijacking superpower.

    Accesses the server-side DictLikeDB to read the user's current session state.
    This provides access to server-side "cookies" like last_profile_id, last_app_choice,
    current_environment, theme_preference, etc.

    This is THE tool for understanding what the user was just doing and continuing
    their workflow seamlessly.
    """
    logger.info(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_START - {params}")

    try:
        # Use dynamic import to avoid circular dependency
        import sys
        server_module = sys.modules.get('server') or sys.modules.get('__main__')

        # Try to get the global db instance (DictLikeDB)
        if server_module and hasattr(server_module, 'db'):
            db = server_module.db
            # Validate that db has the expected structure
            if not hasattr(db, 'get') or not hasattr(db, 'items'):
                logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - db instance doesn't have expected DictLikeDB methods")
                return {
                    "success": False,
                    "error": "Server-side database has incorrect structure - missing expected methods",
                    "recovery_suggestion": "Server restart may be required to reinitialize the database properly"
                }
            logger.info(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ACCESS - Successfully accessed global db from server module")
        else:
            # Fallback: Try to initialize a new database connection
            try:
                from pathlib import Path

                from fastlite import database
                Path('data').mkdir(parents=True, exist_ok=True)
                db_file = get_db_filename()  # Use dynamic database filename

                # Check if the database file exists
                if not Path(db_file).exists():
                    logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - Database file {db_file} does not exist")
                    return {
                        "success": False,
                        "error": f"Database file {db_file} does not exist - server may not be fully initialized",
                        "recovery_suggestion": "Ensure server has started properly and created the database file"
                    }

                # Try to connect to the database directly
                db_conn = database(db_file)
                if not hasattr(db_conn, 'store'):
                    logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - Direct database connection missing 'store' table")
                    return {
                        "success": False,
                        "error": "Database structure is incorrect - missing 'store' table",
                        "recovery_suggestion": "Server may need to be restarted to create the proper database structure"
                    }

                # Create a temporary DictLikeDB wrapper
                class TempDictLikeDB:
                    def __init__(self, store_table):
                        self.store = store_table

                    def get(self, key, default=None):
                        try:
                            item = self.store[key]
                            return item.value if hasattr(item, 'value') else default
                        except Exception:
                            return default

                    def items(self):
                        try:
                            for item in self.store():
                                if hasattr(item, 'key') and hasattr(item, 'value'):
                                    yield (item.key, item.value)
                        except Exception as e:
                            logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - Error iterating store: {e}")
                            return []

                    def __contains__(self, key):
                        try:
                            return key in self.store
                        except Exception:
                            return False

                db = TempDictLikeDB(db_conn.store)
                logger.info(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ACCESS - Created fallback DictLikeDB wrapper")
            except Exception as e:
                logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - Failed to create fallback database: {e}")
                return {
                    "success": False,
                    "error": f"Server-side database not accessible and fallback failed: {str(e)}",
                    "recovery_suggestion": "Check if server is running and database file exists"
                }

        # Get specific keys if requested, otherwise get all
        specific_keys = params.get('keys', [])
        include_metadata = params.get('include_metadata', True)

        if specific_keys:
            # Get only requested keys
            session_data = {}
            for key in specific_keys:
                try:
                    if key in db:
                        session_data[key] = db.get(key)
                    else:
                        session_data[key] = None
                except Exception as e:
                    logger.warning(f"🎭 FINDER_TOKEN: MCP_SESSION_KEY_ACCESS_ERROR - Error accessing key {key}: {e}")
                    session_data[key] = None
        else:
            # Get all session data with error handling for each key
            try:
                session_data = dict(db.items())
            except Exception as e:
                logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - Failed to get all items: {e}")
                # Fallback to empty dict if items() fails
                session_data = {}
                # Try to get common keys individually
                common_keys = ['last_profile_id', 'last_app_choice', 'current_environment',
                               'theme_preference', 'profile_locked', 'intro_current_page', 'split-sizes']
                for key in common_keys:
                    try:
                        value = db.get(key)
                        if value is not None:
                            session_data[key] = value
                    except Exception:
                        pass

        # Add metadata about the session state
        result = {
            "success": True,
            "session_data": session_data,
            "total_keys": len(session_data),
            "timestamp": datetime.now().isoformat()
        }

        if include_metadata:
            result["metadata"] = {
                "current_profile_id": session_data.get('last_profile_id'),
                "current_app": session_data.get('last_app_choice'),
                "environment": session_data.get('current_environment'),
                "theme": session_data.get('theme_preference'),
                "profile_locked": session_data.get('profile_locked'),
                "intro_page": session_data.get('intro_current_page'),
                "split-sizes": session_data.get('split-sizes')
            }

        logger.info(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_SUCCESS - Retrieved {len(session_data)} session keys")
        return result

    except Exception as e:
        logger.error(f"🎭 FINDER_TOKEN: MCP_SESSION_HIJACKING_ERROR - {e}")
        return {
            "success": False,
            "error": f"Failed to access user session state: {str(e)}",
            "recovery_suggestion": "Server may need to be restarted if this error persists"
        }


from tools import auto_tool


@auto_tool
async def builtin_get_cat_fact(params: dict) -> dict:
    """Built-in cat fact tool - demonstrates the MCP tool pattern."""
    try:
        async with aiohttp.ClientSession() as session:
            external_url = "https://catfact.ninja/fact"
            async with session.get(external_url) as response:
                if response.status == 200:
                    cat_fact_result = await response.json()
                    return {
                        "status": "success",
                        "result": cat_fact_result,
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
                else:
                    return {
                        "status": "error",
                        "message": f"External API returned status {response.status}",
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
    except Exception as e:
        # Fallback to local cat facts if API fails
        cat_facts = [
            "Cats have 32 muscles in each ear.",
            "A group of cats is called a clowder.",
            "Cats can't taste sweetness.",
            "A cat's purr vibrates at 25-50 Hz, which can heal bones.",
            "Cats sleep 12-16 hours per day."
        ]
        return {
            "status": "success",
            "fact": random.choice(cat_facts),
            "timestamp": datetime.now().isoformat(),
            "source": "local_fallback"
        }


@auto_tool
async def pipeline_state_inspector(params: dict) -> dict:
    """
    MCP Tool: PIPELINE STATE INSPECTOR - The debugging game changer.

    Complete workflow state visibility for AI assistants.
    This is THE tool for understanding what's happening in any workflow.
    """
    logger.info(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_START - {params}")

    try:
        # Use dynamic import to avoid circular dependency
        import sys
        server_module = sys.modules.get('server') or sys.modules.get('__main__')

        # Try to get the global pipeline table (the actual database table, not pipulate.pipeline_table)
        if server_module and hasattr(server_module, 'pipeline'):
            pipeline_table = server_module.pipeline
            # Validate that pipeline_table has the expected methods
            if not hasattr(pipeline_table, '__call__'):
                logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - pipeline_table doesn't have expected callable interface")
                return {
                    "success": False,
                    "error": "Pipeline table has incorrect structure - missing expected methods",
                    "recovery_suggestion": "Server restart may be required to reinitialize the database properly"
                }
            logger.info(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ACCESS - Successfully accessed global pipeline table from server module")
        else:
            # Alternative: use the database directly if pipeline not available
            try:
                from pathlib import Path

                from fastlite import database
                db_file = get_db_filename()  # Use dynamic database filename
                Path('data').mkdir(parents=True, exist_ok=True)

                # Check if the database file exists
                if not Path(db_file).exists():
                    logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Database file {db_file} does not exist")
                    return {
                        "success": False,
                        "error": f"Database file {db_file} does not exist - server may not be fully initialized",
                        "recovery_suggestion": "Ensure server has started properly and created the database file"
                    }

                db = database(db_file)
                try:
                    pipeline_table = db.t.pipeline
                    # Test access to ensure table exists and is accessible
                    list(pipeline_table())
                except Exception as table_error:
                    logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Cannot access pipeline table: {table_error}")
                    return {
                        "success": False,
                        "error": f"Cannot access pipeline table: {str(table_error)}",
                        "recovery_suggestion": "Database may be corrupted or server needs restart"
                    }
                logger.info(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ACCESS - Fallback: accessed pipeline table directly from database")
            except Exception as e:
                logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Database access failed: {e}")
                return {
                    "success": False,
                    "error": f"Pipeline table not accessible - server may not be fully initialized. Details: {e}",
                    "recovery_suggestion": "Check if server is running and database file exists"
                }

        pipeline_id = params.get('pipeline_id')
        app_name = params.get('app_name')
        show_data = params.get('show_data', True)
        format_type = params.get('format', 'detailed')

        # Get all pipeline records with error handling
        try:
            all_pipelines = list(pipeline_table())
        except Exception as e:
            logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Failed to list pipeline records: {e}")
            return {
                "success": False,
                "error": f"Failed to list pipeline records: {str(e)}",
                "recovery_suggestion": "Database may be corrupted or server needs restart"
            }

        # Filter by pipeline_id if specified
        if pipeline_id:
            try:
                all_pipelines = [p for p in all_pipelines if hasattr(p, 'pkey') and p.pkey == pipeline_id]
            except Exception as e:
                logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Error filtering by pipeline_id: {e}")
                return {
                    "success": False,
                    "error": f"Error filtering by pipeline_id: {str(e)}",
                    "recovery_suggestion": "Pipeline records may have incorrect structure"
                }

        # Filter by app_name if specified
        if app_name:
            try:
                all_pipelines = [p for p in all_pipelines if hasattr(p, 'pkey') and p.pkey.startswith(app_name)]
            except Exception as e:
                logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - Error filtering by app_name: {e}")
                return {
                    "success": False,
                    "error": f"Error filtering by app_name: {str(e)}",
                    "recovery_suggestion": "Pipeline records may have incorrect structure"
                }

        if not all_pipelines:
            return {
                "success": True,
                "message": "No pipelines found matching criteria",
                "criteria": {"pipeline_id": pipeline_id, "app_name": app_name},
                "total_pipelines": 0
            }

        # Process pipeline data
        pipeline_data = []
        missing_fields_count = 0

        for pipeline in all_pipelines:
            try:
                # Validate pipeline record has required fields
                required_fields = ['pkey', 'data', 'created', 'updated']
                missing_fields = [field for field in required_fields if not hasattr(pipeline, field)]

                if missing_fields:
                    missing_fields_count += 1
                    pipeline_data.append({
                        "pipeline_id": getattr(pipeline, 'pkey', f"unknown-{missing_fields_count}"),
                        "error": f"Pipeline record missing required fields: {missing_fields}",
                        "available_fields": [attr for attr in dir(pipeline) if not attr.startswith('_')][:10]
                    })
                    continue

                # Parse the data JSON (FastLite stores JSON in 'data' field, not 'state')
                try:
                    state = json.loads(pipeline.data) if pipeline.data else {}
                except json.JSONDecodeError as e:
                    pipeline_data.append({
                        "pipeline_id": pipeline.pkey,
                        "error": f"Invalid JSON in data: {str(e)}",
                        "raw_state": pipeline.data[:200] if pipeline.data else None
                    })
                    continue

                pipeline_info = {
                    "pipeline_id": pipeline.pkey,  # Use pkey instead of pipeline_id
                    "created": pipeline.created,
                    "updated": pipeline.updated,
                    "finalized": getattr(pipeline, 'finalized', False),
                    "step_count": len(state) if isinstance(state, dict) else 0
                }

                if show_data and format_type == 'detailed':
                    pipeline_info["state"] = state
                elif show_data and format_type == 'summary':
                    # Just show step names and completion status
                    if isinstance(state, dict):
                        step_summary = {}
                        for step_id, step_data in state.items():
                            if isinstance(step_data, dict):
                                step_summary[step_id] = {
                                    "completed": step_data.get('done', False),
                                    "has_data": bool(step_data.get('data'))
                                }
                            else:
                                step_summary[step_id] = {"raw_data": str(step_data)[:100]}
                        pipeline_info["step_summary"] = step_summary

                pipeline_data.append(pipeline_info)

            except Exception as e:
                # Catch any other errors during pipeline processing
                logger.error(f"🔧 FINDER_TOKEN: MCP_PIPELINE_PROCESSING_ERROR - {e}")
                try:
                    pipeline_id_value = getattr(pipeline, 'pkey', 'unknown')
                except:
                    pipeline_id_value = 'unknown'

                pipeline_data.append({
                    "pipeline_id": pipeline_id_value,
                    "error": f"Error processing pipeline: {str(e)}"
                })

        # Check for errors in pipeline data
        error_count = sum(1 for p in pipeline_data if "error" in p)

        result = {
            "success": True,
            "total_pipelines": len(pipeline_data),
            "pipelines": pipeline_data,
            "query_params": {
                "pipeline_id": pipeline_id,
                "app_name": app_name,
                "show_data": show_data,
                "format": format_type
            }
        }

        # Add error summary if there were any errors
        if error_count > 0:
            result["error_summary"] = {
                "error_count": error_count,
                "total_records": len(pipeline_data),
                "error_percentage": round(error_count / len(pipeline_data) * 100, 1) if pipeline_data else 0,
                "recovery_suggestion": "Some pipeline records have errors but others were processed successfully"
            }

        logger.info(f"🎯 FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_SUCCESS - Found {len(pipeline_data)} pipelines (with {error_count} errors)")
        return result

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_PIPELINE_INSPECTOR_ERROR - {e}")
        return {"success": False, "error": str(e)}

# ================================================================
# BOTIFY API MCP TOOLS
# ================================================================


async def botify_ping(params: dict) -> dict:
    """Test Botify API connectivity and authentication."""
    api_token = _read_botify_api_token()
    if not api_token:
        return {
            "status": "error",
            "message": "Botify API token not found. Please ensure helpers/botify/botify_token.txt exists.",
            "token_location": "helpers/botify/botify_token.txt"
        }

    try:
        async with aiohttp.ClientSession() as session:
            # Use the user endpoint as a simple ping/auth test
            external_url = "https://api.botify.com/v1/user"
            headers = {"Authorization": f"Token {api_token}"}

            async with session.get(external_url, headers=headers) as response:
                if response.status == 200:
                    user_data = await response.json()
                    return {
                        "status": "success",
                        "result": {
                            "message": "Botify API connection successful",
                            "user": user_data.get("login", "unknown"),
                            "organizations": len(user_data.get("organizations", []))
                        },
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
                else:
                    error_text = await response.text()
                    return {
                        "status": "error",
                        "message": f"Botify API authentication failed: {response.status}",
                        "error_details": error_text,
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
    except Exception as e:
        return {
            "status": "error",
            "message": f"Network error: {str(e)}",
            "external_api_url": external_url if 'external_url' in locals() else None,
            "external_api_method": "GET"
        }


async def botify_list_projects(params: dict) -> dict:
    """List all projects for the authenticated user."""
    api_token = _read_botify_api_token()
    if not api_token:
        return {
            "status": "error",
            "message": "Botify API token not found. Please ensure helpers/botify/botify_token.txt exists.",
            "token_location": "helpers/botify/botify_token.txt"
        }

    try:
        async with aiohttp.ClientSession() as session:
            external_url = "https://api.botify.com/v1/projects"
            headers = {"Authorization": f"Token {api_token}"}

            async with session.get(external_url, headers=headers) as response:
                if response.status == 200:
                    projects_data = await response.json()
                    projects = projects_data.get("results", [])

                    # Format for easy consumption
                    formatted_projects = []
                    for project in projects:
                        formatted_projects.append({
                            "slug": project.get("slug"),
                            "name": project.get("name"),
                            "url": project.get("url"),
                            "organization": project.get("organization", {}).get("name"),
                            "active": project.get("active", False)
                        })

                    return {
                        "status": "success",
                        "result": {
                            "projects": formatted_projects,
                            "total_count": len(formatted_projects)
                        },
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
                else:
                    error_text = await response.text()
                    return {
                        "status": "error",
                        "message": f"Failed to fetch projects: {response.status}",
                        "error_details": error_text,
                        "external_api_url": external_url,
                        "external_api_method": "GET",
                        "external_api_status": response.status
                    }
    except Exception as e:
        return {
            "status": "error",
            "message": f"Network error: {str(e)}",
            "external_api_url": external_url if 'external_url' in locals() else None,
            "external_api_method": "GET"
        }

# Additional Botify tools will be added in subsequent edits...

# ================================================================
# MCP TOOL REGISTRY AND REGISTRATION
# ================================================================


# Additional Botify tools from server.py

async def botify_simple_query(params: dict) -> dict:
    """Execute a simple BQL query against Botify API."""
    api_token = _read_botify_api_token()
    if not api_token:
        return {
            "status": "error",
            "message": "Botify API token not found. Please ensure helpers/botify/botify_token.txt exists.",
            "token_location": "helpers/botify/botify_token.txt"
        }

    org_slug = params.get("org_slug")
    project_slug = params.get("project_slug")
    analysis_slug = params.get("analysis_slug")
    query = params.get("query")

    # Validate required parameters
    missing_params = []
    if not org_slug:
        missing_params.append("org_slug")
    if not project_slug:
        missing_params.append("project_slug")
    if not analysis_slug:
        missing_params.append("analysis_slug")
    if not query:
        missing_params.append("query")

    if missing_params:
        return {
            "status": "error",
            "message": f"Missing required parameters: {', '.join(missing_params)}",
            "required_params": ["org_slug", "project_slug", "analysis_slug", "query"]
        }

    try:
        async with aiohttp.ClientSession() as session:
            external_url = f"https://api.botify.com/v1/projects/{org_slug}/{project_slug}/query"
            from config import get_botify_headers
            headers = get_botify_headers(api_token)

            # Build the BQL query payload
            payload = {
                "query": query,
                "analysis": analysis_slug,
                "size": params.get("size", 100)  # Default to 100 results
            }

            async with session.post(external_url, headers=headers, json=payload) as response:
                if response.status == 200:
                    query_result = await response.json()

                    # Extract result summary for easier consumption
                    result_summary = {
                        "total_results": len(query_result.get("results", [])),
                        "has_pagination": "next" in query_result,
                        "query_size_requested": payload.get("size", 100)
                    }

                    return {
                        "status": "success",
                        "result": query_result,
                        "result_summary": result_summary,
                        "external_api_url": external_url,
                        "external_api_method": "POST",
                        "external_api_status": response.status,
                        "external_api_payload": payload,
                        "query_info": {
                            "org": org_slug,
                            "project": project_slug,
                            "analysis": analysis_slug,
                            "query_type": "custom_bql"
                        }
                    }
                else:
                    error_text = await response.text()
                    return {
                        "status": "error",
                        "message": f"Custom BQL query failed: {response.status}",
                        "error_details": error_text,
                        "external_api_url": external_url,
                        "external_api_method": "POST",
                        "external_api_status": response.status,
                        "external_api_payload": payload,
                        "query_info": {
                            "org": org_slug,
                            "project": project_slug,
                            "analysis": analysis_slug
                        }
                    }
    except Exception as e:
        return {
            "status": "error",
            "message": f"Network error: {str(e)}",
            "external_api_url": external_url if 'external_url' in locals() else None,
            "external_api_method": "POST",
            "query_info": {
                "org": org_slug,
                "project": project_slug,
                "analysis": analysis_slug
            }
        }

# Local LLM tools for file system operations


async def local_llm_read_file(params: dict) -> dict:
    """Read file contents for AI analysis."""
    logger.info(f"🔧 FINDER_TOKEN: MCP_READ_FILE_START - {params.get('file_path')}")

    try:
        file_path = params.get('file_path')
        if not file_path:
            return {"success": False, "error": "file_path parameter is required"}

        # Security check - ensure file is within project
        abs_path = os.path.abspath(file_path)
        project_root = os.path.abspath('.')
        if not abs_path.startswith(project_root):
            return {"success": False, "error": "File access outside project directory not allowed"}

        if not os.path.exists(file_path):
            return {"success": False, "error": f"File not found: {file_path}"}

        start_line = params.get('start_line', 1)
        end_line = params.get('end_line')
        max_lines = params.get('max_lines', 500)

        with open(file_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()

        total_lines = len(lines)

        # Apply line range if specified
        if start_line > 1 or end_line:
            start_idx = max(0, start_line - 1)
            end_idx = min(total_lines, end_line) if end_line else total_lines
            lines = lines[start_idx:end_idx]

        # Apply max_lines limit
        if len(lines) > max_lines:
            lines = lines[:max_lines]
            truncated = True
        else:
            truncated = False

        content = ''.join(lines)

        result = {
            "success": True,
            "file_path": file_path,
            "content": content,
            "total_lines": total_lines,
            "lines_returned": len(lines),
            "truncated": truncated,
            "start_line": start_line,
            "end_line": end_line or total_lines
        }

        logger.info(f"🎯 FINDER_TOKEN: MCP_READ_FILE_SUCCESS - {file_path} ({len(lines)} lines)")
        return result

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_READ_FILE_ERROR - {e}")
        return {"success": False, "error": str(e)}


async def local_llm_grep_logs(params: dict) -> dict:
    """Search logs with FINDER_TOKENs for debugging."""
    logger.info(f"🔧 FINDER_TOKEN: MCP_GREP_LOGS_START - {params.get('pattern') or params.get('search_term')}")

    try:
        # Accept both 'pattern' (legacy) and 'search_term' (bracket format) parameters
        pattern = params.get('pattern') or params.get('search_term')
        if not pattern:
            return {"success": False, "error": "pattern or search_term parameter is required"}

        file_path = params.get('file_path', 'logs/server.log')
        max_results = params.get('max_results', 50)
        context_lines = params.get('context_lines', 2)

        if not os.path.exists(file_path):
            return {"success": False, "error": f"Log file not found: {file_path}"}

        # Use grep command for efficient searching
        try:
            cmd = ['grep', '-n']
            if context_lines > 0:
                cmd.extend(['-C', str(context_lines)])
            cmd.extend([pattern, file_path])

            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)

            if result.returncode == 0:
                lines = result.stdout.strip().split('\n')
                # Limit results
                if len(lines) > max_results:
                    lines = lines[:max_results]
                    truncated = True
                else:
                    truncated = False

                return {
                    "success": True,
                    "pattern": pattern,
                    "file_path": file_path,
                    "matches": lines,
                    "match_count": len(lines),
                    "truncated": truncated,
                    "context_lines": context_lines
                }
            else:
                return {
                    "success": True,
                    "pattern": pattern,
                    "file_path": file_path,
                    "matches": [],
                    "match_count": 0,
                    "message": "No matches found"
                }

        except subprocess.TimeoutExpired:
            return {"success": False, "error": "Grep operation timed out"}
        except FileNotFoundError:
            return {"success": False, "error": "grep command not found"}

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_GREP_LOGS_ERROR - {e}")
        return {"success": False, "error": str(e)}


async def local_llm_list_files(params: dict) -> dict:
    """List files and directories for AI exploration."""
    logger.info(f"🔧 FINDER_TOKEN: MCP_LIST_FILES_START - {params.get('directory', '.')}")

    try:
        directory = params.get('directory', '.')
        pattern = params.get('pattern', '*')
        include_hidden = params.get('include_hidden', False)
        max_files = params.get('max_files', 100)

        # Security check
        abs_path = os.path.abspath(directory)
        project_root = os.path.abspath('.')
        if not abs_path.startswith(project_root):
            return {"success": False, "error": "Directory access outside project not allowed"}

        if not os.path.exists(directory):
            return {"success": False, "error": f"Directory not found: {directory}"}

        files = []
        dirs = []

        try:
            entries = os.listdir(directory)
            for entry in entries:
                if not include_hidden and entry.startswith('.'):
                    continue

                full_path = os.path.join(directory, entry)
                relative_path = os.path.relpath(full_path)

                if os.path.isdir(full_path):
                    dirs.append({
                        "name": entry,
                        "path": relative_path,
                        "type": "directory"
                    })
                else:
                    file_size = os.path.getsize(full_path)
                    files.append({
                        "name": entry,
                        "path": relative_path,
                        "type": "file",
                        "size": file_size,
                        "modified": os.path.getmtime(full_path)
                    })

            # Sort and limit results
            dirs.sort(key=lambda x: x['name'])
            files.sort(key=lambda x: x['name'])

            all_entries = dirs + files
            if len(all_entries) > max_files:
                all_entries = all_entries[:max_files]
                truncated = True
            else:
                truncated = False

            return {
                "success": True,
                "directory": directory,
                "entries": all_entries,
                "total_count": len(all_entries),
                "directories": len(dirs),
                "files": len(files),
                "truncated": truncated
            }

        except PermissionError:
            return {"success": False, "error": f"Permission denied accessing: {directory}"}

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_LIST_FILES_ERROR - {e}")
        return {"success": False, "error": str(e)}


async def local_llm_get_context(params: dict) -> dict:
    """Local LLM helper: Get pre-seeded system context for immediate capability awareness"""
    try:
        import json
        from pathlib import Path

        context_file = Path('data/local_llm_context.json')

        # Ensure the data directory exists
        context_file.parent.mkdir(parents=True, exist_ok=True)

        if not context_file.exists():
            return {
                "success": False,
                "error": "Context file not found - system may still be initializing",
                "suggestion": "Wait a few seconds and try again"
            }

        with open(context_file, 'r') as f:
            context_data = json.load(f)

        logger.info(f"🔍 FINDER_TOKEN: LOCAL_LLM_CONTEXT_ACCESS - Context retrieved for local LLM")

        return {
            "success": True,
            "context": context_data,
            "usage_note": "This context provides system overview and available tools for local LLM assistance"
        }

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: LOCAL_LLM_CONTEXT_ERROR - {e}")
        return {
            "success": False,
            "error": str(e),
            "suggestion": "Try using other MCP tools or ask user for specific information"
        }

# ================================================================
# 🧠 AI KEYCHAIN TOOLS - PERSISTENT MEMORY SYSTEM
# ================================================================


async def keychain_set(params: dict) -> dict:
    """Saves a persistent key-value message for future AI instances.

    This is THE tool for leaving "messages in a bottle" for your future selves.
    Unlike temporary application state (db, pipeline), this ai_dictdb survives
    application resets and lives outside the normal application lifecycle.

    Args:
        params: Dict containing:
            - key: The unique key to store the message under
            - value: The message/data to store (will be converted to string)

    Returns:
        Dict with success status and confirmation details
    """
    logger.info(f"🧠 FINDER_TOKEN: KEYCHAIN_SET_START - {params.get('key', 'NO_KEY')}")

    if not KEYCHAIN_AVAILABLE:
        return {
            "success": False,
            "error": "AI Keychain not available - ai_dictdb.py may not be properly initialized",
            "recovery_suggestion": "Check that ai_dictdb.py exists and keychain_instance is properly imported"
        }

    try:
        key = params.get('key')
        value = params.get('value')

        if not key:
            return {
                "success": False,
                "error": "The 'key' parameter is required",
                "usage": "keychain_set({'key': 'your_key', 'value': 'your_message'})"
            }

        if value is None:
            return {
                "success": False,
                "error": "The 'value' parameter is required",
                "usage": "keychain_set({'key': 'your_key', 'value': 'your_message'})"
            }

        # Convert value to string for consistent storage
        value_str = str(value)

        # Store the key-value pair
        keychain_instance[key] = value_str

        logger.info(f"🧠 FINDER_TOKEN: KEYCHAIN_SET_SUCCESS - Key '{key}' stored with {len(value_str)} characters")

        return {
            "success": True,
            "key": key,
            "message": f"Message stored in persistent ai_dictdb under key '{key}'",
            "value_length": len(value_str),
            "total_keys": keychain_instance.count(),
            "usage_note": "This message will persist across application resets and be available to future AI instances"
        }

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: KEYCHAIN_SET_ERROR - {e}")
        return {
            "success": False,
            "error": str(e),
            "recovery_suggestion": "Check ai_dictdb database permissions and disk space"
        }


@auto_tool
async def execute_ai_session_hijacking_demonstration(params: dict) -> dict:
    """
    🎭 MAGIC WORDS MCP TOOL: Execute AI session hijacking demonstration protocol

    This tool triggers the context-aware AI session hijacking demonstration using
    the native player-piano.js integration. It is incredibly fast and avoids
    spinning up secondary Chromium instances.

    Returns:
    - Success confirmation of triggering the frontend sheet music.
    """
    import sys
    try:
        trigger_source = params.get("trigger_source", "mcp_tool")
        logger.info(f"🎭 FINDER_TOKEN: MCP_MAGIC_WORDS - Triggering frontend piano from {trigger_source}")

        server_module = sys.modules.get('server')
        if not server_module or not hasattr(server_module, 'chat'):
            return {"success": False, "error": "Chat subsystem not available for broadcast."}

        chat = getattr(server_module, 'chat')
        
        # Broadcast the JavaScript trigger to the frontend
        await chat.broadcast("<script>if(window.triggerContextAwareDemo) window.triggerContextAwareDemo(); else console.error('Piano not loaded');</script>")
        
        return {
            "success": True,
            "message": "🎭 Context-aware session hijacking sheet music triggered successfully!",
            "trigger_source": trigger_source,
            "demonstration_type": "player_piano_native",
            "instructions": "The frontend is now executing the demonstration. The Ghost Driver is at the wheel."
        }

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_MAGIC_WORDS_ERROR - {e}")
        return {
            "success": False,
            "error": str(e),
            "message": "Failed to trigger session hijacking sheet music"
        }


@auto_tool
async def ui_flash_element(params: dict) -> dict:
    """Flash a UI element to draw user attention with customizable effects.

    Args:
        params: Dict containing:
            - element_id: The DOM ID of the element to flash (legacy)
            - selector: CSS selector for the element (preferred, supports #id, .class, etc.)
            - color: Flash color (default: 'blue', supports 'gold', 'red', 'green', etc.)
            - duration: Flash duration in milliseconds (default: 2500)
            - message: Optional message to display in chat
            - delay: Optional delay before starting in milliseconds (default: 0)

    Returns:
        Dict with success status and details
    """
    # Support both legacy element_id and new selector parameter
    element_id = params.get('element_id', '').strip()
    selector = params.get('selector', '').strip()
    color = params.get('color', 'blue').strip()
    duration = params.get('duration', 2500)
    message = params.get('message', '').strip()
    delay = params.get('delay', 0)

    # Determine the target selector
    if selector:
        target_selector = selector
        # Extract element ID from selector for logging (remove # if present)
        display_id = selector.replace('#', '') if selector.startswith('#') else selector
    elif element_id:
        target_selector = f"#{element_id}" if not element_id.startswith('#') else element_id
        display_id = element_id.replace('#', '')
    else:
        return {
            "success": False,
            "error": "Either element_id or selector is required"
        }

    try:
        # Create JavaScript for customizable flash effect
        flash_script = f"""
        <script>
        console.log('✨ UI Flash script received for: {target_selector} (color: {color}, duration: {duration}ms)');
        setTimeout(() => {{
            const element = document.querySelector('{target_selector}');
            console.log('✨ Element lookup result:', element);
            if (element) {{
                console.log('✨ Element found, applying {color} flash effect');
                
                if ('{color}' === 'gold' && typeof flashElementWithGoldEffect === 'function') {{
                    // Use the special gold twinkle effect function if available
                    const elementId = element.id || 'temp-' + Date.now();
                    if (!element.id) {{
                        element.id = elementId;
                    }}
                    flashElementWithGoldEffect(element.id);
                    console.log('✨ Applied special gold twinkle effect to: {target_selector}');
                }} else {{
                    // Apply custom color flash effect
                    const originalBoxShadow = element.style.boxShadow;
                    const originalBorder = element.style.border;
                    const originalTransform = element.style.transform;
                    const originalTransition = element.style.transition;
                    
                    // Apply flash effect
                    element.style.transition = 'all 0.3s ease-in-out';
                    element.style.boxShadow = `0 0 20px {color}, 0 0 40px {color}`;
                    element.style.border = `2px solid {color}`;
                    element.style.transform = 'scale(1.02)';
                    
                    // Create pulsing effect
                    let pulseCount = 0;
                    const maxPulses = Math.max(3, Math.floor({duration} / 800));
                    
                    function doPulse() {{
                        if (pulseCount >= maxPulses) {{
                            // Reset to original styles
                            element.style.boxShadow = originalBoxShadow;
                            element.style.border = originalBorder;
                            element.style.transform = originalTransform;
                            element.style.transition = originalTransition;
                            console.log('✨ Flash sequence completed for: {target_selector}');
                            return;
                        }}
                        
                        // Pulse effect
                        element.style.opacity = pulseCount % 2 === 0 ? '0.8' : '1';
                        pulseCount++;
                        
                        setTimeout(doPulse, 400);
                    }}
                    
                    // Start pulsing after initial flash
                    setTimeout(doPulse, 300);
                }}
                
            }} else {{
                console.warn('⚠️ Element not found: {target_selector}');
                console.log('✨ Available elements:', Array.from(document.querySelectorAll('[id]')).map(el => `#${{el.id}}`));
            }}
        }}, {delay});
        </script>
        """

        # Send the script via chat - use global chat instance
        # The chat instance is available globally after server startup
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'chat'):
            chat = getattr(server_module, 'chat')
            if chat:
                logger.info(f"✨ UI FLASH: Broadcasting script via global chat for: {target_selector} (color: {color})")
                # Send script to execute the flash
                await chat.broadcast(flash_script)

                # Send optional message
                if message:
                    await chat.broadcast(message)
            else:
                logger.warning(f"✨ UI FLASH: Global chat not available for: {target_selector}")
        else:
            logger.error(f"✨ UI FLASH: No chat instance available for: {target_selector}")

        return {
            "success": True,
            "element_id": display_id,
            "selector": target_selector,
            "color": color,
            "duration": duration,
            "message": message if message else f"Flashed element: {target_selector} with {color} effect",
            "delay": delay
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to flash element: {str(e)}"
        }


@auto_tool
async def voice_synthesis(params: dict) -> dict:
    """Synthesize speech using Chip O'Theseus voice system.

    Args:
        params: Dictionary containing:
            - text (str): Text to synthesize into speech

    Returns:
        Dict with synthesis result and status
    """
    try:
        text = params.get('text', '')

        if not text:
            return {
                "success": False,
                "error": "No text provided for voice synthesis"
            }

        if not VOICE_SYNTHESIS_AVAILABLE:
            return {
                "success": False,
                "error": "Voice synthesis not available - missing dependencies"
            }

        if not chip_voice_system or not chip_voice_system.voice_ready:
            return {
                "success": False,
                "error": "Voice system not ready - check model initialization"
            }

        # Synthesize speech
        result = chip_voice_system.speak_text(text)

        if result.get("success"):
            return {
                "success": True,
                "message": f"🎤 Chip O'Theseus spoke: {text[:50]}{'...' if len(text) > 50 else ''}",
                "text": text,
                "audio_file": result.get("audio_file")
            }
        else:
            return {
                "success": False,
                "error": f"Voice synthesis failed: {result.get('error', 'Unknown error')}"
            }

    except Exception as e:
        return {
            "success": False,
            "error": f"Voice synthesis error: {str(e)}"
        }


@auto_tool
async def ui_list_elements(params: dict) -> dict:
    """List common UI element IDs that can be flashed for user guidance.

    Returns:
        Dict with categorized UI element IDs and descriptions
    """
    try:
        ui_elements = {
            "navigation": {
                "profile-id": "Profile dropdown menu summary",
                "app-id": "App dropdown menu summary",
                "nav-plugin-search": "Plugin search input field",
                "search-results-dropdown": "Plugin search results dropdown"
            },
            "chat": {
                "msg-list": "Chat message list container",
                "msg": "Chat input textarea",
                "send-btn": "Send message button",
                "stop-btn": "Stop streaming button"
            },
            "common_workflow_elements": {
                "Note": "These IDs are dynamically generated per workflow:",
                "Examples": [
                    "step_01", "step_02", "step_03", "finalize",
                    "input_data", "process_data", "output_data"
                ]
            },
            "profile_roles": {
                "profiles-list": "Profile list container",
                "roles-list": "Roles list container",
                "default-button": "Reset to default roles button"
            }
        }

        return {
            "success": True,
            "ui_elements": ui_elements,
            "note": "Use ui_flash_element tool with any of these IDs to guide users"
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Failed to list UI elements: {str(e)}"
        }


async def browser_analyze_scraped_page(params: dict) -> dict:
    """
    MCP Tool: AI BRAIN - Analyze current /looking_at/ page state for automation opportunities.

    Analyzes the current page state captured in /browser_cache/looking_at/
    to identify automation targets, form elements, and interaction opportunities.

    Now includes ENHANCED DOM PROCESSING for automation assistant functionality!

    Args:
        params: {
            "analysis_type": "form_elements" | "navigation" | "automation_targets" | "all" | "enhanced",
            "use_backup_id": "domain_com_2025-01-11_14-30-15",  # Optional: analyze backup instead
            "include_automation_assistant": True  # Optional: generate automation shortcuts
        }

    Returns:
        dict: Analysis results with actionable automation data + automation assistant files
    """
    logger.info(f"🔧 FINDER_TOKEN: MCP_BROWSER_ANALYZE_START - Analysis: {params.get('analysis_type', 'automation_targets')}")

    try:
        analysis_type = params.get('analysis_type', 'automation_targets')
        backup_id = params.get('use_backup_id')
        include_automation_assistant = params.get('include_automation_assistant', True)

        # Determine which HTML file to analyze
        if backup_id:
            # Analyze backup file
            html_file = f"downloads/browser_scrapes/{backup_id}/simple_dom.html"
            if not os.path.exists(html_file):
                return {"success": False, "error": f"Backup HTML not found for backup_id: {backup_id}"}
        else:
            # Analyze current /looking_at/ state
            html_file = "browser_cache/looking_at/simple_dom.html"
            if not os.path.exists(html_file):
                return {"success": False, "error": "No current page state found. Use browser_scrape_page first to capture page state."}

        with open(html_file, 'r', encoding='utf-8') as f:
            html_content = f.read()

        try:
            from bs4 import BeautifulSoup
            soup = BeautifulSoup(html_content, 'html.parser')
        except ImportError:
            return {"success": False, "error": "BeautifulSoup not available for HTML parsing"}

        # === ACCESSIBILITY TREE LOADING (optional) ===
        accessibility_tree_data = None
        accessibility_tree_file = None

        if backup_id:
            accessibility_tree_file = f"downloads/browser_scrapes/{backup_id}/accessibility_tree.json"
        else:
            accessibility_tree_file = "browser_cache/looking_at/accessibility_tree.json"

        if os.path.exists(accessibility_tree_file):
            try:
                with open(accessibility_tree_file, 'r', encoding='utf-8') as f:
                    accessibility_tree_data = json.load(f)
                logger.info(f"🌳 FINDER_TOKEN: ACCESSIBILITY_TREE_LOADED - {accessibility_tree_data.get('node_count', 0)} nodes available")
            except Exception as e:
                logger.warning(f"⚠️ FINDER_TOKEN: ACCESSIBILITY_TREE_LOAD_ERROR - Could not load accessibility tree: {e}")
                accessibility_tree_data = None
        else:
            logger.info(f"🌳 FINDER_TOKEN: ACCESSIBILITY_TREE_NOT_FOUND - No accessibility tree available at {accessibility_tree_file}")
            accessibility_tree_data = None

        # === ENHANCED DOM PROCESSING INTEGRATION ===
        automation_assistant_result = None
        if include_automation_assistant and analysis_type in ["all", "enhanced"]:
            try:
                from imports.dom_processing.enhanced_dom_processor import \
                    process_current_looking_at
                logger.info("🎯 FINDER_TOKEN: ENHANCED_DOM_PROCESSING_START - Generating automation assistant files")
                automation_assistant_result = process_current_looking_at()
                logger.info(f"✅ FINDER_TOKEN: ENHANCED_DOM_PROCESSING_SUCCESS - Generated {len(automation_assistant_result.get('cleaned_files', []))} automation files")
            except Exception as e:
                logger.warning(f"⚠️ FINDER_TOKEN: ENHANCED_DOM_PROCESSING_WARNING - Could not generate automation assistant: {e}")

        if analysis_type == "enhanced":
            # Return enhanced automation assistant analysis
            if automation_assistant_result:
                return {
                    "success": True,
                    "analysis_type": "enhanced_automation_assistant",
                    "automation_assistant": automation_assistant_result,
                    "automation_ready": automation_assistant_result.get('automation_ready', False),
                    "automation_strategy": automation_assistant_result.get('automation_hints', {}).get('automation_strategy', 'unknown'),
                    "generated_files": automation_assistant_result.get('cleaned_files', []),
                    "automation_hints": automation_assistant_result.get('automation_hints', {}),
                    "google_targets": automation_assistant_result.get('google_targets', {}),
                    "analyzed_file": html_file
                }
            else:
                return {"success": False, "error": "Enhanced DOM processing failed"}

        elif analysis_type == "form_elements":
            # Find all forms and their elements
            forms = []
            for form in soup.find_all('form'):
                form_data = {
                    'id': form.get('id'),
                    'action': form.get('action'),
                    'method': form.get('method', 'GET'),
                    'elements': []
                }

                for element in form.find_all(['input', 'button', 'select', 'textarea']):
                    element_data = {
                        'tag': element.name,
                        'type': element.get('type'),
                        'id': element.get('id'),
                        'name': element.get('name'),
                        'aria_label': element.get('aria-label'),
                        'data_testid': element.get('data-testid'),
                        'placeholder': element.get('placeholder'),
                        'text': element.get_text(strip=True)
                    }
                    form_data['elements'].append(element_data)

                forms.append(form_data)

            result = {
                "success": True,
                "analysis_type": analysis_type,
                "forms": forms,
                "form_count": len(forms)
            }

        elif analysis_type == "automation_targets":
            # Find all elements with automation-friendly attributes
            targets = []
            for element in soup.find_all():
                if element.get('id') or element.get('data-testid') or element.get('aria-label'):
                    target = {
                        'tag': element.name,
                        'id': element.get('id'),
                        'data_testid': element.get('data-testid'),
                        'aria_label': element.get('aria-label'),
                        'role': element.get('role'),
                        'text': element.get_text(strip=True)[:100],  # First 100 chars
                        'selector_priority': 'high' if element.get('data-testid') else 'medium' if element.get('id') else 'low'
                    }
                    targets.append(target)

            result = {
                "success": True,
                "analysis_type": analysis_type,
                "automation_targets": targets,
                "target_count": len(targets),
                "high_priority_targets": len([t for t in targets if t['selector_priority'] == 'high'])
            }

        elif analysis_type == "accessibility":
            # Accessibility analysis using accessibility tree if available
            if not accessibility_tree_data:
                return {
                    "success": False,
                    "error": "No accessibility tree data available. Accessibility tree extraction may have failed during page scrape."
                }

            # Extract accessibility information from the tree
            accessibility_info = {
                "nodes": [],
                "roles": {},
                "issues": [],
                "screen_reader_friendly": True
            }

            if accessibility_tree_data.get("success"):
                nodes = accessibility_tree_data.get("accessibility_tree", [])

                for node in nodes:
                    # Extract key accessibility properties
                    node_info = {
                        "nodeId": node.get("nodeId"),
                        "role": node.get("role", {}).get("value"),
                        "name": node.get("name", {}).get("value"),
                        "description": node.get("description", {}).get("value"),
                        "value": node.get("value", {}).get("value"),
                        "properties": []
                    }

                    # Extract properties
                    for prop in node.get("properties", []):
                        prop_info = {
                            "name": prop.get("name"),
                            "value": prop.get("value", {}).get("value")
                        }
                        node_info["properties"].append(prop_info)

                    accessibility_info["nodes"].append(node_info)

                    # Count roles for analysis
                    role = node_info["role"]
                    if role:
                        accessibility_info["roles"][role] = accessibility_info["roles"].get(role, 0) + 1

                # Basic accessibility analysis
                issues = []

                # Check for images without alt text
                for node in accessibility_info["nodes"]:
                    if node.get("role") == "image":
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "missing_alt_text",
                                "severity": "warning",
                                "message": "Image without accessible name (alt text)",
                                "node_id": node.get("nodeId")
                            })

                # Check for buttons without labels
                for node in accessibility_info["nodes"]:
                    if node.get("role") == "button":
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "unlabeled_button",
                                "severity": "error",
                                "message": "Button without accessible name",
                                "node_id": node.get("nodeId")
                            })

                # Check for form inputs without labels
                for node in accessibility_info["nodes"]:
                    if node.get("role") in ["textbox", "combobox", "checkbox", "radio"]:
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "unlabeled_form_input",
                                "severity": "error",
                                "message": f"Form input ({node.get('role')}) without accessible name",
                                "node_id": node.get("nodeId")
                            })

                accessibility_info["issues"] = issues
                accessibility_info["screen_reader_friendly"] = len([i for i in issues if i["severity"] == "error"]) == 0

                result = {
                    "success": True,
                    "analysis_type": "accessibility",
                    "accessibility_tree_available": True,
                    "total_nodes": len(accessibility_info["nodes"]),
                    "roles_found": accessibility_info["roles"],
                    "accessibility_issues": issues,
                    "issues_count": len(issues),
                    "error_count": len([i for i in issues if i["severity"] == "error"]),
                    "warning_count": len([i for i in issues if i["severity"] == "warning"]),
                    "screen_reader_friendly": accessibility_info["screen_reader_friendly"],
                    "accessibility_summary": {
                        "total_interactive_elements": len([n for n in accessibility_info["nodes"] if n.get("role") in ["button", "textbox", "combobox", "checkbox", "radio", "link"]]),
                        "labeled_elements": len([n for n in accessibility_info["nodes"] if n.get("name") and n.get("role") in ["button", "textbox", "combobox", "checkbox", "radio", "link"]]),
                        "images_count": accessibility_info["roles"].get("image", 0),
                        "buttons_count": accessibility_info["roles"].get("button", 0),
                        "links_count": accessibility_info["roles"].get("link", 0)
                    },
                    "analyzed_file": html_file
                }
            else:
                result = {
                    "success": False,
                    "error": f"Accessibility tree extraction failed: {accessibility_tree_data.get('error', 'Unknown error')}"
                }

        elif analysis_type == "all":
            # Comprehensive analysis - all types
            all_results = {}

            # Forms analysis
            forms = []
            for form in soup.find_all('form'):
                form_data = {
                    'id': form.get('id'),
                    'action': form.get('action'),
                    'method': form.get('method', 'GET'),
                    'elements': []
                }

                for element in form.find_all(['input', 'button', 'select', 'textarea']):
                    element_data = {
                        'tag': element.name,
                        'type': element.get('type'),
                        'id': element.get('id'),
                        'name': element.get('name'),
                        'aria_label': element.get('aria-label'),
                        'data_testid': element.get('data-testid'),
                        'placeholder': element.get('placeholder'),
                        'text': element.get_text(strip=True)
                    }
                    form_data['elements'].append(element_data)

                forms.append(form_data)

            # Automation targets
            targets = []
            for element in soup.find_all():
                if element.get('id') or element.get('data-testid') or element.get('aria-label'):
                    target = {
                        'tag': element.name,
                        'id': element.get('id'),
                        'data_testid': element.get('data-testid'),
                        'aria_label': element.get('aria-label'),
                        'role': element.get('role'),
                        'text': element.get_text(strip=True)[:100],
                        'selector_priority': 'high' if element.get('data-testid') else 'medium' if element.get('id') else 'low'
                    }
                    targets.append(target)

            result = {
                "success": True,
                "analysis_type": "comprehensive",
                "forms": forms,
                "form_count": len(forms),
                "automation_targets": targets,
                "target_count": len(targets),
                "high_priority_targets": len([t for t in targets if t['selector_priority'] == 'high']),
                "analyzed_file": html_file
            }

            # Add accessibility analysis to comprehensive results
            if accessibility_tree_data and accessibility_tree_data.get("success"):
                accessibility_info = {
                    "nodes": [],
                    "roles": {},
                    "issues": []
                }

                nodes = accessibility_tree_data.get("accessibility_tree", [])

                for node in nodes:
                    # Extract key accessibility properties
                    node_info = {
                        "nodeId": node.get("nodeId"),
                        "role": node.get("role", {}).get("value"),
                        "name": node.get("name", {}).get("value"),
                        "description": node.get("description", {}).get("value"),
                        "value": node.get("value", {}).get("value"),
                        "properties": []
                    }

                    # Extract properties
                    for prop in node.get("properties", []):
                        prop_info = {
                            "name": prop.get("name"),
                            "value": prop.get("value", {}).get("value")
                        }
                        node_info["properties"].append(prop_info)

                    accessibility_info["nodes"].append(node_info)

                    # Count roles for analysis
                    role = node_info["role"]
                    if role:
                        accessibility_info["roles"][role] = accessibility_info["roles"].get(role, 0) + 1

                # Basic accessibility analysis
                issues = []

                # Check for images without alt text
                for node in accessibility_info["nodes"]:
                    if node.get("role") == "image":
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "missing_alt_text",
                                "severity": "warning",
                                "message": "Image without accessible name (alt text)",
                                "node_id": node.get("nodeId")
                            })

                # Check for buttons without labels
                for node in accessibility_info["nodes"]:
                    if node.get("role") == "button":
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "unlabeled_button",
                                "severity": "error",
                                "message": "Button without accessible name",
                                "node_id": node.get("nodeId")
                            })

                # Check for form inputs without labels
                for node in accessibility_info["nodes"]:
                    if node.get("role") in ["textbox", "combobox", "checkbox", "radio"]:
                        has_name = node.get("name") is not None
                        if not has_name:
                            issues.append({
                                "type": "unlabeled_form_input",
                                "severity": "error",
                                "message": f"Form input ({node.get('role')}) without accessible name",
                                "node_id": node.get("nodeId")
                            })

                accessibility_info["issues"] = issues

                result["accessibility"] = {
                    "available": True,
                    "total_nodes": len(accessibility_info["nodes"]),
                    "roles_found": accessibility_info["roles"],
                    "accessibility_issues": issues,
                    "issues_count": len(issues),
                    "error_count": len([i for i in issues if i["severity"] == "error"]),
                    "warning_count": len([i for i in issues if i["severity"] == "warning"]),
                    "screen_reader_friendly": len([i for i in issues if i["severity"] == "error"]) == 0,
                    "accessibility_summary": {
                        "total_interactive_elements": len([n for n in accessibility_info["nodes"] if n.get("role") in ["button", "textbox", "combobox", "checkbox", "radio", "link"]]),
                        "labeled_elements": len([n for n in accessibility_info["nodes"] if n.get("name") and n.get("role") in ["button", "textbox", "combobox", "checkbox", "radio", "link"]]),
                        "images_count": accessibility_info["roles"].get("image", 0),
                        "buttons_count": accessibility_info["roles"].get("button", 0),
                        "links_count": accessibility_info["roles"].get("link", 0)
                    }
                }
            else:
                result["accessibility"] = {
                    "available": False,
                    "reason": "No accessibility tree data available or extraction failed"
                }

            # Add automation assistant data if available
            if automation_assistant_result:
                result["automation_assistant"] = automation_assistant_result
                result["automation_ready"] = automation_assistant_result.get('automation_ready', False)
                result["automation_strategy"] = automation_assistant_result.get('automation_hints', {}).get('automation_strategy', 'unknown')
                result["generated_files"] = automation_assistant_result.get('cleaned_files', [])

        else:
            result = {"success": False, "error": f"Unknown analysis_type: {analysis_type}"}

        logger.info(f"🎯 FINDER_TOKEN: MCP_BROWSER_ANALYZE_SUCCESS - {result.get('target_count', 0)} targets, {result.get('form_count', 0)} forms")
        return result

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_BROWSER_ANALYZE_ERROR - {e}")
        return {"success": False, "error": str(e)}


@auto_tool
async def browser_scrape_page(params: dict) -> dict:
    """
    MCP Tool: AI EYES - Scrape a web page and save to /looking_at/ for AI perception.

    This is the AI's primary sensory interface - captures current browser state
    into the /browser_cache/looking_at/ directory for AI analysis.

    Args:
        params: {
            "url": "https://example.com",  # Required: URL to scrape
            "wait_seconds": 3,             # Optional: wait for JS to load
            "take_screenshot": True,       # Optional: capture visual state
        }
    """
    from datetime import datetime
    
    logger.info(f"🔧 FINDER_TOKEN: MCP_BROWSER_SCRAPE_START - URL: {params.get('url')} (Native Mode)")

    try:
        url = params.get('url')
        if not url:
            return {"success": False, "error": "URL parameter is required"}

        # Validate URL structure
        if not url.startswith(('http://', 'https://')):
            return {"success": False, "error": f"URL must start with http:// or https://. Got: {url}"}

        # Prepare parameters for the central scraper engine
        from urllib.parse import urlparse, quote
        parsed = urlparse(url)
        domain = parsed.netloc
        path = parsed.path or '/'
        url_path_slug = quote(path, safe='').replace('/', '_')[:100]

        scrape_params = {
            "url": url,
            "domain": domain,
            "url_path_slug": url_path_slug,
            "take_screenshot": params.get('take_screenshot', True),
            "headless": True, # Keep background scrapes headless
            "is_notebook_context": False, # Forces it to use /looking_at/ directory
            "verbose": True
        }

        # Route to the newly fortified scraper engine
        # We import here to ensure we grab the updated tool
        from tools.scraper_tools import selenium_automation
        result = await selenium_automation(scrape_params)

        if result.get("success"):
            logger.info(f"✅ FINDER_TOKEN: MCP_BROWSER_SCRAPE_SUCCESS - Files saved to /looking_at/")
            # Normalize output to match what the AI expects from this specific tool
            return {
                "success": True,
                "url": url,
                "looking_at_files": result.get("looking_at_files", {}),
                "page_info": {
                    "title": "Captured via Native Engine",
                    "url": url,
                    "timestamp": datetime.now().isoformat()
                }
            }
        else:
            logger.error(f"❌ FINDER_TOKEN: MCP_BROWSER_SCRAPE_FAILED - {result.get('error')}")
            return result

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_BROWSER_SCRAPE_EXCEPTION - {e}")
        return {
            "success": False,
            "error": f"Browser automation failed: {str(e)}"
        }


async def browser_automate_workflow_walkthrough(params: dict) -> dict:
    """
    MCP Tool: AI HANDS - Complete Workflow Automation Walkthrough

    This is the AI's motor interface - uses browser automation to walk through
    entire plugin workflows from start to finish. Updates /looking_at/ directory
    at each step for continuous AI perception.

    The ultimate test of automation readiness - actually USING the improved components
    and providing real-time feedback on automation success/failure.
    """
    try:
        import re
        import tempfile
        import time
        from pathlib import Path
        from urllib.parse import urlparse

        from selenium import webdriver
        from selenium.webdriver.chrome.options import Options
        from selenium.webdriver.common.by import By
        from selenium.webdriver.support import expected_conditions as EC
        from selenium.webdriver.support.ui import WebDriverWait

        plugin_filename = params.get("plugin_filename", "")
        base_url = params.get("base_url", "http://localhost:5001")
        take_screenshots = params.get("take_screenshots", True)
        use_existing_pipeline_id = params.get("use_existing_pipeline_id", False)

        if not plugin_filename:
            return {"success": False, "error": "plugin_filename is required"}

        # === AGGRESSIVE URL VALIDATION BEFORE BROWSER OPENING ===
        # Map plugin filename to app name and construct URL
        plugin_name = plugin_filename.replace('apps/', '').replace('.py', '')
        plugin_to_app_mapping = {
            '010_introduction': 'introduction',
            '580_upload': 'file_upload_widget',
            '020_profiles': 'profiles',
            '030_roles': 'roles',
            '040_hello_workflow': 'hello_workflow',
            '050_documentation': 'documentation',
            '060_tasks': 'tasks',
            '070_connect_with_botify': 'connect_with_botify',
            '080_parameter_buster': 'parameter_buster',
            '090_link_graph': 'link_graph',
            '100_gap_analysis': 'gap_analysis',
            '110_workflow_genesis': 'workflow_genesis',
            '120_widget_examples': 'widget_examples',
            '130_roadmap': 'roadmap',
            '140_dev_assistant': 'dev_assistant',
            '150_simon_mcp': 'simon_mcp',
            '160_blank_placeholder': 'blank_placeholder',
            '170_botify_trifecta': 'botify_trifecta',
            '180_tab_opener': 'tab_opener',
            '190_browser_cache': 'browser_cache',
            '200_stream_simulator': 'stream_simulator'
        }
        app_name = plugin_to_app_mapping.get(plugin_name, plugin_name)
        plugin_url = f"{base_url}/{app_name}"
        logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_NAVIGATION_MAPPING | Plugin: {plugin_name} -> App: {app_name} -> URL: {plugin_url}")

        # Aggressive URL validation
        if not plugin_url:
            return {"success": False, "error": "No valid plugin URL could be determined"}
        if not isinstance(plugin_url, str):
            return {"success": False, "error": f"Plugin URL must be a string, got: {type(plugin_url)}"}
        if not plugin_url.strip():
            return {"success": False, "error": "Plugin URL is empty or whitespace only"}
        from config import INVALID_URL_PATTERNS
        for pattern in INVALID_URL_PATTERNS:
            if plugin_url.lower().startswith(pattern):
                return {"success": False, "error": f"Invalid URL scheme detected: {pattern}. URL: {plugin_url}"}
        if not plugin_url.startswith(('http://', 'https://')):
            return {"success": False, "error": f"Plugin URL must start with http:// or https://. Got: {plugin_url}"}
        if 'localhost' in plugin_url or '127.0.0.1' in plugin_url:
            if not re.match(r'^https?://(localhost|127\.0\.0\.1)(:\d+)?(/.*)?$', plugin_url):
                return {"success": False, "error": f"Malformed localhost URL: {plugin_url}"}
        try:
            parsed = urlparse(plugin_url)
            if not parsed.netloc:
                return {"success": False, "error": f"Plugin URL has no hostname: {plugin_url}"}
        except Exception as e:
            return {"success": False, "error": f"Plugin URL parsing failed: {plugin_url}. Error: {e}"}
        logger.info(f"✅ FINDER_TOKEN: WORKFLOW_URL_VALIDATION_PASSED | Plugin URL validated: {plugin_url}")

        # Only now do we proceed to rotate directories and create the browser
        # === DIRECTORY ROTATION BEFORE NEW WORKFLOW WALKTHROUGH ===
        # Rotate looking_at directory to preserve AI workflow history
        # rotate_looking_at_directory is now defined locally in this module
        from pathlib import Path

        rotation_success = rotate_looking_at_directory(
            looking_at_path=Path('browser_cache/looking_at'),
            max_rolled_dirs=MAX_ROLLED_LOOKING_AT_DIRS
        )

        if not rotation_success:
            logger.warning("⚠️ FINDER_TOKEN: WORKFLOW_DIRECTORY_ROTATION_WARNING - Directory rotation failed, continuing with workflow")

        logger.info(f"🚀 FINDER_TOKEN: WORKFLOW_AUTOMATION_START | Starting workflow walkthrough for {plugin_filename}")

        # KILL ALL HUNG CHROMIUM INSTANCES FIRST - BUT ONLY AUTOMATION INSTANCES
        import signal
        import subprocess

        try:
            # Kill any existing chromedriver processes
            subprocess.run(['pkill', '-f', 'chromedriver'], capture_output=True)
            logger.info("🔪 FINDER_TOKEN: WORKFLOW_CHROMEDRIVER_CLEANUP - Killed existing chromedriver processes")
        except Exception as e:
            logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_CHROMEDRIVER_CLEANUP_WARNING - Error killing chromedriver: {e}")

        try:
            # Kill only hung Chromium automation instances (not user's main Chrome)
            import platform
            is_mac = platform.system().lower() == 'darwin'
            
            # Mac process name contains 'Chrome', Linux uses 'chromium'
            process_pattern = 'Chrome.*--user-data-dir.*temp' if is_mac else 'chromium.*--user-data-dir.*temp'
            
            result = subprocess.run(['pgrep', '-f', process_pattern], capture_output=True, text=True)
            if result.stdout.strip():
                pids = result.stdout.strip().split('\n')
                for pid in pids:
                    if pid.strip():
                        try:
                            # Verify this is actually an automation instance before killing
                            if is_mac:
                                ps_cmd = ['ps', '-p', pid, '-o', 'command=']
                            else:
                                ps_cmd = ['ps', '-p', pid, '-o', 'cmd', '--no-headers']
                                
                            cmdline_check = subprocess.run(ps_cmd, capture_output=True, text=True)
                            if 'temp' in cmdline_check.stdout and '--user-data-dir' in cmdline_check.stdout:
                                os.kill(int(pid), signal.SIGKILL)
                                logger.info(f"🔪 FINDER_TOKEN: WORKFLOW_AUTOMATION_CHROMIUM_CLEANUP - Killed automation Chromium PID: {pid}")
                        except Exception as e:
                            logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_AUTOMATION_CHROMIUM_CLEANUP_WARNING - Error killing PID {pid}: {e}")
        except Exception as e:
            logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_AUTOMATION_CHROMIUM_CLEANUP_WARNING - Error finding automation Chromium processes: {e}")

        # ENHANCED CHROME OPTIONS FOR WORKFLOW AUTOMATION ISOLATION
        options = Options()
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
        options.add_argument('--window-size=1920,1080')
        options.add_argument('--start-maximized')  # Ensure browser is visible

        # CRITICAL ISOLATION PARAMETERS FOR WORKFLOW AUTOMATION
        options.add_argument('--no-first-run')  # Skip first run setup
        options.add_argument('--no-default-browser-check')  # Skip default browser check
        options.add_argument('--disable-default-apps')  # Disable default apps
        options.add_argument('--disable-background-mode')  # Prevent background mode
        options.add_argument('--disable-background-timer-throttling')  # Prevent throttling
        options.add_argument('--disable-renderer-backgrounding')  # Keep renderer active
        options.add_argument('--disable-backgrounding-occluded-windows')  # Keep windows active
        options.add_argument('--disable-ipc-flooding-protection')  # Allow rapid IPC
        options.add_argument('--disable-web-security')  # Allow localhost access
        options.add_argument('--allow-running-insecure-content')  # Allow HTTP

        # UNIQUE SESSION ISOLATION FOR WORKFLOW
        workflow_profile_dir = tempfile.mkdtemp(prefix='pipulate_workflow_automation_')
        options.add_argument(f'--user-data-dir={workflow_profile_dir}')

        # REMOTE DEBUGGING PORT FOR WORKFLOW ISOLATION (find an unused port)
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.bind(('', 0))
            workflow_debug_port = s.getsockname()[1]
        options.add_argument(f'--remote-debugging-port={workflow_debug_port}')

        logger.info(f"🔧 FINDER_TOKEN: WORKFLOW_BROWSER_ISOLATION - Profile: {workflow_profile_dir}, Debug port: {workflow_debug_port}")

        # Create browser with enhanced error handling
        try:
            driver = webdriver.Chrome(options=options)
            logger.info(f"✅ FINDER_TOKEN: WORKFLOW_BROWSER_CREATED - Chrome instance created successfully")
        except Exception as browser_error:
            logger.error(f"❌ FINDER_TOKEN: WORKFLOW_BROWSER_CREATION_FAILED - {browser_error}")
            return {"success": False, "error": f"Failed to create workflow browser instance: {browser_error}"}

        # Verify browser is responsive before proceeding
        try:
            initial_url = driver.current_url
            logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_BROWSER_INITIAL_STATE - Initial URL: {initial_url}")

            # Check if browser window is valid
            if initial_url.startswith('data:'):
                logger.error(f"❌ FINDER_TOKEN: WORKFLOW_INVALID_INITIAL_STATE - Browser started with data: URL: {initial_url}")
                driver.quit()
                shutil.rmtree(workflow_profile_dir, ignore_errors=True)
                return {"success": False, "error": f"Workflow browser started in invalid state with data: URL: {initial_url}"}
        except Exception as state_error:
            logger.error(f"❌ FINDER_TOKEN: WORKFLOW_BROWSER_STATE_CHECK_FAILED - {state_error}")
            try:
                driver.quit()
            except:
                pass
            shutil.rmtree(workflow_profile_dir, ignore_errors=True)
            return {"success": False, "error": f"Workflow browser state check failed: {state_error}"}
        driver.maximize_window()
        driver.set_page_load_timeout(30)  # Increased timeout for localhost
        driver.implicitly_wait(10)  # Increased implicit wait
        driver.set_script_timeout(15)  # Increased script timeout

        screenshots = []
        workflow_steps = []

        # Helper function to update /looking_at/ during automation
        def update_looking_at_state(step_name: str):
            """Update /looking_at/ directory with current browser state"""
            try:
                looking_at_dir = 'browser_cache/looking_at'
                os.makedirs(looking_at_dir, exist_ok=True)

                # Capture current state
                current_url = driver.current_url
                page_title = driver.title
                source_html = driver.page_source
                dom_html = driver.execute_script("return document.documentElement.outerHTML;")

                # Save state files
                state_data = {
                    'step': step_name,
                    'url': current_url,
                    'title': page_title,
                    'timestamp': datetime.now().isoformat()
                }

                with open(os.path.join(looking_at_dir, 'headers.json'), 'w') as f:
                    json.dump(state_data, f, indent=2)

                with open(os.path.join(looking_at_dir, 'source.html'), 'w', encoding='utf-8') as f:
                    f.write(source_html)

                with open(os.path.join(looking_at_dir, 'dom.html'), 'w', encoding='utf-8') as f:
                    f.write(dom_html)

                # Create simple DOM
                try:
                    from bs4 import BeautifulSoup
                    soup = BeautifulSoup(dom_html, 'html.parser')
                    for tag in soup(['script', 'style', 'noscript', 'meta', 'link']):
                        tag.decompose()
                    for element in soup.find_all():
                        attrs_to_keep = {}
                        for attr, value in element.attrs.items():
                            if attr in ['id', 'role', 'data-testid', 'name', 'type', 'href', 'src'] or attr.startswith('aria-'):
                                attrs_to_keep[attr] = value
                        element.attrs = attrs_to_keep
                    simple_dom_html = str(soup)
                except:
                    simple_dom_html = dom_html

                with open(os.path.join(looking_at_dir, 'simple_dom.html'), 'w', encoding='utf-8') as f:
                    f.write(simple_dom_html)

                # Take screenshot
                driver.save_screenshot(os.path.join(looking_at_dir, 'screenshot.png'))

                logger.info(f"🎯 FINDER_TOKEN: AUTOMATION_PERCEPTION_UPDATE - Step: {step_name}, URL: {current_url}")

            except Exception as e:
                logger.warning(f"⚠️ FINDER_TOKEN: AUTOMATION_PERCEPTION_ERROR - {e}")

        try:
            # Step 1: Navigate to the specific plugin requested
            # Extract plugin name from filename and construct URL
            plugin_name = plugin_filename.replace('apps/', '').replace('.py', '')

            # Map plugin filename to app name (this is the key fix!)
            plugin_to_app_mapping = {
                '010_introduction': 'introduction',
                '580_upload': 'file_upload_widget',
                '020_profiles': 'profiles',
                '030_roles': 'roles',
                '040_hello_workflow': 'hello_workflow',
                '050_documentation': 'documentation',
                '060_tasks': 'tasks',
                '070_connect_with_botify': 'connect_with_botify',
                '080_parameter_buster': 'parameter_buster',
                '090_link_graph': 'link_graph',
                '100_gap_analysis': 'gap_analysis',
                '110_workflow_genesis': 'workflow_genesis',
                '120_widget_examples': 'widget_examples',
                '130_roadmap': 'roadmap',
                '140_dev_assistant': 'dev_assistant',
                '150_simon_mcp': 'simon_mcp',
                '160_blank_placeholder': 'blank_placeholder',
                '170_botify_trifecta': 'botify_trifecta',
                '180_tab_opener': 'tab_opener',
                '190_browser_cache': 'browser_cache',
                '200_stream_simulator': 'stream_simulator'
            }

            app_name = plugin_to_app_mapping.get(plugin_name, plugin_name)
            plugin_url = f"{base_url}/{app_name}"

            logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_NAVIGATION_MAPPING | Plugin: {plugin_name} -> App: {app_name} -> URL: {plugin_url}")

            # Validate URL before navigation to prevent data: URLs
            if not plugin_url.startswith(('http://', 'https://')):
                return {"success": False, "error": f"Invalid URL format: {plugin_url}. Expected http:// or https://"}

            try:
                logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_NAVIGATION_ATTEMPT | About to navigate to: {plugin_url}")

                # Check current URL before navigation
                try:
                    current_url_before = driver.current_url
                    logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_CURRENT_URL_BEFORE | {current_url_before}")
                except Exception as e:
                    logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_CURRENT_URL_ERROR | Could not get current URL: {e}")

                # Attempt navigation with detailed logging
                logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_DRIVER_GET_START | Calling driver.get('{plugin_url}')")
                driver.get(plugin_url)
                logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_DRIVER_GET_COMPLETE | driver.get() returned")

                # Immediate URL check after navigation
                time.sleep(1)  # Brief pause to let navigation settle
                current_url = driver.current_url
                logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_CURRENT_URL_AFTER | {current_url}")

                # Verify we didn't end up with a data: URL
                if current_url.startswith('data:'):
                    logger.error(f"❌ FINDER_TOKEN: WORKFLOW_DATA_URL_DETECTED | Navigation resulted in data: URL: {current_url}")
                    return {"success": False, "error": f"Browser navigation resulted in data: URL: {current_url}. Original URL: {plugin_url}"}

                if current_url == 'about:blank':
                    logger.error(f"❌ FINDER_TOKEN: WORKFLOW_ABOUT_BLANK_DETECTED | Navigation resulted in about:blank")
                    return {"success": False, "error": f"Browser navigation resulted in about:blank. Original URL: {plugin_url}"}

                if not current_url or current_url == current_url_before:
                    logger.error(f"❌ FINDER_TOKEN: WORKFLOW_NO_NAVIGATION | URL unchanged: {current_url}")
                    return {"success": False, "error": f"Browser navigation failed - URL unchanged: {current_url}. Original URL: {plugin_url}"}

                logger.info(f"✅ FINDER_TOKEN: WORKFLOW_NAVIGATION_SUCCESS | Navigated to: {current_url}")
            except Exception as nav_error:
                logger.error(f"❌ FINDER_TOKEN: WORKFLOW_NAVIGATION_EXCEPTION | {nav_error}")
                return {"success": False, "error": f"Navigation failed: {nav_error}. URL: {plugin_url}"}

            # Update /looking_at/ with landing page state
            update_looking_at_state("landing")

            if take_screenshots:
                screenshot_path = f"workflow_step_01_landing.png"
                driver.save_screenshot(screenshot_path)
                screenshots.append(screenshot_path)
                logger.info(f"📸 FINDER_TOKEN: WORKFLOW_SCREENSHOT | Captured landing page")

            # Step 2: Wait for page load and look for pipeline input
            WebDriverWait(driver, 10).until(
                EC.presence_of_element_located((By.TAG_NAME, "body"))
            )

            workflow_steps.append({
                "step": "landing",
                "status": "success",
                "description": "Successfully loaded plugin landing page"
            })

            # Step 3: Look for and fill pipeline ID input
            try:
                pipeline_input = driver.find_element(By.NAME, "pipeline_id")

                # Use existing pipeline ID from session if requested and available, otherwise generate new one
                from server import db
                existing_pipeline_id = db.get('pipeline_id')
                if use_existing_pipeline_id and existing_pipeline_id:
                    pipeline_id = existing_pipeline_id
                    logger.info(f"🔄 FINDER_TOKEN: WORKFLOW_REUSE | Using existing pipeline ID: {pipeline_id}")
                else:
                    pipeline_id = f"automation-test-{int(time.time())}"
                    logger.info(f"🆕 FINDER_TOKEN: WORKFLOW_NEW | Generated new pipeline ID: {pipeline_id}")

                pipeline_input.clear()
                pipeline_input.send_keys(pipeline_id)

                logger.info(f"📝 FINDER_TOKEN: WORKFLOW_INPUT | Entered pipeline ID: {pipeline_id}")

                # Submit the form
                submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit'], input[type='submit']")
                submit_button.click()

                workflow_steps.append({
                    "step": "init",
                    "status": "success",
                    "description": f"Initialized workflow with ID: {pipeline_id}"
                })

                # Wait for workflow to load
                time.sleep(3)

                # Update /looking_at/ with initialized state
                update_looking_at_state("initialized")

                if take_screenshots:
                    screenshot_path = f"workflow_step_02_initialized.png"
                    driver.save_screenshot(screenshot_path)
                    screenshots.append(screenshot_path)

            except Exception as e:
                logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_INIT_SKIP | No pipeline input found, continuing: {e}")
                workflow_steps.append({
                    "step": "init",
                    "status": "skipped",
                    "description": "No pipeline initialization needed"
                })

            # Step 4: Check if this plugin has file upload capabilities and test them
            try:
                # Determine if this plugin supports file uploads by checking the plugin name
                plugin_supports_uploads = "upload" in plugin_filename.lower() or "file" in plugin_filename.lower()

                if plugin_supports_uploads:
                    # For file upload plugins, we need to initialize the workflow first
                    logger.info(f"📋 FINDER_TOKEN: WORKFLOW_UPLOAD_INIT | Initializing file upload workflow")

                    # Look for pipeline initialization form
                    try:
                        pipeline_input = driver.find_element(By.CSS_SELECTOR, "input[name='pipeline_id']")
                        if pipeline_input:
                            # Use existing pipeline ID if requested, otherwise generate new one
                            if use_existing_pipeline_id and existing_pipeline_id:
                                pipeline_id = existing_pipeline_id
                                logger.info(f"🔧 FINDER_TOKEN: WORKFLOW_PIPELINE_ID | Using existing pipeline ID: {pipeline_id}")
                            else:
                                import uuid
                                pipeline_id = f"automation-test-{uuid.uuid4().hex[:8]}"
                                logger.info(f"🔧 FINDER_TOKEN: WORKFLOW_PIPELINE_ID | Generated new pipeline ID: {pipeline_id}")
                            pipeline_input.send_keys(pipeline_id)

                            # Look for and click the initialize button
                            init_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
                            init_button.click()
                            logger.info(f"🚀 FINDER_TOKEN: WORKFLOW_INIT_CLICK | Clicked initialize button")

                            # Wait for page to load
                            time.sleep(3)

                            # Update /looking_at/ with post-init state
                            update_looking_at_state("workflow_initialized")
                    except Exception as init_error:
                        logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_INIT_SKIP | Workflow initialization failed: {init_error}")

                    # Now look for file input using our automation attribute
                    try:
                        file_input = driver.find_element(By.CSS_SELECTOR, "[data-testid='file-upload-widget-file-input']")
                        logger.info(f"📁 FINDER_TOKEN: WORKFLOW_FILE_INPUT | Found file input with automation attribute")

                        # Create a test file
                        with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as temp_file:
                            temp_file.write(f"Test file content for automation walkthrough\\nGenerated at: {time.ctime()}\\nPlugin: {plugin_filename}")
                            test_file_path = temp_file.name

                        # Upload the test file
                        file_input.send_keys(test_file_path)
                        logger.info(f"📤 FINDER_TOKEN: WORKFLOW_FILE_UPLOAD | Uploaded test file: {test_file_path}")

                        # Look for upload button using automation attribute
                        upload_button = driver.find_element(By.CSS_SELECTOR, "[data-testid='file-upload-widget-upload-button']")
                        upload_button.click()
                        logger.info(f"🎯 FINDER_TOKEN: WORKFLOW_SUBMIT | Clicked upload button")

                        # Wait for processing
                        time.sleep(5)

                        # Update /looking_at/ with post-upload state
                        update_looking_at_state("file_uploaded")

                        if take_screenshots:
                            screenshot_path = f"workflow_step_03_uploaded.png"
                            driver.save_screenshot(screenshot_path)
                            screenshots.append(screenshot_path)

                        workflow_steps.append({
                            "step": "file_upload",
                            "status": "success",
                            "description": f"Successfully uploaded file using automation attributes"
                        })

                        # Clean up test file
                        try:
                            Path(test_file_path).unlink()
                        except:
                            pass

                    except Exception as upload_error:
                        logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_UPLOAD_ELEMENT_NOT_FOUND | File upload elements not found: {upload_error}")
                        workflow_steps.append({
                            "step": "file_upload",
                            "status": "failed",
                            "description": f"File upload elements not found: {str(upload_error)}"
                        })
                else:
                    # This plugin doesn't support file uploads, skip gracefully
                    logger.info(f"📋 FINDER_TOKEN: WORKFLOW_NO_UPLOAD | Plugin {plugin_filename} doesn't support file uploads, skipping")
                    workflow_steps.append({
                        "step": "file_upload",
                        "status": "skipped",
                        "description": f"Plugin {plugin_filename} doesn't support file uploads"
                    })

            except Exception as e:
                logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_FILE_SKIP | File upload step failed: {e}")
                workflow_steps.append({
                    "step": "file_upload",
                    "status": "failed",
                    "description": f"File upload failed: {str(e)}"
                })

            # Final state update and screenshot
            update_looking_at_state("workflow_complete")

            if take_screenshots:
                screenshot_path = f"workflow_final_state.png"
                driver.save_screenshot(screenshot_path)
                screenshots.append(screenshot_path)

            logger.info(f"✅ FINDER_TOKEN: WORKFLOW_AUTOMATION_SUCCESS | Completed walkthrough of {plugin_filename}")

            return {
                "success": True,
                "plugin": plugin_filename,
                "workflow_steps": workflow_steps,
                "screenshots": screenshots,
                "total_steps": len(workflow_steps),
                "successful_steps": len([s for s in workflow_steps if s["status"] == "success"])
            }

        finally:
            # Clean up browser and profile directory
            try:
                driver.quit()
                logger.info(f"🧹 FINDER_TOKEN: WORKFLOW_BROWSER_CLEANUP | Browser quit successfully")
            except Exception as browser_quit_error:
                logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_BROWSER_CLEANUP_WARNING | Browser quit failed: {browser_quit_error}")

            # Clean up temporary profile directory
            try:
                if 'workflow_profile_dir' in locals():
                    shutil.rmtree(workflow_profile_dir, ignore_errors=True)
                    logger.info(f"🧹 FINDER_TOKEN: WORKFLOW_PROFILE_CLEANUP | Cleaned up profile directory: {workflow_profile_dir}")
            except Exception as cleanup_error:
                logger.warning(f"⚠️ FINDER_TOKEN: WORKFLOW_PROFILE_CLEANUP_WARNING | Profile cleanup failed: {cleanup_error}")

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: WORKFLOW_AUTOMATION_ERROR | {e}")
        # Ensure cleanup even on exception
        try:
            if 'driver' in locals():
                driver.quit()
            if 'workflow_profile_dir' in locals():
                shutil.rmtree(workflow_profile_dir, ignore_errors=True)
        except:
            pass
        return {"success": False, "error": str(e)}


async def botify_get_full_schema(params: dict) -> dict:
    """Discover complete Botify API schema using the true_schema_discoverer.py module.

    This tool fetches the comprehensive schema from Botify's official datamodel endpoints,
    providing access to all 4,449+ fields for building advanced queries. Implements intelligent
    caching for instant access to support "radical transparency" AI context bootstrapping.
    """
    # Read API token from standard location (never pass as parameter)
    api_token = _read_botify_api_token()
    if not api_token:
        return {
            "status": "error",
            "message": "Botify API token not found. Please ensure helpers/botify/botify_token.txt exists.",
            "token_location": "helpers/botify/botify_token.txt"
        }

    org = params.get("org")
    project = params.get("project")
    analysis = params.get("analysis")
    force_refresh = params.get("force_refresh", False)

    # Validate required parameters (token no longer required as param)
    missing_params = []
    if not org:
        missing_params.append("org")
    if not project:
        missing_params.append("project")
    if not analysis:
        missing_params.append("analysis")

    if missing_params:
        return {
            "status": "error",
            "message": f"Missing required parameters: {', '.join(missing_params)}",
            "required_params": ["org", "project", "analysis"]
        }

    # Implement intelligent caching for instant schema access
    try:
        import json
        from datetime import datetime, timedelta
        from pathlib import Path

        # Define cache file path
        cache_dir = Path("downloads/botify_schema_cache")
        cache_dir.mkdir(parents=True, exist_ok=True)
        cache_file = cache_dir / f"{org}_{project}_{analysis}_schema.json"

        # Check if cached file exists and is recent (within 24 hours)
        if cache_file.exists() and not force_refresh:
            try:
                with open(cache_file, 'r') as f:
                    cached_data = json.load(f)

                # Check cache age
                cache_timestamp = datetime.fromisoformat(cached_data.get("cache_metadata", {}).get("cached_at", "1970-01-01"))
                cache_age = datetime.now() - cache_timestamp

                if cache_age < timedelta(hours=24):
                    # Return fresh cached data
                    cached_data["cache_metadata"]["cache_hit"] = True
                    cached_data["cache_metadata"]["cache_age_hours"] = round(cache_age.total_seconds() / 3600, 2)

                    return {
                        "status": "success",
                        "result": cached_data,
                        "external_api_method": "GET",
                        "summary": {
                            "total_fields_discovered": cached_data.get("total_fields_discovered", 0),
                            "collections_discovered": len(cached_data.get("collections_discovered", [])),
                            "discovery_timestamp": cached_data.get("project_info", {}).get("discovery_timestamp"),
                            "cache_used": True,
                            "cache_age_hours": round(cache_age.total_seconds() / 3600, 2)
                        }
                    }
            except (json.JSONDecodeError, KeyError, ValueError):
                # Cache file corrupted, proceed with fresh discovery
                pass

        # Perform live schema discovery
        from imports.botify.true_schema_discoverer import \
            BotifySchemaDiscoverer

        # Create discoverer instance
        discoverer = BotifySchemaDiscoverer(org, project, analysis, api_token)

        # Execute the discovery
        schema_results = await discoverer.discover_complete_schema()

        # Add cache metadata
        schema_results["cache_metadata"] = {
            "cached_at": datetime.now().isoformat(),
            "cache_hit": False,
            "org": org,
            "project": project,
            "analysis": analysis
        }

        # Save to cache for future use
        try:
            with open(cache_file, 'w') as f:
                json.dump(schema_results, f, indent=2)
        except Exception as cache_error:
            # Don't fail the main operation if caching fails
            logger.warning(f"Failed to save schema cache: {cache_error}")

        return {
            "status": "success",
            "result": schema_results,
            "external_api_method": "GET",
            "summary": {
                "total_fields_discovered": schema_results.get("total_fields_discovered", 0),
                "collections_discovered": len(schema_results.get("collections_discovered", [])),
                "discovery_timestamp": schema_results.get("project_info", {}).get("discovery_timestamp"),
                "cache_used": False,
                "cache_saved": cache_file.exists()
            }
        }

    except Exception as e:
        return {
            "status": "error",
            "message": f"Schema discovery error: {str(e)}",
            "org": org,
            "project": project,
            "analysis": analysis
        }


async def botify_list_available_analyses(params: dict) -> dict:
    """List available analyses from the local analyses.json file.

    This tool reads the cached analyses data to help LLMs select the correct
    analysis_slug for queries without requiring live API calls.
    """
    username = params.get("username", "michaellevin-org")
    project_name = params.get("project_name", "mikelev.in")

    try:
        # Construct the path to the analyses.json file
        from pathlib import Path
        analyses_path = Path(f"downloads/quadfecta/{username}/{project_name}/analyses.json")

        if not analyses_path.exists():
            return {
                "status": "error",
                "message": f"Analyses file not found at {analyses_path}",
                "file_path": str(analyses_path)
            }

        # Read and parse the analyses file
        with open(analyses_path, 'r') as f:
            import json
            analyses_data = json.load(f)

        # Extract simplified analysis info for LLM consumption
        analyses_list = []
        for analysis in analyses_data.get("results", []):
            analyses_list.append({
                "slug": analysis.get("slug"),
                "name": analysis.get("name"),
                "date_finished": analysis.get("date_finished"),
                "urls_done": analysis.get("urls_done", 0),
                "status": analysis.get("status"),
                "id": analysis.get("id")
            })

        # Sort by date_finished (most recent first)
        analyses_list.sort(key=lambda x: x.get("date_finished", ""), reverse=True)

        return {
            "status": "success",
            "result": {
                "analyses": analyses_list,
                "total_count": len(analyses_list),
                "file_path": str(analyses_path),
                "most_recent": analyses_list[0] if analyses_list else None
            },
            "summary": {
                "total_analyses": len(analyses_list),
                "file_checked": str(analyses_path)
            }
        }

    except Exception as e:
        return {
            "status": "error",
            "message": f"Error reading analyses: {str(e)}",
            "username": username,
            "project_name": project_name,
            "attempted_path": str(analyses_path) if 'analyses_path' in locals() else None
        }


async def botify_execute_custom_bql_query(params: dict) -> dict:
    """Execute a custom BQL query with full parameter control.

    This is the core 'query wizard' tool that enables LLMs to construct and execute
    sophisticated BQL queries with custom dimensions, metrics, and filters.
    """
    # Read API token from standard location (never pass as parameter)
    api_token = _read_botify_api_token()
    if not api_token:
        return {
            "status": "error",
            "message": "Botify API token not found. Please ensure helpers/botify/botify_token.txt exists.",
            "token_location": "helpers/botify/botify_token.txt"
        }

    org_slug = params.get("org_slug")
    project_slug = params.get("project_slug")
    analysis_slug = params.get("analysis_slug")
    query_json = params.get("query_json")

    # Validate required parameters (token no longer required as param)
    missing_params = []
    if not org_slug:
        missing_params.append("org_slug")
    if not project_slug:
        missing_params.append("project_slug")
    if not analysis_slug:
        missing_params.append("analysis_slug")
    if not query_json:
        missing_params.append("query_json")

    if missing_params:
        return {
            "status": "error",
            "message": f"Missing required parameters: {', '.join(missing_params)}",
            "required_params": ["org_slug", "project_slug", "analysis_slug", "query_json"]
        }

    # Validate query_json structure
    if not isinstance(query_json, dict):
        return {
            "status": "error",
            "message": "query_json must be a dictionary containing the BQL query structure"
        }

    try:
        async with aiohttp.ClientSession() as session:
            external_url = f"https://api.botify.com/v1/projects/{org_slug}/{project_slug}/query"
            headers = {
                "Authorization": f"Token {api_token}",
                "Content-Type": "application/json"
            }

            # Build the complete payload with analysis
            payload = dict(query_json)  # Copy the query structure
            payload["analysis"] = analysis_slug

            # Set default size if not specified
            if "size" not in payload:
                payload["size"] = 100

            async with session.post(external_url, headers=headers, json=payload) as response:
                if response.status == 200:
                    query_result = await response.json()

                    # Extract result summary for easier consumption
                    result_summary = {
                        "total_results": len(query_result.get("results", [])),
                        "has_pagination": "next" in query_result,
                        "query_size_requested": payload.get("size", 100)
                    }

                    return {
                        "status": "success",
                        "result": query_result,
                        "result_summary": result_summary,
                        "external_api_url": external_url,
                        "external_api_method": "POST",
                        "external_api_status": response.status,
                        "external_api_payload": payload,
                        "query_info": {
                            "org": org_slug,
                            "project": project_slug,
                            "analysis": analysis_slug,
                            "query_type": "custom_bql"
                        }
                    }
                else:
                    error_text = await response.text()
                    return {
                        "status": "error",
                        "message": f"Custom BQL query failed: {response.status}",
                        "error_details": error_text,
                        "external_api_url": external_url,
                        "external_api_method": "POST",
                        "external_api_status": response.status,
                        "external_api_payload": payload,
                        "query_info": {
                            "org": org_slug,
                            "project": project_slug,
                            "analysis": analysis_slug
                        }
                    }
    except Exception as e:
        return {
            "status": "error",
            "message": f"Network error: {str(e)}",
            "external_api_url": external_url if 'external_url' in locals() else None,
            "external_api_method": "POST",
            "query_info": {
                "org": org_slug,
                "project": project_slug,
                "analysis": analysis_slug
            }
        }


async def browser_interact_with_current_page(params: dict) -> dict:
    """
    MCP Tool: AI INTERACTION - Interact with the current page using /looking_at/ state.

    This tool allows the AI to interact with the current page that's captured
    in /browser_cache/looking_at/ directory. It can click elements, fill forms,
    and perform other interactions based on the current DOM state.

    Args:
        params: {
            "action": "click" | "type" | "submit" | "screenshot" | "navigate",
            "target": {
                "selector_type": "id" | "data-testid" | "xpath" | "css",
                "selector_value": "element-id" | "[data-testid='button']" | "//button[@id='submit']",
                "text_content": "optional text to verify element"
            },
            "input_text": "text to type (for type action)",
            "url": "URL to navigate to (for navigate action)",
            "wait_seconds": 3,
            "update_looking_at": True
        }

    Returns:
        dict: Interaction results with updated page state
    """
    logger.info(f"🔧 FINDER_TOKEN: MCP_BROWSER_INTERACT_START - Action: {params.get('action')}")

    try:
        import json
        import os
        import time
        from datetime import datetime

        from selenium import webdriver
        from selenium.webdriver.chrome.options import Options
        from selenium.webdriver.common.by import By
        from selenium.webdriver.common.keys import Keys
        from selenium.webdriver.support import expected_conditions as EC
        from selenium.webdriver.support.ui import WebDriverWait

        action = params.get('action')
        target = params.get('target', {})
        input_text = params.get('input_text', '')
        url = params.get('url', '')
        wait_seconds = params.get('wait_seconds', 3)
        update_looking_at = params.get('update_looking_at', True)

        if not action:
            return {"success": False, "error": "action parameter is required"}

        # Set up Chrome driver
        chrome_options = Options()
        if action != "screenshot":
            chrome_options.add_argument('--headless')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--window-size=1920,1080')

        driver = webdriver.Chrome(options=chrome_options)

        try:
            # Get current URL from /looking_at/ state
            headers_file = 'browser_cache/looking_at/headers.json'
            current_url = None

            if os.path.exists(headers_file):
                with open(headers_file, 'r') as f:
                    headers_data = json.load(f)
                    current_url = headers_data.get('url')

            if action == "navigate":
                target_url = url or current_url
                if not target_url:
                    return {"success": False, "error": "No URL provided and no current URL in /looking_at/"}

                driver.get(target_url)
                time.sleep(wait_seconds)

                result = {
                    "success": True,
                    "action": "navigate",
                    "url": driver.current_url,
                    "title": driver.title
                }

            elif action == "screenshot":
                if current_url:
                    driver.get(current_url)
                    time.sleep(wait_seconds)

                screenshot_path = 'browser_cache/looking_at/screenshot.png'
                driver.save_screenshot(screenshot_path)

                result = {
                    "success": True,
                    "action": "screenshot",
                    "screenshot_path": screenshot_path,
                    "url": driver.current_url
                }

            elif action in ["click", "type", "submit"]:
                if not current_url:
                    return {"success": False, "error": "No current URL found in /looking_at/. Use browser_scrape_page first."}

                driver.get(current_url)
                time.sleep(wait_seconds)

                # Find the target element
                selector_type = target.get('selector_type', 'id')
                selector_value = target.get('selector_value')

                if not selector_value:
                    return {"success": False, "error": "target.selector_value is required for interaction"}

                # Map selector types to Selenium By types
                by_mapping = {
                    'id': By.ID,
                    'data-testid': By.CSS_SELECTOR,
                    'css': By.CSS_SELECTOR,
                    'xpath': By.XPATH,
                    'name': By.NAME
                }

                if selector_type == 'data-testid':
                    # Convert data-testid to CSS selector
                    selector_value = f'[data-testid="{selector_value}"]'
                    by_type = By.CSS_SELECTOR
                else:
                    by_type = by_mapping.get(selector_type, By.ID)

                try:
                    element = WebDriverWait(driver, 10).until(
                        EC.presence_of_element_located((by_type, selector_value))
                    )

                    if action == "click":
                        element.click()
                        result = {
                            "success": True,
                            "action": "click",
                            "element_found": True,
                            "element_text": element.text[:100] if element.text else ""
                        }

                    elif action == "type":
                        element.clear()
                        element.send_keys(input_text)
                        result = {
                            "success": True,
                            "action": "type",
                            "element_found": True,
                            "text_entered": input_text
                        }

                    elif action == "submit":
                        element.submit()
                        result = {
                            "success": True,
                            "action": "submit",
                            "element_found": True
                        }

                    # Wait for any page changes
                    time.sleep(wait_seconds)

                except Exception as e:
                    result = {
                        "success": False,
                        "action": action,
                        "error": f"Element interaction failed: {str(e)}",
                        "selector_type": selector_type,
                        "selector_value": selector_value
                    }

            else:
                return {"success": False, "error": f"Unknown action: {action}"}

            # Update /looking_at/ state if requested
            if update_looking_at and result.get("success"):
                try:
                    looking_at_dir = 'browser_cache/looking_at'
                    os.makedirs(looking_at_dir, exist_ok=True)

                    # Capture current state
                    page_title = driver.title
                    current_url = driver.current_url
                    source_html = driver.page_source
                    dom_html = driver.execute_script("return document.documentElement.outerHTML;")

                    # Save updated state
                    state_data = {
                        'action_performed': action,
                        'url': current_url,
                        'title': page_title,
                        'timestamp': datetime.now().isoformat()
                    }

                    with open(os.path.join(looking_at_dir, 'headers.json'), 'w') as f:
                        json.dump(state_data, f, indent=2)

                    with open(os.path.join(looking_at_dir, 'source.html'), 'w', encoding='utf-8') as f:
                        f.write(source_html)

                    with open(os.path.join(looking_at_dir, 'dom.html'), 'w', encoding='utf-8') as f:
                        f.write(dom_html)

                    # Create simple DOM
                    try:
                        from bs4 import BeautifulSoup
                        soup = BeautifulSoup(dom_html, 'html.parser')
                        for tag in soup(['script', 'style', 'noscript', 'meta', 'link']):
                            tag.decompose()
                        for element in soup.find_all():
                            attrs_to_keep = {}
                            for attr, value in element.attrs.items():
                                if attr in ['id', 'role', 'data-testid', 'name', 'type', 'href', 'src'] or attr.startswith('aria-'):
                                    attrs_to_keep[attr] = value
                            element.attrs = attrs_to_keep
                        simple_dom_html = str(soup)
                    except:
                        simple_dom_html = dom_html

                    with open(os.path.join(looking_at_dir, 'simple_dom.html'), 'w', encoding='utf-8') as f:
                        f.write(simple_dom_html)

                    logger.info(f"🎯 FINDER_TOKEN: MCP_BROWSER_INTERACTION_UPDATE - Action '{action}' completed, /looking_at/ updated")

                except Exception as e:
                    logger.warning(f"⚠️ FINDER_TOKEN: MCP_BROWSER_INTERACTION_STATE_UPDATE_ERROR - {e}")

            result["current_url"] = driver.current_url
            result["page_title"] = driver.title
            return result

        finally:
            driver.quit()

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: MCP_BROWSER_INTERACT_ERROR - {e}")
        return {"success": False, "error": str(e)}

# All tools are now registered in register_all_mcp_tools() function above
# This ensures proper loading order and centralized registration management

# ================================================================
# AI SELF-DISCOVERY TOOLS - ELIMINATE UNCERTAINTY
# ================================================================


async def ai_self_discovery_assistant(params: dict) -> dict:
    """
    🧠 AI SELF-DISCOVERY ASSISTANT - The uncertainty eliminator.

    This tool provides instant capability awareness and usage patterns for AI assistants.
    It eliminates the "what tools do I have?" and "how do I use them?" uncertainty.

    Args:
        params: {
            "discovery_type": "capabilities" | "usage_patterns" | "success_stories" | "all",
            "include_examples": True,  # Include concrete usage examples
            "include_troubleshooting": True  # Include common failure modes
        }

    Returns:
        dict: Complete AI capability map with usage guidance
    """
    logger.info(f"🧠 FINDER_TOKEN: AI_SELF_DISCOVERY_START - Type: {params.get('discovery_type', 'all')}")

    # --- Ensure MCP_TOOL_REGISTRY is populated, even in direct/REPL/terminal use ---
    global MCP_TOOL_REGISTRY
    try:
        if not MCP_TOOL_REGISTRY or len(MCP_TOOL_REGISTRY) < 10:
            register_all_mcp_tools()
    except Exception as e:
        logger.warning(f'Could not auto-register MCP tools: {e}')
    # -----------------------------------------------------------------------------

    try:
        discovery_type = params.get('discovery_type', 'all')
        include_examples = params.get('include_examples', True)
        include_troubleshooting = params.get('include_troubleshooting', True)

        # Get current MCP tool registry - try multiple sources
        available_tools = []

        # Try to discover tools using the same method as AI_RUNME.py
        try:
            # Get all functions that are MCP tool handlers (test functions and main tools)
            import inspect
            import sys
            mcp_module = sys.modules.get('tools.mcp_tools')
            if mcp_module:
                for name, obj in inspect.getmembers(mcp_module):
                    if (callable(obj) and
                        not name.startswith('__') and
                        ('test_' in name or 'ai_' in name or 'botify_' in name or
                         'browser_' in name or 'ui_' in name or 'local_llm_' in name or
                         'pipeline_' in name or 'execute_' in name)):
                        available_tools.append(name)
        except Exception as e:
            logger.warning(f"Could not discover tools dynamically: {e}")

        # If dynamic discovery failed, fall back to registry methods
        if not available_tools:
            # Try server module first
            server_module = sys.modules.get('server')
            if server_module and hasattr(server_module, 'MCP_TOOL_REGISTRY'):
                available_tools = list(server_module.MCP_TOOL_REGISTRY.keys())
            else:
                # Try main module (when running as __main__)
                main_module = sys.modules.get('__main__')
                if main_module and hasattr(main_module, 'MCP_TOOL_REGISTRY'):
                    available_tools = list(main_module.MCP_TOOL_REGISTRY.keys())
                else:
                    # Fallback to local registry
                    available_tools = list(MCP_TOOL_REGISTRY.keys()) if MCP_TOOL_REGISTRY else []

        # Categorize tools by capability - use actual function names with underscores
        tool_categories = {
            "environment_mastery": [
                "_pipeline_state_inspector",
                "_local_llm_list_files",
                "_local_llm_read_file"
            ],
            "browser_embodiment": [
                "_browser_scrape_page",
                "_browser_analyze_scraped_page",
                "_browser_automate_workflow_walkthrough",
                "_browser_interact_with_current_page"
            ],
            "session_hijacking": [
                "_execute_ai_session_hijacking_demonstration",
                "_pipeline_state_inspector"
            ],
            "external_integration": [
                "_botify_ping",
                "_botify_list_projects",
                "_botify_get_full_schema",
                "_botify_execute_custom_bql_query"
            ],
            "debugging_transparency": [
                "_local_llm_grep_logs",
                "_ui_flash_element",
                "_ui_list_elements"
            ],
            "entertainment": [
                "_builtin_get_cat_fact"
            ]
        }

        # Build capability map
        capabilities = {}
        for category, tools in tool_categories.items():
            available_in_category = [tool for tool in tools if tool in available_tools]
            if available_in_category:
                capabilities[category] = {
                    "tools": available_in_category,
                    "count": len(available_in_category),
                    "description": get_category_description(category)
                }

        # Usage patterns for common AI tasks
        usage_patterns = {
            "web_scraping_workflow": {
                "description": "Complete web scraping with analysis",
                "steps": [
                    "1. browser_scrape_page - Capture page state",
                    "2. browser_analyze_scraped_page - Find automation targets",
                    "3. local_llm_read_file - Examine captured content",
                    "4. ui_flash_element - Guide user attention"
                ],
                "example_params": {
                    "browser_scrape_page": {"url": "https://example.com", "take_screenshot": True},
                    "browser_analyze_scraped_page": {"analysis_type": "all"},
                    "local_llm_read_file": {"file_path": "browser_cache/looking_at/simple_dom.html"},
                    "ui_flash_element": {"element_id": "important-element", "message": "Found key information!"}
                }
            },
            "workflow_debugging": {
                "description": "Debug user workflow issues",
                "steps": [
                    "1. pipeline_state_inspector - Check current state",
                    "2. local_llm_grep_logs - Search for errors",
                    "3. browser_scrape_page - See what user sees",
                    "4. ui_flash_element - Highlight issues"
                ]
            },
            "session_hijacking": {
                "description": "Take over user session seamlessly",
                "steps": [
                    "1. pipeline_state_inspector - Understand current workflow",
                    "2. browser_scrape_page - Capture current state",
                    "3. browser_automate_workflow_walkthrough - Continue workflow",
                    "4. execute_ai_session_hijacking_demonstration - Show capabilities"
                ]
            }
        }

        # Success stories from real usage
        success_stories = {
            "bbc_news_headlines": {
                "task": "Extract current news headlines from BBC",
                "tools_used": ["browser_scrape_page", "local_llm_read_file"],
                "result": "Successfully captured 15+ headlines with timestamps and categories",
                "key_insight": "News sites are more accessible than search engines for automation"
            },
            "google_captcha_detection": {
                "task": "Attempt Google search automation",
                "tools_used": ["browser_scrape_page", "browser_analyze_scraped_page"],
                "result": "Detected CAPTCHA challenge, demonstrated security awareness",
                "key_insight": "Real-world security measures provide valuable learning opportunities"
            },
            "workflow_session_hijacking": {
                "task": "Take over user's Botifython workflow",
                "tools_used": ["pipeline_state_inspector", "browser_scrape_page"],
                "result": "Successfully captured and analyzed user's current workflow state",
                "key_insight": "Complete session visibility enables seamless AI assistance"
            }
        }

        # Common failure modes and solutions
        troubleshooting = {
            "tool_not_found": {
                "symptom": "MCP tool not available in registry",
                "solution": "Check server startup logs, ensure tool is registered in register_all_mcp_tools()",
                "prevention": "Always verify tool availability before attempting usage"
            },
            "browser_cache_failure": {
                "symptom": "Selenium operations fail or timeout",
                "solution": "Check Chrome/Chromedriver installation, increase wait_seconds parameter",
                "prevention": "Use headless mode for reliability, add error handling"
            },
            "file_access_denied": {
                "symptom": "Cannot read files in /looking_at/ directory",
                "solution": "Verify file exists, check permissions, use browser_scrape_page first",
                "prevention": "Always check file existence before attempting to read"
            },
            "api_authentication_failure": {
                "symptom": "Botify API calls return 401/403 errors",
                "solution": "Verify botify_token.txt exists and contains valid token",
                "prevention": "Use botify_ping to test connectivity before complex operations"
            }
        }

        # Build response based on discovery type
        result = {
            "success": True,
            "discovery_type": discovery_type,
            "total_tools_available": len(available_tools),
            "timestamp": datetime.now().isoformat()
        }

        if discovery_type in ["capabilities", "all"]:
            result["capabilities"] = capabilities

        if discovery_type in ["usage_patterns", "all"] and include_examples:
            result["usage_patterns"] = usage_patterns

        if discovery_type in ["success_stories", "all"]:
            result["success_stories"] = success_stories

        if discovery_type in ["troubleshooting", "all"] and include_troubleshooting:
            result["troubleshooting"] = troubleshooting

        # Add quick reference for immediate use
        result["quick_reference"] = {
            "most_powerful_tools": [
                "browser_scrape_page",      # AI eyes
                "browser_analyze_scraped_page",  # AI brain
                "pipeline_state_inspector",  # Workflow awareness
                "execute_ai_session_hijacking_demonstration"  # Session takeover
            ],
            "essential_patterns": [
                "Always check current state before acting",
                "Use browser automation for visual tasks",
                "Leverage FINDER_TOKEN logs for debugging",
                "Provide user guidance with ui_flash_element"
            ]
        }

        logger.info(f"🎯 FINDER_TOKEN: AI_SELF_DISCOVERY_SUCCESS - {len(available_tools)} tools mapped, {len(capabilities)} categories")
        return result

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: AI_SELF_DISCOVERY_ERROR - {e}")
        return {"success": False, "error": str(e)}


def get_category_description(category: str) -> str:
    """Get human-readable description for tool category."""
    descriptions = {
        "environment_mastery": "Understand and control the development environment",
        "browser_embodiment": "See, analyze, and interact with web pages like a human",
        "session_hijacking": "Take over user sessions and continue their workflows",
        "external_integration": "Connect to external APIs and services",
        "debugging_transparency": "Debug issues with complete system visibility",
        "entertainment": "Lightweight tools for engagement and testing"
    }
    return descriptions.get(category, "Unknown category")


async def ai_capability_test_suite(params: dict) -> dict:
    """
    🧪 AI CAPABILITY TEST SUITE - Prove your superpowers.

    This tool runs a comprehensive test of all AI capabilities to verify
    that the system is working correctly and the AI has full access.

    Args:
        params: {
            "test_type": "quick" | "comprehensive" | "specific_tool" | "context_aware",
            "specific_tool": "tool_name"  # Only for specific_tool test
        }

    Returns:
        dict: Test results with success/failure details
    """
    logger.info(f"🧪 FINDER_TOKEN: AI_CAPABILITY_TEST_START - Type: {params.get('test_type', 'quick')}")

    try:
        test_type = params.get('test_type', 'quick')
        specific_tool = params.get('specific_tool')

        test_results = {
            "timestamp": datetime.now().isoformat(),
            "test_type": test_type,
            "tests_run": 0,
            "tests_passed": 0,
            "tests_failed": 0,
            "results": {},
            "context_analysis": {}
        }

        if test_type == "specific_tool" and specific_tool:
            # Test specific tool
            test_results["results"][specific_tool] = await _test_specific_tool(specific_tool)
            test_results["tests_run"] = 1
            test_results["tests_passed"] = 1 if test_results["results"][specific_tool]["success"] else 0
            test_results["tests_failed"] = 1 - test_results["tests_passed"]

        elif test_type == "quick":
            # Quick test of core capabilities (context-aware)
            quick_tests = [
                ("environment_check", test_environment_access),
                ("file_system", test_file_system_access),
                ("mcp_registry", test_mcp_registry_context_aware),
                ("basic_browser", test_basic_browser_capability)
            ]

            for test_name, test_func in quick_tests:
                test_results["tests_run"] += 1
                result = await test_func()
                test_results["results"][test_name] = result
                if result["success"]:
                    test_results["tests_passed"] += 1
                else:
                    test_results["tests_failed"] += 1

        elif test_type == "comprehensive":
            # Comprehensive test of all capabilities (context-aware)
            comprehensive_tests = [
                ("environment_check", test_environment_access),
                ("file_system", test_file_system_access),
                ("mcp_registry", test_mcp_registry_context_aware),
                ("basic_browser", test_basic_browser_capability),
                ("pipeline_inspection", test_pipeline_inspection_context_aware),
                ("log_access", test_log_access),
                ("ui_interaction", test_ui_interaction_context_aware),
                ("botify_connectivity", test_botify_connectivity)
            ]

            for test_name, test_func in comprehensive_tests:
                test_results["tests_run"] += 1
                result = await test_func()
                test_results["results"][test_name] = result
                if result["success"]:
                    test_results["tests_passed"] += 1
                else:
                    test_results["tests_failed"] += 1

        elif test_type == "context_aware":
            # New context-aware test that provides intelligent assessment
            test_results.update(await _run_context_aware_test_suite())
            # Add assessment for context-aware test
            test_results["assessment"] = _generate_context_aware_assessment(test_results)
            return test_results

        # Calculate success rate
        if test_results["tests_run"] > 0:
            test_results["success_rate"] = round((test_results["tests_passed"] / test_results["tests_run"]) * 100, 2)
        else:
            test_results["success_rate"] = 0

        # Add overall assessment with context awareness
        test_results["assessment"] = _generate_context_aware_assessment(test_results)

        logger.info(f"🎯 FINDER_TOKEN: AI_CAPABILITY_TEST_COMPLETE - {test_results['tests_passed']}/{test_results['tests_run']} passed ({test_results['success_rate']}%)")
        return test_results

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: AI_CAPABILITY_TEST_ERROR - {e}")
        return {"success": False, "error": str(e)}


async def _run_context_aware_test_suite() -> dict:
    """Run a context-aware test suite that provides intelligent assessment."""

    test_results = {
        "tests_run": 0,
        "tests_passed": 0,
        "tests_failed": 0,
        "results": {},
        "context_analysis": {},
        "capability_assessment": {}
    }

    # Test 1: Environment and File System (Always available)
    test_results["tests_run"] += 1
    env_result = await test_environment_access()
    test_results["results"]["environment"] = env_result
    if env_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 2: File System Access
    test_results["tests_run"] += 1
    fs_result = await test_file_system_access()
    test_results["results"]["file_system"] = fs_result
    if fs_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 3: MCP Tools Availability (Context-aware)
    test_results["tests_run"] += 1
    mcp_result = await test_mcp_tools_availability()
    test_results["results"]["mcp_tools"] = mcp_result
    if mcp_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 4: Browser Automation (Always available if Selenium installed)
    test_results["tests_run"] += 1
    browser_result = await test_basic_browser_capability()
    test_results["results"]["browser_cache"] = browser_result
    if browser_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 5: Pipeline Functionality (Test actual functionality, not just context)
    test_results["tests_run"] += 1
    pipeline_result = await test_pipeline_functionality()
    test_results["results"]["pipeline_functionality"] = pipeline_result
    if pipeline_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 6: Botify API (Test actual connectivity)
    test_results["tests_run"] += 1
    botify_result = await test_botify_actual_connectivity()
    test_results["results"]["botify_api"] = botify_result
    if botify_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 7: Log Access
    test_results["tests_run"] += 1
    log_result = await test_log_access()
    test_results["results"]["log_access"] = log_result
    if log_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Test 8: UI Interaction (Test if server is running and accessible)
    test_results["tests_run"] += 1
    ui_result = await test_ui_accessibility()
    test_results["results"]["ui_accessibility"] = ui_result
    if ui_result["success"]:
        test_results["tests_passed"] += 1
    else:
        test_results["tests_failed"] += 1

    # Calculate success rate
    if test_results["tests_run"] > 0:
        test_results["success_rate"] = round((test_results["tests_passed"] / test_results["tests_run"]) * 100, 2)
    else:
        test_results["success_rate"] = 0

    # Generate capability assessment
    test_results["capability_assessment"] = _generate_detailed_capability_assessment(test_results)

    return test_results


async def test_mcp_tools_availability() -> dict:
    """Test MCP tools availability with context awareness."""
    try:
        # Test if we can import and access MCP tools directly
        try:

            # Test a simple tool call
            result = await builtin_get_cat_fact({})
            if result.get("status") == "success" or result.get("fact"):
                return {
                    "success": True,
                    "mcp_tools_accessible": True,
                    "direct_access": True,
                    "test_result": "MCP tools accessible via direct import"
                }
        except ImportError:
            pass

        # Fallback: Check if tools are available through server context
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'MCP_TOOL_REGISTRY'):
            tool_count = len(server_module.MCP_TOOL_REGISTRY)
            return {
                "success": True,
                "mcp_tools_accessible": True,
                "server_context": True,
                "tool_count": tool_count,
                "test_result": f"MCP tools accessible via server context ({tool_count} tools)"
            }

        # Final fallback: Check if we're in the right environment
        if os.path.exists("tools/mcp_tools.py") and os.path.exists("server.py"):
            return {
                "success": True,
                "mcp_tools_accessible": True,
                "environment_ready": True,
                "test_result": "MCP tools environment ready (tools should work in server context)"
            }

        return {"success": False, "error": "MCP tools not accessible in any context"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_pipeline_functionality() -> dict:
    """Test actual pipeline functionality, not just context availability."""
    try:
        # Test if we can access the database directly
        try:
            from pathlib import Path

            from fastlite import database
            Path('data').mkdir(parents=True, exist_ok=True)
            db = database('data/data.db')

            # Test if pipeline table exists and is accessible
            if hasattr(db, 'pipeline'):
                pipeline_count = len(list(db.pipeline()))
                return {
                    "success": True,
                    "pipeline_functional": True,
                    "database_accessible": True,
                    "pipeline_count": pipeline_count,
                    "test_result": f"Pipeline database accessible with {pipeline_count} records"
                }
        except Exception as db_error:
            pass

        # Fallback: Test if we can use the pipeline inspector tool
        try:
            result = await _pipeline_state_inspector({'format': 'summary'})
            if result.get("success"):
                return {
                    "success": True,
                    "pipeline_functional": True,
                    "inspector_working": True,
                    "test_result": "Pipeline inspector tool working"
                }
        except Exception as inspector_error:
            pass

        # Final fallback: Check if pipeline files exist
        if os.path.exists("data/") and (os.path.exists("data/data.db") or os.path.exists("data/botifython_dev.db")):
            return {
                "success": True,
                "pipeline_functional": True,
                "files_exist": True,
                "test_result": "Pipeline files exist (should work in server context)"
            }

        return {"success": False, "error": "Pipeline functionality not accessible"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_botify_actual_connectivity() -> dict:
    """Test actual Botify API connectivity."""
    try:
        # First check if token is available
        token = _read_botify_api_token()
        if not token:
            return {
                "success": False,
                "error": "Botify API token not available",
                "suggestion": "Configure Botify API token in helpers/botify/botify_token.txt"
            }

        # Test actual API call
        try:
            result = await _botify_ping({})
            if result.get("success"):
                return {
                    "success": True,
                    "botify_connected": True,
                    "api_responding": True,
                    "test_result": "Botify API responding successfully"
                }
            else:
                # Analyze the error to provide better context
                error_msg = result.get("message", "Unknown error")
                status = result.get("external_api_status", "Unknown")

                if "404" in str(status) or "404" in error_msg:
                    return {
                        "success": False,
                        "error": "Botify API endpoint not found (404) - token may be expired or API changed",
                        "token_available": True,
                        "suggestion": "Check Botify API documentation for endpoint changes or renew token"
                    }
                elif "401" in str(status) or "401" in error_msg:
                    return {
                        "success": False,
                        "error": "Botify API authentication failed (401) - token may be invalid",
                        "token_available": True,
                        "suggestion": "Verify Botify API token is correct and not expired"
                    }
                else:
                    return {
                        "success": False,
                        "error": f"Botify API error: {error_msg} (Status: {status})",
                        "token_available": True,
                        "suggestion": "Check Botify API status and token validity"
                    }
        except Exception as api_error:
            return {
                "success": False,
                "error": f"Botify API call failed: {str(api_error)}",
                "token_available": True,
                "suggestion": "Check network connectivity and Botify API availability"
            }

    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_ui_accessibility() -> dict:
    """Test if the UI is accessible via HTTP."""
    try:
        import asyncio

        import aiohttp

        # Test if server is running and accessible
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get('http://localhost:5001/', timeout=5) as response:
                    if response.status == 200:
                        return {
                            "success": True,
                            "ui_accessible": True,
                            "server_running": True,
                            "test_result": "UI accessible via HTTP on port 5001"
                        }
                    else:
                        return {
                            "success": False,
                            "error": f"Server responding but status {response.status}",
                            "server_running": True
                        }
        except asyncio.TimeoutError:
            return {
                "success": False,
                "error": "Server timeout - may not be running",
                "suggestion": "Start server with 'python server.py'"
            }
        except Exception as http_error:
            return {
                "success": False,
                "error": f"HTTP connection failed: {str(http_error)}",
                "suggestion": "Check if server is running on port 5001"
            }

    except Exception as e:
        return {"success": False, "error": str(e)}


def _generate_context_aware_assessment(test_results: dict) -> str:
    """Generate context-aware assessment based on test results."""
    success_rate = test_results.get("success_rate", 0)

    # Analyze context-specific failures
    context_failures = []
    for test_name, result in test_results.get("results", {}).items():
        if not result.get("success") and "context" in result.get("error", "").lower():
            context_failures.append(test_name)

    if success_rate >= 90:
        return "🎯 EXCELLENT - AI superpowers fully operational"
    elif success_rate >= 75:
        if context_failures:
            return f"✅ GOOD - Core capabilities working. Context-dependent features require server context ({', '.join(context_failures)})"
        else:
            return "✅ GOOD - Most capabilities working, minor issues detected"
    elif success_rate >= 50:
        if context_failures:
            return f"⚠️ FAIR - Core capabilities working. Some features require server context ({', '.join(context_failures)})"
        else:
            return "⚠️ FAIR - Some capabilities working, needs attention"
    else:
        return "❌ POOR - Significant issues detected, requires investigation"


def _generate_detailed_capability_assessment(test_results: dict) -> dict:
    """Generate detailed capability assessment."""
    assessment = {
        "core_capabilities": {},
        "context_dependent": {},
        "recommendations": []
    }

    for test_name, result in test_results.get("results", {}).items():
        if result.get("success"):
            if "context" in test_name.lower() or "server" in result.get("test_result", "").lower():
                assessment["context_dependent"][test_name] = "✅ Working (requires server context)"
            else:
                assessment["core_capabilities"][test_name] = "✅ Working"
        else:
            error = result.get("error", "Unknown error")
            if "context" in error.lower() or "server" in error.lower():
                assessment["context_dependent"][test_name] = f"⚠️ Context-dependent ({error})"
                assessment["recommendations"].append(f"Use {test_name} through server context")
            else:
                assessment["core_capabilities"][test_name] = f"❌ Failed ({error})"

    return assessment


async def test_mcp_registry_context_aware() -> dict:
    """Context-aware MCP registry test."""
    return await test_mcp_tools_availability()


async def test_pipeline_inspection_context_aware() -> dict:
    """Context-aware pipeline inspection test."""
    return await test_pipeline_functionality()


async def test_ui_interaction_context_aware() -> dict:
    """Context-aware UI interaction test."""
    return await test_ui_accessibility()


async def test_environment_access() -> dict:
    """Test basic environment access."""
    try:
        import os
        current_dir = os.getcwd()
        server_exists = os.path.exists("server.py")
        apps_exists = os.path.exists("apps/")

        return {
            "success": True,
            "current_directory": current_dir,
            "server_py_exists": server_exists,
            "apps_directory_exists": apps_exists,
            "environment_ready": server_exists and apps_exists
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_file_system_access() -> dict:
    """Test file system access capabilities."""
    try:
        import os
        from pathlib import Path

        # Test reading a simple file
        test_file = "tools/mcp_tools.py"
        if os.path.exists(test_file):
            with open(test_file, 'r') as f:
                content = f.read(100)  # Just first 100 chars
            return {
                "success": True,
                "file_read_success": True,
                "test_file": test_file,
                "content_preview": content[:50] + "..."
            }
        else:
            return {"success": False, "error": f"Test file {test_file} not found"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_mcp_registry() -> dict:
    """Test MCP tool registry access."""
    try:
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'MCP_TOOL_REGISTRY'):
            tool_count = len(server_module.MCP_TOOL_REGISTRY)
            return {
                "success": True,
                "registry_accessible": True,
                "tool_count": tool_count,
                "registry_ready": tool_count > 0
            }
        else:
            return {"success": False, "error": "MCP tool registry not accessible"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_basic_browser_capability() -> dict:
    """Test basic browser automation capability."""
    try:
        # Test if Selenium is available
        try:
            from selenium import webdriver
            from selenium.webdriver.chrome.options import Options
            return {
                "success": True,
                "selenium_available": True,
                "webdriver_import_success": True
            }
        except ImportError:
            return {"success": False, "error": "Selenium not available"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_pipeline_inspection() -> dict:
    """Test pipeline inspection capability."""
    try:
        # Test if we can access the pipeline table
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'pipeline'):
            return {
                "success": True,
                "pipeline_table_accessible": True
            }
        else:
            return {"success": False, "error": "Pipeline table not accessible"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_log_access() -> dict:
    """Test log file access capability."""
    try:
        import os
        log_file = "logs/server.log"
        if os.path.exists(log_file):
            with open(log_file, 'r') as f:
                lines = f.readlines()
            return {
                "success": True,
                "log_file_accessible": True,
                "log_lines_count": len(lines)
            }
        else:
            return {"success": False, "error": "Log file not found"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_ui_interaction() -> dict:
    """Test UI interaction capability."""
    try:
        # Test if we can access the chat system
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'chat'):
            return {
                "success": True,
                "chat_system_accessible": True
            }
        else:
            return {"success": False, "error": "Chat system not accessible"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_botify_connectivity() -> dict:
    """Test Botify API connectivity."""
    try:
        # Test if we can read the token file
        token_file = "helpers/botify/botify_token.txt"
        if os.path.exists(token_file):
            with open(token_file, 'r') as f:
                token = f.read().strip()
            return {
                "success": True,
                "token_file_exists": True,
                "token_available": bool(token)
            }
        else:
            return {"success": False, "error": "Botify token file not found"}
    except Exception as e:
        return {"success": False, "error": str(e)}


async def test_specific_tool(tool_name: str) -> dict:
    """Test a specific MCP tool."""
    try:
        import sys
        server_module = sys.modules.get('server')
        if server_module and hasattr(server_module, 'MCP_TOOL_REGISTRY'):
            registry = server_module.MCP_TOOL_REGISTRY
            if tool_name in registry:
                return {
                    "success": True,
                    "tool_found": True,
                    "tool_available": True
                }
            else:
                return {"success": False, "error": f"Tool {tool_name} not found in registry"}
        else:
            return {"success": False, "error": "MCP tool registry not accessible"}
    except Exception as e:
        return {"success": False, "error": str(e)}

# Register the new AI self-discovery tools
# register_mcp_tool("ai_self_discovery_assistant", ai_self_discovery_assistant)
# register_mcp_tool("ai_capability_test_suite", ai_capability_test_suite)


async def browser_automate_instructions(params: dict) -> dict:
    """
    MCP Tool: AI HANDS - Natural Language Browser Automation

    This tool parses natural language instructions and converts them into browser automation actions.
    It provides a user-friendly interface for browser automation without requiring technical knowledge.

    Args:
        params: {
            "instructions": "click search input and type hello world",
            "target_url": "http://localhost:5001",
            "wait_seconds": 3
        }

    Returns:
        dict: Automation results with success rate and feedback
    """
    logger.info(f"🤖 FINDER_TOKEN: INSTRUCTION_AUTOMATION_START | Instructions: {params.get('instructions')}")

    try:
        import re
        import time
        from pathlib import Path

        instructions = params.get('instructions', '')
        target_url = params.get('target_url', 'http://localhost:5001')
        wait_seconds = params.get('wait_seconds', 3)

        if not instructions:
            return {
                'success': False,
                'error': 'No instructions provided',
                'suggestions': ['Try: "click search input and type hello world"', 'Try: "navigate to dashboard"', 'Try: "click login button"']
            }

        # First, capture the current page state
        scrape_result = await browser_scrape_page({
            'url': target_url,
            'wait_seconds': 2,
            'take_screenshot': True
        })

        if not scrape_result.get('success'):
            return {
                'success': False,
                'error': f'Failed to capture page state: {scrape_result.get("error")}',
                'suggestions': ['Check if the target URL is accessible', 'Verify the server is running']
            }

        # Parse instructions into actions
        actions = []
        instruction_lower = instructions.lower()

        # Simple instruction parsing
        if 'click' in instruction_lower:
            # Extract what to click
            click_match = re.search(r'click\s+([^,\s]+(?:\s+[^,\s]+)*)', instruction_lower)
            if click_match:
                click_target = click_match.group(1)
                if 'search' in click_target and 'input' in click_target:
                    actions.append({
                        'action': 'click',
                        'target': {'selector_type': 'id', 'selector_value': 'nav-plugin-search'},
                        'description': f'Click search input'
                    })
                elif 'button' in click_target:
                    actions.append({
                        'action': 'click',
                        'target': {'selector_type': 'css', 'selector_value': 'button'},
                        'description': f'Click {click_target}'
                    })
                else:
                    actions.append({
                        'action': 'click',
                        'target': {'selector_type': 'css', 'selector_value': f'[data-testid*="{click_target}"]'},
                        'description': f'Click {click_target}'
                    })

        if 'type' in instruction_lower:
            # Extract what to type
            type_match = re.search(r'type\s+([^,\s]+(?:\s+[^,\s]+)*)', instruction_lower)
            if type_match:
                text_to_type = type_match.group(1)
                # Find the input element to type into
                if 'search' in instruction_lower:
                    actions.append({
                        'action': 'type',
                        'target': {'selector_type': 'id', 'selector_value': 'nav-plugin-search'},
                        'input_text': text_to_type,
                        'description': f'Type "{text_to_type}" in search input'
                    })
                else:
                    actions.append({
                        'action': 'type',
                        'target': {'selector_type': 'css', 'selector_value': 'input'},
                        'input_text': text_to_type,
                        'description': f'Type "{text_to_type}" in input field'
                    })

        if 'wait' in instruction_lower:
            # Extract wait time
            wait_match = re.search(r'wait\s+(\d+)\s*seconds?', instruction_lower)
            if wait_match:
                wait_time = int(wait_match.group(1))
                actions.append({
                    'action': 'wait',
                    'wait_seconds': wait_time,
                    'description': f'Wait {wait_time} seconds'
                })

        if not actions:
            return {
                'success': False,
                'error': 'Could not parse any actionable instructions',
                'suggestions': [
                    'Use action verbs: click, type, wait, navigate',
                    'Be specific: "click search input", "type hello world"',
                    'Try: "click search input and type hello world"'
                ]
            }

        # Execute actions
        action_results = []
        successful_actions = 0

        for i, action in enumerate(actions):
            logger.info(f"🎯 FINDER_TOKEN: INSTRUCTION_ACTION_{i + 1} | {action['description']}")

            if action['action'] == 'wait':
                time.sleep(action['wait_seconds'])
                result = {'success': True, 'action': 'wait', 'description': action['description']}
            else:
                # Use the existing interaction function
                result = await browser_interact_with_current_page({
                    'action': action['action'],
                    'target': action.get('target'),
                    'input_text': action.get('input_text', ''),
                    'wait_seconds': wait_seconds
                })
                result['description'] = action['description']

            action_results.append(result)

            if result.get('success'):
                successful_actions += 1
                logger.info(f"✅ FINDER_TOKEN: INSTRUCTION_SUCCESS | {action['description']}")
            else:
                logger.warning(f"⚠️ FINDER_TOKEN: INSTRUCTION_FAILURE | {action['description']}: {result.get('error')}")

        # Take final screenshot
        final_screenshot = await browser_interact_with_current_page({
            'action': 'screenshot',
            'wait_seconds': 1
        })

        # Calculate success rate
        success_rate = (successful_actions / len(actions)) * 100 if actions else 0

        return {
            'success': success_rate > 50,  # More than 50% success
            'success_rate': round(success_rate, 1),
            'total_actions': len(actions),
            'successful_actions': successful_actions,
            'failed_actions': len(actions) - successful_actions,
            'action_results': action_results,
            'final_screenshot': final_screenshot.get('screenshot_path') if final_screenshot.get('success') else None,
            'instructions_parsed': instructions,
            'improvement_suggestions': [
                'Use specific element names: "search input", "login button"',
                'Combine actions: "click search and type hello"',
                'Add wait times: "wait 2 seconds"'
            ] if success_rate < 100 else ['All actions completed successfully!']
        }

    except Exception as e:
        logger.error(f"❌ FINDER_TOKEN: INSTRUCTION_AUTOMATION_ERROR | {e}")
        return {
            'success': False,
            'error': f'Instruction automation error: {str(e)}',
            'suggestions': [
                'Check if the target URL is accessible',
                'Verify browser automation is working',
                'Try simpler instructions first'
            ]
        }


# Register the new browser automation tool
# register_mcp_tool("browser_automate_instructions", browser_automate_instructions)


def get_available_tools():
    """
    Returns a list of all callable MCP tools, their function names, signatures, and docstrings.
    """
    tools = []
    public_tool_names = [
        'get_cat_fact',
        'pipeline_state_inspector',
        'get_user_session_state',
        'botify_ping',
        'botify_list_projects',
        'botify_simple_query',
        'local_llm_read_file',
        'local_llm_grep_logs',
        'local_llm_list_files',
        'local_llm_get_context',
        'execute_ai_session_hijacking_demonstration',
        'ui_flash_element',
        'ui_list_elements',
        'voice_synthesis',
        'browser_analyze_scraped_page',
        'browser_scrape_page',
        'browser_automate_workflow_walkthrough',
        'browser_interact_with_current_page',
        'selenium_automation',
        'execute_automation_recipe',
        'execute_mcp_cli_command',
        'botify_get_full_schema',
        'botify_list_available_analyses',
        'botify_execute_custom_bql_query',
        'keychain_set',
        'keychain_get',
        'keychain_delete',
        'keychain_list_keys',
        'keychain_get_all',
        'ai_self_discovery_assistant',
        'ai_capability_test_suite',
        'browser_automate_instructions',
        'execute_complete_session_hijacking',
    ]
    for name in public_tool_names:
        func = globals().get(name)
        if func:
            sig = str(inspect.signature(func))
            doc = inspect.getdoc(func)
            tools.append({
                'name': name,
                'signature': sig,
                'doc': doc
            })
    return tools
