- 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>
231 lines
8.3 KiB
Python
231 lines
8.3 KiB
Python
"""
|
|
Free MCP Server Integrations - Boar Blockchain, Solana Token Analysis, autonsol
|
|
|
|
Adds free, keyless MCP data sources to the caching shield fallback engine.
|
|
All three require NO API keys - pure free data.
|
|
|
|
Boar Blockchain (50 tools): EVM data - balances, txs, blocks, ENS, ERC-20
|
|
Solana Token Analysis (6 tools): Risk scoring 0-100, pump.fun signals, momentum
|
|
autonsol/sol-mcp: Real-time token risk + rug flags
|
|
|
|
Usage:
|
|
from app.caching_shield.mcp_sources import MCPDataSources
|
|
mcp = MCPDataSources()
|
|
score = await mcp.solana_risk_score("EPjFWd...")
|
|
balance = await mcp.evm_balance("0x...", chain_id=1)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import time
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger("mcp_sources")
|
|
|
|
# Smithery MCP endpoints (HTTP transport)
|
|
BOAR_URL = "https://server.smithery.ai/@boar-network/blockchain-advanced/mcp"
|
|
SOLANA_TOKEN_URL = "https://server.smithery.ai/@insomniactools/solana-agentkit-mcp/mcp"
|
|
AUTONSOL_URL = "https://server.smithery.ai/..." # placeholder - need exact URL
|
|
|
|
# Cache
|
|
_l1: dict[str, tuple] = {}
|
|
_cache_hits = 0
|
|
_cache_misses = 0
|
|
|
|
|
|
class MCPDataSources:
|
|
"""Wraps free MCP servers as cached data sources."""
|
|
|
|
def __init__(self):
|
|
self._http: httpx.AsyncClient | None = None
|
|
|
|
async def _get_http(self) -> httpx.AsyncClient:
|
|
if self._http is None:
|
|
self._http = httpx.AsyncClient(
|
|
timeout=15.0,
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
"User-Agent": "rmi-caching-shield/1.0",
|
|
},
|
|
)
|
|
return self._http
|
|
|
|
async def _mcp_call(self, url: str, tool: str, args: dict, ttl: int = 60) -> dict | None:
|
|
"""Make a cached MCP tool call."""
|
|
global _cache_hits, _cache_misses
|
|
cache_key = f"mcp:{tool}:{json.dumps(args, sort_keys=True)}"
|
|
|
|
# L1 cache
|
|
entry = _l1.get(cache_key)
|
|
if entry:
|
|
expiry, data = entry
|
|
if time.monotonic() < expiry:
|
|
_cache_hits += 1
|
|
return data
|
|
del _l1[cache_key]
|
|
_cache_misses += 1
|
|
|
|
http = await self._get_http()
|
|
try:
|
|
# MCP JSON-RPC call
|
|
r = await http.post(
|
|
url,
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {"name": tool, "arguments": args},
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
result = data.get("result", {}).get("content", [{}])[0].get("text", "")
|
|
if result:
|
|
try:
|
|
parsed = json.loads(result)
|
|
except Exception:
|
|
parsed = {"value": str(result)[:500]}
|
|
_l1[cache_key] = (time.monotonic() + ttl, parsed)
|
|
return parsed
|
|
except Exception as e:
|
|
logger.debug(f"MCP {tool} error: {e}")
|
|
return None
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# BOAR BLOCKCHAIN - EVM Data (50 tools, FREE, keyless)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def evm_balance(self, address: str, chain: str = "ethereum") -> dict | None:
|
|
"""Get ETH/native balance. Free, no key."""
|
|
return await self._mcp_call(
|
|
BOAR_URL,
|
|
"get_balance",
|
|
{
|
|
"address": address,
|
|
"chain": chain,
|
|
},
|
|
ttl=15,
|
|
)
|
|
|
|
async def evm_transactions(self, address: str, chain: str = "ethereum", limit: int = 10) -> dict | None:
|
|
"""Get recent transactions."""
|
|
return await self._mcp_call(
|
|
BOAR_URL,
|
|
"get_transactions",
|
|
{
|
|
"address": address,
|
|
"chain": chain,
|
|
"limit": limit,
|
|
},
|
|
ttl=30,
|
|
)
|
|
|
|
async def evm_contract_info(self, address: str, chain: str = "ethereum") -> dict | None:
|
|
"""Get contract/ERC-20 info."""
|
|
return await self._mcp_call(
|
|
BOAR_URL,
|
|
"get_contract_info",
|
|
{
|
|
"address": address,
|
|
"chain": chain,
|
|
},
|
|
ttl=300,
|
|
)
|
|
|
|
async def ens_resolve(self, name: str) -> dict | None:
|
|
"""Resolve ENS name to address."""
|
|
return await self._mcp_call(
|
|
BOAR_URL,
|
|
"resolve_ens",
|
|
{
|
|
"name": name,
|
|
},
|
|
ttl=3600,
|
|
)
|
|
|
|
async def evm_block_info(self, block_number: int | None = None, chain: str = "ethereum") -> dict | None:
|
|
"""Get block info."""
|
|
args = {"chain": chain}
|
|
if block_number:
|
|
args["block_number"] = block_number
|
|
return await self._mcp_call(BOAR_URL, "get_block", args, ttl=60)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# SOLANA TOKEN ANALYSIS - Risk Scoring (6 tools, NO AUTH)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def solana_risk_score(self, mint: str) -> dict | None:
|
|
"""Get token risk score 0-100. Free, no auth."""
|
|
return await self._mcp_call(
|
|
SOLANA_TOKEN_URL,
|
|
"analyze_token",
|
|
{
|
|
"token_address": mint,
|
|
},
|
|
ttl=60,
|
|
)
|
|
|
|
async def solana_momentum(self, mint: str) -> dict | None:
|
|
"""Get BUY/SELL momentum signals."""
|
|
return await self._mcp_call(
|
|
SOLANA_TOKEN_URL,
|
|
"get_momentum",
|
|
{
|
|
"token_address": mint,
|
|
},
|
|
ttl=15,
|
|
)
|
|
|
|
async def solana_batch_screen(self, mints: list[str]) -> dict | None:
|
|
"""Batch screen multiple tokens for risk."""
|
|
return await self._mcp_call(
|
|
SOLANA_TOKEN_URL,
|
|
"batch_screen",
|
|
{
|
|
"token_addresses": mints[:10],
|
|
},
|
|
ttl=45,
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# AUTONSOL/SOL-MCP - Rug Detection (FREE)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
async def rug_check(self, mint: str) -> dict | None:
|
|
"""Real-time rug pull detection with flags."""
|
|
return await self._mcp_call(
|
|
AUTONSOL_URL,
|
|
"check_token",
|
|
{
|
|
"mint": mint,
|
|
},
|
|
ttl=60,
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# STATS
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
def stats(self) -> dict:
|
|
return {
|
|
"cache_hits": _cache_hits,
|
|
"cache_misses": _cache_misses,
|
|
"sources": {
|
|
"boar_blockchain": "50 tools, EVM, FREE keyless",
|
|
"solana_token_analysis": "6 tools, risk scoring, NO AUTH",
|
|
"autonsol": "rug detection, FREE",
|
|
},
|
|
}
|
|
|
|
|
|
# Singleton
|
|
_mcp: MCPDataSources | None = None
|
|
|
|
|
|
def get_mcp_sources() -> MCPDataSources:
|
|
global _mcp
|
|
if _mcp is None:
|
|
_mcp = MCPDataSources()
|
|
return _mcp
|