# /// script
# requires-python = ">=3.10"
# dependencies = ["mcp>=1.2,<2"]
# ///
"""Dead Giveaway as an MCP server: current rules against Claude's and ChatGPT's habits, and a checker.

The rules are rebuilt from Reddit every time the field guide updates; this server fetches them (cached for an hour), so a
client always gets the current list. Run with uv, no install step:

  uv run dead_giveaway_mcp.py               # stdio server
  claude mcp add dead-giveaway -- uv run /path/to/dead_giveaway_mcp.py
Environment: GIVEAWAY_RULES_URL  base URL of the rules folder (default below).
"""
import json, os, re, time, urllib.request

from mcp.server.fastmcp import FastMCP

BASE = os.environ.get("GIVEAWAY_RULES_URL", "https://data.machinemorning.com/giveaway/rules/").rstrip("/") + "/"
QUOTED = re.compile(r'"[^"\n]*"|“[^”]*”|`[^`\n]*`')
_cache = {}
def visible_text(markup):
    """Text a reader sees on an HTML page: scripts and styles removed, tags dropped, one block per line."""
    markup = re.sub(r"(?is)<(script|style)\b.*?</\1>", " ", markup)
    markup = re.sub(r"(?i)<br\s*/?>|</(p|div|li|h[1-6]|section|tr|td|th|blockquote|figcaption|button|a)>", "\n", markup)
    text = re.sub(r"<[^>]+>", " ", markup)
    text = text.replace("&nbsp;", " ").replace("&amp;", "&").replace("&mdash;", "\u2014").replace("&ndash;", "\u2013")
    return "\n".join(re.sub(r"[ \t]+", " ", l).strip() for l in text.splitlines() if l.strip())


mcp = FastMCP("dead-giveaway")


def rules(vendor):
    vendor = vendor.lower()
    hit = _cache.get(vendor)
    if hit and time.time() - hit[0] < 3600:
        return hit[1]
    req = urllib.request.Request(BASE + f"{vendor}.json", headers={"User-Agent": "dead-giveaway-mcp/1"})
    with urllib.request.urlopen(req, timeout=20) as r:
        data = json.loads(r.read().decode())
    _cache[vendor] = (time.time(), data)
    return data


@mcp.tool()
def get_rules(vendor: str = "claude", task: str = "") -> str:
    """Current rules for avoiding the habits Reddit users point out in work made by a model.
    vendor: "claude" or "chatgpt". task: "writing", "web_ui", "slides", "code", or empty for all.
    Each rule says what to do instead and links to the evidence."""
    d = rules(vendor)
    out = [f"Rules against {d['vendor']}'s habits, rebuilt {d['generated'][:16]} UTC from {d['people']:,} people's comments."]
    for h in d["habits"]:
        if task and h["applies_to"] != task:
            continue
        sub = (" The same applies to " + ", ".join(f'"{x}"' for x in h["substitutes"]) + ".") if h.get("substitutes") else ""
        out.append(f"- {h['name']}: {h['instruction']}{sub} ({h['people_60d']} people in the last 60 days, {h['card_url']})")
    return "\n".join(out)


@mcp.tool()
def check_text(text: str, vendor: str = "claude", kind: str = "text") -> str:
    """Check a draft for the habits that have a mechanical check. kind: "text" (prose), "html", "css", "code" or "commit" (a commit message).
    Returns one line per hit with the habit and a link to the evidence, or "no hits"."""
    d = rules(vendor)
    kinds = {"html", "css", "text"} if kind == "html" else {kind}
    hits, source = [], text
    seen = visible_text(source) if kind == "html" else source    # prose rules read what a visitor sees
    for h in d["habits"]:
        for c in h.get("checks", []):
            if c["target"] not in kinds:
                continue
            rx = re.compile(c["regex"], re.I if "i" in c.get("flags", "") else 0)
            found = [(1, m.group(0)) for m in rx.finditer(text)] if c["target"] == "commit" else []
            for no, line in enumerate((seen if c["target"] == "text" else source).splitlines() if c["target"] != "commit" else [], 1):
                probe = QUOTED.sub("Q", line) if c["target"] == "text" else line
                found += [(no, m.group(0)) for m in rx.finditer(probe)]
            if len(found) < c.get("min", 1):    # habits about frequency count only when they repeat
                continue
            hits += [f"line {no}: {h['name']} ({c['what']}): {g[:60]!r}  {h['card_url']}" for no, g in found]
    return "\n".join(hits) if hits else "no hits"


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