#!/usr/bin/env python3 """signaldaemon — local stdio MCP bridge to the hosted API. Exposes the same two tools as the hosted remote MCP (https://api.signaldaemon.com/mcp) for clients that only speak stdio. This bridge holds no methodology — it is a thin client over the public API. Setup: pip install mcp export SIGNALDAEMON_API_KEY=cns_... # self-serve: https://signaldaemon.com/console python mcp_server.py The server starts and answers introspection (tools/list) without a key; tool calls require SIGNALDAEMON_API_KEY. """ import json import os import urllib.request from mcp.server.fastmcp import FastMCP from mcp.types import ToolAnnotations API_BASE = os.getenv("SIGNALDAEMON_API_BASE", "https://api.signaldaemon.com") mcp = FastMCP("signaldaemon") def _call(path: str, body: dict) -> dict: key = os.getenv("SIGNALDAEMON_API_KEY", "") if not key: return { "error": "missing_api_key", "hint": "Set SIGNALDAEMON_API_KEY. Self-serve a key at " "https://signaldaemon.com/console (or a no-login demo key " "at https://signaldaemon.com/#access).", } req = urllib.request.Request( API_BASE + path, data=json.dumps(body).encode(), headers={"x-api-key": key, "Content-Type": "application/json"}, ) try: with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) except urllib.error.HTTPError as e: try: detail = json.loads(e.read().decode()) except Exception: detail = {"detail": str(e)} return {"error": f"http_{e.code}", **detail} @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True)) def get_market_narratives(limit: int = 8) -> dict: """Return top crypto market narratives with strength, momentum, and price divergence signals. Includes a market_snapshot (regime, market_7d, fear_greed) that every divergence reading is relative to.""" return _call("/v1/narratives", {"limit": limit}) @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True)) def get_clean_feed(query: str, category: str = None, limit: int = 8) -> dict: """Return a curated, de-noised, source-attributed feed of articles for a query topic. Reports coverage honestly ("thin" rather than padding).""" return _call("/v1/feed", {"query": query, "category": category, "limit": limit}) @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True)) def vet_trade(symbol: str, side: str, horizon: str = None, source: str = None) -> dict: """Before placing a trade, check whether the crypto/AI/macro narrative layer supports, cautions, or contradicts it. Returns a stance (support|caution|contradict|no_signal) with reason + confidence. Read-only, NOT a trade recommendation — it vets YOUR candidate {symbol, side} against cross-source narrative convergence and capital-vs-narrative divergence, and abstains (no_signal) when there is no narrative coverage.""" return _call("/v1/vet", {"symbol": symbol, "side": side, "horizon": horizon, "source": source}) if __name__ == "__main__": mcp.run()