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.
- spice_mcp-0.1.4/PKG-INFO +121 -0
- spice_mcp-0.1.4/README.md +100 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/pyproject.toml +1 -1
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/client.py +9 -4
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/extract.py +48 -4
- spice_mcp-0.1.4/src/spice_mcp/adapters/dune/query_wrapper.py +86 -0
- spice_mcp-0.1.4/src/spice_mcp/adapters/dune/user_agent.py +9 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/server.py +87 -22
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/execute_query.py +6 -4
- spice_mcp-0.1.4/test_mcp_cursor/.env.example +9 -0
- spice_mcp-0.1.4/test_mcp_cursor/CURSOR_SETUP.md +159 -0
- spice_mcp-0.1.4/test_mcp_cursor/INSTALL.md +71 -0
- spice_mcp-0.1.4/test_mcp_cursor/QUICK_START.md +96 -0
- spice_mcp-0.1.4/test_mcp_cursor/README.md +38 -0
- spice_mcp-0.1.4/test_mcp_cursor/TEST_RESULTS.md +50 -0
- spice_mcp-0.1.4/test_mcp_cursor/cursor_mcp_config.json +11 -0
- spice_mcp-0.1.4/test_mcp_cursor/setup_cursor_mcp.sh +58 -0
- spice_mcp-0.1.4/test_mcp_cursor/test_issue_8_scenarios.py +335 -0
- spice_mcp-0.1.4/test_mcp_cursor/test_real_api.py +394 -0
- spice_mcp-0.1.4/tests/fastmcp/test_dune_query_schema_validation.py +139 -0
- spice_mcp-0.1.3/.hypothesis/constants/03230d036f66298e +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/06eb11a7027a88cd +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/0f801575b114e021 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/1eeb4d051122e156 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/366c70be49a7442b +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/4753a088eccf5421 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/4a324fb33357cad2 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/4cc858fdf36f013d +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/5405706eff340311 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/6cfc775ad89a5b6a +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/71bc6f8cf28bcc19 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/851c74a7cccd312b +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/8c85c7b24d65d86a +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/8d1eb008907ca218 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/9da684ed8a9b8d9c +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/adc83b19e793491b +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/ae79689a9c0be3fc +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/c9f1afd2a84c92ac +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/d427659fa4d8fdef +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/d946c4cbd3f551fb +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/da39a3ee5e6b4b0d +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/df167ab23556209e +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/e5d028d6ca3bc6c6 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/ea30e4add0a6171c +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/ebbaf5778e99a3e9 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/ebfda9399227f401 +0 -4
- spice_mcp-0.1.3/.hypothesis/constants/f8058dd8f3978a9f +0 -4
- spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/3297e56a00db6e8d +0 -0
- spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/5206044c103f45ca +0 -2
- spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/8c16f33e2460573e +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/04e6b3400353b141/c67ab7f30dd41ebd +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/3297e56a00db6e8d/1236c0a13f38a176 +0 -0
- spice_mcp-0.1.3/.hypothesis/examples/5206044c103f45ca/e4201915c9a55aa8 +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/05f4853fe973fb8f +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/57bd1d7010b0a3e5 +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/6c3ff7a3026f98fd +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/ccd03e0f6bc2e1a2 +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/8c16f33e2460573e/d179779eaa64628d +0 -1
- spice_mcp-0.1.3/.hypothesis/examples/c67ab7f30dd41ebd/31f5e37d90db6ce6 +0 -1
- spice_mcp-0.1.3/.hypothesis/unicode_data/15.1.0/charmap.json.gz +0 -0
- spice_mcp-0.1.3/.hypothesis/unicode_data/15.1.0/codec-utf-8.json.gz +0 -0
- spice_mcp-0.1.3/PKG-INFO +0 -198
- spice_mcp-0.1.3/README.md +0 -177
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/.gitignore +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/.python-version +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/CONTRIBUTING.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/LICENSE +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/architecture.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/codex_cli.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/codex_cli_tools.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/config.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/development.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/discovery.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/dune_api.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/index.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/installation.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/docs/tools.md +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/pytest.ini +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/scripts/bridgez/make_circle_comparison.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/scripts/codex_tools_doctor.sh +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/admin.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/cache.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/helpers.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/transport.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/types.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/typing_utils.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/dune/urls.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/http_client.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/spellbook/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/adapters/spellbook/explorer.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/config.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/errors.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/models.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/core/ports.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/logging/query_history.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/mcp/tools/base.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/observability/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/observability/logging.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/polars_utils.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/py.typed +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/discovery_service.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/query_admin_service.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/src/spice_mcp/service_layer/query_service.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/cassettes/.gitkeep +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/config/environments.yaml +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/config/test_queries.yaml +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/conftest.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_resources_and_validation.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_server_fastmcp.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/fastmcp/test_server_mcp_extras.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_age.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_errors.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/http_stubbed/test_pagination.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/test_spellbook_discovery.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/integration/test_user_journeys.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/live/test_live_basic.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/live/test_live_sui.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/mcp/conftest.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/mcp/test_tool_contracts.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_cache.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_cache_consistency.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_discovery.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_dune_adapter.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_edge_cases.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_parsing.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_query_history.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_show_rewrite.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_timeout.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_typing_utils.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/offline/test_urls.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/property/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/property/test_property_based.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/comprehensive_test_runner.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/run_tests.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_api_health.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_cache_functionality.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_data_types.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_dune_connectivity.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_dune_query_execution.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_error_handling.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_mcp_simulation.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_mcp_tools.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_performance.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_query_lifecycle.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_resilience.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/scripts/test_resource_management.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/style/test_polars_lazy.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/api_client.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/helpers.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/query_factory.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/support/test_data.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_additional_mcp_tools.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_execute_query_tool.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_health_tool.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_query_service.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_schemas.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/tools/test_unified_discover.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/validation/__init__.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/tests/validation/test_error_messages.py +0 -0
- {spice_mcp-0.1.3 → spice_mcp-0.1.4}/uv.lock +0 -0
spice_mcp-0.1.4/PKG-INFO
ADDED
|
@@ -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
|
+
[](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
|
+
[](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.
|
|
@@ -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 =
|
|
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":
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
338
|
-
@overload
|
|
339
|
-
|
|
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
|
+
|
|
@@ -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":
|
|
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":
|
|
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
|
-
|
|
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]
|
|
203
|
+
parameters: Optional[dict[str, Any]] = None,
|
|
209
204
|
refresh: bool = False,
|
|
210
|
-
max_age: float
|
|
211
|
-
limit: int
|
|
212
|
-
offset: int
|
|
213
|
-
sample_count: int
|
|
214
|
-
sort_by: str
|
|
215
|
-
columns: list[str]
|
|
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]
|
|
218
|
-
timeout_seconds: float
|
|
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=
|
|
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=
|
|
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 =
|
|
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":
|
|
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":
|
|
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
|