feat: add world-intel-mcp server — 36 tools across 14 intelligence domains
New MCP server providing real-time global intelligence: financial markets (Yahoo Finance, CoinGecko), economic indicators (FRED, EIA, World Bank), conflict tracking (ACLED, UCDP, HDX), military flights (OpenSky), infrastructure monitoring (Cloudflare Radar, NGA cable health), maritime warnings (NGA), climate anomalies (Open-Meteo), news aggregation (RSS, GDELT), prediction markets (Polymarket), displacement data (UNHCR), aviation delays (FAA), cyber threats (Feodo, CISA KEV, SANS, URLhaus), country intelligence briefs (Ollama LLM), and HTML report generation (Jinja2 + Chart.js). Includes: SQLite TTL cache, per-source circuit breakers, async HTTP fetcher with retry/rate-limiting, Click CLI with 27 commands, 4 analysis modules (instability scoring, geo-convergence, signal aggregation, news clustering), 4 HTML report templates, and 28 unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""SQLite TTL cache for world-intel-mcp.
|
||||
|
||||
Simple key-value cache with per-key TTL, WAL mode, and automatic eviction.
|
||||
No external deps — just stdlib sqlite3.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("world-intel-mcp.cache")
|
||||
|
||||
_DEFAULT_DB = Path.home() / ".cache" / "world-intel-mcp" / "cache.db"
|
||||
|
||||
|
||||
class Cache:
|
||||
"""SQLite-backed TTL cache."""
|
||||
|
||||
def __init__(self, db_path: Path | None = None):
|
||||
self.db_path = db_path or _DEFAULT_DB
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._conn: sqlite3.Connection | None = None
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self) -> None:
|
||||
conn = self._get_conn()
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
expires_at REAL NOT NULL,
|
||||
created_at REAL NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at)")
|
||||
conn.commit()
|
||||
|
||||
def _get_conn(self) -> sqlite3.Connection:
|
||||
if self._conn is None:
|
||||
self._conn = sqlite3.connect(str(self.db_path), timeout=10)
|
||||
self._conn.execute("PRAGMA journal_mode=WAL")
|
||||
self._conn.execute("PRAGMA busy_timeout=5000")
|
||||
self._conn.execute("PRAGMA synchronous=NORMAL")
|
||||
return self._conn
|
||||
|
||||
def get(self, key: str) -> Any | None:
|
||||
"""Get a cached value. Returns None if missing or expired."""
|
||||
conn = self._get_conn()
|
||||
row = conn.execute(
|
||||
"SELECT value, expires_at FROM cache WHERE key = ?", (key,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
value, expires_at = row
|
||||
if time.time() > expires_at:
|
||||
conn.execute("DELETE FROM cache WHERE key = ?", (key,))
|
||||
conn.commit()
|
||||
return None
|
||||
return json.loads(value)
|
||||
|
||||
def set(self, key: str, value: Any, ttl_seconds: int) -> None:
|
||||
"""Store a value with TTL in seconds."""
|
||||
conn = self._get_conn()
|
||||
now = time.time()
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO cache (key, value, expires_at, created_at) VALUES (?, ?, ?, ?)",
|
||||
(key, json.dumps(value, default=str), now + ttl_seconds, now),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
"""Delete a specific key."""
|
||||
conn = self._get_conn()
|
||||
conn.execute("DELETE FROM cache WHERE key = ?", (key,))
|
||||
conn.commit()
|
||||
|
||||
def evict_expired(self) -> int:
|
||||
"""Remove all expired entries. Returns count removed."""
|
||||
conn = self._get_conn()
|
||||
cursor = conn.execute("DELETE FROM cache WHERE expires_at < ?", (time.time(),))
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
"""Cache statistics."""
|
||||
conn = self._get_conn()
|
||||
now = time.time()
|
||||
total = conn.execute("SELECT COUNT(*) FROM cache").fetchone()[0]
|
||||
expired = conn.execute(
|
||||
"SELECT COUNT(*) FROM cache WHERE expires_at < ?", (now,)
|
||||
).fetchone()[0]
|
||||
return {
|
||||
"total_entries": total,
|
||||
"active_entries": total - expired,
|
||||
"expired_entries": expired,
|
||||
"db_path": str(self.db_path),
|
||||
}
|
||||
|
||||
def close(self) -> None:
|
||||
if self._conn:
|
||||
self._conn.close()
|
||||
self._conn = None
|
||||
Reference in New Issue
Block a user