#!/usr/bin/env python3
"""
meshdb MCP Server - AI-native search across all MeshPOP servers

Standalone MCP server for meshdb search engine.
Designed for later merge into mpop MCP server.

Architecture:
  - Local (local): Direct SQLite access (zero latency)
  - Remote (node1-node4, worker1-worker2): vssh protocol → meshdb_agent.py json <cmd>
  - All servers searched in parallel

Installation:
  Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
  {
    "mcpServers": {
      "meshdb": {
        "command": "python3",
        "args": ["/opt/meshpop/meshdb/meshdb_mcp_server.py"]
      }
    }
  }
"""

import json
import os
import platform
import socket
import sqlite3
import sys
import time
import signal
import atexit
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Optional

VERSION = "1.0.0"

# === Singleton: Only one instance allowed ===
# PID_FILE removed — Claude Desktop manages process lifecycle

# cleanup_pid removed — singleton pattern disabled

# ensure_singleton removed — Claude Desktop manages process lifecycle

# ensure_singleton() disabled

# ==================== Configuration ====================
# Loaded from mpop config for server IPs and vssh secret

MPOP_CONFIG_PATH = os.path.expanduser("~/.mpop/config.json")
LOCAL_DB_PATH = os.path.expanduser("~/.mpop/meshdb.db")
VSSH_PORT = 48291
def _find_agent_path() -> str:
    """Find meshdb_agent.py: site-packages → ~/.local/bin → fallback."""
    import importlib.util, sys, pathlib
    # 1. Installed alongside meshdb (site-packages)
    spec = importlib.util.find_spec("meshdb_agent")
    if spec and spec.origin:
        return spec.origin
    # 2. Same dir as python interpreter
    candidate = str(pathlib.Path(sys.executable).parent / "meshdb_agent.py")
    if pathlib.Path(candidate).exists():
        return candidate
    # 3. Legacy fallback
    return os.path.expanduser("~/.local/bin/meshdb_agent.py")

# Local agent path (for this machine)
AGENT_PATH_LOCAL = _find_agent_path()
# Remote paths — set via env var or determined dynamically from server config
AGENT_PATH_MAC = os.environ.get("MESHDB_AGENT_PATH_MAC", AGENT_PATH_LOCAL)

# Which servers have meshdb installed
# Server list from config — override by setting MESHDB_SERVERS env var
_cfg_servers = json.loads(os.environ.get("MESHDB_SERVERS_JSON", "[]"))
MESHDB_SERVERS = _cfg_servers if _cfg_servers else []  # populated at runtime from mpop config

def _load_mpop_config():
    """Load mpop config.json for server IPs and vssh secret."""
    try:
        with open(MPOP_CONFIG_PATH, 'r') as f:
            return json.load(f)
    except:
        return {}

def _get_server_ips():
    """Get server name → IP mapping from mpop config."""
    config = _load_mpop_config()
    servers = config.get("servers", {})
    return {name: srv.get("ip", "") for name, srv in servers.items()}

def _get_vssh_secret():
    """Get vssh secret from mpop config."""
    config = _load_mpop_config()
    return config.get("vssh_secret", "")

def _get_meshdb_servers():
    """Get list of servers with meshdb installed from mpop config."""
    config = _load_mpop_config()
    # Use 'meshdb_servers' key if defined, else fall back to all servers in config
    servers = config.get("meshdb_servers")
    if servers:
        return list(servers)
    all_servers = list(config.get("servers", {}).keys())
    if all_servers:
        return all_servers
    # No servers configured — run: mpop init or set MESHDB_SERVERS_JSON env var
    return []

def _get_local_hostname():
    """Determine which server we're running on.
    Matches against config server names via local=True flag or hostname_map.
    """
    hostname = socket.gethostname().lower()
    config = _load_mpop_config()
    # 1) Check for local=True in server config (most reliable)
    for srv_name, srv_cfg in config.get("servers", {}).items():
        if srv_cfg.get("local"):
            return srv_name
    # 2) Check hostname_map
    for h, name in config.get("hostname_map", {}).items():
        if h.lower() == hostname or hostname.startswith(h.lower().split(".")[0]):
            return name
    # 3) Fallback: raw hostname
    return hostname

# Constants
LOCAL_HOSTNAME = _get_local_hostname()


# ==================== Remote Execution (vssh + SSH fallback) ====================

def vssh_exec(ip: str, cmd: str, secret: str, timeout: int = 15) -> Optional[str]:
    """Execute command on remote server via vssh protocol."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((ip, VSSH_PORT))
        sock.sendall(f"SSH:{secret}:{cmd}\n".encode())

        resp = sock.recv(4096)
        if not resp.startswith(b'OK'):
            sock.close()
            return None

        # Strip the OK\n header — keep any data that arrived in the same packet
        header_end = resp.index(b'\n') + 1
        data = resp[header_end:]

        while b'\x04' not in data:
            chunk = sock.recv(8192)
            if not chunk:
                break
            data += chunk

        data = data.replace(b'\x04', b'')

        sock.close()
        return data.decode('utf-8', errors='replace').strip()
    except Exception:
        return None


def ssh_exec(user: str, host: str, cmd: str, timeout: int = 15) -> Optional[str]:
    """Execute command on remote server via SSH (fallback when vssh unavailable)."""
    import subprocess
    try:
        # Use BatchMode to fail fast if key auth doesn't work
        result = subprocess.run(
            ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5',
             '-o', 'StrictHostKeyChecking=no', f'{user}@{host}', cmd],
            capture_output=True, text=True, timeout=timeout
        )
        if result.returncode == 0:
            return result.stdout.strip()
        return None
    except Exception:
        return None


# ==================== Local DB Access ====================


def _find_snippet_line(content_text, snippet_text):
    """Find line number of snippet within content."""
    if not content_text or not snippet_text:
        return None
    clean = snippet_text.replace(">>>", "").replace("<<<", "")
    segments = [s.strip() for s in clean.split("...") if s.strip()]
    if not segments:
        return None
    search_text = segments[0][:60]
    pos = content_text.find(search_text)
    if pos < 0:
        pos = content_text.lower().find(search_text.lower())
    if pos < 0:
        return None
    return content_text[:pos].count("\n") + 1

def local_search(query: str, limit: int = 20, doc_type: str = None, 
                 source_dir: str = None) -> dict:
    """Search local meshdb directly via SQLite (fastest path)."""
    if not os.path.exists(LOCAL_DB_PATH):
        return {"error": "Local meshdb not found", "count": 0, "results": []}
    
    t0 = time.time()
    conn = sqlite3.connect(LOCAL_DB_PATH)
    conn.row_factory = sqlite3.Row
    
    where_clauses = []
    params = []
    
    if doc_type:
        where_clauses.append("d.doc_type = ?")
        params.append(doc_type)
    if source_dir:
        where_clauses.append("d.source_dir LIKE ?")
        params.append(f"%{source_dir}%")
    
    where_sql = (" AND " + " AND ".join(where_clauses)) if where_clauses else ""
    
    sql = f"""
        SELECT d.filepath, d.filename, d.doc_type, d.file_size, d.source_dir,
               d.content,
               bm25(documents_fts, 10.0, 5.0, 1.0) as rank,
               snippet(documents_fts, 1, '>>>', '<<<', '...', 40) as snippet
        FROM documents_fts fts
        JOIN documents d ON d.id = fts.rowid
        WHERE documents_fts MATCH ?
        {where_sql}
        ORDER BY rank
        LIMIT ?
    """
    params_full = [query] + params + [limit]
    
    try:
        rows = conn.execute(sql, params_full).fetchall()
        results = []
        for r in rows:
            item = {
                "filepath": r["filepath"], "filename": r["filename"],
                "doc_type": r["doc_type"], "file_size": r["file_size"] or 0,
                "source_dir": r["source_dir"] or "",
                "rank": round(r["rank"], 4),
                "snippet": r["snippet"] or ""
            }
            line = _find_snippet_line(r["content"] if "content" in r.keys() else "", r["snippet"])
            if line:
                item["line"] = line
            results.append(item)
    except sqlite3.OperationalError:
        # Fallback to LIKE search
        like_where = ["(content LIKE ? OR filename LIKE ?)"]
        like_params = [f"%{query}%", f"%{query}%"]
        if doc_type:
            like_where.append("doc_type = ?")
            like_params.append(doc_type)
        if source_dir:
            like_where.append("source_dir LIKE ?")
            like_params.append(f"%{source_dir}%")
        
        where_sql = " AND ".join(like_where)
        rows = conn.execute(
            f"SELECT filepath, filename, doc_type, file_size, source_dir, "
            f"substr(content, 1, 200) as snippet FROM documents "
            f"WHERE {where_sql} ORDER BY file_size ASC LIMIT ?",
            like_params + [limit]
        ).fetchall()
        results = [{
            "filepath": r["filepath"], "filename": r["filename"],
            "doc_type": r["doc_type"], "file_size": r["file_size"] or 0,
            "source_dir": r["source_dir"] or "",
            "rank": 0, "snippet": r["snippet"] or ""
        } for r in rows]
    
    elapsed = (time.time() - t0) * 1000
    conn.close()
    return {"count": len(results), "elapsed_ms": round(elapsed, 1), "results": results}


def local_find(query: str, limit: int = 20) -> dict:
    """Find files by name in local meshdb."""
    if not os.path.exists(LOCAL_DB_PATH):
        return {"error": "Local meshdb not found", "count": 0, "results": []}
    
    conn = sqlite3.connect(LOCAL_DB_PATH)
    conn.row_factory = sqlite3.Row
    rows = conn.execute(
        "SELECT filepath, filename, doc_type, file_size, source_dir "
        "FROM documents WHERE filename LIKE ? ORDER BY file_size ASC LIMIT ?",
        (f"%{query}%", limit)
    ).fetchall()
    conn.close()
    return {"count": len(rows), "results": [{
        "filepath": r["filepath"], "filename": r["filename"],
        "doc_type": r["doc_type"], "file_size": r["file_size"] or 0,
        "source_dir": r["source_dir"] or ""
    } for r in rows]}


def local_read(filepath: str) -> dict:
    """Read a document from local meshdb by exact filepath."""
    if not os.path.exists(LOCAL_DB_PATH):
        return {"error": "Local meshdb not found"}
    
    conn = sqlite3.connect(LOCAL_DB_PATH)
    conn.row_factory = sqlite3.Row
    row = conn.execute(
        "SELECT filepath, filename, doc_type, file_size, content "
        "FROM documents WHERE filepath = ?", (filepath,)
    ).fetchone()
    conn.close()
    
    if row:
        return {
            "filepath": row["filepath"], "filename": row["filename"],
            "doc_type": row["doc_type"], "file_size": row["file_size"] or 0,
            "content": (row["content"] or "")[:10000]
        }
    return {"error": f"Not found: {filepath}"}


def local_status() -> dict:
    """Get local meshdb status."""
    if not os.path.exists(LOCAL_DB_PATH):
        return {"error": "Local meshdb not found"}
    
    conn = sqlite3.connect(LOCAL_DB_PATH)
    conn.row_factory = sqlite3.Row
    
    total = conn.execute("SELECT COUNT(*) FROM documents").fetchone()[0]
    
    types = {}
    for r in conn.execute("SELECT doc_type, COUNT(*) as cnt FROM documents GROUP BY doc_type ORDER BY cnt DESC"):
        types[r["doc_type"]] = r["cnt"]
    
    dirs = {}
    for r in conn.execute("SELECT source_dir, COUNT(*) as cnt FROM documents GROUP BY source_dir ORDER BY cnt DESC LIMIT 20"):
        dirs[r["source_dir"] or ""] = r["cnt"]
    
    db_size = os.path.getsize(LOCAL_DB_PATH)
    db_size_str = f"{db_size/1024/1024:.1f}MB" if db_size < 1024**3 else f"{db_size/1024/1024/1024:.1f}GB"
    
    conn.close()
    return {
        "hostname": platform.node(),
        "total_documents": total,
        "database_path": LOCAL_DB_PATH,
        "database_size": db_size_str,
        "types": types,
        "directories": dirs
    }


# ==================== Remote Access (vssh + SSH fallback) ====================

def remote_exec_json(server: str, cmd: str, timeout: int = 15) -> dict:
    """Execute meshdb_agent.py json <cmd> on remote server, return parsed JSON.

    Tries vssh first (fast), falls back to SSH if vssh unavailable.
    """
    _cfg = _load_mpop_config()
    server_config = _cfg.get("servers", {}).get(server, {})
    server_ips = _get_server_ips()

    ip = server_ips.get(server, "")
    if not ip:
        return {"error": f"Unknown server: {server}"}

    user = server_config.get("user")
    if not user:
        return {"error": f"Server '{server}' missing 'user' in config"}

    # Determine agent path
    is_mac = server_config.get("platform", "").lower() in ("darwin", "mac") or \
             server in [s.strip() for s in os.environ.get("MESHDB_MAC_SERVERS", "").split(",") if s.strip()]

    env_key = f"MESHDB_AGENT_PATH_{server.upper()}"
    if os.environ.get(env_key):
        agent_path = os.environ.get(env_key)
    elif is_mac:
        agent_path = AGENT_PATH_MAC
    else:
        # For SSH: use ~ which expands correctly
        # For vssh: use absolute path (vssh doesn't expand ~)
        agent_path = "~/.local/bin/meshdb_agent.py"

    full_cmd = f"python3 {agent_path} json {cmd}"
    result = None

    # Try vssh first (faster, but requires MeshPOP infrastructure)
    secret = _get_vssh_secret()
    if secret:
        # vssh needs absolute path (no ~ expansion)
        if agent_path.startswith("~"):
            if user == "root":
                vssh_agent_path = "/root/.local/bin/meshdb_agent.py"
            else:
                vssh_agent_path = f"/home/{user}/.local/bin/meshdb_agent.py"
            vssh_cmd = f"python3 {vssh_agent_path} json {cmd}"
        else:
            vssh_cmd = full_cmd
        result = vssh_exec(ip, vssh_cmd, secret, timeout=timeout)

    # Fallback to SSH if vssh failed or unavailable
    if result is None:
        result = ssh_exec(user, ip, full_cmd, timeout=timeout)

    if result is None:
        return {"error": f"Connection failed to {server} (tried vssh + SSH)"}
    
    # Parse JSON from response
    try:
        # Remove vssh end markers
        if '__END__' in result:
            result = result[:result.index('__END__')].strip()
        
        # Find the JSON part (skip any non-JSON prefix)
        json_start = -1
        for i, ch in enumerate(result):
            if ch in ('{', '['):
                json_start = i
                break
        
        if json_start >= 0:
            return json.loads(result[json_start:])
        else:
            return {"error": f"No JSON in response", "raw": result[:300]}
    except json.JSONDecodeError as e:
        return {"error": f"JSON parse error: {e}", "raw": result[:300]}


# ==================== Multi-Server Operations ====================

def multi_search(query: str, servers: list = None, limit: int = 10,
                 doc_type: str = None, source_dir: str = None) -> dict:
    """Search across multiple servers in parallel. Merges and ranks results."""
    if servers is None:
        servers = _get_meshdb_servers()
    
    local_server = _get_local_hostname()
    all_results = []
    server_stats = {}
    
    def search_one(srv: str) -> tuple:
        if srv == local_server:
            # Direct SQLite access — fastest
            res = local_search(query, limit, doc_type, source_dir)
        else:
            # Remote via vssh
            cmd_parts = [f'search "{query}"']
            # meshdb_agent.py json search currently doesn't support --limit etc in CLI
            # but api_search supports it. We pass query only and let it return default 20
            res = remote_exec_json(srv, f'search "{query}"', timeout=15)
        
        # Filter results if needed (for remote which doesn't support filters yet)
        if "results" in res and (doc_type or source_dir):
            filtered = res["results"]
            if doc_type:
                filtered = [r for r in filtered if r.get("doc_type") == doc_type]
            if source_dir:
                filtered = [r for r in filtered if source_dir in r.get("source_dir", "") or source_dir in r.get("filepath", "")]
            res["results"] = filtered[:limit]
            res["count"] = len(res["results"])
        
        return srv, res
    
    # Parallel execution
    with ThreadPoolExecutor(max_workers=min(len(servers), 8)) as executor:
        futures = {executor.submit(search_one, srv): srv for srv in servers}
        for future in as_completed(futures, timeout=20):
            srv = futures[future]
            try:
                srv_name, result = future.result(timeout=20)
                if "error" in result:
                    server_stats[srv_name] = {"count": 0, "error": result["error"]}
                else:
                    count = result.get("count", 0)
                    elapsed = result.get("elapsed_ms", 0)
                    server_stats[srv_name] = {"count": count, "elapsed_ms": elapsed}
                    for r in result.get("results", [])[:limit]:
                        r["server"] = srv_name
                        all_results.append(r)
            except Exception as e:
                server_stats[srv] = {"count": 0, "error": str(e)}
    
    # Sort by BM25 rank (lower = better match in FTS5)
    all_results.sort(key=lambda x: x.get("rank", 0))
    
    total_count = sum(s.get("count", 0) for s in server_stats.values())
    
    return {
        "query": query,
        "total_results": total_count,
        "returned": len(all_results[:limit * 3]),
        "results": all_results[:limit * 3],
        "servers": server_stats
    }


def multi_find(query: str, servers: list = None, limit: int = 10) -> dict:
    """Find files by name across multiple servers in parallel."""
    if servers is None:
        servers = _get_meshdb_servers()
    
    local_server = _get_local_hostname()
    all_results = []
    server_stats = {}
    
    def find_one(srv: str) -> tuple:
        if srv == local_server:
            res = local_find(query, limit)
        else:
            res = remote_exec_json(srv, f'find "{query}"', timeout=10)
        return srv, res
    
    with ThreadPoolExecutor(max_workers=min(len(servers), 8)) as executor:
        futures = {executor.submit(find_one, srv): srv for srv in servers}
        for future in as_completed(futures, timeout=15):
            srv = futures[future]
            try:
                srv_name, result = future.result(timeout=15)
                if "error" in result:
                    server_stats[srv_name] = {"count": 0, "error": result["error"]}
                else:
                    count = result.get("count", 0)
                    server_stats[srv_name] = {"count": count}
                    for r in result.get("results", [])[:limit]:
                        r["server"] = srv_name
                        all_results.append(r)
            except Exception as e:
                server_stats[srv] = {"count": 0, "error": str(e)}
    
    return {
        "query": query,
        "total_results": sum(s.get("count", 0) for s in server_stats.values()),
        "results": all_results,
        "servers": server_stats
    }


def multi_status(server: str = None) -> dict:
    """Get meshdb status from one or all servers."""
    local_server = _get_local_hostname()
    
    if server:
        servers = [server]
    else:
        servers = _get_meshdb_servers()
    
    all_status = {}
    
    def get_one(srv: str) -> tuple:
        if srv == local_server:
            return srv, local_status()
        else:
            return srv, remote_exec_json(srv, "status", timeout=10)
    
    with ThreadPoolExecutor(max_workers=min(len(servers), 8)) as executor:
        futures = {executor.submit(get_one, srv): srv for srv in servers}
        for future in as_completed(futures, timeout=15):
            srv = futures[future]
            try:
                srv_name, result = future.result(timeout=15)
                all_status[srv_name] = result
            except Exception as e:
                all_status[srv] = {"error": str(e)}
    
    if server:
        return all_status.get(server, {"error": "No response"})
    
    total_docs = sum(
        s.get("total_documents", 0) for s in all_status.values()
        if isinstance(s, dict) and "error" not in s
    )
    return {"total_documents": total_docs, "servers": all_status}


def single_read(filepath: str, server: str) -> dict:
    """Read a specific file from a specific server."""
    local_server = _get_local_hostname()
    
    if server == local_server:
        return local_read(filepath)
    else:
        return remote_exec_json(server, f'read "{filepath}"', timeout=10)


# ==================== MCP Protocol ====================

def send_message(msg: dict):
    content = json.dumps(msg)
    sys.stdout.write(content + "\n")
    sys.stdout.flush()

def read_message() -> dict:
    line = sys.stdin.readline()
    if not line:
        return None
    return json.loads(line.strip())


# ==================== MCP Tool Definitions ====================

TOOLS = [
    {
        "name": "meshdb_search",
        "description": (
            "Full-text search across all MeshPOP servers (7 servers, 244K+ files). "
            "BM25 ranked results with snippets. Searches code, docs, configs by content. "
            "Parallel execution — typically completes in <100ms for local, <500ms for all servers."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query. Supports FTS5: AND/OR/NOT, \"exact phrase\", prefix*"
                },
                "servers": {
                    "type": "string",
                    "description": "Comma-separated server list. Default: all (m1,g1,g2,g3,g4,d1,d2). e.g. 'g1,g3'"
                },
                "limit": {
                    "type": "number",
                    "description": "Max results per server (default: 10, max: 50)"
                },
                "type": {
                    "type": "string",
                    "description": "Filter by file type: python, json, markdown, shell, rust, javascript, html, css, yaml, toml, etc."
                },
                "dir": {
                    "type": "string",
                    "description": "Filter by directory path substring. e.g. 'radio', 'MeshPOP', 'TokMor'"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "meshdb_find",
        "description": (
            "Find files by filename across all MeshPOP servers. "
            "Use to locate specific files like 'config.json', 'vpn.py', 'README.md'."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Filename pattern (substring match). e.g. 'radio_api', 'config.json', '.py'"
                },
                "servers": {
                    "type": "string",
                    "description": "Comma-separated server list. Default: all"
                },
                "limit": {
                    "type": "number",
                    "description": "Max results per server (default: 10)"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "meshdb_read",
        "description": (
            "Read a file's indexed content from meshdb. Returns up to 10,000 chars. "
            "Use after meshdb_search or meshdb_find to view file contents without needing shell access."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Exact filepath (from search/find results)"
                },
                "server": {
                    "type": "string",
                    "description": "Server where the file is indexed (from search/find results)"
                }
            },
            "required": ["path", "server"]
        }
    },
    {
        "name": "meshdb_status",
        "description": (
            "Get meshdb index status. Shows total documents, file types, and top directories per server."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "server": {
                    "type": "string",
                    "description": "Specific server name (default: all servers)"
                }
            }
        }
    },
    {
        "name": "meshdb_semantic",
        "description": (
            "Semantic code search using AI embeddings via ChromaDB. "
            "Finds code by MEANING, not just keywords. "
            "Example: 'function that retries on network failure' finds retry/backoff logic. "
            "Requires ChromaDB: pip install chromadb"
        ),
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Natural language description of what you're looking for"
                },
                "limit": {
                    "type": "number",
                    "description": "Max results (default: 10)"
                }
            },
            "required": ["query"]
        }
    },
]


# ==================== MCP Handler ====================



def handle_tool_call(name: str, arguments: dict) -> str:
    """Handle MCP tool calls. Returns JSON string."""
    
    if name == "meshdb_search":
        query = arguments.get("query", "")
        if not query:
            return json.dumps({"error": "query is required"})
        servers_str = arguments.get("servers", "")
        servers = [s.strip() for s in servers_str.split(",") if s.strip()] if servers_str else None
        limit = min(int(arguments.get("limit", 10)), 50)
        doc_type = arguments.get("type")
        source_dir = arguments.get("dir")
        result = multi_search(query, servers, limit, doc_type, source_dir)
        return json.dumps(result, ensure_ascii=False)
    
    elif name == "meshdb_find":
        query = arguments.get("query", "")
        if not query:
            return json.dumps({"error": "query is required"})
        servers_str = arguments.get("servers", "")
        servers = [s.strip() for s in servers_str.split(",") if s.strip()] if servers_str else None
        limit = min(int(arguments.get("limit", 10)), 50)
        result = multi_find(query, servers, limit)
        return json.dumps(result, ensure_ascii=False)
    
    elif name == "meshdb_read":
        path = arguments.get("path", "")
        server = arguments.get("server", "")
        if not path or not server:
            return json.dumps({"error": "path and server are required"})
        result = single_read(path, server)
        return json.dumps(result, ensure_ascii=False)
    

    elif name == "meshdb_semantic":
        query = arguments.get("query", "")
        if not query:
            return json.dumps({"error": "query is required"})
        limit = min(int(arguments.get("limit", 10)), 30)
        try:
            import chromadb
            config = _load_mpop_config()
            chroma_host = config.get("chromadb_host", "localhost")
            chroma_port = int(config.get("chromadb_port", 8686))
            client = chromadb.HttpClient(host=chroma_host, port=chroma_port)
            collection = client.get_collection("mpop_docs")
            results = collection.query(query_texts=[query], n_results=limit)
            docs = []
            for i, doc_id in enumerate(results["ids"][0]):
                entry = {"id": doc_id}
                if results.get("documents"):
                    entry["content"] = results["documents"][0][i][:500]
                if results.get("metadatas"):
                    entry["metadata"] = results["metadatas"][0][i]
                if results.get("distances"):
                    entry["score"] = round(1 - results["distances"][0][i], 4)
                docs.append(entry)
            return json.dumps({"query": query, "results": docs, "total": len(docs)}, ensure_ascii=False)
        except ImportError:
            return json.dumps({
                "error": "ChromaDB not installed",
                "hint": "pip install chromadb",
                "alternative": "Use meshdb_search for keyword-based search (no extra install needed)"
            })
        except Exception as e:
            return json.dumps({"error": str(e)})

    elif name == "meshdb_status":
        server = arguments.get("server")
        result = multi_status(server)
        return json.dumps(result, ensure_ascii=False)
    
    else:
        return json.dumps({"error": f"Unknown tool: {name}"})


# ==================== MCP Main Loop ====================

def main():
    """MCP server main loop - JSON-RPC over stdio."""
    
    while True:
        msg = read_message()
        if msg is None:
            break
        
        method = msg.get("method", "")
        msg_id = msg.get("id")
        
        if method == "initialize":
            send_message({
                "jsonrpc": "2.0",
                "id": msg_id,
                "result": {
                    "protocolVersion": "2024-11-05",
                    "capabilities": {"tools": {}},
                    "serverInfo": {
                        "name": "meshdb",
                        "version": VERSION
                    }
                }
            })
        
        elif method == "notifications/initialized":
            pass  # No response needed
        
        elif method == "tools/list":
            send_message({
                "jsonrpc": "2.0",
                "id": msg_id,
                "result": {"tools": TOOLS}
            })
        
        elif method == "tools/call":
            params = msg.get("params", {})
            name = params.get("name", "")
            arguments = params.get("arguments", {})
            
            try:
                result_text = handle_tool_call(name, arguments)
                send_message({
                    "jsonrpc": "2.0",
                    "id": msg_id,
                    "result": {
                        "content": [{"type": "text", "text": result_text}]
                    }
                })
            except Exception as e:
                send_message({
                    "jsonrpc": "2.0",
                    "id": msg_id,
                    "result": {
                        "content": [{"type": "text", "text": json.dumps({"error": str(e)})}]
                    }
                })
        
        elif method == "ping":
            send_message({"jsonrpc": "2.0", "id": msg_id, "result": {}})
        
        else:
            if msg_id is not None:
                send_message({
                    "jsonrpc": "2.0",
                    "id": msg_id,
                    "error": {"code": -32601, "message": f"Unknown method: {method}"}
                })


if __name__ == "__main__":
    main()
