crypto-intel-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cristian Amigo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: crypto-intel-mcp
3
+ Version: 0.1.0
4
+ Summary: Crypto market intelligence (price, momentum, trending) over MCP.
5
+ Author-email: Cristian Amigo <cstamigo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cstamigo-droid/crypto-intel-mcp
8
+ Project-URL: Repository, https://github.com/cstamigo-droid/crypto-intel-mcp
9
+ Project-URL: Bug Tracker, https://github.com/cstamigo-droid/crypto-intel-mcp/issues
10
+ Keywords: mcp,model-context-protocol,crypto,cryptocurrency,coingecko,momentum,llm,agents
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp>=1.2.0
25
+ Requires-Dist: pydantic>=2.6
26
+ Requires-Dist: python-dotenv>=1.0
27
+ Requires-Dist: requests>=2.31
28
+ Dynamic: license-file
29
+
30
+ # crypto-intel-mcp
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?logo=python&logoColor=white)](https://www.python.org) [![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-7C3AED.svg)](https://modelcontextprotocol.io) [![crypto-intel-mcp MCP server](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp/badges/score.svg)](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp)
33
+
34
+ ![crypto-intel-mcp - coin analysis demo](docs/demo.png)
35
+
36
+
37
+ **Crypto market intelligence for any LLM, over the [Model Context Protocol](https://modelcontextprotocol.io).**
38
+
39
+ Gives any MCP client (Claude Desktop, Claude Code, agents) clean tools for crypto
40
+ price, momentum, and what's trending — and blends them into a single scored
41
+ verdict. Runs entirely on **CoinGecko's free public API (no key)**, fails
42
+ gracefully (a missing source returns *"no data"*, never a fabricated value), and
43
+ outputs Markdown by default or JSON on demand.
44
+
45
+ ```
46
+ # Crypto Intelligence — BTC
47
+
48
+ **Signal:** Lean positive `[........|##......]` +22/100 · confidence 80%
49
+
50
+ Building momentum → LEAN LONG. BTC momentum: 24h +1.9% · 7d +4.4% · 30d -17.0%.
51
+ - In CoinGecko trending (elevated attention).
52
+ - Total crypto market cap +1.8% 24h, BTC dominance 57%.
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Tools
58
+
59
+ | Tool | What it does | Source | Status |
60
+ |------|--------------|--------|:------:|
61
+ | `crypto_analyze` | **Hero tool.** Blends momentum + trending + market tide into one scored verdict (RISK-ON → AVOID). | composite | ✅ |
62
+ | `crypto_get_quote` | Live price, 24h change, market-cap rank, volume, distance from ATH. | CoinGecko | ✅ |
63
+ | `crypto_momentum` | Multi-timeframe momentum (24h/7d/30d) scored into one directional signal. | CoinGecko | ✅ |
64
+ | `crypto_trending` | The coins the market is searching for most right now. | CoinGecko | ✅ |
65
+
66
+ Every tool returns **Markdown** (human-readable, default) or **JSON**
67
+ (`response_format="json"`) for programmatic use.
68
+
69
+ ---
70
+
71
+ ## Live demo output (2026-06-15)
72
+
73
+ ```
74
+ crypto_get_quote bitcoin:
75
+ BTC $66,504.00 +3.4% 24h · rank #1 · -47% from ATH
76
+
77
+ crypto_momentum bitcoin:
78
+ Lean positive [........|##......] +21/100 · confidence 80%
79
+ BTC momentum: 24h +3.4% · 7d +4.9% · 30d -14.8%
80
+
81
+ crypto_trending:
82
+ ZEC, PENGU, SIREN, TAO, NEAR, BTC, HYPE, GRAM, SUI, H
83
+
84
+ crypto_analyze bitcoin:
85
+ Lean positive [........|###.....] +35/100 · confidence 80%
86
+ Building momentum → LEAN LONG. In CoinGecko trending (elevated attention).
87
+ Total crypto market cap +3.6% 24h, BTC dominance 57%.
88
+ ```
89
+
90
+ All values are live from CoinGecko's free public API — no key, no fabrication.
91
+
92
+ ---
93
+
94
+ ## Quick start
95
+
96
+ ```bash
97
+ git clone https://github.com/cstamigo-droid/crypto-intel-mcp crypto-intel-mcp
98
+ cd crypto-intel-mcp
99
+ python -m venv .venv && .venv\Scripts\activate # Windows
100
+ pip install -r requirements.txt
101
+
102
+ python -m crypto_intel_mcp # starts the MCP server over stdio (no .env needed)
103
+ ```
104
+
105
+ **Smoke test** (hits the live CoinGecko API and prints each signal):
106
+
107
+ ```bash
108
+ python tests/test_smoke.py bitcoin
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Use it in Claude Desktop
114
+
115
+ Add this to `claude_desktop_config.json`
116
+ (`%APPDATA%\Claude\` on Windows, `~/Library/Application Support/Claude/` on macOS),
117
+ then restart Claude Desktop:
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "crypto-intel-mcp": {
123
+ "command": "python",
124
+ "args": ["-m", "crypto_intel_mcp"],
125
+ "cwd": "C:/path/to/crypto-intel-mcp"
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Why it's built this way
134
+
135
+ - **Uniform result contract.** Every source returns the same `Result` shape, so
136
+ an LLM can reason across tools instead of parsing N formats.
137
+ - **Graceful degradation.** A source with no data returns *"no data"*, not a fake
138
+ value. Missing data never invents an answer.
139
+ - **Resilient + cached.** A short per-source TTL cache avoids hammering
140
+ rate-limited endpoints when an agent calls several tools in one turn.
141
+
142
+ ---
143
+
144
+ ## Disclaimer
145
+
146
+ For research and educational use only. Data comes from third-party sources and
147
+ may be delayed or incomplete.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,122 @@
1
+ # crypto-intel-mcp
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?logo=python&logoColor=white)](https://www.python.org) [![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-7C3AED.svg)](https://modelcontextprotocol.io) [![crypto-intel-mcp MCP server](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp/badges/score.svg)](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp)
4
+
5
+ ![crypto-intel-mcp - coin analysis demo](docs/demo.png)
6
+
7
+
8
+ **Crypto market intelligence for any LLM, over the [Model Context Protocol](https://modelcontextprotocol.io).**
9
+
10
+ Gives any MCP client (Claude Desktop, Claude Code, agents) clean tools for crypto
11
+ price, momentum, and what's trending — and blends them into a single scored
12
+ verdict. Runs entirely on **CoinGecko's free public API (no key)**, fails
13
+ gracefully (a missing source returns *"no data"*, never a fabricated value), and
14
+ outputs Markdown by default or JSON on demand.
15
+
16
+ ```
17
+ # Crypto Intelligence — BTC
18
+
19
+ **Signal:** Lean positive `[........|##......]` +22/100 · confidence 80%
20
+
21
+ Building momentum → LEAN LONG. BTC momentum: 24h +1.9% · 7d +4.4% · 30d -17.0%.
22
+ - In CoinGecko trending (elevated attention).
23
+ - Total crypto market cap +1.8% 24h, BTC dominance 57%.
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Tools
29
+
30
+ | Tool | What it does | Source | Status |
31
+ |------|--------------|--------|:------:|
32
+ | `crypto_analyze` | **Hero tool.** Blends momentum + trending + market tide into one scored verdict (RISK-ON → AVOID). | composite | ✅ |
33
+ | `crypto_get_quote` | Live price, 24h change, market-cap rank, volume, distance from ATH. | CoinGecko | ✅ |
34
+ | `crypto_momentum` | Multi-timeframe momentum (24h/7d/30d) scored into one directional signal. | CoinGecko | ✅ |
35
+ | `crypto_trending` | The coins the market is searching for most right now. | CoinGecko | ✅ |
36
+
37
+ Every tool returns **Markdown** (human-readable, default) or **JSON**
38
+ (`response_format="json"`) for programmatic use.
39
+
40
+ ---
41
+
42
+ ## Live demo output (2026-06-15)
43
+
44
+ ```
45
+ crypto_get_quote bitcoin:
46
+ BTC $66,504.00 +3.4% 24h · rank #1 · -47% from ATH
47
+
48
+ crypto_momentum bitcoin:
49
+ Lean positive [........|##......] +21/100 · confidence 80%
50
+ BTC momentum: 24h +3.4% · 7d +4.9% · 30d -14.8%
51
+
52
+ crypto_trending:
53
+ ZEC, PENGU, SIREN, TAO, NEAR, BTC, HYPE, GRAM, SUI, H
54
+
55
+ crypto_analyze bitcoin:
56
+ Lean positive [........|###.....] +35/100 · confidence 80%
57
+ Building momentum → LEAN LONG. In CoinGecko trending (elevated attention).
58
+ Total crypto market cap +3.6% 24h, BTC dominance 57%.
59
+ ```
60
+
61
+ All values are live from CoinGecko's free public API — no key, no fabrication.
62
+
63
+ ---
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ git clone https://github.com/cstamigo-droid/crypto-intel-mcp crypto-intel-mcp
69
+ cd crypto-intel-mcp
70
+ python -m venv .venv && .venv\Scripts\activate # Windows
71
+ pip install -r requirements.txt
72
+
73
+ python -m crypto_intel_mcp # starts the MCP server over stdio (no .env needed)
74
+ ```
75
+
76
+ **Smoke test** (hits the live CoinGecko API and prints each signal):
77
+
78
+ ```bash
79
+ python tests/test_smoke.py bitcoin
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Use it in Claude Desktop
85
+
86
+ Add this to `claude_desktop_config.json`
87
+ (`%APPDATA%\Claude\` on Windows, `~/Library/Application Support/Claude/` on macOS),
88
+ then restart Claude Desktop:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "crypto-intel-mcp": {
94
+ "command": "python",
95
+ "args": ["-m", "crypto_intel_mcp"],
96
+ "cwd": "C:/path/to/crypto-intel-mcp"
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Why it's built this way
105
+
106
+ - **Uniform result contract.** Every source returns the same `Result` shape, so
107
+ an LLM can reason across tools instead of parsing N formats.
108
+ - **Graceful degradation.** A source with no data returns *"no data"*, not a fake
109
+ value. Missing data never invents an answer.
110
+ - **Resilient + cached.** A short per-source TTL cache avoids hammering
111
+ rate-limited endpoints when an agent calls several tools in one turn.
112
+
113
+ ---
114
+
115
+ ## Disclaimer
116
+
117
+ For research and educational use only. Data comes from third-party sources and
118
+ may be delayed or incomplete.
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,7 @@
1
+ """crypto-intel-mcp — Crypto market intelligence (price, momentum, trending) over MCP.
2
+
3
+ An MCP server giving any MCP client (Claude Desktop, Claude Code, agents) clean,
4
+ uniform tools with graceful degradation. Generated from mcp-factory.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Allow `python -m crypto_intel_mcp` to launch the server."""
2
+ from .server import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,80 @@
1
+ """Composite crypto verdict — momentum, adjusted for attention and market tide.
2
+
3
+ Base score is the multi-timeframe momentum. Two context adjustments:
4
+ - In CoinGecko's trending list -> small boost (the market is watching).
5
+ - Total crypto market cap 24h move -> a small tide nudge (rising tide lifts).
6
+ The result is one scored Result, so it renders with the gauge like any signal.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from . import coingecko
11
+ from .result import Result, clamp
12
+ from .sources import momentum, trending
13
+
14
+
15
+ def _verdict(score: float) -> tuple[str, str]:
16
+ if score >= 40:
17
+ return "Strong momentum", "RISK-ON"
18
+ if score >= 15:
19
+ return "Building momentum", "LEAN LONG"
20
+ if score > -15:
21
+ return "Neutral", "WAIT"
22
+ if score > -40:
23
+ return "Weakening", "LEAN SHORT / AVOID"
24
+ return "Breaking down", "AVOID"
25
+
26
+
27
+ def analyze(query: str) -> Result:
28
+ coin_id = coingecko.resolve(query)
29
+ if not coin_id:
30
+ return Result.failed("analyze", f"no coin matches '{query}'")
31
+
32
+ mom = momentum.fetch(query)
33
+ if not mom.ok or mom.score is None:
34
+ return Result.failed("analyze", mom.error or "no momentum data")
35
+
36
+ score = mom.score
37
+ notes = []
38
+
39
+ # Attention: is the coin trending right now?
40
+ tr = trending.fetch()
41
+ is_trending = False
42
+ if tr.ok:
43
+ is_trending = any(coin_id == c.split(" ")[0].lower() for c in tr.data.get("trending", []))
44
+ # trending stores symbols; also check the symbol from momentum summary
45
+ sym = mom.summary.split(" ")[0].lower()
46
+ is_trending = is_trending or any(sym == c.split(" ")[0].lower() for c in tr.data.get("trending", []))
47
+ if is_trending:
48
+ score = clamp(score + 10, -100, 100)
49
+ notes.append("In CoinGecko trending (elevated attention).")
50
+
51
+ # Market tide: total crypto market cap 24h direction.
52
+ mcap_chg = None
53
+ try:
54
+ g = coingecko.global_data()
55
+ mcap_chg = g.get("market_cap_change_percentage_24h_usd")
56
+ btc_dom = g.get("market_cap_percentage", {}).get("btc")
57
+ except Exception:
58
+ btc_dom = None
59
+ if mcap_chg is not None:
60
+ score = clamp(score + clamp(mcap_chg, -5, 5), -100, 100)
61
+ notes.append(f"Total crypto market cap {mcap_chg:+.1f}% 24h"
62
+ + (f", BTC dominance {btc_dom:.0f}%." if btc_dom is not None else "."))
63
+
64
+ verdict, action = _verdict(score)
65
+ return Result(
66
+ source="analyze",
67
+ ok=True,
68
+ score=round(score, 1),
69
+ confidence=mom.confidence,
70
+ summary=f"{verdict} → {action}. {mom.summary}",
71
+ data={
72
+ "verdict": verdict,
73
+ "action": action,
74
+ "momentum_score": mom.score,
75
+ "is_trending": is_trending,
76
+ "market_cap_change_24h_pct": round(mcap_chg, 2) if mcap_chg is not None else None,
77
+ "notes": notes,
78
+ "url": f"https://www.coingecko.com/en/coins/{coin_id}",
79
+ },
80
+ )
@@ -0,0 +1,40 @@
1
+ """Tiny thread-safe TTL cache.
2
+
3
+ External APIs are slow and rate-limited. A short in-process cache means an agent
4
+ that calls several tools on the same input in one turn — or a composite tool that
5
+ fans out to every source — doesn't refetch the same data seconds apart. TTLs are
6
+ per-key, chosen by each source to match how fast its data goes stale.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import time
12
+ from typing import Any, Callable
13
+
14
+ _lock = threading.Lock()
15
+ _store: dict[str, tuple[float, Any]] = {}
16
+
17
+
18
+ def get_or_fetch(key: str, ttl_s: float, fetch: Callable[[], Any]) -> Any:
19
+ """Return a cached value if fresh, otherwise call `fetch()` and store it.
20
+
21
+ `fetch` runs OUTSIDE the lock so a slow network call never blocks other
22
+ cache readers. A rare duplicate fetch under concurrency is acceptable.
23
+ """
24
+ now = time.monotonic()
25
+ with _lock:
26
+ hit = _store.get(key)
27
+ if hit is not None and (now - hit[0]) < ttl_s:
28
+ return hit[1]
29
+
30
+ value = fetch()
31
+
32
+ with _lock:
33
+ _store[key] = (time.monotonic(), value)
34
+ return value
35
+
36
+
37
+ def clear() -> None:
38
+ """Drop all cached entries (used by tests)."""
39
+ with _lock:
40
+ _store.clear()
@@ -0,0 +1,58 @@
1
+ """Low-level CoinGecko client (free public API, no key required). Cached raw pulls.
2
+
3
+ CoinGecko's free tier is generous and keyless, which makes this server run out of
4
+ the box — ideal for demos. TTLs are short for prices, longer for slow data.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import requests
9
+
10
+ from . import cache
11
+
12
+ _BASE = "https://api.coingecko.com/api/v3"
13
+ _TTL_RESOLVE = 3600.0 # symbol -> id mapping barely changes
14
+ _TTL_MARKET = 60.0 # prices age fast
15
+ _TTL_TRENDING = 300.0
16
+ _TTL_GLOBAL = 300.0
17
+
18
+
19
+ def _get(path: str, params: dict | None = None) -> object:
20
+ resp = requests.get(_BASE + path, params=params or {}, timeout=15)
21
+ resp.raise_for_status()
22
+ return resp.json()
23
+
24
+
25
+ def resolve(query: str) -> str | None:
26
+ """Resolve a symbol/name/id to a CoinGecko coin id (cached). None if not found."""
27
+ q = query.strip().lower()
28
+
29
+ def _do():
30
+ data = _get("/search", {"query": q})
31
+ coins = data.get("coins", [])
32
+ return coins[0]["id"] if coins else None
33
+
34
+ return cache.get_or_fetch(f"cg:resolve:{q}", _TTL_RESOLVE, _do)
35
+
36
+
37
+ def market(coin_id: str) -> dict | None:
38
+ """Rich market data for one coin id, incl. multi-timeframe % (cached)."""
39
+
40
+ def _do():
41
+ data = _get("/coins/markets", {
42
+ "vs_currency": "usd",
43
+ "ids": coin_id,
44
+ "price_change_percentage": "24h,7d,30d",
45
+ })
46
+ return data[0] if data else None
47
+
48
+ return cache.get_or_fetch(f"cg:market:{coin_id}", _TTL_MARKET, _do)
49
+
50
+
51
+ def trending() -> dict:
52
+ """The current most-searched coins on CoinGecko (cached)."""
53
+ return cache.get_or_fetch("cg:trending", _TTL_TRENDING, lambda: _get("/search/trending"))
54
+
55
+
56
+ def global_data() -> dict:
57
+ """Global market data: total market cap, BTC dominance, 24h change (cached)."""
58
+ return cache.get_or_fetch("cg:global", _TTL_GLOBAL, lambda: _get("/global").get("data", {}))
@@ -0,0 +1,90 @@
1
+ """Render a Result as Markdown (human, default) or JSON (machine) for MCP output."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from enum import Enum
6
+
7
+ from .result import Result, clamp
8
+
9
+
10
+ class ResponseFormat(str, Enum):
11
+ """Output format for tool responses."""
12
+
13
+ MARKDOWN = "markdown"
14
+ JSON = "json"
15
+
16
+
17
+ def label(score: float) -> str:
18
+ """Human label for a -100..+100 directional score."""
19
+ if score >= 40:
20
+ return "Strong positive"
21
+ if score >= 12:
22
+ return "Lean positive"
23
+ if score > -12:
24
+ return "Neutral"
25
+ if score > -40:
26
+ return "Lean negative"
27
+ return "Strong negative"
28
+
29
+
30
+ def _gauge(score: float) -> str:
31
+ """A compact ASCII gauge for a -100..+100 score.
32
+
33
+ Negative fills left of center, positive fills right, e.g.
34
+ -100 -> `[########|........]`, +50 -> `[........|####....]`.
35
+ """
36
+ n = 8 # half-width
37
+ s = clamp(score, -100, 100)
38
+ filled = int(round(abs(s) / 100 * n))
39
+ if s < 0:
40
+ left, right = "." * (n - filled) + "#" * filled, "." * n
41
+ elif s > 0:
42
+ left, right = "." * n, "#" * filled + "." * (n - filled)
43
+ else:
44
+ left = right = "." * n
45
+ return f"[{left}|{right}]"
46
+
47
+
48
+ def _fmt_value(v: object) -> str:
49
+ if isinstance(v, bool):
50
+ return "yes" if v else "no"
51
+ if isinstance(v, float):
52
+ return f"{v:,.2f}"
53
+ if isinstance(v, int):
54
+ return f"{v:,}"
55
+ if isinstance(v, list):
56
+ return ", ".join(str(x) for x in v[:12]) + (" …" if len(v) > 12 else "")
57
+ return str(v)
58
+
59
+
60
+ def render(result: Result, title: str, fmt: ResponseFormat) -> str:
61
+ """Format a single Result for return to an MCP client."""
62
+ if fmt == ResponseFormat.JSON:
63
+ return json.dumps(result.to_dict(), indent=2, default=str)
64
+
65
+ lines = [f"# {title}"]
66
+ if not result.ok:
67
+ lines += ["", f"⚠️ No data available — {result.error}"]
68
+ return "\n".join(lines)
69
+
70
+ if result.score is not None:
71
+ conf = f" · confidence {result.confidence:.0%}" if result.confidence is not None else ""
72
+ lines += [
73
+ "",
74
+ f"**Signal:** {label(result.score)} "
75
+ f"`{_gauge(result.score)}` {result.score:+.0f}/100{conf}",
76
+ ]
77
+ if result.summary:
78
+ lines += ["", result.summary]
79
+
80
+ if result.data:
81
+ lines += ["", "## Details"]
82
+ for k, v in result.data.items():
83
+ if v is None or k == "url":
84
+ continue
85
+ lines.append(f"- **{k.replace('_', ' ')}:** {_fmt_value(v)}")
86
+ url = result.data.get("url")
87
+ if url:
88
+ lines += ["", f"Source: {url}"]
89
+
90
+ return "\n".join(lines)
@@ -0,0 +1,52 @@
1
+ """The single return type every source and tool produces.
2
+
3
+ A uniform shape lets an LLM reason *across* heterogeneous tools without each one
4
+ inventing its own format. Plain-data tools fill `summary` + `data`. Analysis
5
+ tools additionally set `score` (-100..+100) and `confidence` (0..1), which unlocks
6
+ the gauge rendering in `formatting.py`.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ def clamp(x: float, lo: float, hi: float) -> float:
15
+ """Constrain x to the inclusive [lo, hi] range."""
16
+ return max(lo, min(hi, x))
17
+
18
+
19
+ @dataclass
20
+ class Result:
21
+ """Uniform result object returned by every source in `sources/`.
22
+
23
+ Attributes:
24
+ source: short id of the producing source, e.g. "weather".
25
+ ok: True if data was fetched and the result is meaningful.
26
+ summary: one-line human-readable takeaway.
27
+ data: raw structured details (source-specific). Put a `url`
28
+ permalink here so an agent can cite the source.
29
+ score: optional -100..+100 directional score (analysis tools only).
30
+ confidence: optional 0..1 — how much to trust this result.
31
+ error: failure reason when ok is False; None otherwise.
32
+ """
33
+
34
+ source: str
35
+ ok: bool
36
+ summary: str = ""
37
+ data: dict[str, Any] = field(default_factory=dict)
38
+ score: float | None = None
39
+ confidence: float | None = None
40
+ error: str | None = None
41
+
42
+ def to_dict(self) -> dict[str, Any]:
43
+ return asdict(self)
44
+
45
+ @classmethod
46
+ def failed(cls, source: str, error: str) -> "Result":
47
+ """Graceful degradation: a missing source NEVER fabricates a value.
48
+
49
+ Returns ok=False with no score, so a composite tool can simply skip it
50
+ and the agent sees an honest "no data".
51
+ """
52
+ return cls(source=source, ok=False, summary=f"no data ({error})", error=error)
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ """crypto-intel-mcp — Crypto market intelligence (price, momentum, trending) over MCP.
3
+
4
+ Transport: stdio (local — Claude Desktop / Claude Code / agents).
5
+
6
+ Each tool returns a uniform Result rendered as Markdown (default) or JSON.
7
+ Sources fail gracefully: a missing data source returns "no data", never a
8
+ fabricated value. Powered by CoinGecko's free public API — no key required.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from pathlib import Path
14
+
15
+ from dotenv import load_dotenv
16
+ from mcp.server.fastmcp import FastMCP
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ from . import analyze
20
+ from .formatting import ResponseFormat, render
21
+ from .sources import momentum, quote, trending
22
+
23
+ # Load .env from the project root (parent of this package), if present.
24
+ load_dotenv(Path(__file__).resolve().parent.parent / ".env")
25
+
26
+ mcp = FastMCP("crypto_intel_mcp")
27
+
28
+
29
+ # ─── shared helpers ──────────────────────────────────────────────────────────
30
+
31
+ async def _run(fn, *args):
32
+ """Run a blocking source function in a thread so the event loop stays free."""
33
+ loop = asyncio.get_running_loop()
34
+ return await loop.run_in_executor(None, lambda: fn(*args))
35
+
36
+
37
+ class CoinInput(BaseModel):
38
+ """Input for single-coin tools."""
39
+
40
+ model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
41
+
42
+ coin: str = Field(
43
+ ...,
44
+ description="Coin symbol, name, or id — e.g. 'BTC', 'bitcoin', 'ETH', 'solana'.",
45
+ min_length=1,
46
+ max_length=50,
47
+ )
48
+ response_format: ResponseFormat = Field(
49
+ default=ResponseFormat.MARKDOWN,
50
+ description="'markdown' for a human-readable report or 'json' for structured data.",
51
+ )
52
+
53
+
54
+ class MarketInput(BaseModel):
55
+ """Input for market-wide tools that take no specific coin."""
56
+
57
+ model_config = ConfigDict(extra="forbid")
58
+
59
+ response_format: ResponseFormat = Field(
60
+ default=ResponseFormat.MARKDOWN,
61
+ description="'markdown' for a human-readable report or 'json' for structured data.",
62
+ )
63
+
64
+
65
+ # ─── tools ───────────────────────────────────────────────────────────────────
66
+
67
+ @mcp.tool(
68
+ name="crypto_get_quote",
69
+ annotations={"title": "Crypto Price Snapshot", "readOnlyHint": True,
70
+ "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
71
+ )
72
+ async def crypto_get_quote(params: CoinInput) -> str:
73
+ """Get a current price snapshot for a cryptocurrency.
74
+
75
+ Informational (not a buy/sell signal): live price, 24h change, market-cap
76
+ rank, 24h volume, and distance from all-time high.
77
+
78
+ Examples:
79
+ - "What's bitcoin trading at?" -> coin='BTC'
80
+ - "How far is ETH from its all-time high?" -> coin='ethereum'
81
+ """
82
+ sig = await _run(quote.fetch, params.coin)
83
+ return render(sig, f"Quote — {params.coin}", params.response_format)
84
+
85
+
86
+ @mcp.tool(
87
+ name="crypto_momentum",
88
+ annotations={"title": "Crypto Momentum Signal", "readOnlyHint": True,
89
+ "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
90
+ )
91
+ async def crypto_momentum(params: CoinInput) -> str:
92
+ """Score a coin's price momentum across 24h, 7d and 30d into one signal.
93
+
94
+ Returns a directional score from -100 (breaking down) to +100 (strong
95
+ broad-based upward momentum), weighting recent action more heavily.
96
+
97
+ Examples:
98
+ - "Does solana have momentum?" -> coin='SOL'
99
+ - "Is bitcoin trending up or down?" -> coin='bitcoin'
100
+ """
101
+ sig = await _run(momentum.fetch, params.coin)
102
+ return render(sig, f"Momentum — {params.coin}", params.response_format)
103
+
104
+
105
+ @mcp.tool(
106
+ name="crypto_trending",
107
+ annotations={"title": "Trending Coins", "readOnlyHint": True,
108
+ "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
109
+ )
110
+ async def crypto_trending(params: MarketInput) -> str:
111
+ """List the coins the market is searching for most right now (CoinGecko trending).
112
+
113
+ A breadth/attention indicator — useful for spotting where retail interest is
114
+ concentrating. Takes no coin.
115
+
116
+ Examples:
117
+ - "What crypto is trending right now?"
118
+ - "Show me the hot coins today."
119
+ """
120
+ sig = await _run(trending.fetch, "")
121
+ return render(sig, "Trending Crypto", params.response_format)
122
+
123
+
124
+ @mcp.tool(
125
+ name="crypto_analyze",
126
+ annotations={"title": "Full Crypto Analysis (Composite)", "readOnlyHint": True,
127
+ "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
128
+ )
129
+ async def crypto_analyze(params: CoinInput) -> str:
130
+ """Run a full analysis of a coin and return one scored verdict.
131
+
132
+ Blends multi-timeframe momentum with whether the coin is trending and the
133
+ overall crypto market tide into a single verdict (RISK-ON → AVOID) with a
134
+ gauge and the reasoning behind it.
135
+
136
+ This is the primary tool — prefer it for "should I look at X?" questions.
137
+
138
+ Examples:
139
+ - "Give me a full read on bitcoin" -> coin='BTC'
140
+ - "Should I be looking at SOL?" -> coin='solana'
141
+ """
142
+ sig = await _run(analyze.analyze, params.coin)
143
+ return render(sig, f"Crypto Intelligence — {params.coin}", params.response_format)
144
+
145
+
146
+ def main() -> None:
147
+ """Console entrypoint — runs the MCP server over stdio."""
148
+ mcp.run()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,6 @@
1
+ """Data sources. Each module exposes `fetch(query: str) -> Result`.
2
+
3
+ Add one module per external API/endpoint. Keep the network call dumb, wrap it in
4
+ `cache.get_or_fetch`, and return `Result.failed(...)` on any missing data —
5
+ never a fabricated value.
6
+ """
@@ -0,0 +1,65 @@
1
+ """Crypto momentum signal — blends 24h / 7d / 30d price trend into one score.
2
+
3
+ Score -100..+100: positive = broad upward momentum across timeframes, negative =
4
+ breaking down. Each timeframe has its own volatility scale (a +8% day is as
5
+ significant as a +40% month) and weight (recent action counts more).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .. import coingecko
10
+ from ..result import Result, clamp
11
+
12
+ # (field, weight, scale) — |pct| == scale contributes ±100 at that timeframe.
13
+ _TIMEFRAMES = [
14
+ ("price_change_percentage_24h_in_currency", 0.5, 8.0),
15
+ ("price_change_percentage_7d_in_currency", 0.3, 20.0),
16
+ ("price_change_percentage_30d_in_currency", 0.2, 40.0),
17
+ ]
18
+
19
+
20
+ def fetch(query: str) -> Result:
21
+ coin_id = coingecko.resolve(query)
22
+ if not coin_id:
23
+ return Result.failed("momentum", f"no coin matches '{query}'")
24
+ try:
25
+ m = coingecko.market(coin_id)
26
+ except Exception as e:
27
+ return Result.failed("momentum", str(e))
28
+ if not m:
29
+ return Result.failed("momentum", f"no market data for {coin_id}")
30
+
31
+ num = den = 0.0
32
+ used = 0
33
+ for field, weight, scale in _TIMEFRAMES:
34
+ pct = m.get(field)
35
+ if pct is None:
36
+ continue
37
+ num += clamp(pct / scale * 100, -100, 100) * weight
38
+ den += weight
39
+ used += 1
40
+ if den == 0:
41
+ return Result.failed("momentum", "no timeframe data")
42
+
43
+ score = clamp(num / den, -100, 100)
44
+ confidence = min(0.2 + 0.2 * used, 1.0) # 0.8 with all three timeframes
45
+
46
+ c24 = m.get("price_change_percentage_24h_in_currency")
47
+ c7 = m.get("price_change_percentage_7d_in_currency")
48
+ c30 = m.get("price_change_percentage_30d_in_currency")
49
+
50
+ def f(x):
51
+ return f"{x:+.1f}%" if x is not None else "n/a"
52
+
53
+ return Result(
54
+ source="momentum",
55
+ ok=True,
56
+ score=round(score, 1),
57
+ confidence=round(confidence, 2),
58
+ summary=f"{(m.get('symbol') or '').upper()} momentum: 24h {f(c24)} · 7d {f(c7)} · 30d {f(c30)}.",
59
+ data={
60
+ "change_24h_pct": round(c24, 2) if c24 is not None else None,
61
+ "change_7d_pct": round(c7, 2) if c7 is not None else None,
62
+ "change_30d_pct": round(c30, 2) if c30 is not None else None,
63
+ "url": f"https://www.coingecko.com/en/coins/{coin_id}",
64
+ },
65
+ )
@@ -0,0 +1,49 @@
1
+ """Crypto price snapshot — price, 24h move, market cap rank, volume, ATH distance.
2
+
3
+ Informational (no directional score): use it to anchor any analysis with the
4
+ current state of a coin.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .. import coingecko
9
+ from ..result import Result
10
+
11
+
12
+ def fetch(query: str) -> Result:
13
+ coin_id = coingecko.resolve(query)
14
+ if not coin_id:
15
+ return Result.failed("quote", f"no coin matches '{query}'")
16
+ try:
17
+ m = coingecko.market(coin_id)
18
+ except Exception as e:
19
+ return Result.failed("quote", str(e))
20
+ if not m:
21
+ return Result.failed("quote", f"no market data for {coin_id}")
22
+
23
+ price = m.get("current_price")
24
+ chg = m.get("price_change_percentage_24h")
25
+ ath_dist = m.get("ath_change_percentage")
26
+ sym = (m.get("symbol") or "").upper()
27
+ # Adaptive price format: commas for >=$1, more decimals for sub-dollar coins.
28
+ price_str = f"{price:,.2f}" if price is None or price >= 1 else f"{price:.8f}".rstrip("0")
29
+ summary = (
30
+ f"{sym} ${price_str} · {chg:+.1f}% 24h · rank #{m.get('market_cap_rank', '?')} "
31
+ f"· {ath_dist:+.0f}% from ATH"
32
+ )
33
+ return Result(
34
+ source="quote",
35
+ ok=True,
36
+ summary=summary,
37
+ data={
38
+ "name": m.get("name"),
39
+ "symbol": sym,
40
+ "price_usd": price,
41
+ "change_24h_pct": round(chg, 2) if chg is not None else None,
42
+ "market_cap": m.get("market_cap"),
43
+ "market_cap_rank": m.get("market_cap_rank"),
44
+ "volume_24h": m.get("total_volume"),
45
+ "ath": m.get("ath"),
46
+ "pct_from_ath": round(ath_dist, 1) if ath_dist is not None else None,
47
+ "url": f"https://www.coingecko.com/en/coins/{coin_id}",
48
+ },
49
+ )
@@ -0,0 +1,34 @@
1
+ """What's trending on CoinGecko right now — the most-searched coins.
2
+
3
+ A breadth/attention signal: coins the market is actively looking at. Takes no
4
+ specific coin (the optional query argument is ignored).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .. import coingecko
9
+ from ..result import Result
10
+
11
+
12
+ def fetch(query: str = "") -> Result:
13
+ try:
14
+ t = coingecko.trending()
15
+ except Exception as e:
16
+ return Result.failed("trending", str(e))
17
+ coins = t.get("coins", [])
18
+ if not coins:
19
+ return Result.failed("trending", "no trending data")
20
+
21
+ symbols = [c["item"]["symbol"].upper() for c in coins[:10]]
22
+ detail = [
23
+ f"{c['item']['symbol'].upper()} (#{c['item'].get('market_cap_rank') or '?'})"
24
+ for c in coins[:10]
25
+ ]
26
+ return Result(
27
+ source="trending",
28
+ ok=True,
29
+ summary="Trending now: " + ", ".join(symbols),
30
+ data={
31
+ "trending": detail,
32
+ "url": "https://www.coingecko.com/en/highlights/trending-crypto",
33
+ },
34
+ )
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: crypto-intel-mcp
3
+ Version: 0.1.0
4
+ Summary: Crypto market intelligence (price, momentum, trending) over MCP.
5
+ Author-email: Cristian Amigo <cstamigo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cstamigo-droid/crypto-intel-mcp
8
+ Project-URL: Repository, https://github.com/cstamigo-droid/crypto-intel-mcp
9
+ Project-URL: Bug Tracker, https://github.com/cstamigo-droid/crypto-intel-mcp/issues
10
+ Keywords: mcp,model-context-protocol,crypto,cryptocurrency,coingecko,momentum,llm,agents
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: mcp>=1.2.0
25
+ Requires-Dist: pydantic>=2.6
26
+ Requires-Dist: python-dotenv>=1.0
27
+ Requires-Dist: requests>=2.31
28
+ Dynamic: license-file
29
+
30
+ # crypto-intel-mcp
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?logo=python&logoColor=white)](https://www.python.org) [![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-7C3AED.svg)](https://modelcontextprotocol.io) [![crypto-intel-mcp MCP server](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp/badges/score.svg)](https://glama.ai/mcp/servers/cstamigo-droid/crypto-intel-mcp)
33
+
34
+ ![crypto-intel-mcp - coin analysis demo](docs/demo.png)
35
+
36
+
37
+ **Crypto market intelligence for any LLM, over the [Model Context Protocol](https://modelcontextprotocol.io).**
38
+
39
+ Gives any MCP client (Claude Desktop, Claude Code, agents) clean tools for crypto
40
+ price, momentum, and what's trending — and blends them into a single scored
41
+ verdict. Runs entirely on **CoinGecko's free public API (no key)**, fails
42
+ gracefully (a missing source returns *"no data"*, never a fabricated value), and
43
+ outputs Markdown by default or JSON on demand.
44
+
45
+ ```
46
+ # Crypto Intelligence — BTC
47
+
48
+ **Signal:** Lean positive `[........|##......]` +22/100 · confidence 80%
49
+
50
+ Building momentum → LEAN LONG. BTC momentum: 24h +1.9% · 7d +4.4% · 30d -17.0%.
51
+ - In CoinGecko trending (elevated attention).
52
+ - Total crypto market cap +1.8% 24h, BTC dominance 57%.
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Tools
58
+
59
+ | Tool | What it does | Source | Status |
60
+ |------|--------------|--------|:------:|
61
+ | `crypto_analyze` | **Hero tool.** Blends momentum + trending + market tide into one scored verdict (RISK-ON → AVOID). | composite | ✅ |
62
+ | `crypto_get_quote` | Live price, 24h change, market-cap rank, volume, distance from ATH. | CoinGecko | ✅ |
63
+ | `crypto_momentum` | Multi-timeframe momentum (24h/7d/30d) scored into one directional signal. | CoinGecko | ✅ |
64
+ | `crypto_trending` | The coins the market is searching for most right now. | CoinGecko | ✅ |
65
+
66
+ Every tool returns **Markdown** (human-readable, default) or **JSON**
67
+ (`response_format="json"`) for programmatic use.
68
+
69
+ ---
70
+
71
+ ## Live demo output (2026-06-15)
72
+
73
+ ```
74
+ crypto_get_quote bitcoin:
75
+ BTC $66,504.00 +3.4% 24h · rank #1 · -47% from ATH
76
+
77
+ crypto_momentum bitcoin:
78
+ Lean positive [........|##......] +21/100 · confidence 80%
79
+ BTC momentum: 24h +3.4% · 7d +4.9% · 30d -14.8%
80
+
81
+ crypto_trending:
82
+ ZEC, PENGU, SIREN, TAO, NEAR, BTC, HYPE, GRAM, SUI, H
83
+
84
+ crypto_analyze bitcoin:
85
+ Lean positive [........|###.....] +35/100 · confidence 80%
86
+ Building momentum → LEAN LONG. In CoinGecko trending (elevated attention).
87
+ Total crypto market cap +3.6% 24h, BTC dominance 57%.
88
+ ```
89
+
90
+ All values are live from CoinGecko's free public API — no key, no fabrication.
91
+
92
+ ---
93
+
94
+ ## Quick start
95
+
96
+ ```bash
97
+ git clone https://github.com/cstamigo-droid/crypto-intel-mcp crypto-intel-mcp
98
+ cd crypto-intel-mcp
99
+ python -m venv .venv && .venv\Scripts\activate # Windows
100
+ pip install -r requirements.txt
101
+
102
+ python -m crypto_intel_mcp # starts the MCP server over stdio (no .env needed)
103
+ ```
104
+
105
+ **Smoke test** (hits the live CoinGecko API and prints each signal):
106
+
107
+ ```bash
108
+ python tests/test_smoke.py bitcoin
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Use it in Claude Desktop
114
+
115
+ Add this to `claude_desktop_config.json`
116
+ (`%APPDATA%\Claude\` on Windows, `~/Library/Application Support/Claude/` on macOS),
117
+ then restart Claude Desktop:
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "crypto-intel-mcp": {
123
+ "command": "python",
124
+ "args": ["-m", "crypto_intel_mcp"],
125
+ "cwd": "C:/path/to/crypto-intel-mcp"
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Why it's built this way
134
+
135
+ - **Uniform result contract.** Every source returns the same `Result` shape, so
136
+ an LLM can reason across tools instead of parsing N formats.
137
+ - **Graceful degradation.** A source with no data returns *"no data"*, not a fake
138
+ value. Missing data never invents an answer.
139
+ - **Resilient + cached.** A short per-source TTL cache avoids hammering
140
+ rate-limited endpoints when an agent calls several tools in one turn.
141
+
142
+ ---
143
+
144
+ ## Disclaimer
145
+
146
+ For research and educational use only. Data comes from third-party sources and
147
+ may be delayed or incomplete.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ crypto_intel_mcp/__init__.py
5
+ crypto_intel_mcp/__main__.py
6
+ crypto_intel_mcp/analyze.py
7
+ crypto_intel_mcp/cache.py
8
+ crypto_intel_mcp/coingecko.py
9
+ crypto_intel_mcp/formatting.py
10
+ crypto_intel_mcp/result.py
11
+ crypto_intel_mcp/server.py
12
+ crypto_intel_mcp.egg-info/PKG-INFO
13
+ crypto_intel_mcp.egg-info/SOURCES.txt
14
+ crypto_intel_mcp.egg-info/dependency_links.txt
15
+ crypto_intel_mcp.egg-info/entry_points.txt
16
+ crypto_intel_mcp.egg-info/requires.txt
17
+ crypto_intel_mcp.egg-info/top_level.txt
18
+ crypto_intel_mcp/sources/__init__.py
19
+ crypto_intel_mcp/sources/momentum.py
20
+ crypto_intel_mcp/sources/quote.py
21
+ crypto_intel_mcp/sources/trending.py
22
+ tests/test_smoke.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ crypto-intel-mcp = crypto_intel_mcp.server:main
@@ -0,0 +1,4 @@
1
+ mcp>=1.2.0
2
+ pydantic>=2.6
3
+ python-dotenv>=1.0
4
+ requests>=2.31
@@ -0,0 +1 @@
1
+ crypto_intel_mcp
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "crypto-intel-mcp"
3
+ version = "0.1.0"
4
+ description = "Crypto market intelligence (price, momentum, trending) over MCP."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ authors = [{ name = "Cristian Amigo", email = "cstamigo@gmail.com" }]
8
+ keywords = ["mcp", "model-context-protocol", "crypto", "cryptocurrency", "coingecko", "momentum", "llm", "agents"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.10",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ "Topic :: Office/Business :: Financial :: Investment",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ ]
21
+ license = "MIT"
22
+ license-files = ["LICENSE"]
23
+ dependencies = [
24
+ "mcp>=1.2.0",
25
+ "pydantic>=2.6",
26
+ "python-dotenv>=1.0",
27
+ "requests>=2.31",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/cstamigo-droid/crypto-intel-mcp"
32
+ Repository = "https://github.com/cstamigo-droid/crypto-intel-mcp"
33
+ "Bug Tracker" = "https://github.com/cstamigo-droid/crypto-intel-mcp/issues"
34
+
35
+ [project.scripts]
36
+ crypto-intel-mcp = "crypto_intel_mcp.server:main"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=77"]
40
+ build-backend = "setuptools.build_meta"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["crypto_intel_mcp*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """Smoke test — hits the real CoinGecko API and prints each Result.
2
+
3
+ Run: python tests/test_smoke.py [COIN]
4
+ This is a live network test, not a unit test.
5
+ """
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Make stdout UTF-8 safe so the live demo output (em-dashes, arrows) prints on
10
+ # Windows consoles (cp1252) without crashing. No PYTHONUTF8 env var required.
11
+ if hasattr(sys.stdout, "reconfigure"):
12
+ sys.stdout.reconfigure(encoding="utf-8")
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
15
+
16
+ from dotenv import load_dotenv # noqa: E402
17
+
18
+ load_dotenv(Path(__file__).resolve().parent.parent / ".env")
19
+
20
+ from crypto_intel_mcp import analyze # noqa: E402
21
+ from crypto_intel_mcp.formatting import ResponseFormat, render # noqa: E402
22
+ from crypto_intel_mcp.sources import momentum, quote, trending # noqa: E402
23
+
24
+
25
+ def main() -> None:
26
+ coin = sys.argv[1] if len(sys.argv) > 1 else "bitcoin"
27
+ print(f"\n{'=' * 70}\n QUOTE\n{'=' * 70}")
28
+ print(render(quote.fetch(coin), f"quote — {coin}", ResponseFormat.MARKDOWN))
29
+
30
+ print(f"\n{'=' * 70}\n MOMENTUM\n{'=' * 70}")
31
+ print(render(momentum.fetch(coin), f"momentum — {coin}", ResponseFormat.MARKDOWN))
32
+
33
+ print(f"\n{'=' * 70}\n TRENDING\n{'=' * 70}")
34
+ print(render(trending.fetch(), "trending", ResponseFormat.MARKDOWN))
35
+
36
+ print(f"\n{'=' * 70}\n ANALYZE (composite)\n{'=' * 70}")
37
+ print(render(analyze.analyze(coin), f"analyze — {coin}", ResponseFormat.MARKDOWN))
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()