spice-mcp 0.1.4__py3-none-any.whl → 0.1.6__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.

Potentially problematic release.


This version of spice-mcp might be problematic. Click here for more details.

@@ -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
- if isinstance(q, str):
51
- q_rewritten = _maybe_rewrite_show_sql(q)
52
- if q_rewritten is not None:
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
- sql_eff = _maybe_rewrite_show_sql(sql) or sql
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
- """Rewrite certain SHOW statements to information_schema SELECTs for portability.
237
-
238
- This allows running discovery-style commands through the parameterized raw SQL
239
- template which expects SELECT statements.
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
- s = sql.strip()
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
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
+ import os
4
5
  import time
5
6
  from typing import TYPE_CHECKING, overload
6
- import os
7
7
 
8
8
  from ..http_client import HttpClient
9
9
  from . import cache as _cache
@@ -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,14 +296,70 @@ 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
- """Parse SQL file to extract column names from SELECT statements."""
352
+ """
353
+ Parse SQL file to extract column names from SELECT statements.
354
+
355
+ Note: This is a best-effort heuristic and may not be perfect for complex SQL.
356
+ For accurate column information, use Dune's DESCRIBE TABLE or query the actual table.
357
+ """
269
358
  try:
270
359
  with open(sql_file, encoding="utf-8") as f:
271
360
  sql = f.read()
272
361
 
273
- # Look for SELECT ... FROM patterns
274
- # Match: SELECT col1, col2, col3 FROM ...
362
+ # Look for SELECT ... FROM patterns (simple heuristic)
275
363
  select_match = re.search(
276
364
  r"SELECT\s+(.+?)\s+FROM",
277
365
  sql,
@@ -280,27 +368,20 @@ class SpellbookExplorer(CatalogExplorer):
280
368
 
281
369
  if select_match:
282
370
  cols_str = select_match.group(1)
283
- # Split by comma, but handle function calls and aliases
371
+ # Simple split - may not handle all nested cases perfectly
372
+ # This is OK since column info is optional and best-effort
284
373
  cols = []
285
374
  for col in cols_str.split(","):
286
375
  col = col.strip()
287
- # Extract column name (handle aliases: col AS alias -> col)
288
- if " AS " in col.upper():
289
- col = col.split(" AS ", 1)[0].strip()
290
- elif " " in col and not col.startswith("("):
291
- # Might be alias without AS
292
- parts = col.split()
293
- col = parts[0].strip()
294
-
295
- # Clean up function calls: function(col) -> col
296
- col = re.sub(r"^\w+\((.+)\)", r"\1", col)
376
+ # Basic cleanup - remove obvious SQL noise
377
+ col = col.split()[-1] if col else ""
297
378
  col = col.strip().strip('"').strip("'")
298
379
 
299
- if col and col not in ["*", "DISTINCT"]:
380
+ if col and col not in ["*", "DISTINCT", "FROM"]:
300
381
  cols.append(
301
382
  TableColumn(
302
383
  name=col,
303
- dune_type="VARCHAR", # Default, can't infer from SQL
384
+ dune_type="VARCHAR",
304
385
  polars_dtype="Utf8",
305
386
  )
306
387
  )
@@ -310,4 +391,3 @@ class SpellbookExplorer(CatalogExplorer):
310
391
  pass
311
392
 
312
393
  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, Optional
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: Optional[dict[str, Any]] = None,
214
+ parameters: dict[str, Any] | None = None,
204
215
  refresh: bool = False,
205
- max_age: Optional[float] = None,
206
- limit: Optional[int] = None,
207
- offset: Optional[int] = None,
208
- sample_count: Optional[int] = None,
209
- sort_by: Optional[str] = None,
210
- columns: Optional[list[str]] = None,
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: Optional[dict[str, Any]] = None,
213
- timeout_seconds: Optional[float] = None,
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: Optional[dict[str, Any]] = None,
289
+ parameters: dict[str, Any] | None = None,
279
290
  refresh: bool = False,
280
- max_age: Optional[float] = None,
281
- limit: Optional[int] = None,
282
- offset: Optional[int] = None,
283
- sample_count: Optional[int] = None,
284
- sort_by: Optional[str] = None,
285
- columns: Optional[list[str]] = None,
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: Optional[dict[str, Any]] = None,
288
- timeout_seconds: Optional[float] = None,
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,28 +342,12 @@ 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,
341
348
  limit: int = 50,
342
349
  source: Literal["dune", "spellbook", "both"] = "both",
343
- include_columns: bool = True,
350
+ include_columns: bool = False,
344
351
  ) -> dict[str, Any]:
345
352
  """
346
353
  Unified discovery implementation that can search Dune API, Spellbook repo, or both.
@@ -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
 
@@ -441,14 +496,22 @@ def dune_discover(
441
496
  schema: str | None = None,
442
497
  limit: int = 50,
443
498
  source: Literal["dune", "spellbook", "both"] = "both",
444
- include_columns: bool = True,
499
+ include_columns: bool = False,
445
500
  ) -> dict[str, Any]:
446
501
  """
447
- Unified discovery tool for Dune tables and Spellbook models.
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 can search both Dune's live schemas (via SQL queries) and Spellbook's
450
- dbt models (via GitHub repo parsing) in a single call. You don't need to decide
451
- which source to use - it can search both automatically.
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
@@ -457,22 +520,34 @@ def dune_discover(
457
520
  limit: Maximum number of tables to return
458
521
  source: Where to search - "dune" (Dune API only), "spellbook" (GitHub repo only),
459
522
  or "both" (default: searches both and merges results)
460
- include_columns: Whether to include column details for Spellbook models (default: True)
523
+ include_columns: Whether to include column details (default: False).
524
+ Note: Column info from Spellbook SQL is unreliable.
525
+ Use dune_describe_table on the actual Dune table for accurate columns.
461
526
 
462
527
  Returns:
463
528
  Dictionary with:
464
529
  - 'schemas': List of matching schema names
465
530
  - 'tables': List of table/model objects, each with:
466
- - schema: Schema name
467
- - table: Table/model name
468
- - fully_qualified_name: schema.table
531
+ - schema: Schema name (Spellbook subproject name)
532
+ - table: Table/model name (Spellbook model name)
533
+ - fully_qualified_name: schema.table (Spellbook format)
469
534
  - source: "dune" or "spellbook"
470
- - columns: Column details (for Spellbook models, if include_columns=True)
535
+ - dune_schema: Actual Dune schema name (for Spellbook models)
536
+ - dune_alias: Actual Dune table alias (for Spellbook models)
537
+ - dune_table: Verified, queryable Dune table name (e.g., "sui_walrus.base_table")
538
+ - verified: True (all returned tables are verified to exist)
471
539
  - 'source': The source parameter used
540
+ - 'message': Helpful message if no tables found
541
+
542
+ Note: To get accurate column information, use dune_describe_table on the dune_table value.
472
543
 
473
544
  Examples:
474
- # Search both sources for layerzero
475
- dune_discover(keyword="layerzero")
545
+ # Search both sources for walrus - returns verified tables only
546
+ dune_discover(keyword="walrus")
547
+ # → Returns tables with dune_table field like "sui_walrus.base_table"
548
+
549
+ # Use the dune_table field to query immediately
550
+ dune_query(query="SELECT * FROM sui_walrus.base_table LIMIT 10")
476
551
 
477
552
  # Search only Spellbook
478
553
  dune_discover(keyword=["layerzero", "bridge"], source="spellbook")
@@ -500,26 +575,6 @@ def dune_discover(
500
575
  })
501
576
 
502
577
 
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
578
  def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
524
579
  _ensure_initialized()
525
580
  assert DISCOVERY_SERVICE is not None
@@ -559,7 +614,7 @@ def _spellbook_find_models_impl(
559
614
  keyword: str | list[str] | None = None,
560
615
  schema: str | None = None,
561
616
  limit: int = 50,
562
- include_columns: bool = True,
617
+ include_columns: bool = False,
563
618
  ) -> dict[str, Any]:
564
619
  """
565
620
  Implementation for spellbook model discovery.
@@ -594,10 +649,22 @@ def _spellbook_find_models_impl(
594
649
  matches_keyword = any(kw.lower() in table_name for kw in keywords)
595
650
 
596
651
  if matches_keyword:
652
+ # Get model details including resolved Dune table names
653
+ models_dict = SPELLBOOK_EXPLORER._load_models()
654
+ model_details = None
655
+ for m in models_dict.get(schema_name, []):
656
+ if m["name"] == table_summary.table:
657
+ model_details = m
658
+ break
659
+
597
660
  model_info: dict[str, Any] = {
598
661
  "schema": schema_name,
599
662
  "table": table_summary.table,
600
663
  "fully_qualified_name": f"{schema_name}.{table_summary.table}",
664
+ # Include resolved Dune table names if available
665
+ "dune_schema": model_details.get("dune_schema") if model_details else None,
666
+ "dune_alias": model_details.get("dune_alias") if model_details else None,
667
+ "dune_table": model_details.get("dune_table") if model_details else None,
601
668
  }
602
669
 
603
670
  # Include column details if requested
@@ -629,10 +696,22 @@ def _spellbook_find_models_impl(
629
696
  out["models"] = []
630
697
 
631
698
  for table_summary in tables:
699
+ # Get model details including resolved Dune table names
700
+ models_dict = SPELLBOOK_EXPLORER._load_models()
701
+ model_details = None
702
+ for m in models_dict.get(schema, []):
703
+ if m["name"] == table_summary.table:
704
+ model_details = m
705
+ break
706
+
632
707
  model_info: dict[str, Any] = {
633
708
  "schema": schema,
634
709
  "table": table_summary.table,
635
710
  "fully_qualified_name": f"{schema}.{table_summary.table}",
711
+ # Include resolved Dune table names if available
712
+ "dune_schema": model_details.get("dune_schema") if model_details else None,
713
+ "dune_alias": model_details.get("dune_alias") if model_details else None,
714
+ "dune_table": model_details.get("dune_table") if model_details else None,
636
715
  }
637
716
 
638
717
  # Include column details if requested
@@ -656,47 +735,6 @@ def _spellbook_find_models_impl(
656
735
  return out
657
736
 
658
737
 
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
738
  # Resources
701
739
  @app.resource(uri="spice:history/tail/{n}", name="Query History Tail", description="Tail last N lines from query history")
702
740
  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
- # Rewrite SHOW statements to portable information_schema SELECTs
105
- q_use = _maybe_rewrite_show_sql(query) or query
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
- s = sql.strip()
399
- m = re.match(r"^SHOW\s+SCHEMAS\s+LIKE\s+'([^']+)'\s*;?$", s, flags=re.IGNORECASE)
400
- if m:
401
- pat = m.group(1)
402
- return (
403
- "SELECT schema_name AS Schema FROM information_schema.schemata "
404
- f"WHERE schema_name LIKE '{pat}'"
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
4
- Summary: MCP server for Dune Analytics data access
3
+ Version: 0.1.6
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=4-Ay2FQf_vo-eB6I9Kul3f1PgS78PPuUJ7zldebOtKU,9424
11
- spice_mcp/adapters/dune/extract.py,sha256=30D-5NyiOXDtMZoo1dU9pf5yAAFR_ALn6JWvhipPRG0,30405
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=Km64otc00u9ieUhpZmL2aNYb9ETt6PoNb8czShIuPbY,2925
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=Q3UfEGlALizCDeeW_ZZVRRedzwMXiknmxwSkeOnxxgc,11515
20
+ spice_mcp/adapters/spellbook/explorer.py,sha256=PQCUJjXnaf8YSziINVNSqIXsns_y6l62JgjBId4b7jo,14654
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=lHYEI76oQpPcpPT9pEoGXpUmAjDFXlOXuUrlOWV8s2c,28729
27
+ spice_mcp/mcp/server.py,sha256=M6IPxQD--eecdNryTIL8JlkWPAXrj256P0UCEktdOt0,32438
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=CJtoNKpRY6pCkyqjwFy_cTYqTIg9-EsZA8GrH-2MFjk,16262
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-0.1.4.dist-info/METADATA,sha256=MyGS87Cwkx0G5urib8a2JbqZVP4stYdGeVknRxzPw5A,5053
38
- spice_mcp-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- spice_mcp-0.1.4.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
40
- spice_mcp-0.1.4.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
41
- spice_mcp-0.1.4.dist-info/RECORD,,
37
+ spice_mcp/service_layer/verification_service.py,sha256=dPA88p9zKqg62bNjN_4c5QFEUBHCWjZph8pn2a5zrUI,6057
38
+ spice_mcp-0.1.6.dist-info/METADATA,sha256=i_C2MMquWFMaSl-offWWUmHIzcCT9JwbETV_JngudO0,6093
39
+ spice_mcp-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
40
+ spice_mcp-0.1.6.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
41
+ spice_mcp-0.1.6.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
42
+ spice_mcp-0.1.6.dist-info/RECORD,,