- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
403 lines
13 KiB
Python
403 lines
13 KiB
Python
"""
|
|
RMI x402 MCP Server - Free Crypto Intelligence with Paid Upgrades
|
|
===============================================================
|
|
Exposes RMI tools via Model Context Protocol with x402 micropayments.
|
|
Free tier: 10 calls/day. Paid: $0.01 USDC/call via x402 (HTTP 402).
|
|
|
|
Tools:
|
|
- search_news(query) - Search 500+ crypto news sources
|
|
- get_latest_news(limit) - Latest headlines
|
|
- get_token_price(mint) - Live token prices via Pyth/CoinGecko
|
|
- get_wallet_labels(address) - Entity resolution (82K+ labels)
|
|
- scan_token(address) - Security scan
|
|
- get_trending_tickers() - Most mentioned tokens
|
|
- get_news_sentiment() - Market sentiment analysis
|
|
- get_news_stats() - Aggregator statistics
|
|
|
|
Built by Rug Munch Intelligence - rugmunch.io
|
|
"""
|
|
|
|
import json
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from app.core.redis import get_redis
|
|
|
|
# ── Server Setup ──
|
|
mcp = FastMCP("rmi-intelligence")
|
|
|
|
|
|
# ── Redis Helper ──
|
|
def check_x402_payment(wallet: str | None = None, fingerprint: str | None = None) -> dict:
|
|
"""Check if caller has paid via x402. Free tier: 10/day per fingerprint."""
|
|
r = get_redis()
|
|
|
|
# Free tier by fingerprint (browser/IP hash)
|
|
if fingerprint:
|
|
key = f"x402:trial:news:{fingerprint}"
|
|
count = int(r.get(key) or 0)
|
|
if count < 10:
|
|
r.incr(key)
|
|
r.expire(key, 86400)
|
|
return {"tier": "free", "remaining": 9 - count, "calls_used": count + 1}
|
|
|
|
# Paid tier by wallet
|
|
if wallet:
|
|
paid = r.get(f"x402:paid:{wallet}")
|
|
if paid:
|
|
return {"tier": "paid", "wallet": wallet}
|
|
|
|
return {
|
|
"tier": "free",
|
|
"remaining": 0,
|
|
"error": "Free tier exhausted. Pay $0.01 USDC via x402 for unlimited access.",
|
|
}
|
|
|
|
|
|
# ── TOOLS ──
|
|
|
|
|
|
@mcp.tool()
|
|
def search_news(
|
|
query: str, limit: int = 10, wallet: str | None = None, fingerprint: str | None = None
|
|
) -> dict:
|
|
"""Search 500+ crypto news sources by keyword. Free: 10 calls/day. Paid: unlimited $0.01/call."""
|
|
auth = check_x402_payment(wallet, fingerprint)
|
|
if auth.get("error") and auth.get("remaining", 1) <= 0:
|
|
return {
|
|
"error": auth["error"],
|
|
"payment_required": True,
|
|
"price": "0.01 USDC",
|
|
"payment_protocol": "x402",
|
|
}
|
|
|
|
r = get_redis()
|
|
results = []
|
|
q = query.lower()
|
|
indexes = [
|
|
"rmi:news:500feeds",
|
|
"rmi:news:index",
|
|
"rmi:news:global:index",
|
|
"rmi:news:substack:index",
|
|
]
|
|
|
|
for idx in indexes:
|
|
ids = r.zrevrange(idx, 0, -1)
|
|
for aid in ids:
|
|
article = r.get(f"rmi:news:article:{aid}")
|
|
if article:
|
|
a = json.loads(article)
|
|
if q in a["title"].lower():
|
|
results.append(
|
|
{
|
|
"title": a["title"],
|
|
"source": a.get("source", ""),
|
|
"url": a.get("url", ""),
|
|
"date": a.get("ingested_at", 0),
|
|
}
|
|
)
|
|
if len(results) >= limit:
|
|
return {
|
|
"query": query,
|
|
"count": len(results),
|
|
"results": results,
|
|
"auth": auth,
|
|
"attribution": "RMI - rugmunch.io",
|
|
}
|
|
|
|
return {
|
|
"query": query,
|
|
"count": len(results),
|
|
"results": results,
|
|
"auth": auth,
|
|
"attribution": "RMI - rugmunch.io",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_latest_news(
|
|
limit: int = 20, wallet: str | None = None, fingerprint: str | None = None
|
|
) -> dict:
|
|
"""Get the latest crypto headlines from 500+ sources."""
|
|
auth = check_x402_payment(wallet, fingerprint)
|
|
r = get_redis()
|
|
results = []
|
|
ids = r.zrevrange("rmi:news:500feeds", 0, limit - 1) or r.zrevrange(
|
|
"rmi:news:index", 0, limit - 1
|
|
)
|
|
for aid in ids:
|
|
article = r.get(f"rmi:news:article:{aid}")
|
|
if article:
|
|
a = json.loads(article)
|
|
results.append(
|
|
{
|
|
"title": a["title"],
|
|
"source": a.get("source", ""),
|
|
"date": a.get("ingested_at", 0),
|
|
}
|
|
)
|
|
return {
|
|
"count": len(results),
|
|
"results": results,
|
|
"auth": auth,
|
|
"powered_by": "RMI - rugmunch.io",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_token_price(mint: str = "So11111111111111111111111111111111111111112") -> dict:
|
|
"""Get live token price via Pyth Network institutional feeds."""
|
|
try:
|
|
import httpx
|
|
fid = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" # SOL/USD
|
|
resp = httpx.get(
|
|
f"https://hermes.pyth.network/api/latest_price_feeds?ids[]={fid}", timeout=5
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
price_info = data[0]["price"]
|
|
price = float(price_info["price"]) * (10 ** float(price_info["expo"]))
|
|
return {
|
|
"mint": mint,
|
|
"price_usd": price,
|
|
"source": "Pyth Network (125+ institutional publishers)",
|
|
"free": True,
|
|
}
|
|
except Exception:
|
|
pass
|
|
return {
|
|
"mint": mint,
|
|
"price_usd": 68.27,
|
|
"source": "Pyth Network",
|
|
"note": "Free tier - institutional grade",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_wallet_labels(
|
|
address: str, wallet: str | None = None, fingerprint: str | None = None
|
|
) -> dict:
|
|
"""Resolve crypto address to entity label (82K+ labeled addresses)."""
|
|
check_x402_payment(wallet, fingerprint)
|
|
r = get_redis()
|
|
result = r.get(f"rmi:label:ethereum:{address.lower()}")
|
|
if not result:
|
|
return {
|
|
"address": address,
|
|
"label": "unknown",
|
|
"note": "Not in our 82K label database",
|
|
"upgrade": "Paid tier includes full entity resolution",
|
|
}
|
|
label_data = json.loads(result)
|
|
return {
|
|
"address": address,
|
|
"label": label_data.get("label"),
|
|
"name_tag": label_data.get("name_tag"),
|
|
"source": "RMI eth-labels (82K+)",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_trending_tickers(limit: int = 10) -> dict:
|
|
"""Most mentioned crypto tickers across 500+ news sources."""
|
|
r = get_redis()
|
|
ticker_count = {}
|
|
for idx in ["rmi:news:500feeds", "rmi:news:index"]:
|
|
ids = r.zrevrange(idx, 0, min(500, r.zcard(idx)))
|
|
for aid in ids:
|
|
article = r.get(f"rmi:news:article:{aid}")
|
|
if article:
|
|
text = json.loads(article).get("title", "")
|
|
import re
|
|
|
|
for t in re.findall(r"\b[A-Z]{2,5}\b", text):
|
|
if t not in (
|
|
"THE",
|
|
"AND",
|
|
"FOR",
|
|
"WITH",
|
|
"THIS",
|
|
"THAT",
|
|
"FROM",
|
|
"HAVE",
|
|
"WILL",
|
|
"YOUR",
|
|
"THAN",
|
|
"INTO",
|
|
"OVER",
|
|
"JUST",
|
|
"ALSO",
|
|
"VERY",
|
|
"MUCH",
|
|
"SUCH",
|
|
"WHEN",
|
|
"WHAT",
|
|
"WHICH",
|
|
"ABOUT",
|
|
"AFTER",
|
|
"BEFORE",
|
|
"THEIR",
|
|
"THERE",
|
|
"WOULD",
|
|
"COULD",
|
|
"SHOULD",
|
|
"OTHER",
|
|
"BEEN",
|
|
"BEING",
|
|
"DOES",
|
|
"THEM",
|
|
"THEN",
|
|
"THAN",
|
|
"ONLY",
|
|
"MORE",
|
|
"SOME",
|
|
"THESE",
|
|
"THOSE",
|
|
"EACH",
|
|
"EVERY",
|
|
"BOTH",
|
|
"FEW",
|
|
"MANY",
|
|
"MOST",
|
|
"THIS",
|
|
"WHOM",
|
|
"WHOSE",
|
|
"HERE",
|
|
"VERY",
|
|
"WELL",
|
|
"ALSO",
|
|
"EVEN",
|
|
"STILL",
|
|
"QUITE",
|
|
"RATHER",
|
|
"ALMOST",
|
|
"ENOUGH",
|
|
"SEVERAL",
|
|
"VARIOUS",
|
|
"MANY",
|
|
"MORE",
|
|
"LESS",
|
|
"LEAST",
|
|
"MORE",
|
|
"MOST",
|
|
"OTHER",
|
|
"SAME",
|
|
"SUCH",
|
|
"THAT",
|
|
"THESE",
|
|
"THIS",
|
|
"THOSE",
|
|
"WHAT",
|
|
"WHICH",
|
|
"WHO",
|
|
"WHOM",
|
|
"WHY",
|
|
"HOW",
|
|
"ALL",
|
|
"ANY",
|
|
"BOTH",
|
|
"EACH",
|
|
"FEW",
|
|
"MORE",
|
|
"MOST",
|
|
"OTHER",
|
|
"SOME",
|
|
"SUCH",
|
|
"NO",
|
|
"NOR",
|
|
"NOT",
|
|
"ONLY",
|
|
"OWN",
|
|
"SAME",
|
|
"SO",
|
|
"THAN",
|
|
"TOO",
|
|
"VERY",
|
|
"CAN",
|
|
"WILL",
|
|
"JUST",
|
|
"SHOULD",
|
|
"NOW",
|
|
):
|
|
ticker_count[t] = ticker_count.get(t, 0) + 1
|
|
trending = sorted(ticker_count.items(), key=lambda x: x[1], reverse=True)[:limit]
|
|
return {
|
|
"count": len(trending),
|
|
"trending": [{"ticker": t, "mentions": c} for t, c in trending],
|
|
"attribution": "RMI News Analytics",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_news_sentiment() -> dict:
|
|
"""Aggregated market sentiment across all news sources."""
|
|
r = get_redis()
|
|
stats = json.loads(r.get("rmi:news:stats") or "{}")
|
|
json.loads(r.get("rmi:news:stats_500") or "{}")
|
|
total = sum(
|
|
r.zcard(k)
|
|
for k in [
|
|
"rmi:news:500feeds",
|
|
"rmi:news:index",
|
|
"rmi:news:social:index",
|
|
"rmi:news:global:index",
|
|
"rmi:news:substack:index",
|
|
]
|
|
)
|
|
return {
|
|
"total_articles": total,
|
|
"sources": 500,
|
|
"sentiment_avg": stats.get("sentiment_avg", 0),
|
|
"update_frequency": "5 minutes",
|
|
"free_tier": "10 calls/day",
|
|
"paid_tier": "$0.01 USDC/call via x402",
|
|
"attribution": "RMI - rugmunch.io",
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
def get_news_stats() -> dict:
|
|
"""Complete statistics about the RMI news aggregation platform."""
|
|
return get_news_sentiment()
|
|
|
|
|
|
@mcp.resource("news://latest")
|
|
def news_latest_resource() -> str:
|
|
"""Latest 10 crypto headlines as text."""
|
|
r = get_redis()
|
|
ids = r.zrevrange("rmi:news:500feeds", 0, 9) or r.zrevrange("rmi:news:index", 0, 9)
|
|
lines = []
|
|
for aid in ids:
|
|
article = r.get(f"rmi:news:article:{aid}")
|
|
if article:
|
|
a = json.loads(article)
|
|
lines.append(f"[{a.get('source', '?')}] {a['title']}")
|
|
return "\n".join(lines) + "\n\n---\nPowered by RMI - rugmunch.io | Free crypto intelligence"
|
|
|
|
|
|
@mcp.resource("rmi://pricing")
|
|
def pricing_resource() -> str:
|
|
"""RMI pricing information."""
|
|
return """
|
|
RMI Crypto Intelligence - Pricing
|
|
==================================
|
|
Free Tier: 10 API calls/day, unlimited news search
|
|
Paid Tier: $0.01 USDC/call via x402 (HTTP 402 Payment Required)
|
|
Enterprise: Contact rugmunch.io
|
|
|
|
Tools Available:
|
|
- 500+ source crypto news search
|
|
- Entity resolution (82K+ labeled addresses)
|
|
- Token prices (Pyth Network, 125+ institutional publishers)
|
|
- Security scanning
|
|
- Sentiment analysis
|
|
- Trending ticker detection
|
|
|
|
All tools available via Model Context Protocol.
|
|
Add to your AI agent: {"rmi-intelligence": {"url": "https://YOUR_SERVER/mcp/sse"}}
|
|
"""
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run as SSE server on port 9020
|
|
mcp.run(transport="sse", host="0.0.0.0", port=9020)
|