spice-mcp 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (168) hide show
  1. spice_mcp-0.1.4/PKG-INFO +121 -0
  2. spice_mcp-0.1.4/README.md +100 -0
  3. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/pyproject.toml +1 -1
  4. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/client.py +9 -4
  5. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/extract.py +48 -4
  6. spice_mcp-0.1.4/src/spice_mcp/adapters/dune/query_wrapper.py +86 -0
  7. spice_mcp-0.1.4/src/spice_mcp/adapters/dune/user_agent.py +9 -0
  8. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/server.py +87 -22
  9. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/execute_query.py +6 -4
  10. spice_mcp-0.1.4/test_mcp_cursor/.env.example +9 -0
  11. spice_mcp-0.1.4/test_mcp_cursor/CURSOR_SETUP.md +159 -0
  12. spice_mcp-0.1.4/test_mcp_cursor/INSTALL.md +71 -0
  13. spice_mcp-0.1.4/test_mcp_cursor/QUICK_START.md +96 -0
  14. spice_mcp-0.1.4/test_mcp_cursor/README.md +38 -0
  15. spice_mcp-0.1.4/test_mcp_cursor/TEST_RESULTS.md +50 -0
  16. spice_mcp-0.1.4/test_mcp_cursor/cursor_mcp_config.json +11 -0
  17. spice_mcp-0.1.4/test_mcp_cursor/setup_cursor_mcp.sh +58 -0
  18. spice_mcp-0.1.4/test_mcp_cursor/test_issue_8_scenarios.py +335 -0
  19. spice_mcp-0.1.4/test_mcp_cursor/test_real_api.py +394 -0
  20. spice_mcp-0.1.4/tests/fastmcp/test_dune_query_schema_validation.py +139 -0
  21. spice_mcp-0.1.3/.hypothesis/constants/03230d036f66298e +0 -4
  22. spice_mcp-0.1.3/.hypothesis/constants/06eb11a7027a88cd +0 -4
  23. spice_mcp-0.1.3/.hypothesis/constants/0f801575b114e021 +0 -4
  24. spice_mcp-0.1.3/.hypothesis/constants/1eeb4d051122e156 +0 -4
  25. spice_mcp-0.1.3/.hypothesis/constants/366c70be49a7442b +0 -4
  26. spice_mcp-0.1.3/.hypothesis/constants/4753a088eccf5421 +0 -4
  27. spice_mcp-0.1.3/.hypothesis/constants/4a324fb33357cad2 +0 -4
  28. spice_mcp-0.1.3/.hypothesis/constants/4cc858fdf36f013d +0 -4
  29. spice_mcp-0.1.3/.hypothesis/constants/5405706eff340311 +0 -4
  30. spice_mcp-0.1.3/.hypothesis/constants/6cfc775ad89a5b6a +0 -4
  31. spice_mcp-0.1.3/.hypothesis/constants/71bc6f8cf28bcc19 +0 -4
  32. spice_mcp-0.1.3/.hypothesis/constants/851c74a7cccd312b +0 -4
  33. spice_mcp-0.1.3/.hypothesis/constants/8c85c7b24d65d86a +0 -4
  34. spice_mcp-0.1.3/.hypothesis/constants/8d1eb008907ca218 +0 -4
  35. spice_mcp-0.1.3/.hypothesis/constants/9da684ed8a9b8d9c +0 -4
  36. spice_mcp-0.1.3/.hypothesis/constants/adc83b19e793491b +0 -4
  37. spice_mcp-0.1.3/.hypothesis/constants/ae79689a9c0be3fc +0 -4
  38. spice_mcp-0.1.3/.hypothesis/constants/c9f1afd2a84c92ac +0 -4
  39. spice_mcp-0.1.3/.hypothesis/constants/d427659fa4d8fdef +0 -4
  40. spice_mcp-0.1.3/.hypothesis/constants/d946c4cbd3f551fb +0 -4
  41. spice_mcp-0.1.3/.hypothesis/constants/da39a3ee5e6b4b0d +0 -4
  42. spice_mcp-0.1.3/.hypothesis/constants/df167ab23556209e +0 -4
  43. spice_mcp-0.1.3/.hypothesis/constants/e5d028d6ca3bc6c6 +0 -4
  44. spice_mcp-0.1.3/.hypothesis/constants/ea30e4add0a6171c +0 -4
  45. spice_mcp-0.1.3/.hypothesis/constants/ebbaf5778e99a3e9 +0 -4
  46. spice_mcp-0.1.3/.hypothesis/constants/ebfda9399227f401 +0 -4
  47. spice_mcp-0.1.3/.hypothesis/constants/f8058dd8f3978a9f +0 -4
  48. spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/3297e56a00db6e8d +0 -0
  49. spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/5206044c103f45ca +0 -2
  50. spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/8c16f33e2460573e +0 -1
  51. spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/c67ab7f30dd41ebd +0 -1
  52. spice_mcp-0.1.3/.hypothesis/examples/3297e56a00db6e8d/1236c0a13f38a176 +0 -0
  53. spice_mcp-0.1.3/.hypothesis/examples/5206044c103f45ca/e4201915c9a55aa8 +0 -1
  54. spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/05f4853fe973fb8f +0 -1
  55. spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/57bd1d7010b0a3e5 +0 -1
  56. spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/6c3ff7a3026f98fd +0 -1
  57. spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/ccd03e0f6bc2e1a2 +0 -1
  58. spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/d179779eaa64628d +0 -1
  59. spice_mcp-0.1.3/.hypothesis/examples/c67ab7f30dd41ebd/31f5e37d90db6ce6 +0 -1
  60. spice_mcp-0.1.3/.hypothesis/unicode_data/15.1.0/charmap.json.gz +0 -0
  61. spice_mcp-0.1.3/.hypothesis/unicode_data/15.1.0/codec-utf-8.json.gz +0 -0
  62. spice_mcp-0.1.3/PKG-INFO +0 -198
  63. spice_mcp-0.1.3/README.md +0 -177
  64. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/.gitignore +0 -0
  65. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/.python-version +0 -0
  66. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/CONTRIBUTING.md +0 -0
  67. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/LICENSE +0 -0
  68. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/architecture.md +0 -0
  69. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/codex_cli.md +0 -0
  70. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/codex_cli_tools.md +0 -0
  71. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/config.md +0 -0
  72. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/development.md +0 -0
  73. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/discovery.md +0 -0
  74. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/dune_api.md +0 -0
  75. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/index.md +0 -0
  76. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/installation.md +0 -0
  77. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/tools.md +0 -0
  78. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/pytest.ini +0 -0
  79. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/scripts/bridgez/make_circle_comparison.py +0 -0
  80. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/scripts/codex_tools_doctor.sh +0 -0
  81. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/__init__.py +0 -0
  82. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/__init__.py +0 -0
  83. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/__init__.py +0 -0
  84. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/admin.py +0 -0
  85. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/cache.py +0 -0
  86. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/helpers.py +0 -0
  87. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/transport.py +0 -0
  88. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/types.py +0 -0
  89. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/typing_utils.py +0 -0
  90. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/urls.py +0 -0
  91. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/http_client.py +0 -0
  92. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/spellbook/__init__.py +0 -0
  93. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/spellbook/explorer.py +0 -0
  94. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/config.py +0 -0
  95. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/__init__.py +0 -0
  96. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/errors.py +0 -0
  97. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/models.py +0 -0
  98. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/ports.py +0 -0
  99. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/logging/query_history.py +0 -0
  100. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/__init__.py +0 -0
  101. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/__init__.py +0 -0
  102. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/base.py +0 -0
  103. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/observability/__init__.py +0 -0
  104. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/observability/logging.py +0 -0
  105. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/polars_utils.py +0 -0
  106. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/py.typed +0 -0
  107. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/__init__.py +0 -0
  108. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/discovery_service.py +0 -0
  109. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/query_admin_service.py +0 -0
  110. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/query_service.py +0 -0
  111. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/cassettes/.gitkeep +0 -0
  112. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/config/environments.yaml +0 -0
  113. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/config/test_queries.yaml +0 -0
  114. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/conftest.py +0 -0
  115. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_resources_and_validation.py +0 -0
  116. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_server_fastmcp.py +0 -0
  117. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_server_mcp_extras.py +0 -0
  118. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_age.py +0 -0
  119. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_errors.py +0 -0
  120. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_pagination.py +0 -0
  121. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/__init__.py +0 -0
  122. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/test_spellbook_discovery.py +0 -0
  123. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/test_user_journeys.py +0 -0
  124. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/live/test_live_basic.py +0 -0
  125. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/live/test_live_sui.py +0 -0
  126. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/mcp/conftest.py +0 -0
  127. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/mcp/test_tool_contracts.py +0 -0
  128. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_cache.py +0 -0
  129. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_cache_consistency.py +0 -0
  130. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_discovery.py +0 -0
  131. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_dune_adapter.py +0 -0
  132. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_edge_cases.py +0 -0
  133. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_parsing.py +0 -0
  134. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_query_history.py +0 -0
  135. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_show_rewrite.py +0 -0
  136. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_timeout.py +0 -0
  137. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_typing_utils.py +0 -0
  138. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_urls.py +0 -0
  139. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/property/__init__.py +0 -0
  140. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/property/test_property_based.py +0 -0
  141. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/comprehensive_test_runner.py +0 -0
  142. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/run_tests.py +0 -0
  143. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_api_health.py +0 -0
  144. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_cache_functionality.py +0 -0
  145. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_data_types.py +0 -0
  146. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_dune_connectivity.py +0 -0
  147. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_dune_query_execution.py +0 -0
  148. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_error_handling.py +0 -0
  149. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_mcp_simulation.py +0 -0
  150. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_mcp_tools.py +0 -0
  151. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_performance.py +0 -0
  152. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_query_lifecycle.py +0 -0
  153. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_resilience.py +0 -0
  154. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_resource_management.py +0 -0
  155. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/style/test_polars_lazy.py +0 -0
  156. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/api_client.py +0 -0
  157. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/helpers.py +0 -0
  158. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/query_factory.py +0 -0
  159. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/test_data.py +0 -0
  160. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_additional_mcp_tools.py +0 -0
  161. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_execute_query_tool.py +0 -0
  162. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_health_tool.py +0 -0
  163. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_query_service.py +0 -0
  164. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_schemas.py +0 -0
  165. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_unified_discover.py +0 -0
  166. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/validation/__init__.py +0 -0
  167. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/validation/test_error_messages.py +0 -0
  168. {spice_mcp-0.1.3 → spice_mcp-0.1.4}/uv.lock +0 -0
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: spice-mcp
3
+ Version: 0.1.4
4
+ Summary: MCP server for Dune Analytics data access
5
+ Author-email: Evan-Kim2028 <ekcopersonal@gmail.com>
6
+ License-File: LICENSE
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.13
13
+ Requires-Dist: aiohttp>=3.9.5
14
+ Requires-Dist: fastmcp>=0.3.0
15
+ Requires-Dist: mcp>=0.9.0
16
+ Requires-Dist: polars>=1.35.1
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: rich-argparse>=1.5.2
19
+ Requires-Dist: rich>=13.3.3
20
+ Description-Content-Type: text/markdown
21
+
22
+ # spice-mcp
23
+
24
+ [![PyPI version](https://img.shields.io/pypi/v/spice-mcp.svg)](https://pypi.org/project/spice-mcp/)
25
+ <a href="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp">
26
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp/badge" alt="Spice MCP server" />
27
+ </a>
28
+
29
+ An MCP server that provides AI agents with direct access to [Dune Analytics](https://dune.com/) data. Execute queries, discover schemas and tables, and manage saved queries—all through a clean, type-safe interface optimized for AI workflows.
30
+
31
+ ## Why spice-mcp?
32
+
33
+ - **Agent-friendly**: Designed for AI agents using the Model Context Protocol (MCP)
34
+ - **Efficient**: Polars-first pipeline keeps data lazy until needed, reducing memory usage
35
+ - **Discovery**: Built-in tools to explore Dune's extensive blockchain datasets
36
+ - **Type-safe**: Fully typed parameters and responses with FastMCP
37
+ - **Reproducible**: Automatic query history logging and SQL artifact storage
38
+
39
+ ## Quick Start
40
+
41
+ 1. **Install**:
42
+ ```bash
43
+ uv pip install spice-mcp
44
+ ```
45
+
46
+ 2. **Set API key** (choose one method):
47
+ - **Option A**: Create a `.env` file in your project root:
48
+ ```bash
49
+ echo "DUNE_API_KEY=your-api-key-here" > .env
50
+ ```
51
+ - **Option B**: Export in your shell:
52
+ ```bash
53
+ export DUNE_API_KEY=your-api-key-here
54
+ ```
55
+
56
+ 3. **Use with Cursor IDE**:
57
+ Add to Cursor Settings → MCP Servers:
58
+ ```json
59
+ {
60
+ "name": "spice-mcp",
61
+ "command": "spice-mcp",
62
+ "env": {
63
+ "DUNE_API_KEY": "your-dune-api-key-here"
64
+ }
65
+ }
66
+ ```
67
+
68
+ **Note**: Query history logging is enabled by default. Logs are saved to `logs/queries.jsonl` (or `~/.spice_mcp/logs/queries.jsonl` if not in a project directory). To customize paths, set `SPICE_QUERY_HISTORY` and `SPICE_ARTIFACT_ROOT` environment variables.
69
+
70
+ ## Core Tools
71
+
72
+ | Tool | Description | Key Parameters |
73
+ |------|-------------|----------------|
74
+ | `dune_query` | Execute queries by ID, URL, or raw SQL | `query` (str), `parameters` (object), `limit` (int), `offset` (int), `format` (`preview\|raw\|metadata\|poll`), `refresh` (bool), `timeout_seconds` (float) |
75
+ | `dune_query_info` | Get metadata for a saved query | `query` (str - ID or URL) |
76
+ | `dune_discover` | Unified discovery across Dune API and Spellbook | `keyword` (str\|list), `schema` (str), `limit` (int), `source` (`dune\|spellbook\|both`), `include_columns` (bool) |
77
+ | `dune_find_tables` | Search schemas and list tables | `keyword` (str), `schema` (str), `limit` (int) |
78
+ | `dune_describe_table` | Get column metadata for a table | `schema` (str), `table` (str) |
79
+ | `spellbook_find_models` | Search Spellbook dbt models | `keyword` (str\|list), `schema` (str), `limit` (int), `include_columns` (bool) |
80
+ | `dune_health_check` | Verify API key and configuration | (no parameters) |
81
+ | `dune_query_create` | Create a new saved query | `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
82
+ | `dune_query_update` | Update an existing saved query | `query_id` (int), `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
83
+ | `dune_query_fork` | Fork an existing saved query | `source_query_id` (int), `name` (str) |
84
+
85
+ ## Resources
86
+
87
+ - `spice:history/tail/{n}` — View last N lines of query history (1-1000)
88
+ - `spice:artifact/{sha}` — Retrieve stored SQL by SHA-256 hash
89
+
90
+ ## What is Dune?
91
+
92
+ [Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API. It aggregates on-chain data from Ethereum, Solana, Polygon, and other chains into queryable SQL tables. See the [Dune Docs](https://dune.com/docs) for more information.
93
+
94
+ ## Installation
95
+
96
+ **From PyPI** (recommended):
97
+ ```bash
98
+ uv pip install spice-mcp
99
+ ```
100
+
101
+ **From source**:
102
+ ```bash
103
+ git clone https://github.com/Evan-Kim2028/spice-mcp.git
104
+ cd spice-mcp
105
+ uv sync
106
+ uv pip install -e .
107
+ ```
108
+
109
+ **Requirements**: Python 3.13+
110
+
111
+ ## Documentation
112
+
113
+ - [Tool Reference](docs/tools.md) — Complete tool documentation with parameters
114
+ - [Architecture](docs/architecture.md) — Code structure and design patterns
115
+ - [Discovery Guide](docs/discovery.md) — How to explore Dune schemas and tables
116
+ - [Dune API Guide](docs/dune_api.md) — Understanding Dune's data structure
117
+ - [Configuration](docs/config.md) — Environment variables and settings
118
+
119
+ ## License
120
+
121
+ See [LICENSE](LICENSE) file for details.
@@ -0,0 +1,100 @@
1
+ # spice-mcp
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/spice-mcp.svg)](https://pypi.org/project/spice-mcp/)
4
+ <a href="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp">
5
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp/badge" alt="Spice MCP server" />
6
+ </a>
7
+
8
+ An MCP server that provides AI agents with direct access to [Dune Analytics](https://dune.com/) data. Execute queries, discover schemas and tables, and manage saved queries—all through a clean, type-safe interface optimized for AI workflows.
9
+
10
+ ## Why spice-mcp?
11
+
12
+ - **Agent-friendly**: Designed for AI agents using the Model Context Protocol (MCP)
13
+ - **Efficient**: Polars-first pipeline keeps data lazy until needed, reducing memory usage
14
+ - **Discovery**: Built-in tools to explore Dune's extensive blockchain datasets
15
+ - **Type-safe**: Fully typed parameters and responses with FastMCP
16
+ - **Reproducible**: Automatic query history logging and SQL artifact storage
17
+
18
+ ## Quick Start
19
+
20
+ 1. **Install**:
21
+ ```bash
22
+ uv pip install spice-mcp
23
+ ```
24
+
25
+ 2. **Set API key** (choose one method):
26
+ - **Option A**: Create a `.env` file in your project root:
27
+ ```bash
28
+ echo "DUNE_API_KEY=your-api-key-here" > .env
29
+ ```
30
+ - **Option B**: Export in your shell:
31
+ ```bash
32
+ export DUNE_API_KEY=your-api-key-here
33
+ ```
34
+
35
+ 3. **Use with Cursor IDE**:
36
+ Add to Cursor Settings → MCP Servers:
37
+ ```json
38
+ {
39
+ "name": "spice-mcp",
40
+ "command": "spice-mcp",
41
+ "env": {
42
+ "DUNE_API_KEY": "your-dune-api-key-here"
43
+ }
44
+ }
45
+ ```
46
+
47
+ **Note**: Query history logging is enabled by default. Logs are saved to `logs/queries.jsonl` (or `~/.spice_mcp/logs/queries.jsonl` if not in a project directory). To customize paths, set `SPICE_QUERY_HISTORY` and `SPICE_ARTIFACT_ROOT` environment variables.
48
+
49
+ ## Core Tools
50
+
51
+ | Tool | Description | Key Parameters |
52
+ |------|-------------|----------------|
53
+ | `dune_query` | Execute queries by ID, URL, or raw SQL | `query` (str), `parameters` (object), `limit` (int), `offset` (int), `format` (`preview\|raw\|metadata\|poll`), `refresh` (bool), `timeout_seconds` (float) |
54
+ | `dune_query_info` | Get metadata for a saved query | `query` (str - ID or URL) |
55
+ | `dune_discover` | Unified discovery across Dune API and Spellbook | `keyword` (str\|list), `schema` (str), `limit` (int), `source` (`dune\|spellbook\|both`), `include_columns` (bool) |
56
+ | `dune_find_tables` | Search schemas and list tables | `keyword` (str), `schema` (str), `limit` (int) |
57
+ | `dune_describe_table` | Get column metadata for a table | `schema` (str), `table` (str) |
58
+ | `spellbook_find_models` | Search Spellbook dbt models | `keyword` (str\|list), `schema` (str), `limit` (int), `include_columns` (bool) |
59
+ | `dune_health_check` | Verify API key and configuration | (no parameters) |
60
+ | `dune_query_create` | Create a new saved query | `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
61
+ | `dune_query_update` | Update an existing saved query | `query_id` (int), `name` (str), `query_sql` (str), `description` (str), `tags` (list), `parameters` (list) |
62
+ | `dune_query_fork` | Fork an existing saved query | `source_query_id` (int), `name` (str) |
63
+
64
+ ## Resources
65
+
66
+ - `spice:history/tail/{n}` — View last N lines of query history (1-1000)
67
+ - `spice:artifact/{sha}` — Retrieve stored SQL by SHA-256 hash
68
+
69
+ ## What is Dune?
70
+
71
+ [Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API. It aggregates on-chain data from Ethereum, Solana, Polygon, and other chains into queryable SQL tables. See the [Dune Docs](https://dune.com/docs) for more information.
72
+
73
+ ## Installation
74
+
75
+ **From PyPI** (recommended):
76
+ ```bash
77
+ uv pip install spice-mcp
78
+ ```
79
+
80
+ **From source**:
81
+ ```bash
82
+ git clone https://github.com/Evan-Kim2028/spice-mcp.git
83
+ cd spice-mcp
84
+ uv sync
85
+ uv pip install -e .
86
+ ```
87
+
88
+ **Requirements**: Python 3.13+
89
+
90
+ ## Documentation
91
+
92
+ - [Tool Reference](docs/tools.md) — Complete tool documentation with parameters
93
+ - [Architecture](docs/architecture.md) — Code structure and design patterns
94
+ - [Discovery Guide](docs/discovery.md) — How to explore Dune schemas and tables
95
+ - [Dune API Guide](docs/dune_api.md) — Understanding Dune's data structure
96
+ - [Configuration](docs/config.md) — Environment variables and settings
97
+
98
+ ## License
99
+
100
+ See [LICENSE](LICENSE) file for details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spice-mcp"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "MCP server for Dune Analytics data access"
5
5
  authors = [
6
6
  { name = "Evan-Kim2028", email = "ekcopersonal@gmail.com" }
@@ -24,6 +24,10 @@ from ...polars_utils import collect_preview
24
24
  from ..http_client import HttpClient, HttpClientConfig
25
25
  from . import extract, urls
26
26
 
27
+ # Use wrapper to avoid FastMCP detecting overloads in extract.query()
28
+ # Note: We still import extract for _determine_input_type and other non-overloaded functions
29
+ from .query_wrapper import execute_query as _execute_dune_query
30
+
27
31
 
28
32
  class DuneAdapter(QueryExecutor, CatalogExplorer):
29
33
  """Thin façade around the vendored extract module."""
@@ -47,7 +51,7 @@ class DuneAdapter(QueryExecutor, CatalogExplorer):
47
51
  q_rewritten = _maybe_rewrite_show_sql(q)
48
52
  if q_rewritten is not None:
49
53
  q = q_rewritten
50
- result = extract.query(
54
+ result = _execute_dune_query(
51
55
  query_or_execution=q,
52
56
  verbose=False,
53
57
  refresh=request.refresh,
@@ -123,9 +127,10 @@ class DuneAdapter(QueryExecutor, CatalogExplorer):
123
127
  pass
124
128
 
125
129
  url = urls.get_query_results_url(query_id, parameters=params, csv=False)
130
+ from .user_agent import get_user_agent
126
131
  headers = {
127
132
  "X-Dune-API-Key": self._api_key(),
128
- "User-Agent": extract.get_user_agent(),
133
+ "User-Agent": get_user_agent(),
129
134
  }
130
135
  try:
131
136
  resp = self._http.request("GET", url, headers=headers)
@@ -196,8 +201,8 @@ class DuneAdapter(QueryExecutor, CatalogExplorer):
196
201
  def _run_sql(self, sql: str, *, limit: int | None = None) -> pl.DataFrame:
197
202
  self._ensure_api_key()
198
203
  sql_eff = _maybe_rewrite_show_sql(sql) or sql
199
- df = extract.query(
200
- sql_eff,
204
+ df = _execute_dune_query(
205
+ query_or_execution=sql_eff,
201
206
  verbose=False,
202
207
  performance="medium",
203
208
  timeout_seconds=self.config.default_timeout_seconds,
@@ -289,6 +289,14 @@ def query(
289
289
 
290
290
  # execute or retrieve query
291
291
  if query_id:
292
+ # Check if this is a parameterized query (raw SQL via template or parameterized query)
293
+ # For parameterized queries, results don't exist until execution, so skip GET attempt
294
+ is_parameterized = (
295
+ parameters is not None
296
+ and len(parameters) > 0
297
+ and ('query' in parameters or any(k != 'query' for k in parameters))
298
+ )
299
+
292
300
  if cache and load_from_cache and not refresh:
293
301
  cache_result, cache_execution = _cache.load_from_cache(
294
302
  execute_kwargs, result_kwargs, output_kwargs
@@ -301,7 +309,8 @@ def query(
301
309
  age = get_query_latest_age(**execute_kwargs, verbose=verbose) # type: ignore
302
310
  if age is None or age > max_age:
303
311
  refresh = True
304
- if not refresh:
312
+ # Skip GET results attempt for parameterized queries - they need execution first
313
+ if not refresh and not is_parameterized:
305
314
  df = get_results(**execute_kwargs, **result_kwargs)
306
315
  if df is not None:
307
316
  return process_result(df, execution, **output_kwargs)
@@ -334,9 +343,44 @@ def query(
334
343
  return execution
335
344
 
336
345
 
337
- @overload
338
- @overload
339
- @overload
346
+ if TYPE_CHECKING:
347
+ @overload
348
+ def _process_result(
349
+ df: pl.DataFrame,
350
+ execution: Execution | None,
351
+ execute_kwargs: ExecuteKwargs,
352
+ result_kwargs: RetrievalKwargs,
353
+ cache: bool,
354
+ save_to_cache: bool,
355
+ cache_dir: str | None,
356
+ include_execution: Literal[False],
357
+ ) -> pl.DataFrame: ...
358
+
359
+ @overload
360
+ def _process_result(
361
+ df: pl.DataFrame,
362
+ execution: Execution | None,
363
+ execute_kwargs: ExecuteKwargs,
364
+ result_kwargs: RetrievalKwargs,
365
+ cache: bool,
366
+ save_to_cache: bool,
367
+ cache_dir: str | None,
368
+ include_execution: Literal[True],
369
+ ) -> tuple[pl.DataFrame, Execution]: ...
370
+
371
+ @overload
372
+ def _process_result(
373
+ df: pl.DataFrame,
374
+ execution: Execution | None,
375
+ execute_kwargs: ExecuteKwargs,
376
+ result_kwargs: RetrievalKwargs,
377
+ cache: bool,
378
+ save_to_cache: bool,
379
+ cache_dir: str | None,
380
+ include_execution: bool,
381
+ ) -> pl.DataFrame | tuple[pl.DataFrame, Execution]: ...
382
+
383
+
340
384
  def _process_result(
341
385
  df: pl.DataFrame,
342
386
  execution: Execution | None,
@@ -0,0 +1,86 @@
1
+ """Wrapper for extract.query() to avoid FastMCP overload detection.
2
+
3
+ This module provides a clean interface to extract.query() without exposing
4
+ the @overload decorators that FastMCP detects during runtime validation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+ from collections.abc import Mapping, Sequence
11
+
12
+ from ..http_client import HttpClient
13
+ from .types import Execution, Performance, Query
14
+
15
+
16
+ def execute_query(
17
+ query_or_execution: Query | Execution,
18
+ *,
19
+ verbose: bool = True,
20
+ refresh: bool = False,
21
+ max_age: float | None = None,
22
+ parameters: Mapping[str, Any] | None = None,
23
+ api_key: str | None = None,
24
+ performance: Performance = "medium",
25
+ poll: bool = True,
26
+ poll_interval: float = 1.0,
27
+ timeout_seconds: float | None = None,
28
+ limit: int | None = None,
29
+ offset: int | None = None,
30
+ sample_count: int | None = None,
31
+ sort_by: str | None = None,
32
+ columns: Sequence[str] | None = None,
33
+ extras: Mapping[str, Any] | None = None,
34
+ types: Sequence[type] | Mapping[str, type] | None = None,
35
+ all_types: Sequence[type] | Mapping[str, type] | None = None,
36
+ cache: bool = True,
37
+ cache_dir: str | None = None,
38
+ save_to_cache: bool = True,
39
+ load_from_cache: bool = True,
40
+ include_execution: bool = False,
41
+ http_client: HttpClient | None = None,
42
+ ) -> Any:
43
+ """
44
+ Execute a Dune query without exposing overloads to FastMCP.
45
+
46
+ This is a wrapper around extract.query() that has a single, non-overloaded
47
+ signature. FastMCP won't detect overloads when inspecting this function.
48
+ """
49
+ # Import here to avoid FastMCP seeing overloads during module import
50
+ from . import extract
51
+
52
+ # Call the actual query function - FastMCP won't trace through this wrapper
53
+ try:
54
+ return extract.query(
55
+ query_or_execution=query_or_execution,
56
+ verbose=verbose,
57
+ refresh=refresh,
58
+ max_age=max_age,
59
+ parameters=parameters,
60
+ api_key=api_key,
61
+ performance=performance,
62
+ poll=poll,
63
+ poll_interval=poll_interval,
64
+ timeout_seconds=timeout_seconds,
65
+ limit=limit,
66
+ offset=offset,
67
+ sample_count=sample_count,
68
+ sort_by=sort_by,
69
+ columns=columns,
70
+ extras=extras,
71
+ types=types,
72
+ all_types=all_types,
73
+ cache=cache,
74
+ cache_dir=cache_dir,
75
+ save_to_cache=save_to_cache,
76
+ load_from_cache=load_from_cache,
77
+ include_execution=include_execution,
78
+ http_client=http_client,
79
+ )
80
+ except NotImplementedError as exc:
81
+ # Provide additional context to help diagnose overload issues
82
+ raise RuntimeError(
83
+ "Underlying extract.query() raised NotImplementedError. "
84
+ "This suggests we're calling an overloaded stub."
85
+ ) from exc
86
+
@@ -0,0 +1,9 @@
1
+ """User agent utility to avoid importing overloaded functions."""
2
+
3
+ ADAPTER_VERSION = "0.1.4"
4
+
5
+
6
+ def get_user_agent() -> str:
7
+ """Get user agent string for HTTP requests."""
8
+ return f"spice-mcp/{ADAPTER_VERSION}"
9
+
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
- from typing import Any, Literal
5
+ from typing import Any, Literal, Optional
6
6
 
7
7
  os.environ.setdefault("FASTMCP_NO_BANNER", "1")
8
8
  os.environ.setdefault("FASTMCP_LOG_LEVEL", "ERROR")
@@ -22,7 +22,6 @@ try:
22
22
  except Exception:
23
23
  pass
24
24
 
25
- from ..adapters.dune import extract as dune_extract
26
25
  from ..adapters.dune import urls as dune_urls
27
26
  from ..adapters.dune.admin import DuneAdminAdapter
28
27
  from ..adapters.dune.client import DuneAdapter
@@ -144,9 +143,10 @@ def compute_health_status() -> dict[str, Any]:
144
143
  if tmpl:
145
144
  tid = dune_urls.get_query_id(tmpl)
146
145
  url = dune_urls.url_templates["query"].format(query_id=tid)
146
+ from ..adapters.dune.user_agent import get_user_agent as get_dune_user_agent
147
147
  headers = {
148
148
  "X-Dune-API-Key": os.getenv("DUNE_API_KEY", ""),
149
- "User-Agent": dune_extract.get_user_agent(),
149
+ "User-Agent": get_dune_user_agent(),
150
150
  }
151
151
  client = HTTP_CLIENT or HttpClient(Config.from_env().http)
152
152
  resp = client.request("GET", url, headers=headers, timeout=5.0)
@@ -169,9 +169,10 @@ def dune_query_info(query: str) -> dict[str, Any]:
169
169
  try:
170
170
  qid = dune_urls.get_query_id(query)
171
171
  url = dune_urls.url_templates["query"].format(query_id=qid)
172
+ from ..adapters.dune.user_agent import get_user_agent as get_dune_user_agent
172
173
  headers = {
173
174
  "X-Dune-API-Key": dune_urls.get_api_key(),
174
- "User-Agent": dune_extract.get_user_agent(),
175
+ "User-Agent": get_dune_user_agent(),
175
176
  }
176
177
  client = HTTP_CLIENT or HttpClient(Config.from_env().http)
177
178
  resp = client.request("GET", url, headers=headers, timeout=10.0)
@@ -197,33 +198,55 @@ def dune_query_info(query: str) -> dict[str, Any]:
197
198
  })
198
199
 
199
200
 
200
- @app.tool(
201
- name="dune_query",
202
- title="Run Dune Query",
203
- description="Execute Dune queries and return agent-optimized preview.",
204
- tags={"dune", "query"},
205
- )
206
- def dune_query(
201
+ def _dune_query_impl(
207
202
  query: str,
208
- parameters: dict[str, Any] | None = None,
203
+ parameters: Optional[dict[str, Any]] = None,
209
204
  refresh: bool = False,
210
- max_age: float | None = None,
211
- limit: int | None = None,
212
- offset: int | None = None,
213
- sample_count: int | None = None,
214
- sort_by: str | None = None,
215
- columns: list[str] | None = None,
205
+ max_age: Optional[float] = None,
206
+ limit: Optional[int] = None,
207
+ offset: Optional[int] = None,
208
+ sample_count: Optional[int] = None,
209
+ sort_by: Optional[str] = None,
210
+ columns: Optional[list[str]] = None,
216
211
  format: Literal["preview", "raw", "metadata", "poll"] = "preview",
217
- extras: dict[str, Any] | None = None,
218
- timeout_seconds: float | None = None,
212
+ extras: Optional[dict[str, Any]] = None,
213
+ timeout_seconds: Optional[float] = None,
219
214
  ) -> dict[str, Any]:
215
+ """Internal implementation of dune_query to avoid FastMCP overload detection."""
220
216
  _ensure_initialized()
221
217
  assert EXECUTE_QUERY_TOOL is not None
218
+
219
+ # Normalize parameters: handle case where MCP client passes JSON string
220
+ # This can happen if FastMCP's schema generation doesn't match client expectations
221
+ normalized_parameters = parameters
222
+ if isinstance(parameters, str):
223
+ try:
224
+ import json
225
+ normalized_parameters = json.loads(parameters)
226
+ except (json.JSONDecodeError, TypeError):
227
+ return error_response(
228
+ ValueError(f"parameters must be a dict or JSON string, got {type(parameters).__name__}"),
229
+ context={
230
+ "tool": "dune_query",
231
+ "query": query,
232
+ "parameters_type": type(parameters).__name__,
233
+ }
234
+ )
235
+
236
+ # Normalize extras similarly
237
+ normalized_extras = extras
238
+ if isinstance(extras, str):
239
+ try:
240
+ import json
241
+ normalized_extras = json.loads(extras)
242
+ except (json.JSONDecodeError, TypeError):
243
+ normalized_extras = None
244
+
222
245
  try:
223
246
  # Execute query synchronously
224
247
  return EXECUTE_QUERY_TOOL.execute(
225
248
  query=query,
226
- parameters=parameters,
249
+ parameters=normalized_parameters,
227
250
  refresh=refresh,
228
251
  max_age=max_age,
229
252
  limit=limit,
@@ -232,7 +255,7 @@ def dune_query(
232
255
  sort_by=sort_by,
233
256
  columns=columns,
234
257
  format=format,
235
- extras=extras,
258
+ extras=normalized_extras,
236
259
  timeout_seconds=timeout_seconds,
237
260
  )
238
261
  except Exception as e:
@@ -244,6 +267,48 @@ def dune_query(
244
267
  })
245
268
 
246
269
 
270
+ @app.tool(
271
+ name="dune_query",
272
+ title="Run Dune Query",
273
+ description="Execute Dune queries and return agent-optimized preview.",
274
+ tags={"dune", "query"},
275
+ )
276
+ def dune_query(
277
+ query: str,
278
+ parameters: Optional[dict[str, Any]] = None,
279
+ refresh: bool = False,
280
+ max_age: Optional[float] = None,
281
+ limit: Optional[int] = None,
282
+ offset: Optional[int] = None,
283
+ sample_count: Optional[int] = None,
284
+ sort_by: Optional[str] = None,
285
+ columns: Optional[list[str]] = None,
286
+ format: Literal["preview", "raw", "metadata", "poll"] = "preview",
287
+ extras: Optional[dict[str, Any]] = None,
288
+ timeout_seconds: Optional[float] = None,
289
+ ) -> dict[str, Any]:
290
+ """Execute Dune queries (by ID, URL, or raw SQL) and return agent-optimized preview.
291
+
292
+ This wrapper ensures FastMCP doesn't detect overloads in imported functions.
293
+ """
294
+ # Always ensure parameters is explicitly passed (even if None) to avoid FastMCP
295
+ # overload detection when the keyword is omitted
296
+ return _dune_query_impl(
297
+ query=query,
298
+ parameters=parameters,
299
+ refresh=refresh,
300
+ max_age=max_age,
301
+ limit=limit,
302
+ offset=offset,
303
+ sample_count=sample_count,
304
+ sort_by=sort_by,
305
+ columns=columns,
306
+ format=format,
307
+ extras=extras,
308
+ timeout_seconds=timeout_seconds,
309
+ )
310
+
311
+
247
312
  @app.tool(
248
313
  name="dune_health_check",
249
314
  title="Health Check",
@@ -5,8 +5,10 @@ import re
5
5
  import time
6
6
  from typing import Any
7
7
 
8
- from ...adapters.dune import extract as dune_extract
9
8
  from ...adapters.dune import urls as dune_urls
9
+ from ...adapters.dune.query_wrapper import execute_query as execute_dune_query
10
+ # Import user_agent from separate module to avoid importing overloaded functions
11
+ from ...adapters.dune.user_agent import get_user_agent as get_dune_user_agent
10
12
  from ...adapters.http_client import HttpClient
11
13
  from ...config import Config
12
14
  from ...core.errors import error_response
@@ -103,7 +105,7 @@ class ExecuteQueryTool(MCPTool):
103
105
  q_use = _maybe_rewrite_show_sql(query) or query
104
106
  # Poll-only: return execution handle without fetching results
105
107
  if format == "poll":
106
- exec_obj = dune_extract.query(
108
+ exec_obj = execute_dune_query(
107
109
  q_use,
108
110
  parameters=parameters,
109
111
  api_key=self.config.dune.api_key,
@@ -333,7 +335,7 @@ class ExecuteQueryTool(MCPTool):
333
335
  query_id = dune_urls.get_query_id(query)
334
336
  headers = {
335
337
  "X-Dune-API-Key": dune_urls.get_api_key(),
336
- "User-Agent": dune_extract.get_user_agent(),
338
+ "User-Agent": get_dune_user_agent(),
337
339
  }
338
340
  resp = self._http.request(
339
341
  "GET",
@@ -362,7 +364,7 @@ class ExecuteQueryTool(MCPTool):
362
364
  url = dune_urls.get_execution_status_url(execution_id)
363
365
  headers = {
364
366
  "X-Dune-API-Key": dune_urls.get_api_key(),
365
- "User-Agent": dune_extract.get_user_agent(),
367
+ "User-Agent": get_dune_user_agent(),
366
368
  }
367
369
  resp = self._http.request("GET", url, headers=headers, timeout=10.0)
368
370
  data = resp.json()
@@ -0,0 +1,9 @@
1
+ # Dune API Key
2
+ # Get your API key from https://dune.com/settings/api
3
+ DUNE_API_KEY=your-api-key-here
4
+
5
+ # Optional: Customize query history location
6
+ # SPICE_QUERY_HISTORY=logs/queries.jsonl
7
+
8
+ # Optional: Customize artifact storage location
9
+ # SPICE_ARTIFACT_ROOT=logs/artifacts