Add SQLite-cached check_holidays MCP tool with pre-analysis checklist

- Add check_holidays MCP tool in server.py with SQLite caching (cache-first, API-fallback)
- Add Section 6 Pre-Analysis Checklist (market status, holidays, news, analysis type) to SKILL.md
- Add holidays-data named volume to docker-compose.yml for DB persistence
- Add API_CO_ID_KEY and HOLIDAYS_DB_PATH env vars to Dockerfile and .env.example
- Remove scripts/ directory (replaced by MCP tool), slim down Dockerfile
This commit is contained in:
Achmad
2026-05-17 07:31:51 +00:00
parent 568be26adc
commit c31096fe4b
5 changed files with 361 additions and 2 deletions
+243
View File
@@ -817,6 +817,249 @@ def clear_session() -> str:
return "Session ID cleared"
# ---------------------------------------------------------------------------
# Holiday Checker (api.co.id) — SQLite backed
# ---------------------------------------------------------------------------
import sqlite3
from datetime import date as dt_date, datetime
from pathlib import Path
_HOLIDAYS_API_KEY: str | None = None
_HOLIDAYS_DB: str | None = None
def _get_holidays_api_key() -> str | None:
global _HOLIDAYS_API_KEY
if _HOLIDAYS_API_KEY is None:
_HOLIDAYS_API_KEY = os.environ.get("API_CO_ID_KEY") or ""
return _HOLIDAYS_API_KEY or None
def _get_holidays_db_path() -> str:
global _HOLIDAYS_DB
if _HOLIDAYS_DB is None:
_HOLIDAYS_DB = os.environ.get("HOLIDAYS_DB_PATH") or str(Path(__file__).parent / "holidays.db")
return _HOLIDAYS_DB
def _init_holidays_db() -> sqlite3.Connection:
db_path = _get_holidays_db_path()
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS holidays (
date TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
is_national INTEGER NOT NULL DEFAULT 1
)
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date)")
conn.commit()
return conn
def _cache_holidays(items: list[dict]) -> None:
conn = _init_holidays_db()
for h in items:
conn.execute(
"INSERT OR REPLACE INTO holidays (date, name, type, is_national) VALUES (?, ?, ?, ?)",
(h["date"], h["name"], h.get("type", "Public Holiday"), 1 if h.get("is_national_holiday", True) else 0),
)
conn.commit()
conn.close()
def _query_cached_holidays(
start_date: str = "",
end_date: str = "",
limit: int = 0,
) -> list[dict]:
conn = _init_holidays_db()
today = datetime.now().strftime("%Y-%m-%d")
if start_date and end_date:
rows = conn.execute(
"SELECT date, name, type, is_national FROM holidays WHERE date >= ? AND date <= ? ORDER BY date",
(start_date, end_date),
).fetchall()
elif start_date:
rows = conn.execute(
"SELECT date, name, type, is_national FROM holidays WHERE date >= ? ORDER BY date",
(start_date,),
).fetchall()
elif limit:
rows = conn.execute(
"SELECT date, name, type, is_national FROM holidays WHERE date >= ? ORDER BY date LIMIT ?",
(today, limit),
).fetchall()
else:
rows = conn.execute(
"SELECT date, name, type, is_national FROM holidays WHERE date >= ? ORDER BY date",
(today,),
).fetchall()
conn.close()
return [
{"date": r[0], "name": r[1], "type": r[2], "is_national_holiday": bool(r[3])}
for r in rows
]
def _fetch_and_cache_holidays(year: int) -> list[dict]:
"""Fetch holidays for a year from API and cache them."""
api_key = _get_holidays_api_key()
if not api_key:
return []
resp = requests.get(
"https://use.api.co.id/holidays/indonesia/",
headers={"x-api-co-id": api_key},
params={"year": year},
timeout=10,
)
if resp.status_code != 200:
return []
data = resp.json()
if not data.get("is_success"):
return []
items = data.get("data", [])
if not isinstance(items, list):
items = [items]
if items:
_cache_holidays(items)
return items
def _format_holidays(items: list[dict]) -> str:
if not items:
return "No holidays found."
today = datetime.now().date()
lines: list[str] = []
lines.append(f"{'Date':<15} {'Day':<10} {'Holiday':<45} {'Type':<20}")
lines.append("-" * 90)
for h in items:
d_str = h.get("date", "?")
name = h.get("name", "?")
typ = h.get("type", "?")
try:
dt = datetime.strptime(d_str, "%Y-%m-%d").date()
day_name = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()]
delta = (dt - today).days
if delta == 0:
suffix = " ⬥ TODAY"
elif delta == 1:
suffix = " ⬥ Tomorrow"
elif delta > 0:
suffix = f" ({delta}d away)"
elif delta < 0:
suffix = f" ({abs(delta)}d ago)"
else:
suffix = ""
except ValueError:
day_name = "?"
suffix = ""
lines.append(f"{d_str:<15} {day_name:<10} {name:<45} {typ:<20}{suffix}")
return "\n".join(lines)
@mcp.tool()
def check_holidays(
mode: str = "upcoming",
year: int | None = None,
date: str | None = None,
start_date: str | None = None,
end_date: str | None = None,
limit: int = 10,
refresh: bool = False,
) -> str:
"""Check Indonesian public holidays (cached in SQLite).
Modes:
upcoming (default): next N holidays
year: all holidays for a given year (pass `year`)
check: check if a specific date is a holiday (pass `date`, format YYYY-MM-DD)
range: holidays in a date range (pass `start_date` & `end_date`)
Pass refresh=True to re-fetch from API instead of using cache.
Requires API_CO_ID_KEY env var to be set for API calls.
"""
match mode:
case "check":
if not date:
return "Error: `date` param required (YYYY-MM-DD)"
# Try cache first
cached = _query_cached_holidays(start_date=date, end_date=date)
if cached and not refresh:
return _format_holidays(cached)
# Fall back to API
api_key = _get_holidays_api_key()
if not api_key:
return "⚠ No cached data and API_CO_ID_KEY not set."
resp = requests.get(
"https://use.api.co.id/holidays/indonesia/check/date",
headers={"x-api-co-id": api_key},
params={"date": date},
timeout=10,
)
if resp.status_code != 200:
return f"API error: HTTP {resp.status_code}"
data = resp.json()
if not data.get("is_success"):
return f"API error: {data.get('message', 'unknown')}"
d = data.get("data", {})
if not d or not d.get("is_holiday"):
return f"{date} is NOT a holiday."
items = [d.get("holiday", {})]
return _format_holidays(items)
case "range":
if not start_date or not end_date:
return "Error: `start_date` and `end_date` required"
cached = _query_cached_holidays(start_date=start_date, end_date=end_date)
if cached and not refresh:
return _format_holidays(cached)
api_key = _get_holidays_api_key()
if not api_key:
return "⚠ No cached data and API_CO_ID_KEY not set."
resp = requests.get(
"https://use.api.co.id/holidays/indonesia/check/range",
headers={"x-api-co-id": api_key},
params={"start_date": start_date, "end_date": end_date},
timeout=10,
)
if resp.status_code != 200:
return f"API error: HTTP {resp.status_code}"
data = resp.json()
if not data.get("is_success"):
return f"API error: {data.get('message', 'unknown')}"
items = data.get("data", [])
if not isinstance(items, list):
items = [items]
return _format_holidays(items)
case "year":
y = year or datetime.now().year
cached = _query_cached_holidays(start_date=f"{y}-01-01", end_date=f"{y}-12-31")
if cached and not refresh:
return _format_holidays(cached)
items = _fetch_and_cache_holidays(y)
return _format_holidays(items)
case _: # upcoming
cached = _query_cached_holidays(limit=limit)
if cached and not refresh:
return _format_holidays(cached)
# Fetch current & next year to build cache, then return from cache
current_year = datetime.now().year
_fetch_and_cache_holidays(current_year)
_fetch_and_cache_holidays(current_year + 1)
cached = _query_cached_holidays(limit=limit)
return _format_holidays(cached) if cached else "No upcoming holidays found."
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TradingView Screener MCP Server")
parser.add_argument(