spice-mcp 0.1.2__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/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 -116
- spice_mcp/mcp/tools/base.py +1 -1
- spice_mcp/mcp/tools/execute_query.py +26 -59
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/METADATA +18 -13
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/RECORD +16 -16
- spice_mcp/mcp/tools/sui_package_overview.py +0 -56
- spice_mcp/service_layer/sui_service.py +0 -131
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.2.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,8 +220,8 @@ async def dune_query(
|
|
|
229
220
|
_ensure_initialized()
|
|
230
221
|
assert EXECUTE_QUERY_TOOL is not None
|
|
231
222
|
try:
|
|
232
|
-
# Execute query
|
|
233
|
-
return
|
|
223
|
+
# Execute query synchronously
|
|
224
|
+
return EXECUTE_QUERY_TOOL.execute(
|
|
234
225
|
query=query,
|
|
235
226
|
parameters=parameters,
|
|
236
227
|
refresh=refresh,
|
|
@@ -259,11 +250,11 @@ async def dune_query(
|
|
|
259
250
|
description="Validate Dune API key presence and logging setup.",
|
|
260
251
|
tags={"health"},
|
|
261
252
|
)
|
|
262
|
-
|
|
253
|
+
def dune_health_check() -> dict[str, Any]:
|
|
263
254
|
return compute_health_status()
|
|
264
255
|
|
|
265
256
|
|
|
266
|
-
|
|
257
|
+
def _dune_find_tables_impl(
|
|
267
258
|
keyword: str | None = None,
|
|
268
259
|
schema: str | None = None,
|
|
269
260
|
limit: int = 50,
|
|
@@ -279,15 +270,183 @@ async def _dune_find_tables_impl(
|
|
|
279
270
|
return out
|
|
280
271
|
|
|
281
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
|
+
|
|
282
438
|
@app.tool(
|
|
283
439
|
name="dune_find_tables",
|
|
284
440
|
title="Find Tables",
|
|
285
441
|
description="Search schemas and optionally list tables.",
|
|
286
442
|
tags={"dune", "schema"},
|
|
287
443
|
)
|
|
288
|
-
|
|
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
|
+
"""
|
|
289
448
|
try:
|
|
290
|
-
return
|
|
449
|
+
return _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
|
|
291
450
|
except Exception as e:
|
|
292
451
|
return error_response(e, context={
|
|
293
452
|
"tool": "dune_find_tables",
|
|
@@ -296,7 +455,7 @@ async def dune_find_tables(keyword: str | None = None, schema: str | None = None
|
|
|
296
455
|
})
|
|
297
456
|
|
|
298
457
|
|
|
299
|
-
|
|
458
|
+
def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
300
459
|
_ensure_initialized()
|
|
301
460
|
assert DISCOVERY_SERVICE is not None
|
|
302
461
|
desc = DISCOVERY_SERVICE.describe_table(schema, table)
|
|
@@ -320,9 +479,9 @@ async def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
|
|
|
320
479
|
description="Describe columns for a schema.table on Dune.",
|
|
321
480
|
tags={"dune", "schema"},
|
|
322
481
|
)
|
|
323
|
-
|
|
482
|
+
def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
|
|
324
483
|
try:
|
|
325
|
-
return
|
|
484
|
+
return _dune_describe_table_impl(schema=schema, table=table)
|
|
326
485
|
except Exception as e:
|
|
327
486
|
return error_response(e, context={
|
|
328
487
|
"tool": "dune_describe_table",
|
|
@@ -331,68 +490,151 @@ async def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
|
|
|
331
490
|
})
|
|
332
491
|
|
|
333
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
|
+
|
|
334
594
|
@app.tool(
|
|
335
|
-
name="
|
|
336
|
-
title="
|
|
337
|
-
description="
|
|
338
|
-
tags={"
|
|
595
|
+
name="spellbook_find_models",
|
|
596
|
+
title="Search Spellbook",
|
|
597
|
+
description="Search dbt models in Spellbook GitHub repository.",
|
|
598
|
+
tags={"spellbook", "dbt", "schema"},
|
|
339
599
|
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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,
|
|
344
605
|
) -> dict[str, Any]:
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
"""
|
|
347
618
|
try:
|
|
348
|
-
return
|
|
349
|
-
|
|
619
|
+
return _spellbook_find_models_impl(
|
|
620
|
+
keyword=keyword,
|
|
621
|
+
schema=schema,
|
|
622
|
+
limit=limit,
|
|
623
|
+
include_columns=include_columns,
|
|
350
624
|
)
|
|
351
625
|
except Exception as e:
|
|
352
626
|
return error_response(e, context={
|
|
353
|
-
"tool": "
|
|
354
|
-
"
|
|
355
|
-
"
|
|
627
|
+
"tool": "spellbook_find_models",
|
|
628
|
+
"keyword": keyword,
|
|
629
|
+
"schema": schema,
|
|
356
630
|
})
|
|
357
631
|
|
|
358
632
|
|
|
359
|
-
@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.")
|
|
360
|
-
async def sui_events_preview_resource(hours: str, limit: str, packages: str) -> str:
|
|
361
|
-
import json
|
|
362
|
-
|
|
363
|
-
try:
|
|
364
|
-
hh = int(hours)
|
|
365
|
-
except Exception:
|
|
366
|
-
hh = 72
|
|
367
|
-
try:
|
|
368
|
-
ll = int(limit)
|
|
369
|
-
except Exception:
|
|
370
|
-
ll = 50
|
|
371
|
-
pkgs = []
|
|
372
|
-
if packages and packages != "_":
|
|
373
|
-
pkgs = [p.strip() for p in packages.split(",") if p.strip()]
|
|
374
|
-
|
|
375
|
-
_ensure_initialized()
|
|
376
|
-
assert SUI_SERVICE is not None
|
|
377
|
-
try:
|
|
378
|
-
result = SUI_SERVICE.events_preview(pkgs, hours=hh, limit=ll)
|
|
379
|
-
payload = {"ok": True, **result}
|
|
380
|
-
except Exception as exc:
|
|
381
|
-
payload = error_response(
|
|
382
|
-
exc,
|
|
383
|
-
context={
|
|
384
|
-
"resource": "sui_events_preview",
|
|
385
|
-
"packages": pkgs,
|
|
386
|
-
"hours": hh,
|
|
387
|
-
"limit": ll,
|
|
388
|
-
},
|
|
389
|
-
)
|
|
390
|
-
return json.dumps(payload)
|
|
391
633
|
|
|
392
634
|
|
|
393
635
|
# Resources
|
|
394
636
|
@app.resource(uri="spice:history/tail/{n}", name="Query History Tail", description="Tail last N lines from query history")
|
|
395
|
-
|
|
637
|
+
def history_tail(n: str) -> str:
|
|
396
638
|
from collections import deque
|
|
397
639
|
try:
|
|
398
640
|
nn = int(n)
|
|
@@ -418,7 +660,7 @@ async def history_tail(n: str) -> str:
|
|
|
418
660
|
|
|
419
661
|
|
|
420
662
|
@app.resource(uri="spice:artifact/{sha}", name="SQL Artifact", description="SQL artifact by SHA-256")
|
|
421
|
-
|
|
663
|
+
def sql_artifact(sha: str) -> str:
|
|
422
664
|
import os
|
|
423
665
|
import re
|
|
424
666
|
|
|
@@ -437,43 +679,6 @@ async def sql_artifact(sha: str) -> str:
|
|
|
437
679
|
return ""
|
|
438
680
|
|
|
439
681
|
|
|
440
|
-
@app.resource(
|
|
441
|
-
uri="spice:sui/package_overview/{hours}/{timeout_seconds}/{packages}",
|
|
442
|
-
name="Sui Package Overview (cmd)",
|
|
443
|
-
description="Compact overview for Sui package activity as a command-style resource."
|
|
444
|
-
)
|
|
445
|
-
async def sui_package_overview_cmd(hours: str, timeout_seconds: str, packages: str) -> str:
|
|
446
|
-
import json
|
|
447
|
-
|
|
448
|
-
try:
|
|
449
|
-
hh = int(hours)
|
|
450
|
-
except Exception:
|
|
451
|
-
hh = 72
|
|
452
|
-
try:
|
|
453
|
-
tt = float(timeout_seconds)
|
|
454
|
-
except Exception:
|
|
455
|
-
tt = 30.0
|
|
456
|
-
pkgs = []
|
|
457
|
-
if packages and packages != "_":
|
|
458
|
-
pkgs = [p.strip() for p in packages.split(",") if p.strip()]
|
|
459
|
-
|
|
460
|
-
_ensure_initialized()
|
|
461
|
-
assert SUI_SERVICE is not None
|
|
462
|
-
try:
|
|
463
|
-
result = SUI_SERVICE.package_overview(pkgs, hours=hh, timeout_seconds=tt)
|
|
464
|
-
except Exception as exc:
|
|
465
|
-
result = error_response(
|
|
466
|
-
exc,
|
|
467
|
-
context={
|
|
468
|
-
"resource": "sui_package_overview",
|
|
469
|
-
"packages": pkgs,
|
|
470
|
-
"hours": hh,
|
|
471
|
-
"timeout_seconds": tt,
|
|
472
|
-
},
|
|
473
|
-
)
|
|
474
|
-
return json.dumps(result)
|
|
475
|
-
|
|
476
|
-
|
|
477
682
|
def main() -> None:
|
|
478
683
|
# Do not initialize at startup; defer until first tool call so env issues
|
|
479
684
|
# don't break MCP handshake. Disable banner to keep stdio clean.
|
|
@@ -488,7 +693,7 @@ if __name__ == "__main__":
|
|
|
488
693
|
description="Create a new saved Dune query (name + SQL).",
|
|
489
694
|
tags={"dune", "admin"},
|
|
490
695
|
)
|
|
491
|
-
|
|
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]:
|
|
492
697
|
_ensure_initialized()
|
|
493
698
|
assert QUERY_ADMIN_SERVICE is not None
|
|
494
699
|
try:
|
|
@@ -503,7 +708,7 @@ async def dune_query_create(name: str, query_sql: str, description: str | None =
|
|
|
503
708
|
description="Update fields of a saved Dune query (name/SQL/description/tags/parameters).",
|
|
504
709
|
tags={"dune", "admin"},
|
|
505
710
|
)
|
|
506
|
-
|
|
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]:
|
|
507
712
|
_ensure_initialized()
|
|
508
713
|
assert QUERY_ADMIN_SERVICE is not None
|
|
509
714
|
try:
|
|
@@ -518,7 +723,7 @@ async def dune_query_update(query_id: int, name: str | None = None, query_sql: s
|
|
|
518
723
|
description="Fork an existing saved Dune query.",
|
|
519
724
|
tags={"dune", "admin"},
|
|
520
725
|
)
|
|
521
|
-
|
|
726
|
+
def dune_query_fork(source_query_id: int, name: str | None = None) -> dict[str, Any]:
|
|
522
727
|
_ensure_initialized()
|
|
523
728
|
assert QUERY_ADMIN_SERVICE is not None
|
|
524
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
|
|