spice-mcp 0.1.3__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 +22 -33
- spice_mcp/adapters/dune/extract.py +49 -5
- spice_mcp/adapters/dune/query_wrapper.py +86 -0
- spice_mcp/adapters/dune/user_agent.py +9 -0
- spice_mcp/adapters/spellbook/explorer.py +84 -1
- spice_mcp/mcp/server.py +199 -99
- spice_mcp/mcp/tools/execute_query.py +19 -25
- spice_mcp/service_layer/verification_service.py +185 -0
- spice_mcp-0.1.5.dist-info/METADATA +133 -0
- {spice_mcp-0.1.3.dist-info → spice_mcp-0.1.5.dist-info}/RECORD +13 -10
- spice_mcp-0.1.3.dist-info/METADATA +0 -198
- {spice_mcp-0.1.3.dist-info → spice_mcp-0.1.5.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.3.dist-info → spice_mcp-0.1.5.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.3.dist-info → spice_mcp-0.1.5.dist-info}/licenses/LICENSE +0 -0
spice_mcp/mcp/server.py
CHANGED
|
@@ -22,7 +22,6 @@ try:
|
|
|
22
22
|
except Exception:
|
|
23
23
|
pass
|
|
24
24
|
|
|
25
|
-
from ..adapters.dune import extract as dune_extract
|
|
26
25
|
from ..adapters.dune import urls as dune_urls
|
|
27
26
|
from ..adapters.dune.admin import DuneAdminAdapter
|
|
28
27
|
from ..adapters.dune.client import DuneAdapter
|
|
@@ -34,6 +33,7 @@ from ..logging.query_history import QueryHistory
|
|
|
34
33
|
from ..service_layer.discovery_service import DiscoveryService
|
|
35
34
|
from ..service_layer.query_admin_service import QueryAdminService
|
|
36
35
|
from ..service_layer.query_service import QueryService
|
|
36
|
+
from ..service_layer.verification_service import VerificationService
|
|
37
37
|
from .tools.execute_query import ExecuteQueryTool
|
|
38
38
|
|
|
39
39
|
logger = logging.getLogger(__name__)
|
|
@@ -48,6 +48,7 @@ QUERY_ADMIN_SERVICE: QueryAdminService | None = None
|
|
|
48
48
|
DISCOVERY_SERVICE: DiscoveryService | None = None
|
|
49
49
|
SPELLBOOK_EXPLORER: SpellbookExplorer | None = None
|
|
50
50
|
HTTP_CLIENT: HttpClient | None = None
|
|
51
|
+
VERIFICATION_SERVICE: VerificationService | None = None
|
|
51
52
|
|
|
52
53
|
EXECUTE_QUERY_TOOL: ExecuteQueryTool | None = None
|
|
53
54
|
|
|
@@ -58,7 +59,7 @@ app = FastMCP("spice-mcp")
|
|
|
58
59
|
def _ensure_initialized() -> None:
|
|
59
60
|
"""Initialize configuration and tool instances if not already initialized."""
|
|
60
61
|
global CONFIG, QUERY_HISTORY, DUNE_ADAPTER, QUERY_SERVICE, DISCOVERY_SERVICE, QUERY_ADMIN_SERVICE
|
|
61
|
-
global EXECUTE_QUERY_TOOL, HTTP_CLIENT, SPELLBOOK_EXPLORER
|
|
62
|
+
global EXECUTE_QUERY_TOOL, HTTP_CLIENT, SPELLBOOK_EXPLORER, VERIFICATION_SERVICE
|
|
62
63
|
|
|
63
64
|
if CONFIG is not None and EXECUTE_QUERY_TOOL is not None:
|
|
64
65
|
return
|
|
@@ -96,6 +97,15 @@ def _ensure_initialized() -> None:
|
|
|
96
97
|
|
|
97
98
|
# Initialize Spellbook explorer (lazy, clones repo on first use)
|
|
98
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
|
+
)
|
|
99
109
|
|
|
100
110
|
EXECUTE_QUERY_TOOL = ExecuteQueryTool(CONFIG, QUERY_SERVICE, QUERY_HISTORY)
|
|
101
111
|
|
|
@@ -144,9 +154,10 @@ def compute_health_status() -> dict[str, Any]:
|
|
|
144
154
|
if tmpl:
|
|
145
155
|
tid = dune_urls.get_query_id(tmpl)
|
|
146
156
|
url = dune_urls.url_templates["query"].format(query_id=tid)
|
|
157
|
+
from ..adapters.dune.user_agent import get_user_agent as get_dune_user_agent
|
|
147
158
|
headers = {
|
|
148
159
|
"X-Dune-API-Key": os.getenv("DUNE_API_KEY", ""),
|
|
149
|
-
"User-Agent":
|
|
160
|
+
"User-Agent": get_dune_user_agent(),
|
|
150
161
|
}
|
|
151
162
|
client = HTTP_CLIENT or HttpClient(Config.from_env().http)
|
|
152
163
|
resp = client.request("GET", url, headers=headers, timeout=5.0)
|
|
@@ -169,9 +180,10 @@ def dune_query_info(query: str) -> dict[str, Any]:
|
|
|
169
180
|
try:
|
|
170
181
|
qid = dune_urls.get_query_id(query)
|
|
171
182
|
url = dune_urls.url_templates["query"].format(query_id=qid)
|
|
183
|
+
from ..adapters.dune.user_agent import get_user_agent as get_dune_user_agent
|
|
172
184
|
headers = {
|
|
173
185
|
"X-Dune-API-Key": dune_urls.get_api_key(),
|
|
174
|
-
"User-Agent":
|
|
186
|
+
"User-Agent": get_dune_user_agent(),
|
|
175
187
|
}
|
|
176
188
|
client = HTTP_CLIENT or HttpClient(Config.from_env().http)
|
|
177
189
|
resp = client.request("GET", url, headers=headers, timeout=10.0)
|
|
@@ -197,13 +209,7 @@ def dune_query_info(query: str) -> dict[str, Any]:
|
|
|
197
209
|
})
|
|
198
210
|
|
|
199
211
|
|
|
200
|
-
|
|
201
|
-
name="dune_query",
|
|
202
|
-
title="Run Dune Query",
|
|
203
|
-
description="Execute Dune queries and return agent-optimized preview.",
|
|
204
|
-
tags={"dune", "query"},
|
|
205
|
-
)
|
|
206
|
-
def dune_query(
|
|
212
|
+
def _dune_query_impl(
|
|
207
213
|
query: str,
|
|
208
214
|
parameters: dict[str, Any] | None = None,
|
|
209
215
|
refresh: bool = False,
|
|
@@ -217,13 +223,41 @@ def dune_query(
|
|
|
217
223
|
extras: dict[str, Any] | None = None,
|
|
218
224
|
timeout_seconds: float | None = None,
|
|
219
225
|
) -> dict[str, Any]:
|
|
226
|
+
"""Internal implementation of dune_query to avoid FastMCP overload detection."""
|
|
220
227
|
_ensure_initialized()
|
|
221
228
|
assert EXECUTE_QUERY_TOOL is not None
|
|
229
|
+
|
|
230
|
+
# Normalize parameters: handle case where MCP client passes JSON string
|
|
231
|
+
# This can happen if FastMCP's schema generation doesn't match client expectations
|
|
232
|
+
normalized_parameters = parameters
|
|
233
|
+
if isinstance(parameters, str):
|
|
234
|
+
try:
|
|
235
|
+
import json
|
|
236
|
+
normalized_parameters = json.loads(parameters)
|
|
237
|
+
except (json.JSONDecodeError, TypeError):
|
|
238
|
+
return error_response(
|
|
239
|
+
ValueError(f"parameters must be a dict or JSON string, got {type(parameters).__name__}"),
|
|
240
|
+
context={
|
|
241
|
+
"tool": "dune_query",
|
|
242
|
+
"query": query,
|
|
243
|
+
"parameters_type": type(parameters).__name__,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Normalize extras similarly
|
|
248
|
+
normalized_extras = extras
|
|
249
|
+
if isinstance(extras, str):
|
|
250
|
+
try:
|
|
251
|
+
import json
|
|
252
|
+
normalized_extras = json.loads(extras)
|
|
253
|
+
except (json.JSONDecodeError, TypeError):
|
|
254
|
+
normalized_extras = None
|
|
255
|
+
|
|
222
256
|
try:
|
|
223
257
|
# Execute query synchronously
|
|
224
258
|
return EXECUTE_QUERY_TOOL.execute(
|
|
225
259
|
query=query,
|
|
226
|
-
parameters=
|
|
260
|
+
parameters=normalized_parameters,
|
|
227
261
|
refresh=refresh,
|
|
228
262
|
max_age=max_age,
|
|
229
263
|
limit=limit,
|
|
@@ -232,7 +266,7 @@ def dune_query(
|
|
|
232
266
|
sort_by=sort_by,
|
|
233
267
|
columns=columns,
|
|
234
268
|
format=format,
|
|
235
|
-
extras=
|
|
269
|
+
extras=normalized_extras,
|
|
236
270
|
timeout_seconds=timeout_seconds,
|
|
237
271
|
)
|
|
238
272
|
except Exception as e:
|
|
@@ -244,6 +278,60 @@ def dune_query(
|
|
|
244
278
|
})
|
|
245
279
|
|
|
246
280
|
|
|
281
|
+
@app.tool(
|
|
282
|
+
name="dune_query",
|
|
283
|
+
title="Run Dune Query",
|
|
284
|
+
description="Execute Dune queries and return agent-optimized preview.",
|
|
285
|
+
tags={"dune", "query"},
|
|
286
|
+
)
|
|
287
|
+
def dune_query(
|
|
288
|
+
query: str,
|
|
289
|
+
parameters: dict[str, Any] | None = None,
|
|
290
|
+
refresh: bool = False,
|
|
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,
|
|
297
|
+
format: Literal["preview", "raw", "metadata", "poll"] = "preview",
|
|
298
|
+
extras: dict[str, Any] | None = None,
|
|
299
|
+
timeout_seconds: float | None = None,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Execute Dune queries (by ID, URL, or raw SQL) and return agent-optimized preview.
|
|
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
|
+
|
|
315
|
+
This wrapper ensures FastMCP doesn't detect overloads in imported functions.
|
|
316
|
+
"""
|
|
317
|
+
# Always ensure parameters is explicitly passed (even if None) to avoid FastMCP
|
|
318
|
+
# overload detection when the keyword is omitted
|
|
319
|
+
return _dune_query_impl(
|
|
320
|
+
query=query,
|
|
321
|
+
parameters=parameters,
|
|
322
|
+
refresh=refresh,
|
|
323
|
+
max_age=max_age,
|
|
324
|
+
limit=limit,
|
|
325
|
+
offset=offset,
|
|
326
|
+
sample_count=sample_count,
|
|
327
|
+
sort_by=sort_by,
|
|
328
|
+
columns=columns,
|
|
329
|
+
format=format,
|
|
330
|
+
extras=extras,
|
|
331
|
+
timeout_seconds=timeout_seconds,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
247
335
|
@app.tool(
|
|
248
336
|
name="dune_health_check",
|
|
249
337
|
title="Health Check",
|
|
@@ -254,22 +342,6 @@ def dune_health_check() -> dict[str, Any]:
|
|
|
254
342
|
return compute_health_status()
|
|
255
343
|
|
|
256
344
|
|
|
257
|
-
def _dune_find_tables_impl(
|
|
258
|
-
keyword: str | None = None,
|
|
259
|
-
schema: str | None = None,
|
|
260
|
-
limit: int = 50,
|
|
261
|
-
) -> dict[str, Any]:
|
|
262
|
-
_ensure_initialized()
|
|
263
|
-
assert DISCOVERY_SERVICE is not None
|
|
264
|
-
out: dict[str, Any] = {}
|
|
265
|
-
if keyword:
|
|
266
|
-
out["schemas"] = DISCOVERY_SERVICE.find_schemas(keyword)
|
|
267
|
-
if schema:
|
|
268
|
-
tables = DISCOVERY_SERVICE.list_tables(schema, limit=limit)
|
|
269
|
-
out["tables"] = [summary.table for summary in tables]
|
|
270
|
-
return out
|
|
271
|
-
|
|
272
|
-
|
|
273
345
|
def _unified_discover_impl(
|
|
274
346
|
keyword: str | list[str] | None = None,
|
|
275
347
|
schema: str | None = None,
|
|
@@ -315,6 +387,8 @@ def _unified_discover_impl(
|
|
|
315
387
|
"table": summary.table,
|
|
316
388
|
"fully_qualified_name": f"{schema}.{summary.table}",
|
|
317
389
|
"source": "dune",
|
|
390
|
+
"dune_table": f"{schema}.{summary.table}",
|
|
391
|
+
"verified": True,
|
|
318
392
|
}
|
|
319
393
|
for summary in tables
|
|
320
394
|
]
|
|
@@ -350,11 +424,57 @@ def _unified_discover_impl(
|
|
|
350
424
|
"table": model["table"],
|
|
351
425
|
"fully_qualified_name": model["fully_qualified_name"],
|
|
352
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"),
|
|
353
431
|
}
|
|
354
432
|
if "columns" in model:
|
|
355
433
|
table_info["columns"] = model["columns"]
|
|
356
434
|
out["tables"].append(table_info)
|
|
357
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
|
+
|
|
358
478
|
# Deduplicate and sort schemas
|
|
359
479
|
out["schemas"] = sorted(list(set(out["schemas"])))
|
|
360
480
|
|
|
@@ -379,11 +499,19 @@ def dune_discover(
|
|
|
379
499
|
include_columns: bool = True,
|
|
380
500
|
) -> dict[str, Any]:
|
|
381
501
|
"""
|
|
382
|
-
|
|
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.
|
|
383
507
|
|
|
384
|
-
This tool
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
387
515
|
|
|
388
516
|
Args:
|
|
389
517
|
keyword: Search term(s) - can be a string or list of strings
|
|
@@ -398,16 +526,25 @@ def dune_discover(
|
|
|
398
526
|
Dictionary with:
|
|
399
527
|
- 'schemas': List of matching schema names
|
|
400
528
|
- 'tables': List of table/model objects, each with:
|
|
401
|
-
- schema: Schema name
|
|
402
|
-
- table: Table/model name
|
|
403
|
-
- 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)
|
|
404
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)
|
|
405
537
|
- columns: Column details (for Spellbook models, if include_columns=True)
|
|
406
538
|
- 'source': The source parameter used
|
|
539
|
+
- 'message': Helpful message if no tables found
|
|
407
540
|
|
|
408
541
|
Examples:
|
|
409
|
-
# Search both sources for
|
|
410
|
-
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")
|
|
411
548
|
|
|
412
549
|
# Search only Spellbook
|
|
413
550
|
dune_discover(keyword=["layerzero", "bridge"], source="spellbook")
|
|
@@ -435,26 +572,6 @@ def dune_discover(
|
|
|
435
572
|
})
|
|
436
573
|
|
|
437
574
|
|
|
438
|
-
@app.tool(
|
|
439
|
-
name="dune_find_tables",
|
|
440
|
-
title="Find Tables",
|
|
441
|
-
description="Search schemas and optionally list tables.",
|
|
442
|
-
tags={"dune", "schema"},
|
|
443
|
-
)
|
|
444
|
-
def dune_find_tables(keyword: str | None = None, schema: str | None = None, limit: int = 50) -> dict[str, Any]:
|
|
445
|
-
"""
|
|
446
|
-
Search schemas and optionally list tables in Dune.
|
|
447
|
-
"""
|
|
448
|
-
try:
|
|
449
|
-
return _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
|
|
450
|
-
except Exception as e:
|
|
451
|
-
return error_response(e, context={
|
|
452
|
-
"tool": "dune_find_tables",
|
|
453
|
-
"keyword": keyword,
|
|
454
|
-
"schema": schema,
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
|
|
458
575
|
def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
459
576
|
_ensure_initialized()
|
|
460
577
|
assert DISCOVERY_SERVICE is not None
|
|
@@ -529,10 +646,22 @@ def _spellbook_find_models_impl(
|
|
|
529
646
|
matches_keyword = any(kw.lower() in table_name for kw in keywords)
|
|
530
647
|
|
|
531
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
|
+
|
|
532
657
|
model_info: dict[str, Any] = {
|
|
533
658
|
"schema": schema_name,
|
|
534
659
|
"table": table_summary.table,
|
|
535
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,
|
|
536
665
|
}
|
|
537
666
|
|
|
538
667
|
# Include column details if requested
|
|
@@ -564,10 +693,22 @@ def _spellbook_find_models_impl(
|
|
|
564
693
|
out["models"] = []
|
|
565
694
|
|
|
566
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
|
+
|
|
567
704
|
model_info: dict[str, Any] = {
|
|
568
705
|
"schema": schema,
|
|
569
706
|
"table": table_summary.table,
|
|
570
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,
|
|
571
712
|
}
|
|
572
713
|
|
|
573
714
|
# Include column details if requested
|
|
@@ -591,47 +732,6 @@ def _spellbook_find_models_impl(
|
|
|
591
732
|
return out
|
|
592
733
|
|
|
593
734
|
|
|
594
|
-
@app.tool(
|
|
595
|
-
name="spellbook_find_models",
|
|
596
|
-
title="Search Spellbook",
|
|
597
|
-
description="Search dbt models in Spellbook GitHub repository.",
|
|
598
|
-
tags={"spellbook", "dbt", "schema"},
|
|
599
|
-
)
|
|
600
|
-
def spellbook_find_models(
|
|
601
|
-
keyword: str | list[str] | None = None,
|
|
602
|
-
schema: str | None = None,
|
|
603
|
-
limit: int = 50,
|
|
604
|
-
include_columns: bool = True,
|
|
605
|
-
) -> dict[str, Any]:
|
|
606
|
-
"""
|
|
607
|
-
Search Spellbook dbt models from GitHub repository.
|
|
608
|
-
|
|
609
|
-
Args:
|
|
610
|
-
keyword: Search term(s) to find models - can be a string or list of strings
|
|
611
|
-
schema: Schema/subproject name to list tables from
|
|
612
|
-
limit: Maximum number of models to return
|
|
613
|
-
include_columns: Whether to include column details in results (default: True)
|
|
614
|
-
|
|
615
|
-
Returns:
|
|
616
|
-
Dictionary with 'schemas' and 'models' keys
|
|
617
|
-
"""
|
|
618
|
-
try:
|
|
619
|
-
return _spellbook_find_models_impl(
|
|
620
|
-
keyword=keyword,
|
|
621
|
-
schema=schema,
|
|
622
|
-
limit=limit,
|
|
623
|
-
include_columns=include_columns,
|
|
624
|
-
)
|
|
625
|
-
except Exception as e:
|
|
626
|
-
return error_response(e, context={
|
|
627
|
-
"tool": "spellbook_find_models",
|
|
628
|
-
"keyword": keyword,
|
|
629
|
-
"schema": schema,
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
735
|
# Resources
|
|
636
736
|
@app.resource(uri="spice:history/tail/{n}", name="Query History Tail", description="Tail last N lines from query history")
|
|
637
737
|
def history_tail(n: str) -> str:
|
|
@@ -1,12 +1,14 @@
|
|
|
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
|
-
from ...adapters.dune import extract as dune_extract
|
|
9
7
|
from ...adapters.dune import urls as dune_urls
|
|
8
|
+
from ...adapters.dune.query_wrapper import execute_query as execute_dune_query
|
|
9
|
+
|
|
10
|
+
# Import user_agent from separate module to avoid importing overloaded functions
|
|
11
|
+
from ...adapters.dune.user_agent import get_user_agent as get_dune_user_agent
|
|
10
12
|
from ...adapters.http_client import HttpClient
|
|
11
13
|
from ...config import Config
|
|
12
14
|
from ...core.errors import error_response
|
|
@@ -99,11 +101,13 @@ class ExecuteQueryTool(MCPTool):
|
|
|
99
101
|
) -> dict[str, Any]:
|
|
100
102
|
t0 = time.time()
|
|
101
103
|
try:
|
|
102
|
-
#
|
|
103
|
-
|
|
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
|
|
104
108
|
# Poll-only: return execution handle without fetching results
|
|
105
109
|
if format == "poll":
|
|
106
|
-
exec_obj =
|
|
110
|
+
exec_obj = execute_dune_query(
|
|
107
111
|
q_use,
|
|
108
112
|
parameters=parameters,
|
|
109
113
|
api_key=self.config.dune.api_key,
|
|
@@ -333,7 +337,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
333
337
|
query_id = dune_urls.get_query_id(query)
|
|
334
338
|
headers = {
|
|
335
339
|
"X-Dune-API-Key": dune_urls.get_api_key(),
|
|
336
|
-
"User-Agent":
|
|
340
|
+
"User-Agent": get_dune_user_agent(),
|
|
337
341
|
}
|
|
338
342
|
resp = self._http.request(
|
|
339
343
|
"GET",
|
|
@@ -362,7 +366,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
362
366
|
url = dune_urls.get_execution_status_url(execution_id)
|
|
363
367
|
headers = {
|
|
364
368
|
"X-Dune-API-Key": dune_urls.get_api_key(),
|
|
365
|
-
"User-Agent":
|
|
369
|
+
"User-Agent": get_dune_user_agent(),
|
|
366
370
|
}
|
|
367
371
|
resp = self._http.request("GET", url, headers=headers, timeout=10.0)
|
|
368
372
|
data = resp.json()
|
|
@@ -393,22 +397,12 @@ def _categorize_query(q: str) -> str:
|
|
|
393
397
|
|
|
394
398
|
|
|
395
399
|
def _maybe_rewrite_show_sql(sql: str) -> str | None:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if re.match(r"^SHOW\s+SCHEMAS\s*;?$", s, flags=re.IGNORECASE):
|
|
405
|
-
return "SELECT schema_name AS Schema FROM information_schema.schemata"
|
|
406
|
-
|
|
407
|
-
m = re.match(r"^SHOW\s+TABLES\s+FROM\s+([A-Za-z0-9_\.]+)\s*;?$", s, flags=re.IGNORECASE)
|
|
408
|
-
if m:
|
|
409
|
-
schema = m.group(1)
|
|
410
|
-
return (
|
|
411
|
-
"SELECT table_name AS Table FROM information_schema.tables "
|
|
412
|
-
f"WHERE table_schema = '{schema}'"
|
|
413
|
-
)
|
|
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
|
|
414
408
|
return None
|