checking system…
Docs / back / src/maf/sources/adapters/eodhd.py · line 25
Python · 81 lines
 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