| """ |
| Eth Labels MCP Tool — Direct access to eth-labels.db via MCP |
| |
| Provides an MCP tool to directly access the local eth-labels.db SQLite database |
| containing 115K labeled EVM addresses. Allows SQL queries against the database |
| with proper safety measures. |
| """ |
|
|
| import asyncio |
| import logging |
| from typing import Any, Dict, List |
|
|
| log = logging.getLogger(__name__) |
|
|
|
|
| def _safe_select_query(sql: str) -> bool: |
| """ |
| Check if an SQL statement is a safe SELECT query. |
| |
| Only allows SELECT statements with safety restrictions: |
| - No writes (INSERT/UPDATE/DELETE/CREATE etc.) |
| - No dangerous keywords like UNION (unless in approved cases) |
| - No complex join patterns that could abuse the data |
| |
| Args: |
| sql: SQL query string to validate |
| |
| Returns: |
| True if SQL is safe, False otherwise |
| """ |
| |
| sql_upper = sql.strip().upper() |
| |
| |
| if not sql_upper.lstrip().startswith('SELECT'): |
| return False |
| |
| |
| dangerous_keywords = [ |
| 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', |
| 'TRUNCATE', 'REPLACE', 'MERGE', 'WITH.*RECURSIVE', |
| 'INTO', 'OUTFILE', 'DUMPFILE', 'LOAD_DATA' |
| ] |
| |
| for keyword in dangerous_keywords: |
| |
| if keyword == 'WITH.*RECURSIVE': |
| if 'WITH' in sql_upper and 'RECURSIVE' in sql_upper: |
| return False |
| elif keyword in sql_upper: |
| return False |
| |
| return True |
|
|
|
|
| async def query_eth_labels_db_mcp(sql: str, limit: int = 1000) -> Dict[str, Any]: |
| """ |
| MCP tool to query eth-labels.db with SELECT-only safety. |
| |
| Args: |
| sql: SELECT SQL query to run against eth-labels.db |
| limit: Maximum number of results to return (default 1000, max 10000) |
| |
| Returns: |
| Dictionary with 'rows' (list of result rows), 'count' (number of rows), |
| 'truncated' (bool indicating if results were limited) |
| """ |
| |
| if not sql or sql.strip() == "": |
| return {"error": "'sql' parameter required"} |
| |
| if not _safe_select_query(sql): |
| return {"error": "Only SELECT statements allowed - no writes or dangerous operations"} |
| |
| |
| limit = min(limit, 10000) |
| |
| def _execute_query() -> Dict[str, Any]: |
| import sqlite3 |
| from pathlib import Path |
| |
| |
| db_path = Path("/home/dev/rmi/eth-labels.db") |
| if not db_path.exists(): |
| return {"error": f"Database file not found: {db_path}"} |
| |
| try: |
| |
| conn = sqlite3.connect(str(db_path), timeout=5.0) |
| conn.row_factory = sqlite3.Row |
| |
| |
| cursor = conn.cursor() |
| cursor.execute(sql) |
| |
| |
| rows = cursor.fetchall() |
| |
| |
| result_rows = [dict(row) for row in rows[:limit]] |
| |
| conn.close() |
| |
| return { |
| "rows": result_rows, |
| "count": len(result_rows), |
| "truncated": len(rows) > limit, |
| "sql_executed": sql |
| } |
| |
| except sqlite3.Error as e: |
| return {"error": f"SQLite error: {str(e)}"} |
| except Exception as e: |
| return {"error": f"Query execution failed: {str(e)}"} |
| |
| |
| result = await asyncio.to_thread(_execute_query) |
| |
| if isinstance(result, dict) and "error" in result: |
| log.warning("eth_labels_db_mcp_error sql=%s err=%s", sql[:100], result["error"]) |
| |
| return result |
|
|
|
|
| async def get_eth_labels_stats_mcp() -> Dict[str, Any]: |
| """ |
| Get statistics about the eth-labels.db database. |
| |
| Returns: |
| Database statistics including table counts and sample data. |
| """ |
| def _get_stats() -> Dict[str, Any]: |
| import sqlite3 |
| from pathlib import Path |
| |
| db_path = Path("/home/dev/rmi/eth-labels.db") |
| if not db_path.exists(): |
| return {"error": f"Database file not found: {db_path}"} |
| |
| try: |
| conn = sqlite3.connect(str(db_path), timeout=5.0) |
| |
| |
| cursor = conn.cursor() |
| cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") |
| tables = [row[0] for row in cursor.fetchall()] |
| |
| |
| stats = {"tables": tables} |
| |
| if "accounts" in tables: |
| |
| cursor.execute("SELECT COUNT(*) FROM accounts;") |
| stats["total_accounts"] = cursor.fetchone()[0] |
| |
| |
| cursor.execute("SELECT DISTINCT chain_id FROM accounts;") |
| chain_ids = [row[0] for row in cursor.fetchall()] |
| stats["chain_ids"] = chain_ids |
| |
| |
| cursor.execute(""" |
| SELECT chain_id, COUNT(*) as count |
| FROM accounts |
| GROUP BY chain_id |
| ORDER BY count DESC |
| LIMIT 10; |
| """) |
| stats["accounts_by_chain"] = [ |
| {"chain_id": row[0], "count": row[1]} |
| for row in cursor.fetchall() |
| ] |
| |
| conn.close() |
| return stats |
| |
| except Exception as e: |
| return {"error": f"Stats query failed: {str(e)}"} |
| |
| result = await asyncio.to_thread(_get_stats) |
| |
| if isinstance(result, dict) and "error" in result: |
| log.warning("eth_labels_stats_mcp_error err=%s", result["error"]) |
| |
| return result |