Fix dashboard startup and space weather parsing

This commit is contained in:
Marc Shade
2026-06-04 11:18:09 -04:00
parent 625788a830
commit 4528a6ed69
10 changed files with 260 additions and 39 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What This Is ## What This Is
World Intelligence MCP Server — 110 tools across 30+ domains providing real-time global intelligence from free public APIs. Serves four interfaces: MCP stdio (for Claude Code/Cursor), a live Starlette dashboard with SSE, a Click CLI with Rich output, and a collector daemon for 24/7 vector store population. Python 3.11+, built with hatchling. World Intelligence MCP Server — 113 tools across 30+ domains providing real-time global intelligence from free public APIs. Serves four interfaces: MCP stdio (for Claude Code/Cursor), a live Starlette dashboard with SSE, a Click CLI with Rich output, and a collector daemon for 24/7 vector store population. Python 3.11+, built with hatchling.
## Commands ## Commands
+7 -4
View File
@@ -6,7 +6,7 @@
[![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-green)](https://python.org) [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-green)](https://python.org)
[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE) [![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)
Real-time global intelligence across **30+ domains** with **109 MCP tools**, a live ops-center dashboard, a CLI, and a **Qdrant vector store** for enterprise-grade semantic search across accumulated intelligence. All data comes from free, public APIs — no paid subscriptions required. Real-time global intelligence across **30+ domains** with **113 MCP tools**, a live ops-center dashboard, a CLI, and a **Qdrant vector store** for enterprise-grade semantic search across accumulated intelligence. All data comes from free, public APIs — no paid subscriptions required.
Built for AI agents that need world awareness: market conditions, geopolitical risk, military posture, supply chain disruptions, cyber threats, and more — all queryable via the Model Context Protocol. The vector store enables natural language queries like *"military activity near Taiwan"* or *"cyber threats targeting healthcare"* across all historical data. Built for AI agents that need world awareness: market conditions, geopolitical risk, military posture, supply chain disruptions, cyber threats, and more — all queryable via the Model Context Protocol. The vector store enables natural language queries like *"military activity near Taiwan"* or *"cyber threats targeting healthcare"* across all historical data.
@@ -23,7 +23,7 @@ Built for AI agents that need world awareness: market conditions, geopolitical r
| **SEC Filings** | 3 | SEC EDGAR (full-text search, company filings, 8-K material events) | | **SEC Filings** | 3 | SEC EDGAR (full-text search, company filings, 8-K material events) |
| **Company Enrichment** | 1 | Yahoo Finance + GDELT + SEC + GitHub (composite profile) | | **Company Enrichment** | 1 | Yahoo Finance + GDELT + SEC + GitHub (composite profile) |
| **Macro Composite** | 1 | Weighted 6-signal market verdict (Fear&Greed, VIX, sectors, DXY, BTC, yields) | | **Macro Composite** | 1 | Weighted 6-signal market verdict (Fear&Greed, VIX, sectors, DXY, BTC, yields) |
| **Economic Indicators** | 3 | EIA energy, FRED macro, World Bank | | **Economic Indicators** | 6 | AAA fuel prices, EIA energy, FRED macro, World Bank |
| **Central Banks** | 1 | 8 central bank policy rates | | **Central Banks** | 1 | 8 central bank policy rates |
| **BTC Technicals** | 1 | SMA 50/200, golden/death cross, Mayer Multiple | | **BTC Technicals** | 1 | SMA 50/200, golden/death cross, Mayer Multiple |
| **Natural Disasters** | 2 | USGS earthquakes, NASA FIRMS wildfires | | **Natural Disasters** | 2 | USGS earthquakes, NASA FIRMS wildfires |
@@ -57,7 +57,7 @@ Built for AI agents that need world awareness: market conditions, geopolitical r
| **Cross-Domain Analytics** | 3 | Correlation, domain summary, trend detection | | **Cross-Domain Analytics** | 3 | Correlation, domain summary, trend detection |
| **Reports** | 1 | PDF/HTML multi-domain intelligence reports | | **Reports** | 1 | PDF/HTML multi-domain intelligence reports |
**Total: 110 tools** across 30+ intelligence domains. **Total: 113 tools** across 30+ intelligence domains.
--- ---
@@ -194,9 +194,12 @@ collector.py (daemon) ─┘
|------|-------------| |------|-------------|
| `intel_macro_composite` | Weighted market score (0-100) with verdict: RISK_ON to STRONG_CAUTION | | `intel_macro_composite` | Weighted market score (0-100) with verdict: RISK_ON to STRONG_CAUTION |
### Economic (3) ### Economic (6)
| Tool | Description | | Tool | Description |
|------|-------------| |------|-------------|
| `intel_gas_prices` | Daily US retail gasoline, diesel, and E85 prices from AAA |
| `intel_residential_natgas` | US residential natural gas prices from EIA |
| `intel_electricity_rates` | US electricity retail rates by sector/state from EIA |
| `intel_energy_prices` | Brent/WTI crude oil and natural gas from EIA | | `intel_energy_prices` | Brent/WTI crude oil and natural gas from EIA |
| `intel_fred_series` | FRED economic data (GDP, CPI, unemployment, rates) | | `intel_fred_series` | FRED economic data (GDP, CPI, unemployment, rates) |
| `intel_world_bank_indicators` | World Bank development indicators by country | | `intel_world_bank_indicators` | World Bank development indicators by country |
+18 -10
View File
@@ -1,8 +1,8 @@
# World Intel MCP — Feature Parity Roadmap # World Intel MCP — Feature Parity Roadmap
**Benchmark**: [koala73/worldmonitor](https://github.com/koala73/worldmonitor) **Benchmark**: [koala73/worldmonitor](https://github.com/koala73/worldmonitor)
**Updated**: 2026-03-08 **Updated**: 2026-06-04
**Current tools**: 110 (109 intel + 1 status) **Current tools**: 113 (112 intel + 1 status)
--- ---
@@ -20,11 +20,11 @@
| Area | Finding | Status | Action | | Area | Finding | Status | Action |
|------|---------|--------|--------| |------|---------|--------|--------|
| MCP tool parity | 110 tools declared in `TOOLS`; 110 routed in `_dispatch()` | :white_check_mark: | Keep as an invariant | | MCP tool parity | 113 tools declared in `TOOLS`; 113 routed in `_dispatch()` | :white_check_mark: | Keep as an invariant |
| Optional vector runtime | Missing `qdrant-client` / `fastembed` previously surfaced as runtime failures | :white_check_mark: Fixed | Vector features now degrade cleanly and report availability | | Optional vector runtime | Missing `qdrant-client` / `fastembed` previously surfaced as runtime failures | :white_check_mark: Fixed | Vector features now degrade cleanly and report availability |
| Base-environment test run | `pytest -q` fails collection without dev extras because `respx` is not installed | :yellow_circle: | Run `pip install -e ".[dev]"` before full-suite validation | | Base-environment test run | `pytest -q` fails collection without dev extras because `respx` is not installed | :yellow_circle: | Run `pip install -e ".[dev]"` before full-suite validation |
| Core verification | 127 infrastructure/report/vector tests pass in the base environment | :white_check_mark: | `test_reports.py`, `test_cache.py`, `test_analysis.py`, `test_vector_store.py` | | Core verification | 226 non-smoke tests pass with dev extras installed | :white_check_mark: | Full default `pytest` run |
| Documentation drift | Prior roadmap documented 89 tools while the codebase exposed 110 | :white_check_mark: Updated below | Keep roadmap synced with phase increments | | Documentation drift | Prior roadmap documented 89/110 tools while the codebase now exposes 113 | :white_check_mark: Updated below | Keep roadmap synced with phase increments |
| Maintainability | `src/world_intel_mcp/server.py` is ~2.5k lines and remains the main refactor target | :yellow_circle: | Split tool registry and dispatch by domain | | Maintainability | `src/world_intel_mcp/server.py` is ~2.5k lines and remains the main refactor target | :yellow_circle: | Split tool registry and dispatch by domain |
### Implemented Addendum Missing From Prior Roadmap ### Implemented Addendum Missing From Prior Roadmap
@@ -69,7 +69,7 @@
## 1. Data Sources — Complete Inventory ## 1. Data Sources — Complete Inventory
### Markets & Economics (13 tools) ### Markets & Economics (16 tools)
| Tool | WM Equivalent | Status | | Tool | WM Equivalent | Status |
|------|---------------|--------| |------|---------------|--------|
@@ -80,6 +80,9 @@
| `intel_sector_heatmap` | `get-sector-summary` | :white_check_mark: | | `intel_sector_heatmap` | `get-sector-summary` | :white_check_mark: |
| `intel_macro_signals` | `get-macro-signals` | :white_check_mark: | | `intel_macro_signals` | `get-macro-signals` | :white_check_mark: |
| `intel_commodity_quotes` | `list-commodity-quotes` | :white_check_mark: | | `intel_commodity_quotes` | `list-commodity-quotes` | :white_check_mark: |
| `intel_gas_prices` | AAA retail fuel prices | :white_check_mark: |
| `intel_residential_natgas` | EIA residential natural gas prices | :white_check_mark: |
| `intel_electricity_rates` | EIA electricity rates by sector/state | :white_check_mark: |
| `intel_energy_prices` | `get-energy-prices` | :white_check_mark: | | `intel_energy_prices` | `get-energy-prices` | :white_check_mark: |
| `intel_fred_series` | `get-fred-series` | :white_check_mark: | | `intel_fred_series` | `get-fred-series` | :white_check_mark: |
| `intel_world_bank_indicators` | `list-world-bank-indicators` | :white_check_mark: | | `intel_world_bank_indicators` | `list-world-bank-indicators` | :white_check_mark: |
@@ -422,17 +425,22 @@ Added historical cross-category correlation, stored-data summarization, and rece
Added PDF/HTML intelligence report generation over the existing multi-domain data collection stack. PDF output remains optional behind `.[pdf]`, with HTML fallback available when WeasyPrint is not installed. Added PDF/HTML intelligence report generation over the existing multi-domain data collection stack. PDF output remains optional behind `.[pdf]`, with HTML fallback available when WeasyPrint is not installed.
### Phase 18: Consumer Energy Signals (+3 = 113 tools)
`intel_gas_prices`, `intel_residential_natgas`, `intel_electricity_rates`
Added retail fuel, residential natural gas, and electricity-rate tools to round out consumer energy monitoring alongside existing EIA crude, gas, FRED, and World Bank economic signals.
--- ---
## Summary ## Summary
| Category | Current | Notes | | Category | Current | Notes |
|----------|---------|-------| |----------|---------|-------|
| Total MCP tools | 110 | 109 intelligence tools + `intel_status` | | Total MCP tools | 113 | 112 intelligence tools + `intel_status` |
| Tool parity | 110 / 110 | `TOOLS` and `_dispatch()` are aligned | | Tool parity | 113 / 113 | `TOOLS` and `_dispatch()` are aligned |
| Static datasets | 18 | Bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges, trade routes, cloud regions, financial centers | | Static datasets | 18 | Bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges, trade routes, cloud regions, financial centers |
| RSS feeds | 119 | 24 categories | | RSS feeds | 119 | 24 categories |
| Tests in repo | 344 | 11 test files; full suite requires `.[dev]` | | Tests in repo | 244 | 226 non-smoke tests + 18 live smoke tests; full suite requires `.[dev]` |
| Primary remaining gap | Architecture | `server.py` monolith remains the main refactor target | | Primary remaining gap | Architecture | `server.py` monolith remains the main refactor target |
**Bottom line**: 110 tools across 30+ domains, with the roadmap now aligned to the live MCP registry. The main remaining gaps are full-environment test bootstrapping (`.[dev]`) and continued modularization of the monolithic `server.py` tool registry/dispatcher. **Bottom line**: 113 tools across 30+ domains, with the roadmap now aligned to the live MCP registry. The main remaining gaps are full-environment test bootstrapping (`.[dev]`) and continued modularization of the monolithic `server.py` tool registry/dispatcher.
+28 -3
View File
@@ -6,7 +6,9 @@ No external deps — just stdlib sqlite3.
import json import json
import logging import logging
import os
import sqlite3 import sqlite3
import tempfile
import time import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -16,16 +18,39 @@ logger = logging.getLogger("world-intel-mcp.cache")
_DEFAULT_DB = Path.home() / ".cache" / "world-intel-mcp" / "cache.db" _DEFAULT_DB = Path.home() / ".cache" / "world-intel-mcp" / "cache.db"
def _default_db_path() -> Path:
if env_path := os.environ.get("WORLD_INTEL_CACHE_DB"):
return Path(env_path).expanduser()
if xdg_cache := os.environ.get("XDG_CACHE_HOME"):
return Path(xdg_cache).expanduser() / "world-intel-mcp" / "cache.db"
return _DEFAULT_DB
class Cache: class Cache:
"""SQLite-backed TTL cache.""" """SQLite-backed TTL cache."""
def __init__(self, db_path: Path | None = None): def __init__(self, db_path: Path | None = None):
self.db_path = db_path or _DEFAULT_DB explicit_path = db_path is not None
self.db_path.parent.mkdir(parents=True, exist_ok=True) self.db_path = Path(db_path) if db_path is not None else _default_db_path()
self._conn: sqlite3.Connection | None = None self._conn: sqlite3.Connection | None = None
self._init_db() try:
self._init_db()
except (OSError, sqlite3.OperationalError) as exc:
if explicit_path:
raise
fallback = Path(tempfile.gettempdir()) / "world-intel-mcp" / "cache.db"
logger.warning(
"Cache unavailable at %s: %s; falling back to %s",
self.db_path,
exc,
fallback,
)
self.close()
self.db_path = fallback
self._init_db()
def _init_db(self) -> None: def _init_db(self) -> None:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = self._get_conn() conn = self._get_conn()
conn.execute(""" conn.execute("""
CREATE TABLE IF NOT EXISTS cache ( CREATE TABLE IF NOT EXISTS cache (
+37 -1
View File
@@ -648,10 +648,42 @@ app = Starlette(
) )
def run(host: str = "127.0.0.1", port: int = 8501) -> None: def _parse_run_args(argv: list[str] | None = None) -> tuple[str, int]:
"""Parse dashboard CLI args for the console script and module runner."""
import argparse
import os
default_port = int(
os.environ.get(
"WORLD_INTEL_DASHBOARD_PORT",
os.environ.get("PORT", "8501"),
)
)
parser = argparse.ArgumentParser(description="Run the World Intelligence dashboard")
parser.add_argument(
"--host",
default=os.environ.get("WORLD_INTEL_DASHBOARD_HOST", "127.0.0.1"),
help="Interface to bind (default: 127.0.0.1)",
)
parser.add_argument(
"--port",
type=int,
default=default_port,
help="Port to bind (default: 8501, or WORLD_INTEL_DASHBOARD_PORT/PORT)",
)
args = parser.parse_args(argv)
return args.host, args.port
def run(host: str | None = None, port: int | None = None) -> None:
"""Launch the dashboard server.""" """Launch the dashboard server."""
import uvicorn import uvicorn
if host is None or port is None:
parsed_host, parsed_port = _parse_run_args()
host = host or parsed_host
port = parsed_port if port is None else port
logger.info("Starting Intelligence Dashboard on http://%s:%d", host, port) logger.info("Starting Intelligence Dashboard on http://%s:%d", host, port)
uvicorn.run( uvicorn.run(
app, app,
@@ -660,3 +692,7 @@ def run(host: str = "127.0.0.1", port: int = 8501) -> None:
log_level="info", log_level="info",
access_log=False, access_log=False,
) )
if __name__ == "__main__":
run()
+2
View File
@@ -32,6 +32,8 @@ Phase 17: Cross-domain analytics — cross-domain correlation, domain summary, t
(+3 = 109 tools). Historical analysis and early warning from accumulated vector data. (+3 = 109 tools). Historical analysis and early warning from accumulated vector data.
Phase 18: PDF/HTML intelligence reports (+1 = 110 tools). WeasyPrint-based multi-section Phase 18: PDF/HTML intelligence reports (+1 = 110 tools). WeasyPrint-based multi-section
report generation covering 18 intelligence domains in parallel. report generation covering 18 intelligence domains in parallel.
Phase 19: Consumer energy signals (+3 = 113 tools). Retail fuel, residential natural gas,
and electricity rates round out consumer energy monitoring.
""" """
import asyncio import asyncio
+39 -20
View File
@@ -81,6 +81,21 @@ def _classify_xray(flux: float) -> str:
return "A" return "A"
def _parse_kp_row(row) -> tuple[str | None, float] | None:
"""Parse NOAA Kp rows from either current dict or legacy list payloads."""
try:
if isinstance(row, dict):
raw_kp = row.get("Kp", row.get("kp"))
if raw_kp is None:
return None
return row.get("time_tag") or row.get("time"), float(raw_kp)
if isinstance(row, list) and len(row) >= 2:
return row[0], float(row[1])
except (ValueError, TypeError):
return None
return None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public API # Public API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -135,31 +150,35 @@ async def fetch_space_weather(fetcher: Fetcher) -> dict:
# --- Kp index --- # --- Kp index ---
if kp_data and isinstance(kp_data, list) and len(kp_data) > 1: if kp_data and isinstance(kp_data, list) and len(kp_data) > 1:
# First row is header, rest are data [time_tag, Kp, ...] # NOAA has served both legacy list rows [time_tag, Kp, ...] and
try: # current dict rows {"time_tag": "...", "Kp": ...}; tolerate both.
# Get most recent Kp reading latest = next(
latest = kp_data[-1] (
kp_val = float(latest[1]) parsed
for parsed in (_parse_kp_row(row) for row in reversed(kp_data))
if parsed is not None
),
None,
)
if latest is not None:
_, kp_val = latest
result["current_kp"] = kp_val result["current_kp"] = kp_val
result["kp_level"] = _classify_kp(kp_val) result["kp_level"] = _classify_kp(kp_val)
# Last 8 readings (24 hours of 3-hourly data) recent = []
recent = [] for row in kp_data[-8:]:
for row in kp_data[-9:-1]: # skip header parsed = _parse_kp_row(row)
if isinstance(row, list) and len(row) >= 2: if parsed is None:
try: continue
recent.append({ row_time, kp_val = parsed
"time": row[0], recent.append({
"kp": float(row[1]), "time": row_time,
}) "kp": kp_val,
except (ValueError, TypeError, IndexError): })
pass result["kp_recent"] = recent
result["kp_recent"] = recent
except (ValueError, TypeError, IndexError) as exc:
logger.warning("Failed to parse Kp data: %s", exc)
# --- X-ray flux (flare activity) --- # --- X-ray flux (flare activity) ---
if flare_data and isinstance(flare_data, list) and len(flare_data) > 1: if flare_data and isinstance(flare_data, list):
try: try:
# Last entry has the most recent flux reading # Last entry has the most recent flux reading
latest_flare = flare_data[-1] latest_flare = flare_data[-1]
+33
View File
@@ -1,5 +1,6 @@
"""Tests for SQLite TTL cache.""" """Tests for SQLite TTL cache."""
import tempfile
import time import time
from pathlib import Path from pathlib import Path
@@ -64,3 +65,35 @@ def test_complex_values(cache: Cache) -> None:
data = {"nested": {"list": [1, 2, 3], "bool": True, "null": None}} data = {"nested": {"list": [1, 2, 3], "bool": True, "null": None}}
cache.set("complex", data, ttl_seconds=60) cache.set("complex", data, ttl_seconds=60)
assert cache.get("complex") == data assert cache.get("complex") == data
def test_default_path_honors_env_override(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
db_path = tmp_path / "custom-cache.db"
monkeypatch.setenv("WORLD_INTEL_CACHE_DB", str(db_path))
cache = Cache()
try:
assert cache.db_path == db_path
cache.set("env", "ok", ttl_seconds=60)
assert cache.get("env") == "ok"
finally:
cache.close()
def test_default_path_falls_back_when_unavailable(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
bad_path = tmp_path / "cache-dir"
bad_path.mkdir()
monkeypatch.setenv("WORLD_INTEL_CACHE_DB", str(bad_path))
cache = Cache()
try:
expected = Path(tempfile.gettempdir()) / "world-intel-mcp" / "cache.db"
assert cache.db_path == expected
cache.set("fallback", "ok", ttl_seconds=60)
assert cache.get("fallback") == "ok"
finally:
cache.close()
@@ -0,0 +1,29 @@
"""Tests for dashboard runner configuration."""
import pytest
from world_intel_mcp.dashboard.app import _parse_run_args
def test_parse_run_args_port() -> None:
host, port = _parse_run_args(["--port", "8765"])
assert host == "127.0.0.1"
assert port == 8765
def test_parse_run_args_host_and_port() -> None:
host, port = _parse_run_args(["--host", "0.0.0.0", "--port", "9000"])
assert host == "0.0.0.0"
assert port == 9000
def test_parse_run_args_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("WORLD_INTEL_DASHBOARD_HOST", "0.0.0.0")
monkeypatch.setenv("WORLD_INTEL_DASHBOARD_PORT", "7777")
host, port = _parse_run_args([])
assert host == "0.0.0.0"
assert port == 7777
+66
View File
@@ -1176,6 +1176,72 @@ async def test_fetch_central_bank_rates_with_fred(fetcher: Fetcher) -> None:
os.environ.pop("FRED_API_KEY", None) os.environ.pop("FRED_API_KEY", None)
# ---------------------------------------------------------------------------
# Space Weather
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_space_weather_current_kp_dict_payload(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.space_weather import (
_ALERTS_URL,
_FLARE_URL,
_KP_URL,
fetch_space_weather,
)
respx.get(_KP_URL).mock(
return_value=httpx.Response(
200,
json=[
{
"time_tag": "2026-06-04T03:00:00",
"Kp": 4.67,
"a_running": 17,
"station_count": 8,
},
{
"time_tag": "2026-06-04T06:00:00",
"Kp": 5.0,
"a_running": 20,
"station_count": 8,
},
],
)
)
respx.get(_FLARE_URL).mock(
return_value=httpx.Response(
200,
json=[{"time_tag": "2026-06-04T06:00:00Z", "flux": 1.2e-5}],
)
)
respx.get(_ALERTS_URL).mock(
return_value=httpx.Response(
200,
json=[
{
"issue_datetime": "2026-06-04T06:10:00Z",
"message": "Geomagnetic storm conditions observed",
"product_id": "WATA50",
}
],
)
)
result = await fetch_space_weather(fetcher)
assert result["source"] == "noaa-swpc"
assert result["current_kp"] == 5.0
assert result["kp_level"] == "G1 Minor"
assert result["kp_recent"][-1] == {
"time": "2026-06-04T06:00:00",
"kp": 5.0,
}
assert result["latest_flare_class"] == "M1.2"
assert result["alerts"][0]["product_id"] == "WATA50"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# USNI Fleet Tracker # USNI Fleet Tracker
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------