Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 4 additions & 0 deletions backend/blaxel.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type = "function"

[runtime]
transport = "http-stream"
115 changes: 72 additions & 43 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
21 changes: 14 additions & 7 deletions backend/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")),
)


Expand Down Expand Up @@ -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")