From a2acf478a514104414c726f8468dfb49dcf470d5 Mon Sep 17 00:00:00 2001 From: Aritro Ganguly Date: Sat, 14 Mar 2026 11:00:57 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Blaxel=20deployment=20=E2=80=94=20s?= =?UTF-8?q?treamable-http=20MCP=20server=20with=20=20=20Dockerfile=20and?= =?UTF-8?q?=20blaxel.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +++++++++++++++ backend/Dockerfile | 12 ++++++++++++ backend/blaxel.toml | 4 ++++ backend/mcp_server.py | 21 ++++++++++++++------- 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 backend/blaxel.toml diff --git a/README.md b/README.md index 9ba78df..e083d2f 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,21 @@ If both health check and frontend dev server start without errors, you're good t └── TASKS.md ← Master task list ``` +## Live MCP Endpoint + +The Katabatic MCP server is deployed on Blaxel and accessible as a hosted function: + +``` +https://run.blaxel.ai/{workspace}/functions/helicity-mcp/mcp +``` + +This endpoint uses the `streamable-http` transport and exposes all 5 MCP tools: +`get_stress_scores`, `get_stablecoin_detail`, `project_scenario`, `get_active_alerts`, `get_score_history`. + +AI agents can connect directly to this URL using any MCP-compatible client. + +--- + ## Collaborators - Adi Prathapa diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..89bba4b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["python", "mcp_server.py"] diff --git a/backend/blaxel.toml b/backend/blaxel.toml new file mode 100644 index 0000000..143c184 --- /dev/null +++ b/backend/blaxel.toml @@ -0,0 +1,4 @@ +type = "function" + +[runtime] +transport = "http-stream" diff --git a/backend/mcp_server.py b/backend/mcp_server.py index d493236..36ac210 100644 --- a/backend/mcp_server.py +++ b/backend/mcp_server.py @@ -8,12 +8,14 @@ - get_score_history → recent score history for a stablecoin Transports: - - stdio (default) → local agent integration (Claude Desktop, etc.) - - SSE → remote agent integration (set TRANSPORT=sse) + - streamable-http (default) → Blaxel-hosted / remote agent integration + - stdio → local agent integration (set TRANSPORT=stdio) + - sse → SSE remote transport (set TRANSPORT=sse) Usage: - python mcp_server.py # stdio - TRANSPORT=sse python mcp_server.py # SSE on port 8001 + python mcp_server.py # streamable-http on PORT (default 8000) + TRANSPORT=stdio python mcp_server.py # stdio (local) + TRANSPORT=sse python mcp_server.py # SSE on MCP_PORT (default 8001) """ import os @@ -109,6 +111,9 @@ def _record_history(symbol: str, score_data: dict) -> None: "All scores are derived from GENIUS Act attestation data, FDIC Call Reports, " "NOAA weather feeds, and on-chain Etherscan mint/burn flows." ), + stateless_http=True, + host=os.getenv("HOST", "0.0.0.0"), + port=int(os.getenv("PORT", "8000")), ) @@ -300,10 +305,12 @@ async def get_score_history(symbol: str, limit: int = 10) -> dict: # --------------------------------------------------------------------------- if __name__ == "__main__": - transport = os.getenv("TRANSPORT", "stdio") - if transport == "sse": + transport = os.getenv("TRANSPORT", "streamable-http") + if transport == "stdio": + mcp.run(transport="stdio") + elif transport == "sse": port = int(os.getenv("MCP_PORT", "8001")) print(f"Starting Katabatic MCP server on SSE transport (port {port})") mcp.run(transport="sse", host="0.0.0.0", port=port) else: - mcp.run(transport="stdio") + mcp.run(transport="streamable-http") From 9e827ca97f57f7d89837936ba27112cfa62f7ff3 Mon Sep 17 00:00:00 2001 From: Aritro Ganguly Date: Sat, 14 Mar 2026 16:22:11 -0400 Subject: [PATCH 2/2] fix: resolve merge conflict combining availability flags with error handling --- backend/main.py | 115 ++++++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/backend/main.py b/backend/main.py index ceeb2c0..7ed7b93 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,14 +23,23 @@ from app.services.llm_jury import LLMJuryService from app.services.narratives import NarrativeService from app.services.weather_provider import WeatherProvider +from app.services.etherscan_provider import EtherscanProvider cache = Cache() graph_service = KnowledgeGraphService() llm_jury = LLMJuryService() narrative_service = NarrativeService() weather_provider = WeatherProvider(cache) +etherscan_provider = EtherscanProvider(cache) scoring_engine = None # Initialized on startup +# --- Global Availability Flags --- +CLAUDE_AVAILABLE = False +GEMINI_AVAILABLE = False +ETHERSCAN_AVAILABLE = False +UNSILOED_AVAILABLE = False +IPFS_AVAILABLE = False + def envelope(data=None, error=None): return { @@ -65,50 +74,70 @@ async def health(): @app.on_event("startup") async def startup(): - # Validate API keys - keys = { - "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY"), - "GEMINI_API_KEY": os.getenv("GEMINI_API_KEY"), - "ETHERSCAN_API_KEY": os.getenv("ETHERSCAN_API_KEY"), - "PINATA_API_KEY": os.getenv("PINATA_API_KEY"), - "PINATA_SECRET_API_KEY": os.getenv("PINATA_SECRET_API_KEY"), - "UNSILOED_API_KEY": os.getenv("UNSILOED_API_KEY"), - } - for name, val in keys.items(): - if not val: - print(f" WARNING: {name} not set — related features will be disabled") - else: - print(f" OK: {name} is set") - - # Initialize cache - await cache.initialize() - print(" Cache initialized") - - # Initialize webhook database - from app.services.webhooks import initialize_db as init_webhooks_db - await init_webhooks_db() - print(" Webhook database initialized") - - # Build knowledge graph from registry fixtures - from app.services.registry import get_all_symbols, get_reserve_data - - reserves = {} - for symbol in get_all_symbols(): + import traceback + + try: + # Validate API keys and set availability flags + global CLAUDE_AVAILABLE, GEMINI_AVAILABLE, ETHERSCAN_AVAILABLE, UNSILOED_AVAILABLE, IPFS_AVAILABLE + + keys = { + "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY"), + "GEMINI_API_KEY": os.getenv("GEMINI_API_KEY"), + "ETHERSCAN_API_KEY": os.getenv("ETHERSCAN_API_KEY"), + "PINATA_API_KEY": os.getenv("PINATA_API_KEY"), + "PINATA_SECRET_API_KEY": os.getenv("PINATA_SECRET_API_KEY"), + "UNSILOED_API_KEY": os.getenv("UNSILOED_API_KEY"), + } + + CLAUDE_AVAILABLE = bool(keys["ANTHROPIC_API_KEY"]) + GEMINI_AVAILABLE = bool(keys["GEMINI_API_KEY"]) + ETHERSCAN_AVAILABLE = bool(keys["ETHERSCAN_API_KEY"]) + UNSILOED_AVAILABLE = bool(keys["UNSILOED_API_KEY"]) + IPFS_AVAILABLE = bool(keys["PINATA_API_KEY"]) and bool(keys["PINATA_SECRET_API_KEY"]) + + print("\n--- Helicity Service Availability ---") + print(f" [CORE] Jury & Narratives: {'OK' if CLAUDE_AVAILABLE and GEMINI_AVAILABLE else 'PARTIAL' if CLAUDE_AVAILABLE or GEMINI_AVAILABLE else 'DISABLED (Fix: Set ANTHROPIC/GEMINI keys)'}") + print(f" [ON-CHAIN] Peg Stability: {'OK' if ETHERSCAN_AVAILABLE else 'FIXTURE FALLBACK (Fix: Set ETHERSCAN_API_KEY)'}") + print(f" [VISION] PDF Extraction: {'OK' if UNSILOED_AVAILABLE else 'TEXT-ONLY FALLBACK (Fix: Set UNSILOED_API_KEY)'}") + print(f" [IPFS] Decentralized: {'OK' if IPFS_AVAILABLE else 'LOCAL-ONLY (Fix: Set PINATA_API_KEY + PINATA_SECRET_API_KEY)'}") + print("--------------------------------------\n") + + # Initialize cache + await cache.initialize() + print(" Cache initialized") + + # Initialize webhook database try: - reserves[symbol] = get_reserve_data(symbol) - except (ValueError, FileNotFoundError) as e: - print(f" WARNING: Could not load {symbol}: {e}") - - graph_service.build_from_reserves(reserves) - print(f" Knowledge graph built: {graph_service.graph.number_of_nodes()} nodes, {graph_service.graph.number_of_edges()} edges") - - # Initialize scoring engine - global scoring_engine - from app.services.scoring_engine import ScoringEngine - - scoring_engine = ScoringEngine(cache, graph_service, llm_jury, narrative_service) - print(" Scoring engine ready") - print("Helicity API started successfully.") + from app.services.webhooks import initialize_db as init_webhooks_db + await init_webhooks_db() + print(" Webhook database initialized") + except Exception as e: + print(f" WARNING: Webhook DB init failed: {e}") + + # Build knowledge graph from registry fixtures + from app.services.registry import get_all_symbols, get_reserve_data + + reserves = {} + for symbol in get_all_symbols(): + try: + reserves[symbol] = get_reserve_data(symbol) + except (ValueError, FileNotFoundError) as e: + print(f" WARNING: Could not load {symbol}: {e}") + + graph_service.build_from_reserves(reserves) + print(f" Knowledge graph built: {graph_service.graph.number_of_nodes()} nodes, {graph_service.graph.number_of_edges()} edges") + + # Initialize scoring engine + global scoring_engine + from app.services.scoring_engine import ScoringEngine + + scoring_engine = ScoringEngine(cache, graph_service, llm_jury, narrative_service) + print(" Scoring engine ready") + print("Helicity API started successfully.") + except Exception as e: + print(f" ERROR during startup: {e}") + traceback.print_exc() + print(" Helicity API started in DEGRADED mode — /health will still respond.") @app.on_event("shutdown")