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/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,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 directly without semaphore concurrency control
233
- return await EXECUTE_QUERY_TOOL.execute(
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
- async def dune_health_check() -> dict[str, Any]:
253
+ def dune_health_check() -> dict[str, Any]:
263
254
  return compute_health_status()
264
255
 
265
256
 
266
- async def _dune_find_tables_impl(
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
- 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
+ """
289
448
  try:
290
- return await _dune_find_tables_impl(keyword=keyword, schema=schema, limit=limit)
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
- 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]:
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
- async def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
482
+ def dune_describe_table(schema: str, table: str) -> dict[str, Any]:
324
483
  try:
325
- return await _dune_describe_table_impl(schema=schema, table=table)
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="sui_package_overview",
336
- title="Sui Package Overview",
337
- description="Compact overview for Sui package activity.",
338
- 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"},
339
599
  )
340
- async def sui_package_overview(
341
- packages: list[str],
342
- hours: int = 72,
343
- 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,
344
605
  ) -> dict[str, Any]:
345
- _ensure_initialized()
346
- 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
+ """
347
618
  try:
348
- return await SUI_OVERVIEW_TOOL.execute(
349
- 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,
350
624
  )
351
625
  except Exception as e:
352
626
  return error_response(e, context={
353
- "tool": "sui_package_overview",
354
- "packages": packages,
355
- "hours": hours,
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
- async def history_tail(n: str) -> str:
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
- async def sql_artifact(sha: str) -> str:
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
- 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]:
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
- 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]:
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
- 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]:
522
727
  _ensure_initialized()
523
728
  assert QUERY_ADMIN_SERVICE is not None
524
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