spice-mcp 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
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.
- spice_mcp/adapters/dune/client.py +13 -29
- spice_mcp/adapters/dune/extract.py +1 -1
- spice_mcp/adapters/dune/query_wrapper.py +1 -1
- spice_mcp/adapters/spellbook/explorer.py +84 -1
- spice_mcp/mcp/server.py +141 -106
- spice_mcp/mcp/tools/execute_query.py +13 -21
- spice_mcp/service_layer/verification_service.py +185 -0
- {spice_mcp-0.1.4.dist-info → spice_mcp-0.1.5.dist-info}/METADATA +18 -6
- {spice_mcp-0.1.4.dist-info → spice_mcp-0.1.5.dist-info}/RECORD +12 -11
- {spice_mcp-0.1.4.dist-info → spice_mcp-0.1.5.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.4.dist-info → spice_mcp-0.1.5.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.4.dist-info → spice_mcp-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
5
4
|
import time
|
|
6
5
|
from collections.abc import Mapping, Sequence
|
|
7
6
|
from typing import Any
|
|
@@ -47,10 +46,9 @@ class DuneAdapter(QueryExecutor, CatalogExplorer):
|
|
|
47
46
|
self._ensure_api_key()
|
|
48
47
|
start = time.time()
|
|
49
48
|
q = request.query
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
q = q_rewritten
|
|
49
|
+
# Use native SHOW statements directly - they're faster than information_schema queries
|
|
50
|
+
# See issue #10: https://github.com/Evan-Kim2028/spice-mcp/issues/10
|
|
51
|
+
# Removed rewrite to avoid performance issues with information_schema queries
|
|
54
52
|
result = _execute_dune_query(
|
|
55
53
|
query_or_execution=q,
|
|
56
54
|
verbose=False,
|
|
@@ -200,7 +198,9 @@ class DuneAdapter(QueryExecutor, CatalogExplorer):
|
|
|
200
198
|
# Internal helpers --------------------------------------------------------------
|
|
201
199
|
def _run_sql(self, sql: str, *, limit: int | None = None) -> pl.DataFrame:
|
|
202
200
|
self._ensure_api_key()
|
|
203
|
-
|
|
201
|
+
# Use native SHOW statements directly - they're faster than information_schema queries
|
|
202
|
+
# See issue #10: https://github.com/Evan-Kim2028/spice-mcp/issues/10
|
|
203
|
+
sql_eff = sql
|
|
204
204
|
df = _execute_dune_query(
|
|
205
205
|
query_or_execution=sql_eff,
|
|
206
206
|
verbose=False,
|
|
@@ -233,28 +233,12 @@ def _build_preview(lf: pl.LazyFrame, columns: list[str], rowcount: int) -> Resul
|
|
|
233
233
|
|
|
234
234
|
|
|
235
235
|
def _maybe_rewrite_show_sql(sql: str) -> str | None:
|
|
236
|
-
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
"""DEPRECATED: This function is no longer used.
|
|
237
|
+
|
|
238
|
+
Native SHOW statements are now used directly as they're faster than
|
|
239
|
+
information_schema queries in Dune. See issue #10 for details.
|
|
240
|
+
|
|
241
|
+
This function is kept for backward compatibility but is not called.
|
|
240
242
|
"""
|
|
241
|
-
|
|
242
|
-
m = re.match(r"^SHOW\s+SCHEMAS\s+LIKE\s+'([^']+)'\s*;?$", s, flags=re.IGNORECASE)
|
|
243
|
-
if m:
|
|
244
|
-
pat = m.group(1)
|
|
245
|
-
return (
|
|
246
|
-
"SELECT schema_name AS Schema FROM information_schema.schemata "
|
|
247
|
-
f"WHERE schema_name LIKE '{pat}'"
|
|
248
|
-
)
|
|
249
|
-
if re.match(r"^SHOW\s+SCHEMAS\s*;?$", s, flags=re.IGNORECASE):
|
|
250
|
-
return "SELECT schema_name AS Schema FROM information_schema.schemata"
|
|
251
|
-
|
|
252
|
-
m = re.match(r"^SHOW\s+TABLES\s+FROM\s+([A-Za-z0-9_\.]+)\s*;?$", s, flags=re.IGNORECASE)
|
|
253
|
-
if m:
|
|
254
|
-
schema = m.group(1)
|
|
255
|
-
return (
|
|
256
|
-
"SELECT table_name AS Table FROM information_schema.tables "
|
|
257
|
-
f"WHERE table_schema = '{schema}'"
|
|
258
|
-
)
|
|
259
|
-
|
|
243
|
+
# Function body kept for reference but not executed
|
|
260
244
|
return None
|
|
@@ -6,8 +6,8 @@ the @overload decorators that FastMCP detects during runtime validation.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import Any
|
|
10
9
|
from collections.abc import Mapping, Sequence
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from ..http_client import HttpClient
|
|
13
13
|
from .types import Execution, Performance, Query
|
|
@@ -133,11 +133,43 @@ class SpellbookExplorer(CatalogExplorer):
|
|
|
133
133
|
if not schema_yml.exists():
|
|
134
134
|
schema_yml = sql_file.parent.parent / "schema.yml"
|
|
135
135
|
|
|
136
|
+
# Parse dbt config to get actual Dune table name
|
|
137
|
+
config = self._parse_dbt_config(sql_file)
|
|
138
|
+
|
|
139
|
+
# Ignore templated dbt config values like "{{ target.schema }}"
|
|
140
|
+
def _is_templated(val: Any) -> bool:
|
|
141
|
+
try:
|
|
142
|
+
s = str(val)
|
|
143
|
+
except Exception:
|
|
144
|
+
return False
|
|
145
|
+
return "{{" in s and "}}" in s
|
|
146
|
+
|
|
147
|
+
raw_schema = config.get("schema")
|
|
148
|
+
raw_alias = config.get("alias")
|
|
149
|
+
|
|
150
|
+
dune_schema = (
|
|
151
|
+
raw_schema.strip() if isinstance(raw_schema, str) else raw_schema
|
|
152
|
+
)
|
|
153
|
+
dune_alias = (
|
|
154
|
+
raw_alias.strip() if isinstance(raw_alias, str) else raw_alias
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Fall back to original names when values are templated or empty
|
|
158
|
+
if not dune_schema or _is_templated(dune_schema):
|
|
159
|
+
dune_schema = schema_name
|
|
160
|
+
if not dune_alias or _is_templated(dune_alias):
|
|
161
|
+
dune_alias = model_name
|
|
162
|
+
|
|
163
|
+
dune_table = f"{dune_schema}.{dune_alias}"
|
|
164
|
+
|
|
136
165
|
models[schema_name].append({
|
|
137
166
|
"name": model_name,
|
|
138
167
|
"file": sql_file,
|
|
139
168
|
"schema_yml": schema_yml if schema_yml.exists() else None,
|
|
140
169
|
"schema": schema_name,
|
|
170
|
+
"dune_schema": dune_schema,
|
|
171
|
+
"dune_alias": dune_alias,
|
|
172
|
+
"dune_table": dune_table,
|
|
141
173
|
})
|
|
142
174
|
|
|
143
175
|
self._models_cache = models
|
|
@@ -264,6 +296,58 @@ class SpellbookExplorer(CatalogExplorer):
|
|
|
264
296
|
|
|
265
297
|
return []
|
|
266
298
|
|
|
299
|
+
def _parse_dbt_config(self, sql_file: Path) -> dict[str, str]:
|
|
300
|
+
"""
|
|
301
|
+
Parse dbt config block from SQL file to extract schema and alias.
|
|
302
|
+
|
|
303
|
+
Looks for patterns like:
|
|
304
|
+
{{ config(schema='sui_walrus', alias='base_table') }}
|
|
305
|
+
{{ config(schema="sui_walrus", alias="base_table") }}
|
|
306
|
+
|
|
307
|
+
Returns dict with 'schema' and 'alias' keys, or empty dict if not found.
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
with open(sql_file, encoding="utf-8") as f:
|
|
311
|
+
sql = f.read()
|
|
312
|
+
|
|
313
|
+
# Match dbt config block: {{ config(...) }}
|
|
314
|
+
# Use non-greedy match to get first config block
|
|
315
|
+
config_match = re.search(
|
|
316
|
+
r"{{\s*config\s*\((.*?)\)\s*}}",
|
|
317
|
+
sql,
|
|
318
|
+
re.IGNORECASE | re.DOTALL,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if not config_match:
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
config_content = config_match.group(1)
|
|
325
|
+
result: dict[str, str] = {}
|
|
326
|
+
|
|
327
|
+
# Extract schema parameter (supports single and double quotes)
|
|
328
|
+
schema_match = re.search(
|
|
329
|
+
r"schema\s*=\s*['\"]([^'\"]+)['\"]",
|
|
330
|
+
config_content,
|
|
331
|
+
re.IGNORECASE,
|
|
332
|
+
)
|
|
333
|
+
if schema_match:
|
|
334
|
+
result["schema"] = schema_match.group(1)
|
|
335
|
+
|
|
336
|
+
# Extract alias parameter (supports single and double quotes)
|
|
337
|
+
alias_match = re.search(
|
|
338
|
+
r"alias\s*=\s*['\"]([^'\"]+)['\"]",
|
|
339
|
+
config_content,
|
|
340
|
+
re.IGNORECASE,
|
|
341
|
+
)
|
|
342
|
+
if alias_match:
|
|
343
|
+
result["alias"] = alias_match.group(1)
|
|
344
|
+
|
|
345
|
+
return result
|
|
346
|
+
except Exception:
|
|
347
|
+
# On any error (file read, parsing, etc.), return empty dict
|
|
348
|
+
# This allows fallback to using schema_name and model_name
|
|
349
|
+
return {}
|
|
350
|
+
|
|
267
351
|
def _parse_sql_columns(self, sql_file: Path) -> list[TableColumn]:
|
|
268
352
|
"""Parse SQL file to extract column names from SELECT statements."""
|
|
269
353
|
try:
|
|
@@ -310,4 +394,3 @@ class SpellbookExplorer(CatalogExplorer):
|
|
|
310
394
|
pass
|
|
311
395
|
|
|
312
396
|
return []
|
|
313
|
-
|
spice_mcp/mcp/server.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any, Literal
|
|
5
|
+
from typing import Any, Literal
|
|
6
6
|
|
|
7
7
|
os.environ.setdefault("FASTMCP_NO_BANNER", "1")
|
|
8
8
|
os.environ.setdefault("FASTMCP_LOG_LEVEL", "ERROR")
|
|
@@ -33,6 +33,7 @@ from ..logging.query_history import QueryHistory
|
|
|
33
33
|
from ..service_layer.discovery_service import DiscoveryService
|
|
34
34
|
from ..service_layer.query_admin_service import QueryAdminService
|
|
35
35
|
from ..service_layer.query_service import QueryService
|
|
36
|
+
from ..service_layer.verification_service import VerificationService
|
|
36
37
|
from .tools.execute_query import ExecuteQueryTool
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
@@ -47,6 +48,7 @@ QUERY_ADMIN_SERVICE: QueryAdminService | None = None
|
|
|
47
48
|
DISCOVERY_SERVICE: DiscoveryService | None = None
|
|
48
49
|
SPELLBOOK_EXPLORER: SpellbookExplorer | None = None
|
|
49
50
|
HTTP_CLIENT: HttpClient | None = None
|
|
51
|
+
VERIFICATION_SERVICE: VerificationService | None = None
|
|
50
52
|
|
|
51
53
|
EXECUTE_QUERY_TOOL: ExecuteQueryTool | None = None
|
|
52
54
|
|
|
@@ -57,7 +59,7 @@ app = FastMCP("spice-mcp")
|
|
|
57
59
|
def _ensure_initialized() -> None:
|
|
58
60
|
"""Initialize configuration and tool instances if not already initialized."""
|
|
59
61
|
global CONFIG, QUERY_HISTORY, DUNE_ADAPTER, QUERY_SERVICE, DISCOVERY_SERVICE, QUERY_ADMIN_SERVICE
|
|
60
|
-
global EXECUTE_QUERY_TOOL, HTTP_CLIENT, SPELLBOOK_EXPLORER
|
|
62
|
+
global EXECUTE_QUERY_TOOL, HTTP_CLIENT, SPELLBOOK_EXPLORER, VERIFICATION_SERVICE
|
|
61
63
|
|
|
62
64
|
if CONFIG is not None and EXECUTE_QUERY_TOOL is not None:
|
|
63
65
|
return
|
|
@@ -95,6 +97,15 @@ def _ensure_initialized() -> None:
|
|
|
95
97
|
|
|
96
98
|
# Initialize Spellbook explorer (lazy, clones repo on first use)
|
|
97
99
|
SPELLBOOK_EXPLORER = SpellbookExplorer()
|
|
100
|
+
|
|
101
|
+
# Initialize verification service with persistent cache
|
|
102
|
+
from pathlib import Path
|
|
103
|
+
cache_dir = Path.home() / ".spice_mcp"
|
|
104
|
+
cache_dir.mkdir(exist_ok=True)
|
|
105
|
+
VERIFICATION_SERVICE = VerificationService(
|
|
106
|
+
cache_path=cache_dir / "table_verification_cache.json",
|
|
107
|
+
dune_adapter=DUNE_ADAPTER,
|
|
108
|
+
)
|
|
98
109
|
|
|
99
110
|
EXECUTE_QUERY_TOOL = ExecuteQueryTool(CONFIG, QUERY_SERVICE, QUERY_HISTORY)
|
|
100
111
|
|
|
@@ -200,17 +211,17 @@ def dune_query_info(query: str) -> dict[str, Any]:
|
|
|
200
211
|
|
|
201
212
|
def _dune_query_impl(
|
|
202
213
|
query: str,
|
|
203
|
-
parameters:
|
|
214
|
+
parameters: dict[str, Any] | None = None,
|
|
204
215
|
refresh: bool = False,
|
|
205
|
-
max_age:
|
|
206
|
-
limit:
|
|
207
|
-
offset:
|
|
208
|
-
sample_count:
|
|
209
|
-
sort_by:
|
|
210
|
-
columns:
|
|
216
|
+
max_age: float | None = None,
|
|
217
|
+
limit: int | None = None,
|
|
218
|
+
offset: int | None = None,
|
|
219
|
+
sample_count: int | None = None,
|
|
220
|
+
sort_by: str | None = None,
|
|
221
|
+
columns: list[str] | None = None,
|
|
211
222
|
format: Literal["preview", "raw", "metadata", "poll"] = "preview",
|
|
212
|
-
extras:
|
|
213
|
-
timeout_seconds:
|
|
223
|
+
extras: dict[str, Any] | None = None,
|
|
224
|
+
timeout_seconds: float | None = None,
|
|
214
225
|
) -> dict[str, Any]:
|
|
215
226
|
"""Internal implementation of dune_query to avoid FastMCP overload detection."""
|
|
216
227
|
_ensure_initialized()
|
|
@@ -275,20 +286,32 @@ def _dune_query_impl(
|
|
|
275
286
|
)
|
|
276
287
|
def dune_query(
|
|
277
288
|
query: str,
|
|
278
|
-
parameters:
|
|
289
|
+
parameters: dict[str, Any] | None = None,
|
|
279
290
|
refresh: bool = False,
|
|
280
|
-
max_age:
|
|
281
|
-
limit:
|
|
282
|
-
offset:
|
|
283
|
-
sample_count:
|
|
284
|
-
sort_by:
|
|
285
|
-
columns:
|
|
291
|
+
max_age: float | None = None,
|
|
292
|
+
limit: int | None = None,
|
|
293
|
+
offset: int | None = None,
|
|
294
|
+
sample_count: int | None = None,
|
|
295
|
+
sort_by: str | None = None,
|
|
296
|
+
columns: list[str] | None = None,
|
|
286
297
|
format: Literal["preview", "raw", "metadata", "poll"] = "preview",
|
|
287
|
-
extras:
|
|
288
|
-
timeout_seconds:
|
|
298
|
+
extras: dict[str, Any] | None = None,
|
|
299
|
+
timeout_seconds: float | None = None,
|
|
289
300
|
) -> dict[str, Any]:
|
|
290
301
|
"""Execute Dune queries (by ID, URL, or raw SQL) and return agent-optimized preview.
|
|
291
302
|
|
|
303
|
+
⚠️ IMPORTANT: ALWAYS use dune_discover FIRST to find verified table names.
|
|
304
|
+
Do not guess table names or query information_schema directly.
|
|
305
|
+
|
|
306
|
+
The query parameter accepts:
|
|
307
|
+
- Query IDs (e.g., "123456")
|
|
308
|
+
- Query URLs (e.g., "https://dune.com/queries/123456")
|
|
309
|
+
- Raw SQL using tables discovered via dune_discover
|
|
310
|
+
|
|
311
|
+
For Spellbook models, use the 'dune_table' field returned by dune_discover.
|
|
312
|
+
Example: dune_discover(keyword="walrus") → returns dune_table="sui_walrus.base_table"
|
|
313
|
+
Then use: dune_query(query="SELECT * FROM sui_walrus.base_table LIMIT 10")
|
|
314
|
+
|
|
292
315
|
This wrapper ensures FastMCP doesn't detect overloads in imported functions.
|
|
293
316
|
"""
|
|
294
317
|
# Always ensure parameters is explicitly passed (even if None) to avoid FastMCP
|
|
@@ -319,22 +342,6 @@ def dune_health_check() -> dict[str, Any]:
|
|
|
319
342
|
return compute_health_status()
|
|
320
343
|
|
|
321
344
|
|
|
322
|
-
def _dune_find_tables_impl(
|
|
323
|
-
keyword: str | None = None,
|
|
324
|
-
schema: str | None = None,
|
|
325
|
-
limit: int = 50,
|
|
326
|
-
) -> dict[str, Any]:
|
|
327
|
-
_ensure_initialized()
|
|
328
|
-
assert DISCOVERY_SERVICE is not None
|
|
329
|
-
out: dict[str, Any] = {}
|
|
330
|
-
if keyword:
|
|
331
|
-
out["schemas"] = DISCOVERY_SERVICE.find_schemas(keyword)
|
|
332
|
-
if schema:
|
|
333
|
-
tables = DISCOVERY_SERVICE.list_tables(schema, limit=limit)
|
|
334
|
-
out["tables"] = [summary.table for summary in tables]
|
|
335
|
-
return out
|
|
336
|
-
|
|
337
|
-
|
|
338
345
|
def _unified_discover_impl(
|
|
339
346
|
keyword: str | list[str] | None = None,
|
|
340
347
|
schema: str | None = None,
|
|
@@ -380,6 +387,8 @@ def _unified_discover_impl(
|
|
|
380
387
|
"table": summary.table,
|
|
381
388
|
"fully_qualified_name": f"{schema}.{summary.table}",
|
|
382
389
|
"source": "dune",
|
|
390
|
+
"dune_table": f"{schema}.{summary.table}",
|
|
391
|
+
"verified": True,
|
|
383
392
|
}
|
|
384
393
|
for summary in tables
|
|
385
394
|
]
|
|
@@ -415,11 +424,57 @@ def _unified_discover_impl(
|
|
|
415
424
|
"table": model["table"],
|
|
416
425
|
"fully_qualified_name": model["fully_qualified_name"],
|
|
417
426
|
"source": "spellbook",
|
|
427
|
+
# Include resolved Dune table names
|
|
428
|
+
"dune_schema": model.get("dune_schema"),
|
|
429
|
+
"dune_alias": model.get("dune_alias"),
|
|
430
|
+
"dune_table": model.get("dune_table"),
|
|
418
431
|
}
|
|
419
432
|
if "columns" in model:
|
|
420
433
|
table_info["columns"] = model["columns"]
|
|
421
434
|
out["tables"].append(table_info)
|
|
422
435
|
|
|
436
|
+
# Verify Spellbook tables exist in Dune before returning
|
|
437
|
+
if source in ("spellbook", "both") and out["tables"]:
|
|
438
|
+
assert VERIFICATION_SERVICE is not None
|
|
439
|
+
# Extract Spellbook tables that need verification
|
|
440
|
+
spellbook_tables = [
|
|
441
|
+
(t["dune_schema"], t["dune_alias"])
|
|
442
|
+
for t in out["tables"]
|
|
443
|
+
if t.get("source") == "spellbook" and t.get("dune_schema") and t.get("dune_alias")
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
if spellbook_tables:
|
|
447
|
+
# Verify tables exist (uses cache, queries Dune only if needed)
|
|
448
|
+
verification_results = VERIFICATION_SERVICE.verify_tables_batch(spellbook_tables)
|
|
449
|
+
|
|
450
|
+
# Filter: drop only tables explicitly verified as False.
|
|
451
|
+
# Keep tables when verification is True or inconclusive (missing).
|
|
452
|
+
verified_tables = []
|
|
453
|
+
for t in out["tables"]:
|
|
454
|
+
if t.get("source") != "spellbook":
|
|
455
|
+
# Keep Dune tables as-is
|
|
456
|
+
verified_tables.append(t)
|
|
457
|
+
continue
|
|
458
|
+
dune_fqn = t.get("dune_table")
|
|
459
|
+
if not dune_fqn:
|
|
460
|
+
# If we couldn't resolve dune_table, keep it (conservative)
|
|
461
|
+
verified_tables.append(t)
|
|
462
|
+
continue
|
|
463
|
+
vr = verification_results.get(dune_fqn)
|
|
464
|
+
if vr is False:
|
|
465
|
+
# Explicitly known to be non-existent -> drop
|
|
466
|
+
continue
|
|
467
|
+
if vr is True:
|
|
468
|
+
t["verified"] = True
|
|
469
|
+
# If vr is None/missing (inconclusive), keep without setting verified
|
|
470
|
+
verified_tables.append(t)
|
|
471
|
+
|
|
472
|
+
out["tables"] = verified_tables
|
|
473
|
+
|
|
474
|
+
# Add helpful message if no tables found
|
|
475
|
+
if not out["tables"] and len(spellbook_tables) > 0:
|
|
476
|
+
out["message"] = "No verified tables found. Try different keywords or check schema names."
|
|
477
|
+
|
|
423
478
|
# Deduplicate and sort schemas
|
|
424
479
|
out["schemas"] = sorted(list(set(out["schemas"])))
|
|
425
480
|
|
|
@@ -444,11 +499,19 @@ def dune_discover(
|
|
|
444
499
|
include_columns: bool = True,
|
|
445
500
|
) -> dict[str, Any]:
|
|
446
501
|
"""
|
|
447
|
-
|
|
502
|
+
PRIMARY discovery tool for finding tables in Dune.
|
|
503
|
+
|
|
504
|
+
⚠️ IMPORTANT: ALWAYS use this tool instead of querying information_schema directly.
|
|
505
|
+
Querying information_schema is slow and causes lag. This tool uses optimized
|
|
506
|
+
native SHOW statements for fast discovery.
|
|
448
507
|
|
|
449
|
-
This tool
|
|
450
|
-
|
|
451
|
-
|
|
508
|
+
This tool automatically:
|
|
509
|
+
- Parses dbt configs from Spellbook models to resolve actual Dune table names
|
|
510
|
+
- Verifies tables exist in Dune before returning them
|
|
511
|
+
- Returns ONLY verified, queryable tables
|
|
512
|
+
|
|
513
|
+
All returned tables are VERIFIED to exist - you can query them immediately using
|
|
514
|
+
the 'dune_table' field.
|
|
452
515
|
|
|
453
516
|
Args:
|
|
454
517
|
keyword: Search term(s) - can be a string or list of strings
|
|
@@ -463,16 +526,25 @@ def dune_discover(
|
|
|
463
526
|
Dictionary with:
|
|
464
527
|
- 'schemas': List of matching schema names
|
|
465
528
|
- 'tables': List of table/model objects, each with:
|
|
466
|
-
- schema: Schema name
|
|
467
|
-
- table: Table/model name
|
|
468
|
-
- fully_qualified_name: schema.table
|
|
529
|
+
- schema: Schema name (Spellbook subproject name)
|
|
530
|
+
- table: Table/model name (Spellbook model name)
|
|
531
|
+
- fully_qualified_name: schema.table (Spellbook format)
|
|
469
532
|
- source: "dune" or "spellbook"
|
|
533
|
+
- dune_schema: Actual Dune schema name (for Spellbook models)
|
|
534
|
+
- dune_alias: Actual Dune table alias (for Spellbook models)
|
|
535
|
+
- dune_table: Verified, queryable Dune table name (e.g., "sui_walrus.base_table")
|
|
536
|
+
- verified: True (all returned tables are verified to exist)
|
|
470
537
|
- columns: Column details (for Spellbook models, if include_columns=True)
|
|
471
538
|
- 'source': The source parameter used
|
|
539
|
+
- 'message': Helpful message if no tables found
|
|
472
540
|
|
|
473
541
|
Examples:
|
|
474
|
-
# Search both sources for
|
|
475
|
-
dune_discover(keyword="
|
|
542
|
+
# Search both sources for walrus - returns verified tables only
|
|
543
|
+
dune_discover(keyword="walrus")
|
|
544
|
+
# → Returns tables with dune_table field like "sui_walrus.base_table"
|
|
545
|
+
|
|
546
|
+
# Use the dune_table field to query immediately
|
|
547
|
+
dune_query(query="SELECT * FROM sui_walrus.base_table LIMIT 10")
|
|
476
548
|
|
|
477
549
|
# Search only Spellbook
|
|
478
550
|
dune_discover(keyword=["layerzero", "bridge"], source="spellbook")
|
|
@@ -500,26 +572,6 @@ def dune_discover(
|
|
|
500
572
|
})
|
|
501
573
|
|
|
502
574
|
|
|
503
|
-
@app.tool(
|
|
504
|
-
name="dune_find_tables",
|
|
505
|
-
title="Find Tables",
|
|
506
|
-
description="Search schemas and optionally list tables.",
|
|
507
|
-
tags={"dune", "schema"},
|
|
508
|
-
)
|
|
509
|
-
def dune_find_tables(keyword: str | None = None, schema: str | None = None, limit: int = 50) -> dict[str, Any]:
|
|
510
|
-
"""
|
|
511
|
-
Search schemas and optionally list tables in Dune.
|
|
512
|
-
"""
|
|
513
|
-
try:
|
|
514
|
-
return _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
|
|
515
|
-
except Exception as e:
|
|
516
|
-
return error_response(e, context={
|
|
517
|
-
"tool": "dune_find_tables",
|
|
518
|
-
"keyword": keyword,
|
|
519
|
-
"schema": schema,
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
|
|
523
575
|
def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
524
576
|
_ensure_initialized()
|
|
525
577
|
assert DISCOVERY_SERVICE is not None
|
|
@@ -594,10 +646,22 @@ def _spellbook_find_models_impl(
|
|
|
594
646
|
matches_keyword = any(kw.lower() in table_name for kw in keywords)
|
|
595
647
|
|
|
596
648
|
if matches_keyword:
|
|
649
|
+
# Get model details including resolved Dune table names
|
|
650
|
+
models_dict = SPELLBOOK_EXPLORER._load_models()
|
|
651
|
+
model_details = None
|
|
652
|
+
for m in models_dict.get(schema_name, []):
|
|
653
|
+
if m["name"] == table_summary.table:
|
|
654
|
+
model_details = m
|
|
655
|
+
break
|
|
656
|
+
|
|
597
657
|
model_info: dict[str, Any] = {
|
|
598
658
|
"schema": schema_name,
|
|
599
659
|
"table": table_summary.table,
|
|
600
660
|
"fully_qualified_name": f"{schema_name}.{table_summary.table}",
|
|
661
|
+
# Include resolved Dune table names if available
|
|
662
|
+
"dune_schema": model_details.get("dune_schema") if model_details else None,
|
|
663
|
+
"dune_alias": model_details.get("dune_alias") if model_details else None,
|
|
664
|
+
"dune_table": model_details.get("dune_table") if model_details else None,
|
|
601
665
|
}
|
|
602
666
|
|
|
603
667
|
# Include column details if requested
|
|
@@ -629,10 +693,22 @@ def _spellbook_find_models_impl(
|
|
|
629
693
|
out["models"] = []
|
|
630
694
|
|
|
631
695
|
for table_summary in tables:
|
|
696
|
+
# Get model details including resolved Dune table names
|
|
697
|
+
models_dict = SPELLBOOK_EXPLORER._load_models()
|
|
698
|
+
model_details = None
|
|
699
|
+
for m in models_dict.get(schema, []):
|
|
700
|
+
if m["name"] == table_summary.table:
|
|
701
|
+
model_details = m
|
|
702
|
+
break
|
|
703
|
+
|
|
632
704
|
model_info: dict[str, Any] = {
|
|
633
705
|
"schema": schema,
|
|
634
706
|
"table": table_summary.table,
|
|
635
707
|
"fully_qualified_name": f"{schema}.{table_summary.table}",
|
|
708
|
+
# Include resolved Dune table names if available
|
|
709
|
+
"dune_schema": model_details.get("dune_schema") if model_details else None,
|
|
710
|
+
"dune_alias": model_details.get("dune_alias") if model_details else None,
|
|
711
|
+
"dune_table": model_details.get("dune_table") if model_details else None,
|
|
636
712
|
}
|
|
637
713
|
|
|
638
714
|
# Include column details if requested
|
|
@@ -656,47 +732,6 @@ def _spellbook_find_models_impl(
|
|
|
656
732
|
return out
|
|
657
733
|
|
|
658
734
|
|
|
659
|
-
@app.tool(
|
|
660
|
-
name="spellbook_find_models",
|
|
661
|
-
title="Search Spellbook",
|
|
662
|
-
description="Search dbt models in Spellbook GitHub repository.",
|
|
663
|
-
tags={"spellbook", "dbt", "schema"},
|
|
664
|
-
)
|
|
665
|
-
def spellbook_find_models(
|
|
666
|
-
keyword: str | list[str] | None = None,
|
|
667
|
-
schema: str | None = None,
|
|
668
|
-
limit: int = 50,
|
|
669
|
-
include_columns: bool = True,
|
|
670
|
-
) -> dict[str, Any]:
|
|
671
|
-
"""
|
|
672
|
-
Search Spellbook dbt models from GitHub repository.
|
|
673
|
-
|
|
674
|
-
Args:
|
|
675
|
-
keyword: Search term(s) to find models - can be a string or list of strings
|
|
676
|
-
schema: Schema/subproject name to list tables from
|
|
677
|
-
limit: Maximum number of models to return
|
|
678
|
-
include_columns: Whether to include column details in results (default: True)
|
|
679
|
-
|
|
680
|
-
Returns:
|
|
681
|
-
Dictionary with 'schemas' and 'models' keys
|
|
682
|
-
"""
|
|
683
|
-
try:
|
|
684
|
-
return _spellbook_find_models_impl(
|
|
685
|
-
keyword=keyword,
|
|
686
|
-
schema=schema,
|
|
687
|
-
limit=limit,
|
|
688
|
-
include_columns=include_columns,
|
|
689
|
-
)
|
|
690
|
-
except Exception as e:
|
|
691
|
-
return error_response(e, context={
|
|
692
|
-
"tool": "spellbook_find_models",
|
|
693
|
-
"keyword": keyword,
|
|
694
|
-
"schema": schema,
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
735
|
# Resources
|
|
701
736
|
@app.resource(uri="spice:history/tail/{n}", name="Query History Tail", description="Tail last N lines from query history")
|
|
702
737
|
def history_tail(n: str) -> str:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
5
4
|
import time
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
7
|
from ...adapters.dune import urls as dune_urls
|
|
9
8
|
from ...adapters.dune.query_wrapper import execute_query as execute_dune_query
|
|
9
|
+
|
|
10
10
|
# Import user_agent from separate module to avoid importing overloaded functions
|
|
11
11
|
from ...adapters.dune.user_agent import get_user_agent as get_dune_user_agent
|
|
12
12
|
from ...adapters.http_client import HttpClient
|
|
@@ -101,8 +101,10 @@ class ExecuteQueryTool(MCPTool):
|
|
|
101
101
|
) -> dict[str, Any]:
|
|
102
102
|
t0 = time.time()
|
|
103
103
|
try:
|
|
104
|
-
#
|
|
105
|
-
|
|
104
|
+
# Use native SHOW statements directly - they're faster than information_schema queries
|
|
105
|
+
# See issue #10: https://github.com/Evan-Kim2028/spice-mcp/issues/10
|
|
106
|
+
# Removed rewrite to avoid performance issues with information_schema queries
|
|
107
|
+
q_use = query
|
|
106
108
|
# Poll-only: return execution handle without fetching results
|
|
107
109
|
if format == "poll":
|
|
108
110
|
exec_obj = execute_dune_query(
|
|
@@ -395,22 +397,12 @@ def _categorize_query(q: str) -> str:
|
|
|
395
397
|
|
|
396
398
|
|
|
397
399
|
def _maybe_rewrite_show_sql(sql: str) -> str | None:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if re.match(r"^SHOW\s+SCHEMAS\s*;?$", s, flags=re.IGNORECASE):
|
|
407
|
-
return "SELECT schema_name AS Schema FROM information_schema.schemata"
|
|
408
|
-
|
|
409
|
-
m = re.match(r"^SHOW\s+TABLES\s+FROM\s+([A-Za-z0-9_\.]+)\s*;?$", s, flags=re.IGNORECASE)
|
|
410
|
-
if m:
|
|
411
|
-
schema = m.group(1)
|
|
412
|
-
return (
|
|
413
|
-
"SELECT table_name AS Table FROM information_schema.tables "
|
|
414
|
-
f"WHERE table_schema = '{schema}'"
|
|
415
|
-
)
|
|
400
|
+
"""DEPRECATED: This function is no longer used.
|
|
401
|
+
|
|
402
|
+
Native SHOW statements are now used directly as they're faster than
|
|
403
|
+
information_schema queries in Dune. See issue #10 for details.
|
|
404
|
+
|
|
405
|
+
This function is kept for backward compatibility but is not called.
|
|
406
|
+
"""
|
|
407
|
+
# Function body kept for reference but not executed
|
|
416
408
|
return None
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verification Service - Verifies tables exist in Dune with persistent caching.
|
|
3
|
+
|
|
4
|
+
This service provides lazy verification of table existence, caching results
|
|
5
|
+
to avoid repeated queries. Cache persists across server restarts.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..adapters.dune.client import DuneAdapter
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Cache entry expires after 1 week (604800 seconds)
|
|
20
|
+
CACHE_TTL_SECONDS = 604800
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VerificationService:
|
|
24
|
+
"""
|
|
25
|
+
Service for verifying table existence in Dune with persistent caching.
|
|
26
|
+
|
|
27
|
+
Verifies tables exist before returning them to users, ensuring only
|
|
28
|
+
queryable tables are surfaced. Uses persistent cache to avoid repeated
|
|
29
|
+
verification queries.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, cache_path: Path, dune_adapter: DuneAdapter):
|
|
33
|
+
"""
|
|
34
|
+
Initialize verification service.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
cache_path: Path to JSON file for persistent cache storage
|
|
38
|
+
dune_adapter: DuneAdapter instance for querying table existence
|
|
39
|
+
"""
|
|
40
|
+
self.cache_path = cache_path
|
|
41
|
+
self.dune_adapter = dune_adapter
|
|
42
|
+
self._cache: dict[str, dict[str, Any]] = self._load_cache()
|
|
43
|
+
|
|
44
|
+
def verify_tables_batch(
|
|
45
|
+
self, tables: list[tuple[str, str]]
|
|
46
|
+
) -> dict[str, bool]:
|
|
47
|
+
"""
|
|
48
|
+
Verify multiple tables exist in Dune.
|
|
49
|
+
|
|
50
|
+
Uses cache for fast lookups, queries Dune only for uncached tables.
|
|
51
|
+
Results are cached for future use.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
tables: List of (schema, table) tuples to verify
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dict mapping "schema.table" -> bool (exists or not)
|
|
58
|
+
"""
|
|
59
|
+
results: dict[str, bool] = {}
|
|
60
|
+
to_check: list[tuple[str, str]] = []
|
|
61
|
+
|
|
62
|
+
# Check cache first
|
|
63
|
+
for schema, table in tables:
|
|
64
|
+
fqn = f"{schema}.{table}"
|
|
65
|
+
cached = self._get_cached(fqn)
|
|
66
|
+
if cached is not None:
|
|
67
|
+
results[fqn] = cached
|
|
68
|
+
else:
|
|
69
|
+
to_check.append((schema, table))
|
|
70
|
+
|
|
71
|
+
# Verify uncached tables
|
|
72
|
+
if to_check:
|
|
73
|
+
logger.info(f"Verifying {len(to_check)} uncached tables")
|
|
74
|
+
for schema, table in to_check:
|
|
75
|
+
try:
|
|
76
|
+
exists = self._verify_single(schema, table)
|
|
77
|
+
fqn = f"{schema}.{table}"
|
|
78
|
+
results[fqn] = exists
|
|
79
|
+
self._cache_result(fqn, exists)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# Do not hard-cache transient failures as negative results.
|
|
82
|
+
# Leave the table unverified so callers can choose to keep it.
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"Failed to verify {schema}.{table}: {e}. Skipping cache and leaving unverified."
|
|
85
|
+
)
|
|
86
|
+
# Intentionally omit from results and cache on failure
|
|
87
|
+
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def _verify_single(self, schema: str, table: str) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Verify a single table exists using lightweight DESCRIBE query.
|
|
93
|
+
|
|
94
|
+
Uses SHOW COLUMNS which is fast and doesn't require full table scan.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
schema: Schema name
|
|
98
|
+
table: Table name
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if table exists, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
# Use describe_table which internally uses SHOW COLUMNS
|
|
105
|
+
# This is lightweight and fast
|
|
106
|
+
self.dune_adapter.describe_table(schema, table)
|
|
107
|
+
return True
|
|
108
|
+
except Exception:
|
|
109
|
+
# If describe fails, table doesn't exist
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def _get_cached(self, table: str) -> bool | None:
|
|
113
|
+
"""
|
|
114
|
+
Get verification result from cache if fresh.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
table: Fully qualified table name (schema.table)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
bool if cached and fresh, None if cache miss or stale
|
|
121
|
+
"""
|
|
122
|
+
if table not in self._cache:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
entry = self._cache[table]
|
|
126
|
+
timestamp = entry.get("timestamp", 0)
|
|
127
|
+
age = time.time() - timestamp
|
|
128
|
+
|
|
129
|
+
if age < CACHE_TTL_SECONDS:
|
|
130
|
+
return entry.get("exists", False)
|
|
131
|
+
else:
|
|
132
|
+
# Cache entry is stale, remove it
|
|
133
|
+
del self._cache[table]
|
|
134
|
+
self._save_cache()
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _cache_result(self, table: str, exists: bool) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Cache verification result with current timestamp.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
table: Fully qualified table name
|
|
143
|
+
exists: Whether table exists
|
|
144
|
+
"""
|
|
145
|
+
self._cache[table] = {
|
|
146
|
+
"exists": exists,
|
|
147
|
+
"timestamp": time.time(),
|
|
148
|
+
}
|
|
149
|
+
self._save_cache()
|
|
150
|
+
|
|
151
|
+
def _load_cache(self) -> dict[str, dict[str, Any]]:
|
|
152
|
+
"""
|
|
153
|
+
Load verification cache from disk.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict mapping table -> cache entry
|
|
157
|
+
"""
|
|
158
|
+
if not self.cache_path.exists():
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
with open(self.cache_path, encoding="utf-8") as f:
|
|
163
|
+
cache = json.load(f)
|
|
164
|
+
# Validate cache structure
|
|
165
|
+
if isinstance(cache, dict):
|
|
166
|
+
return cache
|
|
167
|
+
return {}
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(f"Failed to load verification cache: {e}")
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
def _save_cache(self) -> None:
|
|
173
|
+
"""Persist verification cache to disk."""
|
|
174
|
+
try:
|
|
175
|
+
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
with open(self.cache_path, "w", encoding="utf-8") as f:
|
|
177
|
+
json.dump(self._cache, f, indent=2)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.warning(f"Failed to save verification cache: {e}")
|
|
180
|
+
|
|
181
|
+
def clear_cache(self) -> None:
|
|
182
|
+
"""Clear verification cache (useful for testing or forced refresh)."""
|
|
183
|
+
self._cache = {}
|
|
184
|
+
if self.cache_path.exists():
|
|
185
|
+
self.cache_path.unlink()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spice-mcp
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: mcp server built ontop of dune api endpoint
|
|
5
5
|
Author-email: Evan-Kim2028 <ekcopersonal@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
@@ -28,11 +28,14 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
An MCP server that provides AI agents with direct access to [Dune Analytics](https://dune.com/) data. Execute queries, discover schemas and tables, and manage saved queries—all through a clean, type-safe interface optimized for AI workflows.
|
|
30
30
|
|
|
31
|
+
**Discover High-Quality Tables**: Leverages [Dune Spellbook](https://github.com/duneanalytics/spellbook), Dune's official GitHub repository of curated dbt models, to surface verified, production-ready tables with rich metadata.
|
|
32
|
+
|
|
31
33
|
## Why spice-mcp?
|
|
32
34
|
|
|
33
35
|
- **Agent-friendly**: Designed for AI agents using the Model Context Protocol (MCP)
|
|
36
|
+
- **High-Quality Discovery**: Leverages Dune Spellbook's GitHub repository to find verified, production-ready tables with rich metadata
|
|
34
37
|
- **Efficient**: Polars-first pipeline keeps data lazy until needed, reducing memory usage
|
|
35
|
-
- **Discovery**: Built-in tools to explore Dune's extensive blockchain datasets
|
|
38
|
+
- **Discovery**: Built-in tools to explore Dune's extensive blockchain datasets from both Dune API and Spellbook
|
|
36
39
|
- **Type-safe**: Fully typed parameters and responses with FastMCP
|
|
37
40
|
- **Reproducible**: Automatic query history logging and SQL artifact storage
|
|
38
41
|
|
|
@@ -73,10 +76,8 @@ An MCP server that provides AI agents with direct access to [Dune Analytics](htt
|
|
|
73
76
|
|------|-------------|----------------|
|
|
74
77
|
| `dune_query` | Execute queries by ID, URL, or raw SQL | `query` (str), `parameters` (object), `limit` (int), `offset` (int), `format` (`preview\|raw\|metadata\|poll`), `refresh` (bool), `timeout_seconds` (float) |
|
|
75
78
|
| `dune_query_info` | Get metadata for a saved query | `query` (str - ID or URL) |
|
|
76
|
-
| `dune_discover` | Unified discovery across Dune API and Spellbook | `keyword` (str\|list), `schema` (str), `limit` (int), `source` (`dune\|spellbook\|both`), `include_columns` (bool) |
|
|
77
|
-
| `dune_find_tables` | Search schemas and list tables | `keyword` (str), `schema` (str), `limit` (int) |
|
|
79
|
+
| `dune_discover` | Unified discovery across Dune API and Spellbook (returns verified tables only). **Leverages Dune Spellbook GitHub repository** for high-quality, curated tables. | `keyword` (str\|list), `schema` (str), `limit` (int), `source` (`dune\|spellbook\|both`), `include_columns` (bool) |
|
|
78
80
|
| `dune_describe_table` | Get column metadata for a table | `schema` (str), `table` (str) |
|
|
79
|
-
| `spellbook_find_models` | Search Spellbook dbt models | `keyword` (str\|list), `schema` (str), `limit` (int), `include_columns` (bool) |
|
|
80
81
|
| `dune_health_check` | Verify API key and configuration | (no parameters) |
|
|
81
82
|
| `dune_query_create` | Create a new saved query | `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
|
|
82
83
|
| `dune_query_update` | Update an existing saved query | `query_id` (int), `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
|
|
@@ -91,6 +92,17 @@ An MCP server that provides AI agents with direct access to [Dune Analytics](htt
|
|
|
91
92
|
|
|
92
93
|
[Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API. It aggregates on-chain data from Ethereum, Solana, Polygon, and other chains into queryable SQL tables. See the [Dune Docs](https://dune.com/docs) for more information.
|
|
93
94
|
|
|
95
|
+
## What is Dune Spellbook?
|
|
96
|
+
|
|
97
|
+
[Dune Spellbook](https://github.com/duneanalytics/spellbook) is Dune's official GitHub repository containing thousands of curated dbt models. These models represent high-quality, production-ready tables that are:
|
|
98
|
+
|
|
99
|
+
- **Verified**: All tables are verified to exist in Dune before being returned
|
|
100
|
+
- **Well-documented**: Rich metadata including column descriptions and types
|
|
101
|
+
- **Maintained**: Regularly updated by the Dune community and team
|
|
102
|
+
- **Production-ready**: Used by analysts and dashboards across the ecosystem
|
|
103
|
+
|
|
104
|
+
spice-mcp automatically clones and parses the Spellbook repository to discover these high-quality tables, parsing dbt config blocks to resolve actual Dune table names and verifying their existence before returning them to you.
|
|
105
|
+
|
|
94
106
|
## Installation
|
|
95
107
|
|
|
96
108
|
**From PyPI** (recommended):
|
|
@@ -7,35 +7,36 @@ spice_mcp/adapters/http_client.py,sha256=CYgSKAsx-5c-uxaNIBCBTgQdaoBe5J3dJvnw8iq
|
|
|
7
7
|
spice_mcp/adapters/dune/__init__.py,sha256=nspEuDpVOktAxm8B066s-d0LwouCYGpvNEexi0mRMN8,386
|
|
8
8
|
spice_mcp/adapters/dune/admin.py,sha256=yxOueVz-rmgC-ZFbT06k59G24yRgYjiEkZlall5hXNQ,3157
|
|
9
9
|
spice_mcp/adapters/dune/cache.py,sha256=7ykT58WN1yHGIN2uV3t7fWOqGb1VJdCvf3I-xZwsv74,4304
|
|
10
|
-
spice_mcp/adapters/dune/client.py,sha256=
|
|
11
|
-
spice_mcp/adapters/dune/extract.py,sha256=
|
|
10
|
+
spice_mcp/adapters/dune/client.py,sha256=zle19bU-I3AzpOF1cp_hEaZUFNQV1RWhtl1LAxObk0g,9052
|
|
11
|
+
spice_mcp/adapters/dune/extract.py,sha256=67x-WCaP13vbMsTKnqNSOVbMs6Dsf0QHi2fLHduYTBI,30405
|
|
12
12
|
spice_mcp/adapters/dune/helpers.py,sha256=BgDKr_g-UqmU2hoMb0ejQZHta_NbKwR1eDJp33sJYNk,227
|
|
13
|
-
spice_mcp/adapters/dune/query_wrapper.py,sha256=
|
|
13
|
+
spice_mcp/adapters/dune/query_wrapper.py,sha256=4dk8D8KJKWoBhuMLzDGupRrXGlG-N0cbM6HCs8wMFvE,2925
|
|
14
14
|
spice_mcp/adapters/dune/transport.py,sha256=eRP-jPY2ZXxvTX9HSjIFqFUlbIzXspgH95jBFoTlpaQ,1436
|
|
15
15
|
spice_mcp/adapters/dune/types.py,sha256=57TMX07u-Gq4BYwRAuZV0xI81nVXgtpp7KBID9YbKyQ,1195
|
|
16
16
|
spice_mcp/adapters/dune/typing_utils.py,sha256=EpWneGDn-eQdo6lkLuESR09KXkDj9OqGz8bEF3JaFkM,574
|
|
17
17
|
spice_mcp/adapters/dune/urls.py,sha256=bcuPERkFQduRTT2BrgzVhoFrMn-Lkvw9NmktcBZYEig,3902
|
|
18
18
|
spice_mcp/adapters/dune/user_agent.py,sha256=c6Kt4zczbuT9mapDoh8-3sgm268MUtvyIRxDF9yJwXQ,218
|
|
19
19
|
spice_mcp/adapters/spellbook/__init__.py,sha256=D2cdVtSUbmAJdbPRvAyKxYS4-wUQ3unXyX4ZFYxenuk,150
|
|
20
|
-
spice_mcp/adapters/spellbook/explorer.py,sha256=
|
|
20
|
+
spice_mcp/adapters/spellbook/explorer.py,sha256=nfecYztzxfBXp9b9IKcP4dVICCnPzhHvPfNJKFhaKxA,14856
|
|
21
21
|
spice_mcp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
spice_mcp/core/errors.py,sha256=jlfTuyRaAaA_oU07KUk-1pDAAa43KG0BbZc5CINXtoE,3256
|
|
23
23
|
spice_mcp/core/models.py,sha256=i0C_-UE16OWyyZo_liooEJeYvbChE5lpK80aN2OF4lk,1795
|
|
24
24
|
spice_mcp/core/ports.py,sha256=nEdeA3UH7v0kB_hbguMrpDljb9EhSxUAO0SdhjpoijQ,1618
|
|
25
25
|
spice_mcp/logging/query_history.py,sha256=doE9lod64uzJxlA2XzHH2-VAmC6WstYAkQ0taEAxiIM,4315
|
|
26
26
|
spice_mcp/mcp/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
27
|
-
spice_mcp/mcp/server.py,sha256=
|
|
27
|
+
spice_mcp/mcp/server.py,sha256=oi0RRaSithCgUt0Qt6Tb3gks32LN0dOOwN7kX-BrN7s,32263
|
|
28
28
|
spice_mcp/mcp/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
29
29
|
spice_mcp/mcp/tools/base.py,sha256=zJkVxLgXR48iZcJeng8cZ2rXvbyicagoGlMN7BK7Img,1041
|
|
30
|
-
spice_mcp/mcp/tools/execute_query.py,sha256=
|
|
30
|
+
spice_mcp/mcp/tools/execute_query.py,sha256=K1YpuQGwvVM20A4_h9zNlkeG37J7jbY7BPzLm6vPAsY,16033
|
|
31
31
|
spice_mcp/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
spice_mcp/observability/logging.py,sha256=ceJUEpKGpf5PAgPBmpB49zjqhdGCAESfLemFUhDSmI8,529
|
|
33
33
|
spice_mcp/service_layer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
spice_mcp/service_layer/discovery_service.py,sha256=202O0SzCZGQukd9kb2JYfarLygZHgiXlHqp_nTAdrWA,730
|
|
35
35
|
spice_mcp/service_layer/query_admin_service.py,sha256=4q1NAAuTui7cm83Aq2rFDLIzKTHX17yzbSoSJyCmLbI,1356
|
|
36
36
|
spice_mcp/service_layer/query_service.py,sha256=q0eAVW5I3sUxm29DgzPN_cH3rZEzmKwmdE3Xj4qP9lI,3878
|
|
37
|
-
spice_mcp
|
|
38
|
-
spice_mcp-0.1.
|
|
39
|
-
spice_mcp-0.1.
|
|
40
|
-
spice_mcp-0.1.
|
|
41
|
-
spice_mcp-0.1.
|
|
37
|
+
spice_mcp/service_layer/verification_service.py,sha256=dPA88p9zKqg62bNjN_4c5QFEUBHCWjZph8pn2a5zrUI,6057
|
|
38
|
+
spice_mcp-0.1.5.dist-info/METADATA,sha256=ag4hjEMz-qxvImxJPxpsQN_0F9BOUdbk6TF8zAXW5I4,6093
|
|
39
|
+
spice_mcp-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
40
|
+
spice_mcp-0.1.5.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
|
|
41
|
+
spice_mcp-0.1.5.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
|
|
42
|
+
spice_mcp-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|