1"""EODHD source adapter — thin shortcut over :class:`MCPSource`. 2 3EODHD hosts an MCP server at ``https://mcpv2.eodhd.dev/v1/mcp`` with 77 4read-only financial-data tools (EOD bars, fundamentals, earnings 5calendar, news, etc.). This adapter pins the URL so YAML bindings only 6need to specify the tool name + ticker. 7 8Config keys: 9 tool: EODHD MCP tool name (required). Examples: 10 - ``get_fundamentals`` — company fundamentals 11 - ``get_eod_data`` — EOD bars 12 - ``get_earnings_calendar`` — upcoming earnings 13 - ``get_news`` — financial news 14 - ``resolve_ticker`` — name/ISIN → SYMBOL.EXCH 15 api_key: optional override; defaults to ``$EODHD_API_KEY``. 16 params: dict of arguments passed to the tool (e.g. ``{symbol: NVDA.US}``). 17 base_url: override base MCP URL (default ``https://mcpv2.eodhd.dev``). 18 19Degrades to ``{"error": "...", "data": ...}`` when ``$EODHD_API_KEY`` is 20unset, so an arena binding that's intended only for users with EODHD 21credentials doesn't crash for everyone else. 22""" 23 24from __future__ import annotations 25 26import os 27from typing import Any 28 29from maf.sources.adapters.mcp_remote import MCPSource 30 31 32DEFAULT_BASE_URL = "https://mcpv2.eodhd.dev/v1/mcp" 33 34 35class EODHDSource(MCPSource): 36 """EODHD-specific shortcut. Resolves the URL + API key automatically.""" 37 38 adapter_name = "eodhd" 39 40 @classmethod 41 def freshness_spec(cls, binding_config: dict[str, Any]) -> dict[str, Any]: 42 tool = binding_config.get("tool") or "(any)" 43 return { 44 "type": "external", 45 "detail": f"EODHD MCP · tool={tool}", 46 } 47 48 async def fetch(self, params: dict[str, Any] | None = None) -> dict[str, Any]: 49 # Compose URL from base + api_key (env or config). Pass into MCPSource. 50 cfg = {**self.config, **(params or {})} 51 base = cfg.get("base_url") or DEFAULT_BASE_URL 52 api_key = cfg.get("api_key") or os.environ.get("EODHD_API_KEY") 53 54 if not api_key: 55 return self._err( 56 cfg.get("tool"), 57 base, 58 "EODHD_API_KEY not set — sign up at eodhd.com and add it to .env", 59 ) 60 61 # Inject into the base URL as ?apikey=... — that's how EODHD's MCP 62 # v1 server authenticates. (v2 uses OAuth.) 63 sep = "&" if "?" in base else "?" 64 url = f"{base}{sep}apikey={api_key}" 65 66 # Delegate to MCPSource.fetch with the composed URL. 67 merged_params: dict[str, Any] = { 68 **(params or {}), 69 "url": url, 70 # ensure 'tool' propagates even when only set in config 71 "tool": cfg.get("tool"), 72 } 73 result = await super().fetch(merged_params) 74 75 # Re-brand the result so freshness map + UI know it's EODHD-shaped. 76 result["type"] = "eodhd" 77 # Don't leak the api-key in the URL we surface back to the caller. 78 if isinstance(result.get("url"), str): 79 result["url"] = base 80 return result