"""
MCP Server for Local Deep Research.

This module provides an MCP (Model Context Protocol) server that exposes
LDR's research capabilities to AI agents like Claude.

Security Notice:
    This server is designed for LOCAL USE ONLY via STDIO transport
    (e.g., Claude Desktop). It has no built-in authentication or rate
    limiting. Do NOT expose this server over a network without implementing
    proper security controls (OAuth, rate limiting, input validation).

    When running locally via STDIO, security is provided by your operating
    system's user permissions.

Tools:
    - quick_research: Fast research summary (1-5 min)
    - detailed_research: Comprehensive analysis (5-15 min)
    - generate_report: Full markdown report (10-30 min)
    - analyze_documents: Search local document collection (30s-2 min)
    - search: Raw search results without LLM processing (5-30s)
    - list_search_engines: List available search engines
    - list_strategies: List available research strategies
    - get_configuration: Get current server configuration

Usage:
    python -m local_deep_research.mcp
    # or
    ldr-mcp
"""

import re
import sys
from typing import Any, Dict, Optional

from loguru import logger
from mcp.server.fastmcp import FastMCP

from local_deep_research.api.research_functions import (
    analyze_documents as ldr_analyze_documents,
    detailed_research as ldr_detailed_research,
    generate_report as ldr_generate_report,
    quick_summary as ldr_quick_summary,
)
from local_deep_research.api.settings_utils import create_settings_snapshot
from local_deep_research.search_system_factory import (
    get_available_strategies,
)

# Create FastMCP server instance
mcp = FastMCP(
    "local-deep-research",
    instructions="AI-powered deep research assistant with iterative analysis using LLMs and web searches",
)


def _classify_error(error_msg: str) -> str:
    """Classify error for client handling."""
    error_lower = error_msg.lower()
    if "503" in error_msg or "unavailable" in error_lower:
        return "service_unavailable"
    if "404" in error_msg or "not found" in error_lower:
        return "model_not_found"
    if (
        "api key" in error_lower
        or "authentication" in error_lower
        or "unauthorized" in error_lower
        or "401" in error_msg
    ):
        return "auth_error"
    if "timeout" in error_lower or "timed out" in error_lower:
        return "timeout"
    if "rate limit" in error_lower or "429" in error_msg:
        return "rate_limit"
    if "connection" in error_lower:
        return "connection_error"
    if "validation" in error_lower or "invalid" in error_lower:
        return "validation_error"
    return "unknown"


class ValidationError(Exception):
    """Raised when parameter validation fails."""

    pass


_COLLECTION_NAME_RE = re.compile(r"^[A-Za-z0-9 _-]{1,100}$")


def _validate_query(query: str) -> str:
    """Validate and sanitize query parameter."""
    if not query or not query.strip():
        raise ValidationError("Query cannot be empty")
    query = query.strip()
    if len(query) > 10000:
        raise ValidationError(
            "Query exceeds maximum length of 10000 characters"
        )
    return query


def _validate_iterations(
    iterations: Optional[int], max_val: int = 20
) -> Optional[int]:
    """Validate iterations parameter."""
    if iterations is None:
        return None
    if not isinstance(iterations, int) or iterations < 1:
        raise ValidationError("Iterations must be a positive integer")
    if iterations > max_val:
        raise ValidationError(f"Iterations cannot exceed {max_val}")
    return iterations


def _validate_questions_per_iteration(qpi: Optional[int]) -> Optional[int]:
    """Validate questions_per_iteration parameter."""
    if qpi is None:
        return None
    if not isinstance(qpi, int) or qpi < 1:
        raise ValidationError(
            "Questions per iteration must be a positive integer"
        )
    if qpi > 10:
        raise ValidationError("Questions per iteration cannot exceed 10")
    return qpi


def _validate_max_results(max_results: int) -> int:
    """Validate max_results parameter."""
    if not isinstance(max_results, int) or max_results < 1:
        raise ValidationError("Max results must be a positive integer")
    if max_results > 100:
        raise ValidationError("Max results cannot exceed 100")
    return max_results


def _validate_temperature(temperature: Optional[float]) -> Optional[float]:
    """Validate temperature parameter."""
    if temperature is None:
        return None
    if not isinstance(temperature, (int, float)):
        raise ValidationError("Temperature must be a number")
    if temperature < 0.0 or temperature > 2.0:
        raise ValidationError("Temperature must be between 0.0 and 2.0")
    return float(temperature)


def _validate_search_engine(engine: Optional[str]) -> Optional[str]:
    """Validate search engine name against available engines."""
    if engine is None:
        return None
    engine = engine.strip()
    if not engine:
        return None
    try:
        from local_deep_research.web_search_engines.search_engines_config import (
            search_config,
        )

        settings = create_settings_snapshot()
        available = search_config(settings_snapshot=settings)
        if engine not in available:
            available_names = sorted(available.keys())
            raise ValidationError(  # noqa: TRY301
                f"Unknown search engine '{engine}'. Available: {', '.join(available_names)}"
            )
    except ValidationError:
        raise
    except Exception:
        logger.exception("Could not load engine config to validate engine")
        raise ValidationError(
            f"Cannot validate search engine '{engine}': engine configuration unavailable"
        )
    return engine


def _validate_strategy(strategy: Optional[str]) -> Optional[str]:
    """Validate strategy name against available strategies."""
    if strategy is None:
        return None
    strategy = strategy.strip()
    if not strategy:
        return None
    available = get_available_strategies(show_all=True)
    available_names = [s["name"] for s in available]
    if strategy not in available_names:
        raise ValidationError(
            f"Unknown strategy '{strategy}'. Available: {', '.join(available_names)}"
        )
    return strategy


def _build_settings_overrides(
    search_engine: Optional[str] = None,
    strategy: Optional[str] = None,
    iterations: Optional[int] = None,
    questions_per_iteration: Optional[int] = None,
    temperature: Optional[float] = None,
) -> Dict[str, Any]:
    """Build settings overrides dict from tool parameters."""
    overrides: dict[str, Any] = {}
    if search_engine is not None:
        search_engine = _validate_search_engine(search_engine)
        if search_engine:
            overrides["search.tool"] = search_engine
    if strategy is not None:
        strategy = _validate_strategy(strategy)
        if strategy:
            overrides["search.search_strategy"] = strategy
    if iterations is not None:
        overrides["search.iterations"] = iterations
    if questions_per_iteration is not None:
        overrides["search.questions_per_iteration"] = questions_per_iteration
    if temperature is not None:
        overrides["llm.temperature"] = temperature
    return overrides


# =============================================================================
# Research Tools
# =============================================================================


@mcp.tool()
def quick_research(
    query: str,
    search_engine: Optional[str] = None,
    strategy: Optional[str] = None,
    iterations: Optional[int] = None,
    questions_per_iteration: Optional[int] = None,
) -> Dict[str, Any]:
    """
    Perform quick research on a topic.

    This tool performs a fast research summary on the given query. It searches
    the web, analyzes sources, and generates a concise summary with findings.

    IMPORTANT: This is a synchronous operation that typically takes 1-5 minutes
    to complete depending on the complexity and configuration.

    Args:
        query: The research question or topic to investigate.
        search_engine: Search engine to use (e.g., "wikipedia", "arxiv", "searxng", "auto").
                      Use list_search_engines() to see available options.
        strategy: Research strategy to use (e.g., "source-based", "rapid", "iterative").
                 Use list_strategies() to see available options.
        iterations: Number of search iterations (1-10). More iterations = deeper research.
        questions_per_iteration: Questions to generate per iteration (1-5).

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - summary: The research summary text
        - findings: List of detailed findings from each search
        - sources: List of source URLs discovered
        - iterations: Number of iterations performed
        - error: Error message (only if status is "error")
        - error_type: Error classification (only if status is "error")
    """
    try:
        # Validate parameters
        query = _validate_query(query)
        iterations = _validate_iterations(iterations, max_val=10)
        questions_per_iteration = _validate_questions_per_iteration(
            questions_per_iteration
        )

        logger.info(f"Starting quick research for query: {query[:100]}...")

        overrides = _build_settings_overrides(
            search_engine=search_engine,
            strategy=strategy,
            iterations=iterations,
            questions_per_iteration=questions_per_iteration,
        )

        settings = (
            create_settings_snapshot(overrides=overrides)
            if overrides
            else create_settings_snapshot()
        )

        result = ldr_quick_summary(query, settings_snapshot=settings)

        return {
            "status": "success",
            "summary": result.get("summary", ""),
            "findings": result.get("findings", []),
            "sources": result.get("sources", []),
            "iterations": result.get("iterations", 0),
            "formatted_findings": result.get("formatted_findings", ""),
        }

    except ValidationError as e:
        logger.warning("Validation failed for quick research")
        return {
            "status": "error",
            "error": str(e),
            "error_type": "validation_error",
        }
    except Exception as e:
        logger.exception(
            f"Quick research failed for query: {query[:100] if query else 'empty'}"
        )
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Quick research failed ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def detailed_research(
    query: str,
    search_engine: Optional[str] = None,
    strategy: Optional[str] = None,
    iterations: Optional[int] = None,
    questions_per_iteration: Optional[int] = None,
) -> Dict[str, Any]:
    """
    Perform detailed research with comprehensive analysis.

    This tool performs a thorough research analysis on the given query, returning
    structured data with detailed findings, sources, and metadata.

    IMPORTANT: This is a synchronous operation that typically takes 5-15 minutes
    to complete depending on the complexity and configuration.

    Args:
        query: The research question or topic to investigate.
        search_engine: Search engine to use (e.g., "wikipedia", "arxiv", "searxng", "auto").
        strategy: Research strategy to use (e.g., "source-based", "iterative", "evidence").
        iterations: Number of search iterations (1-10). More iterations = deeper research.
        questions_per_iteration: Questions to generate per iteration (1-5).

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - query: The original query
        - research_id: Unique identifier for this research
        - summary: The research summary text
        - findings: List of detailed findings
        - sources: List of source URLs
        - iterations: Number of iterations performed
        - metadata: Additional metadata (timestamp, search_tool, strategy)
        - error/error_type: Error info (only if status is "error")
    """
    try:
        # Validate parameters
        query = _validate_query(query)
        iterations = _validate_iterations(iterations, max_val=20)
        questions_per_iteration = _validate_questions_per_iteration(
            questions_per_iteration
        )

        logger.info(f"Starting detailed research for query: {query[:100]}...")

        overrides = _build_settings_overrides(
            search_engine=search_engine,
            strategy=strategy,
            iterations=iterations,
            questions_per_iteration=questions_per_iteration,
        )

        settings = (
            create_settings_snapshot(overrides=overrides)
            if overrides
            else create_settings_snapshot()
        )

        result = ldr_detailed_research(query, settings_snapshot=settings)

        return {
            "status": "success",
            "query": result.get("query", query),
            "research_id": result.get("research_id", ""),
            "summary": result.get("summary", ""),
            "findings": result.get("findings", []),
            "sources": result.get("sources", []),
            "iterations": result.get("iterations", 0),
            "formatted_findings": result.get("formatted_findings", ""),
            "metadata": result.get("metadata", {}),
        }

    except ValidationError as e:
        logger.warning("Validation failed for detailed research")
        return {
            "status": "error",
            "error": str(e),
            "error_type": "validation_error",
        }
    except Exception as e:
        logger.exception(
            f"Detailed research failed for query: {query[:100] if query else 'empty'}"
        )
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Detailed research failed ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def generate_report(
    query: str,
    search_engine: Optional[str] = None,
    searches_per_section: int = 2,
) -> Dict[str, Any]:
    """
    Generate a comprehensive markdown research report.

    This tool generates a full structured research report with sections,
    citations, and comprehensive analysis. The output is formatted as markdown.

    IMPORTANT: This is a synchronous operation that typically takes 10-30 minutes
    to complete due to the comprehensive nature of the report.

    Args:
        query: The research question or topic for the report.
        search_engine: Search engine to use (e.g., "wikipedia", "arxiv", "searxng", "auto").
        searches_per_section: Number of searches per report section (1-10). Default is 2.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - content: The full report content in markdown format
        - metadata: Report metadata (timestamp, query)
        - error/error_type: Error info (only if status is "error")
    """
    try:
        # Validate parameters
        query = _validate_query(query)
        if (
            not isinstance(searches_per_section, int)
            or searches_per_section < 1
        ):
            raise ValidationError(  # noqa: TRY301
                "Searches per section must be a positive integer"
            )
        if searches_per_section > 10:
            raise ValidationError("Searches per section cannot exceed 10")  # noqa: TRY301

        logger.info(f"Starting report generation for query: {query[:100]}...")

        overrides = {}
        if search_engine:
            search_engine = _validate_search_engine(search_engine)
            if search_engine:
                overrides["search.tool"] = search_engine

        settings = (
            create_settings_snapshot(overrides=overrides)
            if overrides
            else create_settings_snapshot()
        )

        result = ldr_generate_report(
            query,
            settings_snapshot=settings,
            searches_per_section=searches_per_section,
        )

        return {
            "status": "success",
            "content": result.get("content", ""),
            "metadata": result.get("metadata", {}),
        }

    except ValidationError as e:
        logger.warning("Validation failed for report generation")
        return {
            "status": "error",
            "error": str(e),
            "error_type": "validation_error",
        }
    except Exception as e:
        logger.exception(
            f"Report generation failed for query: {query[:100] if query else 'empty'}"
        )
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Report generation failed ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def analyze_documents(
    query: str,
    collection_name: str,
    max_results: int = 10,
) -> Dict[str, Any]:
    """
    Search and analyze documents in a local collection.

    This tool performs RAG (Retrieval Augmented Generation) search on a
    local document collection and generates a summary of relevant findings.

    Args:
        query: The search query for the documents.
        collection_name: Name of the local document collection to search.
        max_results: Maximum number of documents to retrieve (1-100). Default is 10.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - summary: Summary of findings from the documents
        - documents: List of matching documents with content and metadata
        - collection: Name of the collection searched
        - document_count: Number of documents found
        - error/error_type: Error info (only if status is "error")
    """
    try:
        # Validate parameters
        query = _validate_query(query)
        if not collection_name or not collection_name.strip():
            raise ValidationError("Collection name cannot be empty")  # noqa: TRY301
        collection_name = collection_name.strip()
        if not _COLLECTION_NAME_RE.match(collection_name):
            raise ValidationError(  # noqa: TRY301
                "Collection name may only contain letters, digits, spaces, hyphens, and underscores (max 100 chars)"
            )
        max_results = _validate_max_results(max_results)

        logger.info(
            f"Analyzing documents in '{collection_name}' for query: {query[:100]}..."
        )

        # Build a settings snapshot the same way the other MCP tools do.
        # Without this, analyze_documents falls back to JSON defaults +
        # LDR_* env vars and silently ignores user-configured providers,
        # API keys, and embedding model. Mirrors quick_research (line 278).
        settings = create_settings_snapshot()

        result = ldr_analyze_documents(
            query=query,
            collection_name=collection_name,
            max_results=max_results,
            settings_snapshot=settings,
        )

        return {
            "status": "success",
            "summary": result.get("summary", ""),
            "documents": result.get("documents", []),
            "collection": result.get("collection", collection_name),
            "document_count": result.get("document_count", 0),
        }

    except ValidationError as e:
        logger.warning("Validation failed for document analysis")
        return {
            "status": "error",
            "error": str(e),
            "error_type": "validation_error",
        }
    except Exception as e:
        logger.exception(
            f"Document analysis failed for collection: {collection_name if collection_name else 'empty'}"
        )
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Document analysis failed ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def search(
    query: str,
    engine: str,
    max_results: int = 10,
) -> Dict[str, Any]:
    """
    Search using a specific engine and return raw results without LLM processing.

    This tool performs a direct search query against the specified engine and
    returns raw results (title, link, snippet). No LLM is involved, making it
    fast and free of LLM costs.

    IMPORTANT: This is a fast operation, typically completing in 5-30 seconds.

    Args:
        query: The search query string.
        engine: Search engine to use (e.g., "arxiv", "wikipedia", "searxng", "brave").
                This is required — use list_search_engines() to see available options.
        max_results: Maximum number of results to return (1-100). Default is 10.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - query: The original query
        - engine: The engine used
        - result_count: Number of results returned
        - results: List of results, each with title, link, and snippet
        - error/error_type: Error info (only if status is "error")
    """
    try:
        # Validate parameters
        query = _validate_query(query)
        max_results = _validate_max_results(max_results)

        # Validate engine is non-empty (required parameter)
        if not engine or not engine.strip():
            raise ValidationError(  # noqa: TRY301
                "Engine name cannot be empty. Use list_search_engines() to see available options."
            )
        engine = engine.strip()

        # Create settings snapshot (reused for all steps)
        settings = create_settings_snapshot()

        # Validate engine name against available engines
        from local_deep_research.web_search_engines.search_engines_config import (
            search_config,
        )

        engines_config = search_config(settings_snapshot=settings)
        if engine not in engines_config:
            available_names = sorted(engines_config.keys())
            raise ValidationError(  # noqa: TRY301
                f"Unknown search engine '{engine}'. Available: {', '.join(available_names)}"
            )

        # Check API key requirement
        engine_config = engines_config[engine]
        if engine_config.get("requires_api_key", False):
            api_key_setting = settings.get(
                f"search.engine.web.{engine}.api_key"
            )
            api_key = None
            if api_key_setting:
                api_key = (
                    api_key_setting.get("value")
                    if isinstance(api_key_setting, dict)
                    else api_key_setting
                )
            if not api_key:
                raise ValidationError(  # noqa: TRY301
                    f"Engine '{engine}' requires an API key. "
                    f"Set the LDR_SEARCH_ENGINE_WEB_{engine.upper()}_API_KEY environment variable "
                    f"or configure it in the UI at search.engine.web.{engine}.api_key"
                )

        logger.info(
            f"Starting search on '{engine}' for query: {query[:100]}..."
        )

        # Set thread-local settings context so that engine constructors
        # which internally call get_llm() or get_setting_from_snapshot()
        # (e.g., arxiv's JournalReputationFilter) can resolve settings.
        from local_deep_research.config.thread_settings import (
            clear_settings_context,
            set_settings_context,
        )
        from local_deep_research.settings.manager import SnapshotSettingsContext

        set_settings_context(SnapshotSettingsContext(settings))
        try:
            return _execute_search(query, engine, max_results, settings)
        finally:
            clear_settings_context()

    except ValidationError as e:
        logger.warning("Validation failed for search")
        return {
            "status": "error",
            "error": str(e),
            "error_type": "validation_error",
        }
    except Exception as e:
        logger.exception(
            f"Search failed for query: {query[:100] if query else 'empty'}"
        )
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Search failed ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


def _execute_search(
    query: str, engine: str, max_results: int, settings: Dict[str, Any]
) -> Dict[str, Any]:
    """Execute the search after settings context is established."""
    from local_deep_research.web_search_engines.search_engine_factory import (
        create_search_engine,
    )

    search_engine = create_search_engine(
        engine_name=engine,
        llm=None,
        settings_snapshot=settings,
        programmatic_mode=True,
        max_results=max_results,
        search_snippets_only=True,
    )

    if search_engine is None:
        return {
            "status": "error",
            "error": f"Failed to create search engine '{engine}'. "
            f"This engine may require an LLM or have other prerequisites. "
            f"Check server logs for details.",
            "error_type": "configuration_error",
        }

    try:
        # Execute search
        results = search_engine.run(query)

        # Normalize results: ensure consistent 'snippet' key
        for result in results:
            if "snippet" not in result and "body" in result:
                result["snippet"] = result["body"]

        return {
            "status": "success",
            "query": query,
            "engine": engine,
            "result_count": len(results),
            "results": results,
        }
    finally:
        from local_deep_research.utilities.resource_utils import safe_close

        safe_close(search_engine, "MCP search engine")


# =============================================================================
# Discovery Tools
# =============================================================================


@mcp.tool()
def list_search_engines() -> Dict[str, Any]:
    """
    List available search engines.

    Returns a list of search engines that can be used with the research tools.
    Each engine has different strengths - some are better for academic research,
    others for current events, etc.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - engines: List of available search engine configurations
        - error/error_type: Error info (only if status is "error")
    """
    try:
        from local_deep_research.api.settings_utils import (
            create_settings_snapshot,
        )
        from local_deep_research.web_search_engines.search_engines_config import (
            search_config,
        )

        settings = create_settings_snapshot()
        engines_config = search_config(settings_snapshot=settings)

        engines = []
        for name, config in engines_config.items():
            engine_info = {
                "name": name,
                "description": config.get("description", ""),
                "strengths": config.get("strengths", []),
                "weaknesses": config.get("weaknesses", []),
                "requires_api_key": config.get("requires_api_key", False),
                "is_local": config.get("is_local", False),
            }
            engines.append(engine_info)

        return {
            "status": "success",
            "engines": sorted(engines, key=lambda x: x["name"]),
        }

    except Exception as e:
        logger.exception("Failed to list search engines")
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Failed to list search engines ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def list_strategies() -> Dict[str, Any]:
    """
    List available research strategies.

    Returns a list of research strategies that can be used with the research tools.
    Each strategy has different characteristics suited for different types of queries.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - strategies: List of available strategies with names and descriptions
        - error/error_type: Error info (only if status is "error")
    """
    try:
        return {
            "status": "success",
            "strategies": get_available_strategies(show_all=True),
        }

    except Exception as e:
        logger.exception("Failed to list strategies")
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Failed to list strategies ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


@mcp.tool()
def get_configuration() -> Dict[str, Any]:
    """
    Get current server configuration.

    Returns the current configuration settings being used by the MCP server,
    including LLM provider, default search engine, and other settings.

    Returns:
        Dictionary containing:
        - status: "success" or "error"
        - config: Current configuration settings
        - error/error_type: Error info (only if status is "error")
    """
    try:
        from local_deep_research.api.settings_utils import (
            create_settings_snapshot,
            extract_setting_value,
        )

        settings = create_settings_snapshot()

        config = {
            "llm": {
                "provider": extract_setting_value(
                    settings, "llm.provider", "unknown"
                ),
                "model": extract_setting_value(
                    settings, "llm.model", "unknown"
                ),
                "temperature": extract_setting_value(
                    settings, "llm.temperature", 0.7
                ),
            },
            "search": {
                "default_engine": extract_setting_value(
                    settings, "search.tool", "auto"
                ),
                "default_strategy": extract_setting_value(
                    settings, "search.search_strategy", "source-based"
                ),
                "iterations": extract_setting_value(
                    settings, "search.iterations", 2
                ),
                "questions_per_iteration": extract_setting_value(
                    settings, "search.questions_per_iteration", 3
                ),
                "max_results": extract_setting_value(
                    settings, "search.max_results", 10
                ),
            },
        }

        return {
            "status": "success",
            "config": config,
        }

    except Exception as e:
        logger.exception("Failed to get configuration")
        error_type = _classify_error(str(e))
        return {
            "status": "error",
            "error": f"Failed to get configuration ({error_type}). Check server logs for details.",
            "error_type": error_type,
        }


# =============================================================================
# Server Entry Point
# =============================================================================


def run_server():
    """Run the MCP server using STDIO transport."""
    # MCP uses stdout for JSON-RPC, so redirect all logging to stderr.
    # This runs in a separate subprocess (ldr-mcp) — logger.remove() only
    # affects this MCP process, not the main LDR application.
    logger.remove()
    logger.add(
        sys.stderr,
        level="INFO",
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}",
    )
    logger.info("Starting Local Deep Research MCP server...")
    mcp.run(transport="stdio")


if __name__ == "__main__":
    run_server()
