"""MCP server for the My New Details API — give an AI agent disposable details.

MCP (Model Context Protocol) is the standard way to hand tools to AI agents
(Claude, and most agent frameworks). This server speaks MCP over stdio and
wraps our public JSON API, so an agent can rent an inbox or number and poll it
for the verification code as part of an automated flow (QA testing being the
obvious use).

Zero dependencies — plain Python 3.10+ standard library — so a customer can
download this one file and run it. Configure with environment variables:

    MND_API_KEY   your API key from https://mynewdetails.com.au/account
    MND_BASE_URL  optional, defaults to https://mynewdetails.com.au

Example Claude Desktop / claude_code MCP config:

    {"mcpServers": {"mynewdetails": {
        "command": "python", "args": ["mcp_server.py"],
        "env": {"MND_API_KEY": "mnd_..."}}}}

The same Acceptable Use Policy applies to agents as to humans: prohibited-site
attempts are recorded and carry the same consequences.
"""
from __future__ import annotations

import json
import os
import sys
import urllib.error
import urllib.request

PROTOCOL_VERSION = "2024-11-05"
SERVER_INFO = {"name": "mynewdetails", "version": "1.0.0"}

TOOLS = [
    {
        "name": "get_account",
        "description": "Check the My New Details wallet balance and current prices.",
        "inputSchema": {"type": "object", "properties": {}, "required": []},
    },
    {
        "name": "rent_email",
        "description": (
            "Rent a disposable Australian email inbox for signing up to a site. "
            "Charges the prepaid wallet. Returns the address and a session id to poll."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {"site": {"type": "string", "description": "The website the inbox is for, e.g. example.com"}},
            "required": ["site"],
        },
    },
    {
        "name": "rent_phone",
        "description": (
            "Rent a disposable Australian phone number for signing up to a site — "
            "guaranteed never used on that site before. Charges the prepaid wallet."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {"site": {"type": "string", "description": "The website the number is for, e.g. example.com"}},
            "required": ["site"],
        },
    },
    {
        "name": "get_session",
        "description": (
            "Poll a rental for received messages. Returns status, messages, and "
            "latest_otp (the newest verification-code-shaped number)."
        ),
        "inputSchema": {
            "type": "object",
            "properties": {"session_id": {"type": "integer", "description": "The id returned by rent_email/rent_phone"}},
            "required": ["session_id"],
        },
    },
]


def api_request(method: str, path: str, body: dict | None = None) -> dict:
    """Call the My New Details API; returns the decoded JSON (including error bodies)."""
    key = os.environ.get("MND_API_KEY", "")
    if not key:
        return {"error": "not_configured",
                "detail": "Set the MND_API_KEY environment variable (create a key at /account)."}
    base = os.environ.get("MND_BASE_URL", "https://mynewdetails.com.au").rstrip("/")
    request = urllib.request.Request(
        base + path,
        method=method,
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
        data=json.dumps(body).encode() if body is not None else None,
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as exc:  # API error bodies are JSON too
        try:
            return json.loads(exc.read().decode())
        except Exception:
            return {"error": "http_error", "detail": f"HTTP {exc.code}"}
    except Exception as exc:
        return {"error": "network_error", "detail": str(exc)}


def call_tool(name: str, arguments: dict, api=api_request) -> dict:
    if name == "get_account":
        result = api("GET", "/api/v1/account")
    elif name == "rent_email":
        result = api("POST", "/api/v1/emails", {"site": arguments.get("site", "")})
    elif name == "rent_phone":
        result = api("POST", "/api/v1/phones", {"site": arguments.get("site", "")})
    elif name == "get_session":
        result = api("GET", f"/api/v1/sessions/{int(arguments.get('session_id', 0))}")
    else:
        return {"content": [{"type": "text", "text": f"Unknown tool: {name}"}], "isError": True}
    return {
        "content": [{"type": "text", "text": json.dumps(result, indent=2)}],
        "isError": bool(result.get("error")),
    }


def handle_message(message: dict, api=api_request) -> dict | None:
    """One JSON-RPC message in, one response out (None for notifications)."""
    method = message.get("method")
    msg_id = message.get("id")
    if method is None or (msg_id is None and method.startswith("notifications/")):
        return None  # notification — nothing to send back

    def ok(result: dict) -> dict:
        return {"jsonrpc": "2.0", "id": msg_id, "result": result}

    if method == "initialize":
        return ok({
            "protocolVersion": PROTOCOL_VERSION,
            "capabilities": {"tools": {}},
            "serverInfo": SERVER_INFO,
        })
    if method == "ping":
        return ok({})
    if method == "tools/list":
        return ok({"tools": TOOLS})
    if method == "tools/call":
        params = message.get("params") or {}
        return ok(call_tool(params.get("name", ""), params.get("arguments") or {}, api=api))
    return {
        "jsonrpc": "2.0", "id": msg_id,
        "error": {"code": -32601, "message": f"Method not found: {method}"},
    }


def main() -> None:
    """Newline-delimited JSON-RPC over stdio, per the MCP stdio transport."""
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            message = json.loads(line)
        except json.JSONDecodeError:
            continue
        response = handle_message(message)
        if response is not None:
            sys.stdout.write(json.dumps(response) + "\n")
            sys.stdout.flush()


if __name__ == "__main__":
    main()
