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/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
- SUI_SERVICE: SuiService | None = None
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, SUI_SERVICE, QUERY_ADMIN_SERVICE
64
- global EXECUTE_QUERY_TOOL, SUI_OVERVIEW_TOOL, HTTP_CLIENT
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
- SUI_SERVICE = SuiService(QUERY_SERVICE)
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
- async def dune_query_info(query: str) -> dict[str, Any]:
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
- async def dune_query(
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
- # Limit concurrent executions to preserve API quota
233
- if 'asyncio' not in globals():
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
- async def dune_health_check() -> dict[str, Any]:
253
+ def dune_health_check() -> dict[str, Any]:
282
254
  return compute_health_status()
283
255
 
284
256
 
285
- async def _dune_find_tables_impl(
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
- async def dune_find_tables(keyword: str | None = None, schema: str | None = None, limit: int = 50) -> dict[str, Any]:
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 await _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
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
- async def _dune_describe_table_impl(schema: str, table: str) -> dict[str, Any]:
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
- async def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
482
+ def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
343
483
  try:
344
- return await _dune_describe_table_impl(schema=schema, table=table)
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="sui_package_overview",
355
- title="Sui Package Overview",
356
- description="Compact overview for Sui package activity.",
357
- tags={"sui"},
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
- async def sui_package_overview(
360
- packages: list[str],
361
- hours: int = 72,
362
- timeout_seconds: float | None = 30,
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
- _ensure_initialized()
365
- assert SUI_OVERVIEW_TOOL is not None
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 await SUI_OVERVIEW_TOOL.execute(
368
- packages=packages, hours=hours, timeout_seconds=timeout_seconds
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": "sui_package_overview",
373
- "packages": packages,
374
- "hours": hours,
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
- async def history_tail(n: str) -> str:
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
- async def sql_artifact(sha: str) -> str:
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
- async 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]:
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
- async 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]:
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
- async def dune_query_fork(source_query_id: int, name: str | None = None) -> dict[str, Any]:
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:
@@ -20,7 +20,7 @@ class MCPTool(ABC):
20
20
  raise NotImplementedError
21
21
 
22
22
  @abstractmethod
23
- async def execute(self, **kwargs) -> dict[str, Any]:
23
+ def execute(self, **kwargs) -> dict[str, Any]:
24
24
  """Execute tool logic and return result dictionary."""
25
25
  raise NotImplementedError
26
26