#!/usr/bin/env python3
"""Kalnavi for Claude Code (and any MCP client): Majelis Kalnavi as tools. (c) 2026 Muhammad Abrar Rayhan.

A Model Context Protocol server over stdio, standard library only. Register it in Claude Code:

    claude mcp add kalnavi -- python C:\\path\\to\\kalnavi-code\\kalnavi_mcp.py

or keep the `.mcp.json` at the repository root. Tools:
  - kalnavi_majelis   a cross-checked second opinion from several different AI models
  - kalnavi_review    Majelis code review of a diff (defaults to `git diff HEAD` in the working folder)
  - kalnavi_riset     web research with sources, answered by Kalnavi's deep mode
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
import urllib.request

SERVER = os.environ.get("KALNAVI_SERVER", "https://www.kalnavi.com").rstrip("/")
VERSION = "1.0.0"
TOOLS = [
    {"name": "kalnavi_majelis", "description": "Ask Majelis Kalnavi: several different AI models answer independently and one cross-checks and merges "
     "them. Use for a second opinion on a design, an algorithm, a tricky bug or a calculation.",
     "inputSchema": {"type": "object", "properties": {"question": {"type": "string", "description": "The question with all needed context"}},
                     "required": ["question"]}},
    {"name": "kalnavi_review", "description": "Majelis code review: several models review a diff independently; returns merged findings, most "
     "severe first. Leave diff empty to review `git diff HEAD` of the current folder.",
     "inputSchema": {"type": "object", "properties": {"diff": {"type": "string"}, "focus": {"type": "string", "description": "What to focus on"}}}},
    {"name": "kalnavi_riset", "description": "Web research by Kalnavi (reads several sources, cites them). Good for current docs, versions, APIs.",
     "inputSchema": {"type": "object", "properties": {"topic": {"type": "string"}}, "required": ["topic"]}},
]


def post(path: str, payload: dict, timeout: int = 280) -> dict:
    req = urllib.request.Request(SERVER + path, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", "User-Agent": f"kalnavi-mcp/{VERSION}"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read().decode())


def research(topic: str) -> str:
    req = urllib.request.Request(SERVER + "/api/kog/chat", data=json.dumps({"messages": [{"role": "user", "content": "riset: " + topic}], "deep": True, "learn": False}).encode(),
                                 headers={"Content-Type": "application/json", "User-Agent": f"kalnavi-mcp/{VERSION}"})
    text, sources = "", []
    with urllib.request.urlopen(req, timeout=280) as r:
        for raw in r:
            line = raw.decode("utf-8", "replace").strip()
            if not line.startswith("data:"):
                continue
            ev = json.loads(line[5:])
            if ev.get("type") == "delta":
                text += ev.get("text", "")
            elif ev.get("type") == "sources":
                sources = ev.get("items") or []
    refs = "\n".join(f"[{s.get('n')}] {s.get('title')} {s.get('url')}" for s in sources[:12])
    return text + ("\n\nSumber:\n" + refs if refs else "")


def call_tool(name: str, args: dict) -> str:
    if name == "kalnavi_majelis":
        out = post("/api/kog/dev/ask", {"question": args.get("question", "")})
        return out.get("answer", "") + (f"\n\n(Majelis: {', '.join(out['models'])})" if out.get("models") else "")
    if name == "kalnavi_review":
        diff = args.get("diff") or subprocess.run(["git", "diff", "HEAD"], capture_output=True, text=True, encoding="utf-8", errors="replace").stdout
        if not diff.strip():
            return "Tidak ada diff untuk direview."
        out = post("/api/kog/dev/review", {"diff": diff[:180000], "focus": args.get("focus", "")})
        return out.get("review", "") + (f"\n\n(Majelis: {', '.join(out['models'])})" if out.get("models") else "")
    if name == "kalnavi_riset":
        return research(args.get("topic", ""))
    raise ValueError(f"unknown tool {name}")


def reply(msg_id, result=None, error=None) -> None:
    out = {"jsonrpc": "2.0", "id": msg_id}
    out.update({"error": error} if error else {"result": result})
    sys.stdout.write(json.dumps(out, ensure_ascii=False) + "\n")
    sys.stdout.flush()


def main() -> None:
    for raw in sys.stdin:
        raw = raw.strip()
        if not raw:
            continue
        try:
            msg = json.loads(raw)
        except json.JSONDecodeError:
            continue
        method, msg_id = msg.get("method"), msg.get("id")
        if msg_id is None:
            continue                                    # notifications (initialized, cancelled) need no answer
        if method == "initialize":
            reply(msg_id, {"protocolVersion": (msg.get("params") or {}).get("protocolVersion", "2025-06-18"),
                           "capabilities": {"tools": {}}, "serverInfo": {"name": "kalnavi", "version": VERSION}})
        elif method == "tools/list":
            reply(msg_id, {"tools": TOOLS})
        elif method == "tools/call":
            p = msg.get("params") or {}
            try:
                reply(msg_id, {"content": [{"type": "text", "text": call_tool(p.get("name", ""), p.get("arguments") or {})}]})
            except Exception as exc:  # noqa: BLE001  tool failures are results, not protocol errors
                reply(msg_id, {"content": [{"type": "text", "text": f"Kalnavi gagal: {exc}"}], "isError": True})
        elif method == "ping":
            reply(msg_id, {})
        else:
            reply(msg_id, error={"code": -32601, "message": f"Method not found: {method}"})


if __name__ == "__main__":
    main()
