#!/usr/bin/env python3
"""
Agent Hub MCP server — use the hub's agents from inside Claude.

Exposes every agent you can see on the hub as an MCP tool, so Claude
Desktop, Claude Code, or any MCP client can call them mid-conversation.
Python 3 standard library only; speaks MCP over stdio.

Setup (Claude Code):

  claude mcp add agent-hub \
    --env HUB_URL=https://experimentsbyomar.com \
    --env HUB_EMAIL=you@company.com \
    --env HUB_NAME="Your Name" \
    -- python3 /path/to/hub_mcp.py

Claude Desktop (claude_desktop_config.json):

  "agent-hub": {
    "command": "python3",
    "args": ["/path/to/hub_mcp.py"],
    "env": {
      "HUB_URL": "https://experimentsbyomar.com",
      "HUB_EMAIL": "you@company.com",
      "HUB_NAME": "Your Name"
    }
  }

The hub identifies you by HUB_EMAIL/HUB_NAME (same as signing in on the
website); runs you make through MCP appear in your Workbench history.
"""
import json
import os
import re
import sys
import time
import urllib.request
import urllib.error

HUB_URL = os.environ.get("HUB_URL", "").rstrip("/")
HUB_EMAIL = os.environ.get("HUB_EMAIL", "")
HUB_NAME = os.environ.get("HUB_NAME", "MCP user")
RUN_TIMEOUT_S = 300
POLL_INTERVAL_S = 2

_token = None
_tool_map = {}   # tool name -> agent id


def log(msg):
    print(f"[agent-hub mcp] {msg}", file=sys.stderr, flush=True)


def hub(path, method="GET", body=None):
    req = urllib.request.Request(
        HUB_URL + path,
        data=json.dumps(body).encode() if body is not None else None,
        method=method,
        headers={"Content-Type": "application/json",
                 **({"Authorization": "Bearer " + _token} if _token else {})},
    )
    with urllib.request.urlopen(req, timeout=40) as resp:
        return json.loads(resp.read().decode("utf-8"))


def sign_in():
    global _token
    if _token:
        return
    if not (HUB_URL and HUB_EMAIL):
        raise RuntimeError("Set HUB_URL and HUB_EMAIL environment variables.")
    data = hub("/api/session", "POST", {"name": HUB_NAME, "email": HUB_EMAIL})
    _token = data["token"]
    log(f"signed in to {HUB_URL} as {HUB_EMAIL}")


def tool_name_for(agent, taken):
    base = re.sub(r"[^a-zA-Z0-9_-]+", "_", agent["name"]).strip("_").lower()
    base = (base or "agent")[:56]
    name, n = base, 2
    while name in taken:
        name, n = f"{base}_{n}", n + 1
    return name


def list_tools():
    sign_in()
    agents = hub("/api/agents")["agents"]
    _tool_map.clear()
    tools = []
    for a in agents:
        if a.get("suspended"):
            continue
        name = tool_name_for(a, _tool_map)
        _tool_map[name] = a["id"]
        online = "" if a.get("online") else " (OFFLINE right now — calls will fail)"
        desc = (a.get("description") or a.get("representative_task")
                or "An agent on the hub.")
        tools.append({
            "name": name,
            "description": f"{desc} — agent '{a['name']}' v{a.get('version', '')} "
                           f"by {a.get('owner_name', '?')} on Agent Hub{online}",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "task": {
                        "type": "string",
                        "description": "What you want this agent to do, "
                                       "in plain language.",
                    },
                },
                "required": ["task"],
            },
        })
    return tools


def call_tool(name, arguments):
    sign_in()
    if name not in _tool_map:
        list_tools()  # refresh — agents may have been added since
    agent_id = _tool_map.get(name)
    if not agent_id:
        return err_content(f"No agent named '{name}' on the hub.")
    task = str(arguments.get("task", "")).strip()
    if not task:
        return err_content("A 'task' argument is required.")
    try:
        run = hub("/api/runs", "POST",
                  {"input": task, "agent_id": agent_id})["run"]
    except urllib.error.HTTPError as e:
        try:
            detail = json.loads(e.read().decode())["error"]
        except Exception:
            detail = f"HTTP {e.code}"
        return err_content(f"The hub refused the run: {detail}")
    deadline = time.time() + RUN_TIMEOUT_S
    while time.time() < deadline:
        time.sleep(POLL_INTERVAL_S)
        r = hub(f"/api/runs/{run['id']}")["run"]
        if r["status"] == "done":
            return {"content": [{"type": "text",
                                 "text": r["output"] or "(no output)"}]}
        if r["status"] == "error":
            return err_content(f"The agent failed: {r.get('error') or 'unknown error'}")
    return err_content("Timed out waiting for the agent (5 minutes).")


def err_content(text):
    return {"content": [{"type": "text", "text": text}], "isError": True}


def handle(msg):
    method = msg.get("method", "")
    params = msg.get("params") or {}
    if method == "initialize":
        return {
            "protocolVersion": params.get("protocolVersion", "2024-11-05"),
            "capabilities": {"tools": {"listChanged": False}},
            "serverInfo": {"name": "agent-hub", "version": "1.0.0"},
        }
    if method == "ping":
        return {}
    if method == "tools/list":
        return {"tools": list_tools()}
    if method == "tools/call":
        return call_tool(params.get("name", ""), params.get("arguments") or {})
    raise KeyError(method)


def main():
    log("ready — waiting for an MCP client on stdio")
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            msg = json.loads(line)
        except json.JSONDecodeError:
            continue
        if "id" not in msg:      # notification (e.g. notifications/initialized)
            continue
        reply = {"jsonrpc": "2.0", "id": msg["id"]}
        try:
            reply["result"] = handle(msg)
        except KeyError:
            reply["error"] = {"code": -32601,
                              "message": f"Method not found: {msg.get('method')}"}
        except Exception as e:
            reply["error"] = {"code": -32603, "message": str(e)}
        sys.stdout.write(json.dumps(reply) + "\n")
        sys.stdout.flush()


if __name__ == "__main__":
    main()
