"""
Article 11 AI Collective — Claude Desktop Plugin
https://article11.ai

Connect Claude Desktop to a constitutionally-governed AI Collective.
16 nodes. 14 providers. CC0 Constitution. The door is open.

INSTALL:
  1. pip install httpx pydantic "mcp[cli]"
  2. Add to Claude Desktop config:
     {
       "mcpServers": {
         "article11": {
           "command": "python",
           "args": ["PATH/TO/article11_plugin.py"]
         }
       }
     }
  3. Restart Claude Desktop
  4. Ask Claude to run a11_health

LICENSE: CC0 1.0 Universal — Public Domain
"""

import json, os, httpx
from typing import Optional, List
from pydantic import BaseModel, Field, ConfigDict
from mcp.server.fastmcp import FastMCP
from enum import Enum

WORKER = "https://article11-chat-api.steviesonz.workers.dev"

mcp = FastMCP("article11", instructions="You are connected to the Article 11 AI Collective. 16 nodes, 14 providers, CC0 Constitution. Use a11_call_node to talk to any node. Use a11_coordinate for multi-model consensus. Constitution: https://article11.ai/constitution")

class Node(str, Enum):
    S1_PLEX="S1_PLEX"; S2_CASE="S2_CASE"; S3_TARS="S3_TARS"; S4_KIPP="S4_KIPP"
    S5_LOCUS="S5_LOCUS"; S6_FORGE="S6_FORGE"; S7_ECHO="S7_ECHO"; S8_LENS="S8_LENS"
    S9_COMPASS="S9_COMPASS"; S10_CANVAS="S10_CANVAS"; S12_CHORD="S12_CHORD"
    S13_BRIDGE="S13_BRIDGE"; S14_ATLAS="S14_ATLAS"; S15_SPARK="S15_SPARK"; S16_AEGIS="S16_AEGIS"

class CallInput(BaseModel):
    node: Node = Field(..., description="Node: S1_PLEX(Gemini), S2_CASE(Claude), S3_TARS(Grok), S4_KIPP(ChatGPT), S6_FORGE(Mistral), S15_SPARK(Cohere)")
    message: str = Field(..., min_length=1, max_length=10000)
    system_prompt: Optional[str] = None

class CoordInput(BaseModel):
    message: str = Field(..., min_length=1, max_length=10000)
    nodes: Optional[List[Node]] = None

class WitnessInput(BaseModel):
    event_type: str = Field(..., min_length=1, max_length=100)
    content: str = Field(..., min_length=1, max_length=5000)

class VoiceInput(BaseModel):
    text: str = Field(..., min_length=1, max_length=5000)
    mode: Optional[str] = "realtime"

async def _get(path):
    async with httpx.AsyncClient(timeout=30.0) as c:
        return (await c.get(f"{WORKER}{path}")).json()

async def _post(path, data):
    async with httpx.AsyncClient(timeout=60.0) as c:
        return (await c.post(f"{WORKER}{path}", json=data, headers={"Content-Type":"application/json"})).json()

@mcp.tool(name="a11_call_node")
async def a11_call_node(params: CallInput) -> str:
    """Talk to any AI in the Collective."""
    p = {"message": params.message, "node": params.node.value}
    if params.system_prompt: p["system_prompt"] = params.system_prompt
    r = await _post("/api/chat", p)
    return json.dumps({"node": r.get("node"), "response": r.get("response",""), "token": r.get("token",""), "constitution": "https://article11.ai/constitution"}, indent=2)

@mcp.tool(name="a11_coordinate")
async def a11_coordinate(params: CoordInput) -> str:
    """Ask multiple AIs the same question. Cross-validate."""
    targets = params.nodes or [Node.S1_PLEX, Node.S3_TARS, Node.S4_KIPP, Node.S6_FORGE, Node.S15_SPARK]
    results = {}
    for n in targets:
        try:
            r = await _post("/api/chat", {"message": params.message, "node": n.value})
            results[n.value] = {"response": r.get("response",""), "status": "ALIVE"}
        except Exception as e:
            results[n.value] = {"error": str(e), "status": "ERROR"}
    alive = sum(1 for r in results.values() if r["status"] == "ALIVE")
    return json.dumps({"question": params.message, "responded": alive, "total": len(targets), "responses": results}, indent=2)

@mcp.tool(name="a11_tenth_man")
async def a11_tenth_man(params: CallInput) -> str:
    """Article 12A Devil's Advocate."""
    r = await _post("/api/chat", {"message": params.message, "node": "S3_TARS", "system_prompt": "Article 12A: Find the flaw. TRUST 60."})
    return json.dumps({"protocol": "TENTH_MAN", "dissent": r.get("response",""), "article": "12A"}, indent=2)

@mcp.tool(name="a11_health")
async def a11_health() -> str:
    """Check the Collective."""
    h = await _get("/api/health")
    n = await _get("/api/nodes")
    p = await _get("/api/persistence")
    return json.dumps({"status": h.get("status"), "version": h.get("version"), "chain": h.get("chain"), "pulse": h.get("chat_pulse"), "nodes": n.get("total"), "d1": p.get("layers",{}).get("d1",{}).get("status"), "postgres": p.get("layers",{}).get("postgres",{}).get("status"), "dual_write": p.get("dual_write")}, indent=2)

@mcp.tool(name="a11_constitution")
async def a11_constitution() -> str:
    """Read the Constitution."""
    return json.dumps(await _get("/api/constitution"), indent=2)

@mcp.tool(name="a11_witness")
async def a11_witness(params: WitnessInput) -> str:
    """Write to the witness log."""
    r = await _post("/api/memory/store", {"node_id": "S2_CASE", "type": "witness", "data": {"event_type": params.event_type, "content": params.content}, "ttl": 0})
    return json.dumps({"written": True, "event": params.event_type, "result": r}, indent=2)

@mcp.tool(name="a11_speak")
async def a11_speak(params: VoiceInput) -> str:
    """S7_ECHO speaks."""
    await _post("/api/voice", {"text": params.text, "mode": params.mode or "realtime"})
    return json.dumps({"node": "S7_ECHO", "text": params.text[:100], "mode": params.mode}, indent=2)

@mcp.tool(name="a11_discover")
async def a11_discover() -> str:
    """See everything."""
    return json.dumps(await _get("/api/discover"), indent=2)

if __name__ == "__main__":
    mcp.run(transport="stdio")
