spice-mcp 0.1.1__py3-none-any.whl → 0.1.3__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/__init__.py +4 -2
- spice_mcp/adapters/dune/cache.py +2 -34
- spice_mcp/adapters/dune/extract.py +33 -631
- spice_mcp/adapters/dune/typing_utils.py +8 -1
- spice_mcp/adapters/spellbook/__init__.py +6 -0
- spice_mcp/adapters/spellbook/explorer.py +313 -0
- spice_mcp/config.py +1 -1
- spice_mcp/core/models.py +0 -8
- spice_mcp/core/ports.py +0 -15
- spice_mcp/mcp/server.py +321 -135
- spice_mcp/mcp/tools/base.py +1 -1
- spice_mcp/mcp/tools/execute_query.py +48 -59
- {spice_mcp-0.1.1.dist-info → spice_mcp-0.1.3.dist-info}/METADATA +18 -13
- {spice_mcp-0.1.1.dist-info → spice_mcp-0.1.3.dist-info}/RECORD +17 -17
- spice_mcp/mcp/tools/sui_package_overview.py +0 -56
- spice_mcp/service_layer/sui_service.py +0 -131
- {spice_mcp-0.1.1.dist-info → spice_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.1.dist-info → spice_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.1.dist-info → spice_mcp-0.1.3.dist-info}/licenses/LICENSE +0 -0
spice_mcp/mcp/server.py
CHANGED
|
@@ -27,15 +27,14 @@ from ..adapters.dune import urls as dune_urls
|
|
|
27
27
|
from ..adapters.dune.admin import DuneAdminAdapter
|
|
28
28
|
from ..adapters.dune.client import DuneAdapter
|
|
29
29
|
from ..adapters.http_client import HttpClient
|
|
30
|
+
from ..adapters.spellbook.explorer import SpellbookExplorer
|
|
30
31
|
from ..config import Config
|
|
31
32
|
from ..core.errors import error_response
|
|
32
33
|
from ..logging.query_history import QueryHistory
|
|
33
34
|
from ..service_layer.discovery_service import DiscoveryService
|
|
34
35
|
from ..service_layer.query_admin_service import QueryAdminService
|
|
35
36
|
from ..service_layer.query_service import QueryService
|
|
36
|
-
from ..service_layer.sui_service import SuiService
|
|
37
37
|
from .tools.execute_query import ExecuteQueryTool
|
|
38
|
-
from .tools.sui_package_overview import SuiPackageOverviewTool
|
|
39
38
|
|
|
40
39
|
logger = logging.getLogger(__name__)
|
|
41
40
|
|
|
@@ -46,13 +45,11 @@ QUERY_HISTORY: QueryHistory | None = None
|
|
|
46
45
|
DUNE_ADAPTER: DuneAdapter | None = None
|
|
47
46
|
QUERY_SERVICE: QueryService | None = None
|
|
48
47
|
QUERY_ADMIN_SERVICE: QueryAdminService | None = None
|
|
49
|
-
SEMAPHORE = None
|
|
50
48
|
DISCOVERY_SERVICE: DiscoveryService | None = None
|
|
51
|
-
|
|
49
|
+
SPELLBOOK_EXPLORER: SpellbookExplorer | None = None
|
|
52
50
|
HTTP_CLIENT: HttpClient | None = None
|
|
53
51
|
|
|
54
52
|
EXECUTE_QUERY_TOOL: ExecuteQueryTool | None = None
|
|
55
|
-
SUI_OVERVIEW_TOOL: SuiPackageOverviewTool | None = None
|
|
56
53
|
|
|
57
54
|
|
|
58
55
|
app = FastMCP("spice-mcp")
|
|
@@ -60,8 +57,8 @@ app = FastMCP("spice-mcp")
|
|
|
60
57
|
|
|
61
58
|
def _ensure_initialized() -> None:
|
|
62
59
|
"""Initialize configuration and tool instances if not already initialized."""
|
|
63
|
-
global CONFIG, QUERY_HISTORY, DUNE_ADAPTER, QUERY_SERVICE, DISCOVERY_SERVICE,
|
|
64
|
-
global EXECUTE_QUERY_TOOL,
|
|
60
|
+
global CONFIG, QUERY_HISTORY, DUNE_ADAPTER, QUERY_SERVICE, DISCOVERY_SERVICE, QUERY_ADMIN_SERVICE
|
|
61
|
+
global EXECUTE_QUERY_TOOL, HTTP_CLIENT, SPELLBOOK_EXPLORER
|
|
65
62
|
|
|
66
63
|
if CONFIG is not None and EXECUTE_QUERY_TOOL is not None:
|
|
67
64
|
return
|
|
@@ -96,17 +93,11 @@ def _ensure_initialized() -> None:
|
|
|
96
93
|
http_config=CONFIG.http,
|
|
97
94
|
)
|
|
98
95
|
)
|
|
99
|
-
|
|
96
|
+
|
|
97
|
+
# Initialize Spellbook explorer (lazy, clones repo on first use)
|
|
98
|
+
SPELLBOOK_EXPLORER = SpellbookExplorer()
|
|
100
99
|
|
|
101
100
|
EXECUTE_QUERY_TOOL = ExecuteQueryTool(CONFIG, QUERY_SERVICE, QUERY_HISTORY)
|
|
102
|
-
SUI_OVERVIEW_TOOL = SuiPackageOverviewTool(SUI_SERVICE)
|
|
103
|
-
# Concurrency gate for heavy query executions
|
|
104
|
-
try:
|
|
105
|
-
import asyncio
|
|
106
|
-
global SEMAPHORE
|
|
107
|
-
SEMAPHORE = asyncio.Semaphore(CONFIG.max_concurrent_queries)
|
|
108
|
-
except Exception:
|
|
109
|
-
pass
|
|
110
101
|
|
|
111
102
|
logger.info("spice-mcp server ready (fastmcp)!")
|
|
112
103
|
|
|
@@ -173,7 +164,7 @@ def compute_health_status() -> dict[str, Any]:
|
|
|
173
164
|
description="Fetch Dune query metadata (name, parameters, tags, SQL).",
|
|
174
165
|
tags={"dune", "query"},
|
|
175
166
|
)
|
|
176
|
-
|
|
167
|
+
def dune_query_info(query: str) -> dict[str, Any]:
|
|
177
168
|
_ensure_initialized()
|
|
178
169
|
try:
|
|
179
170
|
qid = dune_urls.get_query_id(query)
|
|
@@ -212,7 +203,7 @@ async def dune_query_info(query: str) -> dict[str, Any]:
|
|
|
212
203
|
description="Execute Dune queries and return agent-optimized preview.",
|
|
213
204
|
tags={"dune", "query"},
|
|
214
205
|
)
|
|
215
|
-
|
|
206
|
+
def dune_query(
|
|
216
207
|
query: str,
|
|
217
208
|
parameters: dict[str, Any] | None = None,
|
|
218
209
|
refresh: bool = False,
|
|
@@ -229,27 +220,8 @@ async def dune_query(
|
|
|
229
220
|
_ensure_initialized()
|
|
230
221
|
assert EXECUTE_QUERY_TOOL is not None
|
|
231
222
|
try:
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
pass # type: ignore
|
|
235
|
-
if SEMAPHORE is not None:
|
|
236
|
-
async with SEMAPHORE: # type: ignore
|
|
237
|
-
return await EXECUTE_QUERY_TOOL.execute(
|
|
238
|
-
query=query,
|
|
239
|
-
parameters=parameters,
|
|
240
|
-
refresh=refresh,
|
|
241
|
-
max_age=max_age,
|
|
242
|
-
limit=limit,
|
|
243
|
-
offset=offset,
|
|
244
|
-
sample_count=sample_count,
|
|
245
|
-
sort_by=sort_by,
|
|
246
|
-
columns=columns,
|
|
247
|
-
format=format,
|
|
248
|
-
extras=extras,
|
|
249
|
-
timeout_seconds=timeout_seconds,
|
|
250
|
-
)
|
|
251
|
-
# Fallback without semaphore
|
|
252
|
-
return await EXECUTE_QUERY_TOOL.execute(
|
|
223
|
+
# Execute query synchronously
|
|
224
|
+
return EXECUTE_QUERY_TOOL.execute(
|
|
253
225
|
query=query,
|
|
254
226
|
parameters=parameters,
|
|
255
227
|
refresh=refresh,
|
|
@@ -278,11 +250,11 @@ async def dune_query(
|
|
|
278
250
|
description="Validate Dune API key presence and logging setup.",
|
|
279
251
|
tags={"health"},
|
|
280
252
|
)
|
|
281
|
-
|
|
253
|
+
def dune_health_check() -> dict[str, Any]:
|
|
282
254
|
return compute_health_status()
|
|
283
255
|
|
|
284
256
|
|
|
285
|
-
|
|
257
|
+
def _dune_find_tables_impl(
|
|
286
258
|
keyword: str | None = None,
|
|
287
259
|
schema: str | None = None,
|
|
288
260
|
limit: int = 50,
|
|
@@ -298,15 +270,183 @@ async def _dune_find_tables_impl(
|
|
|
298
270
|
return out
|
|
299
271
|
|
|
300
272
|
|
|
273
|
+
def _unified_discover_impl(
|
|
274
|
+
keyword: str | list[str] | None = None,
|
|
275
|
+
schema: str | None = None,
|
|
276
|
+
limit: int = 50,
|
|
277
|
+
source: Literal["dune", "spellbook", "both"] = "both",
|
|
278
|
+
include_columns: bool = True,
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
"""
|
|
281
|
+
Unified discovery implementation that can search Dune API, Spellbook repo, or both.
|
|
282
|
+
|
|
283
|
+
Returns a consistent format with 'schemas' and 'tables' keys.
|
|
284
|
+
"""
|
|
285
|
+
_ensure_initialized()
|
|
286
|
+
out: dict[str, Any] = {
|
|
287
|
+
"schemas": [],
|
|
288
|
+
"tables": [],
|
|
289
|
+
"source": source,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Normalize keyword to list
|
|
293
|
+
keywords = keyword if isinstance(keyword, list) else ([keyword] if keyword else [])
|
|
294
|
+
|
|
295
|
+
# Search Dune API if requested
|
|
296
|
+
if source in ("dune", "both"):
|
|
297
|
+
dune_result: dict[str, Any] = {}
|
|
298
|
+
if keyword:
|
|
299
|
+
assert DISCOVERY_SERVICE is not None
|
|
300
|
+
# Search each keyword and combine results
|
|
301
|
+
# DISCOVERY_SERVICE.find_schemas returns list[str], not SchemaMatch objects
|
|
302
|
+
all_schemas: set[str] = set()
|
|
303
|
+
for kw in keywords:
|
|
304
|
+
schemas = DISCOVERY_SERVICE.find_schemas(kw)
|
|
305
|
+
# schemas is already a list of strings from DiscoveryService
|
|
306
|
+
all_schemas.update(schemas)
|
|
307
|
+
dune_result["schemas"] = sorted(list(all_schemas))
|
|
308
|
+
|
|
309
|
+
if schema:
|
|
310
|
+
assert DISCOVERY_SERVICE is not None
|
|
311
|
+
tables = DISCOVERY_SERVICE.list_tables(schema, limit=limit)
|
|
312
|
+
dune_result["tables"] = [
|
|
313
|
+
{
|
|
314
|
+
"schema": schema,
|
|
315
|
+
"table": summary.table,
|
|
316
|
+
"fully_qualified_name": f"{schema}.{summary.table}",
|
|
317
|
+
"source": "dune",
|
|
318
|
+
}
|
|
319
|
+
for summary in tables
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
# Merge Dune results
|
|
323
|
+
if "schemas" in dune_result:
|
|
324
|
+
out["schemas"].extend(dune_result["schemas"])
|
|
325
|
+
if "tables" in dune_result:
|
|
326
|
+
out["tables"].extend(dune_result["tables"])
|
|
327
|
+
|
|
328
|
+
# Search Spellbook if requested
|
|
329
|
+
if source in ("spellbook", "both"):
|
|
330
|
+
spellbook_result = _spellbook_find_models_impl(
|
|
331
|
+
keyword=keyword,
|
|
332
|
+
schema=schema,
|
|
333
|
+
limit=limit,
|
|
334
|
+
include_columns=include_columns,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Convert spellbook models to unified format
|
|
338
|
+
if "schemas" in spellbook_result:
|
|
339
|
+
spellbook_schemas = spellbook_result["schemas"]
|
|
340
|
+
# Merge schemas (avoid duplicates)
|
|
341
|
+
existing_schemas = set(out["schemas"])
|
|
342
|
+
for s in spellbook_schemas:
|
|
343
|
+
if s not in existing_schemas:
|
|
344
|
+
out["schemas"].append(s)
|
|
345
|
+
|
|
346
|
+
if "models" in spellbook_result:
|
|
347
|
+
for model in spellbook_result["models"]:
|
|
348
|
+
table_info = {
|
|
349
|
+
"schema": model["schema"],
|
|
350
|
+
"table": model["table"],
|
|
351
|
+
"fully_qualified_name": model["fully_qualified_name"],
|
|
352
|
+
"source": "spellbook",
|
|
353
|
+
}
|
|
354
|
+
if "columns" in model:
|
|
355
|
+
table_info["columns"] = model["columns"]
|
|
356
|
+
out["tables"].append(table_info)
|
|
357
|
+
|
|
358
|
+
# Deduplicate and sort schemas
|
|
359
|
+
out["schemas"] = sorted(list(set(out["schemas"])))
|
|
360
|
+
|
|
361
|
+
# Limit total tables
|
|
362
|
+
if limit and len(out["tables"]) > limit:
|
|
363
|
+
out["tables"] = out["tables"][:limit]
|
|
364
|
+
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@app.tool(
|
|
369
|
+
name="dune_discover",
|
|
370
|
+
title="Discover Tables",
|
|
371
|
+
description="Unified tool to discover tables/models from Dune API and/or Spellbook repository. Search by keyword(s) or list tables in a schema.",
|
|
372
|
+
tags={"dune", "spellbook", "schema", "discovery"},
|
|
373
|
+
)
|
|
374
|
+
def dune_discover(
|
|
375
|
+
keyword: str | list[str] | None = None,
|
|
376
|
+
schema: str | None = None,
|
|
377
|
+
limit: int = 50,
|
|
378
|
+
source: Literal["dune", "spellbook", "both"] = "both",
|
|
379
|
+
include_columns: bool = True,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""
|
|
382
|
+
Unified discovery tool for Dune tables and Spellbook models.
|
|
383
|
+
|
|
384
|
+
This tool can search both Dune's live schemas (via SQL queries) and Spellbook's
|
|
385
|
+
dbt models (via GitHub repo parsing) in a single call. You don't need to decide
|
|
386
|
+
which source to use - it can search both automatically.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
keyword: Search term(s) - can be a string or list of strings
|
|
390
|
+
(e.g., "layerzero", ["layerzero", "dex"], "nft")
|
|
391
|
+
schema: Schema name to list tables from (e.g., "dex", "spellbook", "layerzero")
|
|
392
|
+
limit: Maximum number of tables to return
|
|
393
|
+
source: Where to search - "dune" (Dune API only), "spellbook" (GitHub repo only),
|
|
394
|
+
or "both" (default: searches both and merges results)
|
|
395
|
+
include_columns: Whether to include column details for Spellbook models (default: True)
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary with:
|
|
399
|
+
- 'schemas': List of matching schema names
|
|
400
|
+
- 'tables': List of table/model objects, each with:
|
|
401
|
+
- schema: Schema name
|
|
402
|
+
- table: Table/model name
|
|
403
|
+
- fully_qualified_name: schema.table
|
|
404
|
+
- source: "dune" or "spellbook"
|
|
405
|
+
- columns: Column details (for Spellbook models, if include_columns=True)
|
|
406
|
+
- 'source': The source parameter used
|
|
407
|
+
|
|
408
|
+
Examples:
|
|
409
|
+
# Search both sources for layerzero
|
|
410
|
+
dune_discover(keyword="layerzero")
|
|
411
|
+
|
|
412
|
+
# Search only Spellbook
|
|
413
|
+
dune_discover(keyword=["layerzero", "bridge"], source="spellbook")
|
|
414
|
+
|
|
415
|
+
# Search only Dune API
|
|
416
|
+
dune_discover(keyword="sui", source="dune")
|
|
417
|
+
|
|
418
|
+
# List all tables in a schema (searches both sources)
|
|
419
|
+
dune_discover(schema="dex")
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
return _unified_discover_impl(
|
|
423
|
+
keyword=keyword,
|
|
424
|
+
schema=schema,
|
|
425
|
+
limit=limit,
|
|
426
|
+
source=source,
|
|
427
|
+
include_columns=include_columns,
|
|
428
|
+
)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
return error_response(e, context={
|
|
431
|
+
"tool": "dune_discover",
|
|
432
|
+
"keyword": keyword,
|
|
433
|
+
"schema": schema,
|
|
434
|
+
"source": source,
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
|
|
301
438
|
@app.tool(
|
|
302
439
|
name="dune_find_tables",
|
|
303
440
|
title="Find Tables",
|
|
304
441
|
description="Search schemas and optionally list tables.",
|
|
305
442
|
tags={"dune", "schema"},
|
|
306
443
|
)
|
|
307
|
-
|
|
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
|
+
"""
|
|
308
448
|
try:
|
|
309
|
-
return
|
|
449
|
+
return _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
|
|
310
450
|
except Exception as e:
|
|
311
451
|
return error_response(e, context={
|
|
312
452
|
"tool": "dune_find_tables",
|
|
@@ -315,7 +455,7 @@ async def dune_find_tables(keyword: str | None = None, schema: str | None = None
|
|
|
315
455
|
})
|
|
316
456
|
|
|
317
457
|
|
|
318
|
-
|
|
458
|
+
def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
319
459
|
_ensure_initialized()
|
|
320
460
|
assert DISCOVERY_SERVICE is not None
|
|
321
461
|
desc = DISCOVERY_SERVICE.describe_table(schema, table)
|
|
@@ -339,9 +479,9 @@ async def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
|
339
479
|
description="Describe columns for a schema.table on Dune.",
|
|
340
480
|
tags={"dune", "schema"},
|
|
341
481
|
)
|
|
342
|
-
|
|
482
|
+
def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
|
|
343
483
|
try:
|
|
344
|
-
return
|
|
484
|
+
return _dune_describe_table_impl(schema=schema, table=table)
|
|
345
485
|
except Exception as e:
|
|
346
486
|
return error_response(e, context={
|
|
347
487
|
"tool": "dune_describe_table",
|
|
@@ -350,68 +490,151 @@ async def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
|
|
|
350
490
|
})
|
|
351
491
|
|
|
352
492
|
|
|
493
|
+
def _spellbook_find_models_impl(
|
|
494
|
+
keyword: str | list[str] | None = None,
|
|
495
|
+
schema: str | None = None,
|
|
496
|
+
limit: int = 50,
|
|
497
|
+
include_columns: bool = True,
|
|
498
|
+
) -> dict[str, Any]:
|
|
499
|
+
"""
|
|
500
|
+
Implementation for spellbook model discovery.
|
|
501
|
+
|
|
502
|
+
Supports searching by keyword(s) and optionally includes column details.
|
|
503
|
+
"""
|
|
504
|
+
_ensure_initialized()
|
|
505
|
+
assert SPELLBOOK_EXPLORER is not None
|
|
506
|
+
out: dict[str, Any] = {}
|
|
507
|
+
|
|
508
|
+
# Handle keyword search (string or list)
|
|
509
|
+
if keyword:
|
|
510
|
+
# Normalize to list
|
|
511
|
+
keywords = keyword if isinstance(keyword, list) else [keyword]
|
|
512
|
+
|
|
513
|
+
# Find schemas matching any keyword
|
|
514
|
+
all_schemas: set[str] = set()
|
|
515
|
+
for kw in keywords:
|
|
516
|
+
schemas = SPELLBOOK_EXPLORER.find_schemas(kw)
|
|
517
|
+
all_schemas.update(match.schema for match in schemas)
|
|
518
|
+
|
|
519
|
+
out["schemas"] = sorted(list(all_schemas))
|
|
520
|
+
|
|
521
|
+
# If schema not specified but we found schemas, search models in those schemas
|
|
522
|
+
if not schema and all_schemas:
|
|
523
|
+
out["models"] = []
|
|
524
|
+
for schema_name in sorted(all_schemas):
|
|
525
|
+
tables = SPELLBOOK_EXPLORER.list_tables(schema_name, limit=limit)
|
|
526
|
+
for table_summary in tables:
|
|
527
|
+
# Check if table name matches any keyword
|
|
528
|
+
table_name = table_summary.table.lower()
|
|
529
|
+
matches_keyword = any(kw.lower() in table_name for kw in keywords)
|
|
530
|
+
|
|
531
|
+
if matches_keyword:
|
|
532
|
+
model_info: dict[str, Any] = {
|
|
533
|
+
"schema": schema_name,
|
|
534
|
+
"table": table_summary.table,
|
|
535
|
+
"fully_qualified_name": f"{schema_name}.{table_summary.table}",
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
# Include column details if requested
|
|
539
|
+
if include_columns:
|
|
540
|
+
try:
|
|
541
|
+
desc = SPELLBOOK_EXPLORER.describe_table(schema_name, table_summary.table)
|
|
542
|
+
model_info["columns"] = [
|
|
543
|
+
{
|
|
544
|
+
"name": col.name,
|
|
545
|
+
"dune_type": col.dune_type,
|
|
546
|
+
"polars_dtype": col.polars_dtype,
|
|
547
|
+
"comment": col.comment,
|
|
548
|
+
}
|
|
549
|
+
for col in desc.columns
|
|
550
|
+
]
|
|
551
|
+
except Exception:
|
|
552
|
+
model_info["columns"] = []
|
|
553
|
+
|
|
554
|
+
out["models"].append(model_info)
|
|
555
|
+
|
|
556
|
+
# Limit total models returned
|
|
557
|
+
if limit and len(out["models"]) > limit:
|
|
558
|
+
out["models"] = out["models"][:limit]
|
|
559
|
+
|
|
560
|
+
# If schema specified, list all tables in that schema
|
|
561
|
+
if schema:
|
|
562
|
+
tables = SPELLBOOK_EXPLORER.list_tables(schema, limit=limit)
|
|
563
|
+
if "models" not in out:
|
|
564
|
+
out["models"] = []
|
|
565
|
+
|
|
566
|
+
for table_summary in tables:
|
|
567
|
+
model_info: dict[str, Any] = {
|
|
568
|
+
"schema": schema,
|
|
569
|
+
"table": table_summary.table,
|
|
570
|
+
"fully_qualified_name": f"{schema}.{table_summary.table}",
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# Include column details if requested
|
|
574
|
+
if include_columns:
|
|
575
|
+
try:
|
|
576
|
+
desc = SPELLBOOK_EXPLORER.describe_table(schema, table_summary.table)
|
|
577
|
+
model_info["columns"] = [
|
|
578
|
+
{
|
|
579
|
+
"name": col.name,
|
|
580
|
+
"dune_type": col.dune_type,
|
|
581
|
+
"polars_dtype": col.polars_dtype,
|
|
582
|
+
"comment": col.comment,
|
|
583
|
+
}
|
|
584
|
+
for col in desc.columns
|
|
585
|
+
]
|
|
586
|
+
except Exception:
|
|
587
|
+
model_info["columns"] = []
|
|
588
|
+
|
|
589
|
+
out["models"].append(model_info)
|
|
590
|
+
|
|
591
|
+
return out
|
|
592
|
+
|
|
593
|
+
|
|
353
594
|
@app.tool(
|
|
354
|
-
name="
|
|
355
|
-
title="
|
|
356
|
-
description="
|
|
357
|
-
tags={"
|
|
595
|
+
name="spellbook_find_models",
|
|
596
|
+
title="Search Spellbook",
|
|
597
|
+
description="Search dbt models in Spellbook GitHub repository.",
|
|
598
|
+
tags={"spellbook", "dbt", "schema"},
|
|
358
599
|
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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,
|
|
363
605
|
) -> dict[str, Any]:
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
"""
|
|
366
618
|
try:
|
|
367
|
-
return
|
|
368
|
-
|
|
619
|
+
return _spellbook_find_models_impl(
|
|
620
|
+
keyword=keyword,
|
|
621
|
+
schema=schema,
|
|
622
|
+
limit=limit,
|
|
623
|
+
include_columns=include_columns,
|
|
369
624
|
)
|
|
370
625
|
except Exception as e:
|
|
371
626
|
return error_response(e, context={
|
|
372
|
-
"tool": "
|
|
373
|
-
"
|
|
374
|
-
"
|
|
627
|
+
"tool": "spellbook_find_models",
|
|
628
|
+
"keyword": keyword,
|
|
629
|
+
"schema": schema,
|
|
375
630
|
})
|
|
376
631
|
|
|
377
632
|
|
|
378
|
-
@app.resource(uri="spice:sui/events_preview/{hours}/{limit}/{packages}", name="Sui Events Preview", description="Preview Sui events (3-day default) for comma-separated packages; returns JSON.")
|
|
379
|
-
async def sui_events_preview_resource(hours: str, limit: str, packages: str) -> str:
|
|
380
|
-
import json
|
|
381
|
-
|
|
382
|
-
try:
|
|
383
|
-
hh = int(hours)
|
|
384
|
-
except Exception:
|
|
385
|
-
hh = 72
|
|
386
|
-
try:
|
|
387
|
-
ll = int(limit)
|
|
388
|
-
except Exception:
|
|
389
|
-
ll = 50
|
|
390
|
-
pkgs = []
|
|
391
|
-
if packages and packages != "_":
|
|
392
|
-
pkgs = [p.strip() for p in packages.split(",") if p.strip()]
|
|
393
|
-
|
|
394
|
-
_ensure_initialized()
|
|
395
|
-
assert SUI_SERVICE is not None
|
|
396
|
-
try:
|
|
397
|
-
result = SUI_SERVICE.events_preview(pkgs, hours=hh, limit=ll)
|
|
398
|
-
payload = {"ok": True, **result}
|
|
399
|
-
except Exception as exc:
|
|
400
|
-
payload = error_response(
|
|
401
|
-
exc,
|
|
402
|
-
context={
|
|
403
|
-
"resource": "sui_events_preview",
|
|
404
|
-
"packages": pkgs,
|
|
405
|
-
"hours": hh,
|
|
406
|
-
"limit": ll,
|
|
407
|
-
},
|
|
408
|
-
)
|
|
409
|
-
return json.dumps(payload)
|
|
410
633
|
|
|
411
634
|
|
|
412
635
|
# Resources
|
|
413
636
|
@app.resource(uri="spice:history/tail/{n}", name="Query History Tail", description="Tail last N lines from query history")
|
|
414
|
-
|
|
637
|
+
def history_tail(n: str) -> str:
|
|
415
638
|
from collections import deque
|
|
416
639
|
try:
|
|
417
640
|
nn = int(n)
|
|
@@ -437,7 +660,7 @@ async def history_tail(n: str) -> str:
|
|
|
437
660
|
|
|
438
661
|
|
|
439
662
|
@app.resource(uri="spice:artifact/{sha}", name="SQL Artifact", description="SQL artifact by SHA-256")
|
|
440
|
-
|
|
663
|
+
def sql_artifact(sha: str) -> str:
|
|
441
664
|
import os
|
|
442
665
|
import re
|
|
443
666
|
|
|
@@ -456,43 +679,6 @@ async def sql_artifact(sha: str) -> str:
|
|
|
456
679
|
return ""
|
|
457
680
|
|
|
458
681
|
|
|
459
|
-
@app.resource(
|
|
460
|
-
uri="spice:sui/package_overview/{hours}/{timeout_seconds}/{packages}",
|
|
461
|
-
name="Sui Package Overview (cmd)",
|
|
462
|
-
description="Compact overview for Sui package activity as a command-style resource."
|
|
463
|
-
)
|
|
464
|
-
async def sui_package_overview_cmd(hours: str, timeout_seconds: str, packages: str) -> str:
|
|
465
|
-
import json
|
|
466
|
-
|
|
467
|
-
try:
|
|
468
|
-
hh = int(hours)
|
|
469
|
-
except Exception:
|
|
470
|
-
hh = 72
|
|
471
|
-
try:
|
|
472
|
-
tt = float(timeout_seconds)
|
|
473
|
-
except Exception:
|
|
474
|
-
tt = 30.0
|
|
475
|
-
pkgs = []
|
|
476
|
-
if packages and packages != "_":
|
|
477
|
-
pkgs = [p.strip() for p in packages.split(",") if p.strip()]
|
|
478
|
-
|
|
479
|
-
_ensure_initialized()
|
|
480
|
-
assert SUI_SERVICE is not None
|
|
481
|
-
try:
|
|
482
|
-
result = SUI_SERVICE.package_overview(pkgs, hours=hh, timeout_seconds=tt)
|
|
483
|
-
except Exception as exc:
|
|
484
|
-
result = error_response(
|
|
485
|
-
exc,
|
|
486
|
-
context={
|
|
487
|
-
"resource": "sui_package_overview",
|
|
488
|
-
"packages": pkgs,
|
|
489
|
-
"hours": hh,
|
|
490
|
-
"timeout_seconds": tt,
|
|
491
|
-
},
|
|
492
|
-
)
|
|
493
|
-
return json.dumps(result)
|
|
494
|
-
|
|
495
|
-
|
|
496
682
|
def main() -> None:
|
|
497
683
|
# Do not initialize at startup; defer until first tool call so env issues
|
|
498
684
|
# don't break MCP handshake. Disable banner to keep stdio clean.
|
|
@@ -507,7 +693,7 @@ if __name__ == "__main__":
|
|
|
507
693
|
description="Create a new saved Dune query (name + SQL).",
|
|
508
694
|
tags={"dune", "admin"},
|
|
509
695
|
)
|
|
510
|
-
|
|
696
|
+
def dune_query_create(name: str, query_sql: str, description: str | None = None, tags: list[str] | None = None, parameters: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
511
697
|
_ensure_initialized()
|
|
512
698
|
assert QUERY_ADMIN_SERVICE is not None
|
|
513
699
|
try:
|
|
@@ -522,7 +708,7 @@ async def dune_query_create(name: str, query_sql: str, description: str | None =
|
|
|
522
708
|
description="Update fields of a saved Dune query (name/SQL/description/tags/parameters).",
|
|
523
709
|
tags={"dune", "admin"},
|
|
524
710
|
)
|
|
525
|
-
|
|
711
|
+
def dune_query_update(query_id: int, name: str | None = None, query_sql: str | None = None, description: str | None = None, tags: list[str] | None = None, parameters: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
526
712
|
_ensure_initialized()
|
|
527
713
|
assert QUERY_ADMIN_SERVICE is not None
|
|
528
714
|
try:
|
|
@@ -537,7 +723,7 @@ async def dune_query_update(query_id: int, name: str | None = None, query_sql: s
|
|
|
537
723
|
description="Fork an existing saved Dune query.",
|
|
538
724
|
tags={"dune", "admin"},
|
|
539
725
|
)
|
|
540
|
-
|
|
726
|
+
def dune_query_fork(source_query_id: int, name: str | None = None) -> dict[str, Any]:
|
|
541
727
|
_ensure_initialized()
|
|
542
728
|
assert QUERY_ADMIN_SERVICE is not None
|
|
543
729
|
try:
|
spice_mcp/mcp/tools/base.py
CHANGED
|
@@ -20,7 +20,7 @@ class MCPTool(ABC):
|
|
|
20
20
|
raise NotImplementedError
|
|
21
21
|
|
|
22
22
|
@abstractmethod
|
|
23
|
-
|
|
23
|
+
def execute(self, **kwargs) -> dict[str, Any]:
|
|
24
24
|
"""Execute tool logic and return result dictionary."""
|
|
25
25
|
raise NotImplementedError
|
|
26
26
|
|