spice-mcp 0.1.3__py3-none-any.whl → 0.1.4__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.

Potentially problematic release.


This version of spice-mcp might be problematic. Click here for more 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 = 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
+
spice_mcp/mcp/server.py CHANGED
@@ -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,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.
@@ -7,13 +7,15 @@ spice_mcp/adapters/http_client.py,sha256=CYgSKAsx-5c-uxaNIBCBTgQdaoBe5J3dJvnw8iq
7
7
  spice_mcp/adapters/dune/__init__.py,sha256=nspEuDpVOktAxm8B066s-d0LwouCYGpvNEexi0mRMN8,386
8
8
  spice_mcp/adapters/dune/admin.py,sha256=yxOueVz-rmgC-ZFbT06k59G24yRgYjiEkZlall5hXNQ,3157
9
9
  spice_mcp/adapters/dune/cache.py,sha256=7ykT58WN1yHGIN2uV3t7fWOqGb1VJdCvf3I-xZwsv74,4304
10
- spice_mcp/adapters/dune/client.py,sha256=JPrQZ_dtaPcGf6lYUguDxGweOtxG-8qMqOiXuhWL9QA,9122
11
- spice_mcp/adapters/dune/extract.py,sha256=3jrXqlak4Kpl11y7C3L1AhFv09lQ-2pBqmrh-0Mwlm0,28836
10
+ spice_mcp/adapters/dune/client.py,sha256=4-Ay2FQf_vo-eB6I9Kul3f1PgS78PPuUJ7zldebOtKU,9424
11
+ spice_mcp/adapters/dune/extract.py,sha256=30D-5NyiOXDtMZoo1dU9pf5yAAFR_ALn6JWvhipPRG0,30405
12
12
  spice_mcp/adapters/dune/helpers.py,sha256=BgDKr_g-UqmU2hoMb0ejQZHta_NbKwR1eDJp33sJYNk,227
13
+ spice_mcp/adapters/dune/query_wrapper.py,sha256=Km64otc00u9ieUhpZmL2aNYb9ETt6PoNb8czShIuPbY,2925
13
14
  spice_mcp/adapters/dune/transport.py,sha256=eRP-jPY2ZXxvTX9HSjIFqFUlbIzXspgH95jBFoTlpaQ,1436
14
15
  spice_mcp/adapters/dune/types.py,sha256=57TMX07u-Gq4BYwRAuZV0xI81nVXgtpp7KBID9YbKyQ,1195
15
16
  spice_mcp/adapters/dune/typing_utils.py,sha256=EpWneGDn-eQdo6lkLuESR09KXkDj9OqGz8bEF3JaFkM,574
16
17
  spice_mcp/adapters/dune/urls.py,sha256=bcuPERkFQduRTT2BrgzVhoFrMn-Lkvw9NmktcBZYEig,3902
18
+ spice_mcp/adapters/dune/user_agent.py,sha256=c6Kt4zczbuT9mapDoh8-3sgm268MUtvyIRxDF9yJwXQ,218
17
19
  spice_mcp/adapters/spellbook/__init__.py,sha256=D2cdVtSUbmAJdbPRvAyKxYS4-wUQ3unXyX4ZFYxenuk,150
18
20
  spice_mcp/adapters/spellbook/explorer.py,sha256=Q3UfEGlALizCDeeW_ZZVRRedzwMXiknmxwSkeOnxxgc,11515
19
21
  spice_mcp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -22,18 +24,18 @@ spice_mcp/core/models.py,sha256=i0C_-UE16OWyyZo_liooEJeYvbChE5lpK80aN2OF4lk,1795
22
24
  spice_mcp/core/ports.py,sha256=nEdeA3UH7v0kB_hbguMrpDljb9EhSxUAO0SdhjpoijQ,1618
23
25
  spice_mcp/logging/query_history.py,sha256=doE9lod64uzJxlA2XzHH2-VAmC6WstYAkQ0taEAxiIM,4315
24
26
  spice_mcp/mcp/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
25
- spice_mcp/mcp/server.py,sha256=NNTjn5f_OLNd8zSnNsx73WhHzNGW_SBJoCrdzkgOGP4,26275
27
+ spice_mcp/mcp/server.py,sha256=lHYEI76oQpPcpPT9pEoGXpUmAjDFXlOXuUrlOWV8s2c,28729
26
28
  spice_mcp/mcp/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
27
29
  spice_mcp/mcp/tools/base.py,sha256=zJkVxLgXR48iZcJeng8cZ2rXvbyicagoGlMN7BK7Img,1041
28
- spice_mcp/mcp/tools/execute_query.py,sha256=CucjxoBT22VUS-QJV1JrDjoywu2nd13fL3Lfs1qytUg,16093
30
+ spice_mcp/mcp/tools/execute_query.py,sha256=CJtoNKpRY6pCkyqjwFy_cTYqTIg9-EsZA8GrH-2MFjk,16262
29
31
  spice_mcp/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
32
  spice_mcp/observability/logging.py,sha256=ceJUEpKGpf5PAgPBmpB49zjqhdGCAESfLemFUhDSmI8,529
31
33
  spice_mcp/service_layer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
34
  spice_mcp/service_layer/discovery_service.py,sha256=202O0SzCZGQukd9kb2JYfarLygZHgiXlHqp_nTAdrWA,730
33
35
  spice_mcp/service_layer/query_admin_service.py,sha256=4q1NAAuTui7cm83Aq2rFDLIzKTHX17yzbSoSJyCmLbI,1356
34
36
  spice_mcp/service_layer/query_service.py,sha256=q0eAVW5I3sUxm29DgzPN_cH3rZEzmKwmdE3Xj4qP9lI,3878
35
- spice_mcp-0.1.3.dist-info/METADATA,sha256=a44b2EfhkPbQSS_PEoU1LF4Kkr2RfSD1gS6gGlEMmnQ,9343
36
- spice_mcp-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- spice_mcp-0.1.3.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
38
- spice_mcp-0.1.3.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
39
- spice_mcp-0.1.3.dist-info/RECORD,,
37
+ spice_mcp-0.1.4.dist-info/METADATA,sha256=MyGS87Cwkx0G5urib8a2JbqZVP4stYdGeVknRxzPw5A,5053
38
+ spice_mcp-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ spice_mcp-0.1.4.dist-info/entry_points.txt,sha256=4XiXX13Vy-oiUJwlcO_82OltBaxFnEnkJ-76sZGm5os,56
40
+ spice_mcp-0.1.4.dist-info/licenses/LICENSE,sha256=r0GNDnDY1RSkVQp7kEEf6MQU21OrNGJkxUHIsv6eyLk,1079
41
+ spice_mcp-0.1.4.dist-info/RECORD,,
@@ -1,198 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: spice-mcp
3
- Version: 0.1.3
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
- spice-mcp is an MCP server for [Dune](https://dune.com/) Analytics. It wraps a curated subset of the original Spice client inside a clean architecture (`core` models/ports → `adapters.dune` → service layer → FastMCP tools) and adds agent-friendly workflows for discovery. Results are Polars-first in Python and compact, token-efficient in MCP responses.
25
-
26
- [![PyPI version](https://img.shields.io/pypi/v/spice-mcp.svg)](https://pypi.org/project/spice-mcp/)
27
- <a href="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp">
28
- <img width="380" height="200" src="https://glama.ai/mcp/servers/@Evan-Kim2028/spice-mcp/badge" alt="Spice MCP server" />
29
- </a>
30
-
31
- Requirements: Python 3.13+
32
-
33
- This project uses FastMCP for typed, decorator-registered tools and resources.
34
-
35
- ## Highlights
36
- - Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
37
- - Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
38
- - Discovery utilities (find schemas/tables, describe columns)
39
- - JSONL query history + SQL artifacts (SHA-256) for reproducibility
40
- - Rich MCP surface: query info/run, discovery, health, and Dune admin (create/update/fork)
41
-
42
- ## What is Dune?
43
- [Dune](https://dune.com/) is a crypto data platform providing curated blockchain datasets and a public API to run and fetch query results. See the [Dune Docs](https://dune.com/docs) and [Dune API](https://dune.com/docs/api/) for full details.
44
-
45
- ## Quick Start
46
- - Export `DUNE_API_KEY` in your shell (the server can also load a local `.env`; set `SPICE_MCP_SKIP_DOTENV=1` to skip during tests).
47
- - Install from PyPI: `uv pip install spice-mcp`
48
- - Or install from source:
49
- - `uv sync` then `uv pip install -e .`
50
- - Start the FastMCP stdio server:
51
- - `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
52
- - or install the console script via `uv tool install .` and run `spice-mcp`.
53
-
54
- ## Cursor IDE Setup
55
-
56
- To use spice-mcp with Cursor IDE:
57
-
58
- 1. **Install the MCP Server**:
59
- ```bash
60
- # Install from PyPI (recommended)
61
- uv pip install spice-mcp
62
-
63
- # Or install from source
64
- uv sync
65
- uv pip install -e .
66
-
67
- # Or install via uv tool (creates console script)
68
- uv tool install .
69
- ```
70
-
71
- 2. **Configure Cursor**:
72
- - Open Cursor Settings → MCP Servers
73
- - Add new MCP server configuration:
74
- ```json
75
- {
76
- "name": "spice-mcp",
77
- "command": "spice-mcp",
78
- "env": {
79
- "DUNE_API_KEY": "your-dune-api-key-here"
80
- },
81
- "disabled": false
82
- }
83
- ```
84
- Alternatively, if you prefer running from source:
85
- ```json
86
- {
87
- "name": "spice-mcp",
88
- "command": "python",
89
- "args": ["-m", "spice_mcp.mcp.server"],
90
- "env": {
91
- "PYTHONPATH": "/path/to/your/spice-mcp/src",
92
- "DUNE_API_KEY": "your-dune-api-key-here"
93
- },
94
- "disabled": false
95
- }
96
- ```
97
-
98
- 3. **Restart Cursor** to load the MCP server
99
-
100
- 4. **Verify Connection**:
101
- - Open Cursor and use the command palette (Cmd/Ctrl + Shift + P)
102
- - Search for "MCP" or "spice" commands
103
- - Test with `dune_health_check` to verify the connection
104
-
105
- 5. **Available Tools in Cursor**:
106
- - `dune_query`: Run Dune queries by ID, URL, or raw SQL
107
- - `dune_find_tables`: Search schemas and list tables
108
- - `dune_describe_table`: Get column metadata
109
- - `dune_health_check`: Verify API connection
110
-
111
- **Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
112
-
113
- ## MCP Tools and Features
114
-
115
- All tools expose typed parameters, titles, and tags; failures return a consistent error envelope.
116
-
117
- - `dune_query_info` (Query Info, tags: dune, query)
118
- - Fetch saved-query metadata by ID/URL (name, parameters, tags, SQL, version).
119
-
120
- - `dune_query` (Run Dune Query, tags: dune, query)
121
- - Execute by ID/URL/raw SQL with parameters. Supports `refresh`, `max_age`, `limit/offset`, `sample_count`, `sort_by`, `columns`, and `format` = `preview|raw|metadata|poll`; accepts `timeout_seconds`.
122
-
123
- - `dune_health_check` (Health Check, tag: health)
124
- - Checks API key presence, query-history path, logging enabled; best-effort template check when configured.
125
-
126
- - `dune_find_tables` (Find Tables, tags: dune, schema)
127
- - Search schemas by keyword and/or list tables in a schema (`limit`).
128
-
129
- - `dune_describe_table` (Describe Table, tags: dune, schema)
130
- - Column metadata for `schema.table` (Dune types + Polars inferred dtypes when available).
131
-
132
- - Dune Admin tools (tags: dune, admin)
133
- - `dune_query_create(name, query_sql, description?, tags?, parameters?)`
134
- - `dune_query_update(query_id, name?, query_sql?, description?, tags?, parameters?)`
135
- - `dune_query_fork(source_query_id, name?)`
136
-
137
- ### Resources
138
- - `spice:history/tail/{n}` — tail last N lines of query history (1..1000)
139
- - `spice:artifact/{sha}` — fetch stored SQL by 64-hex SHA-256
140
- - `spice:sui/events_preview/{hours}/{limit}/{packages}` — Sui events preview (JSON)
141
- - `spice:sui/package_overview/{hours}/{timeout_seconds}/{packages}` — Sui overview (JSON)
142
-
143
- ## Resources
144
-
145
- - `spice:history/tail/{n}`
146
- - Last `n` lines from the query-history JSONL, clamped to [1, 1000]
147
-
148
- - `spice:artifact/{sha}`
149
- - Returns stored SQL for the SHA-256 (validated as 64 lowercase hex)
150
-
151
- Tests
152
- - Offline/unit tests (no network) live under `tests/offline/` and `tests/http_stubbed/`.
153
- - Live tests under `tests/live/` are skipped by default; enable with `SPICE_TEST_LIVE=1` and a valid `DUNE_API_KEY`.
154
- - Comprehensive scripted runner (tiered):
155
- - Run all tiers: `python tests/scripts/comprehensive_test_runner.py`
156
- - Select tiers: `python tests/scripts/comprehensive_test_runner.py -t 1 -t 3`
157
- - Stop on first failure: `python tests/scripts/comprehensive_test_runner.py --stop`
158
- - Optional JUnit export: `python tests/scripts/comprehensive_test_runner.py --junit tests/scripts/report.xml`
159
- - Pytest directly (offline/default): `uv run pytest -q -m "not live" --cov=src/spice_mcp --cov-report=term-missing`
160
-
161
- Core Tools (with parameters)
162
- - `dune_query`
163
- - Use: Preview/query results by ID, URL, or raw SQL (Polars preview + Dune metadata/pagination).
164
- - Params: `query` (string), `parameters?` (object), `performance?` ('medium'|'large'), `limit?` (int), `offset?` (int), `sort_by?` (string), `columns?` (string[]), `sample_count?` (int), `refresh?` (bool), `max_age?` (number), `timeout_seconds?` (number), `format?` ('preview'|'raw'|'metadata').
165
- - Output: `type`, `rowcount`, `columns`, `data_preview`, `execution_id`, `duration_ms`, `metadata?`, `next_uri?`, `next_offset?`.
166
- - `dune_find_tables`
167
- - Use: Search schemas by keyword and/or list tables for a schema.
168
- - Params: `keyword?` (string), `schema?` (string), `limit?` (int)
169
- - Output: `schemas?` (string[]), `tables?` (string[])
170
- - `dune_describe_table`
171
- - Use: Column descriptions for `schema.table` via SHOW + fallback to 1-row sample inference.
172
- - Params: `schema` (string), `table` (string)
173
- - Output: `columns` ([{ name, dune_type?, polars_dtype?, extra?, comment? }])
174
- - `sui_package_overview`
175
- - Use: Small-window overview for Sui packages (events/transactions/objects) with timeout handling.
176
- - Params: `packages` (string[]), `hours?` (int, default 72), `timeout_seconds?` (number, default 30)
177
- - Output: best-effort counts and previews; may include `*_timeout`/`*_error`
178
- - `dune_health_check`
179
- - Use: Verify API key presence and logging paths
180
- - Output: `api_key_present`, `query_history_path`, `logging_enabled`, `status`
181
-
182
- ## Docs
183
- - See [docs/index.md](docs/index.md) for full documentation:
184
- - Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
185
- - Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
186
-
187
- - Tool reference and schemas: [docs/tools.md](docs/tools.md)
188
- - Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
189
- - Architecture overview: [docs/architecture.md](docs/architecture.md)
190
- - Installation and configuration: [docs/installation.md](docs/installation.md), [docs/config.md](docs/config.md)
191
- - Development and linting: [docs/development.md](docs/development.md)
192
-
193
- Notes
194
- - Legacy Spice code now lives under `src/spice_mcp/adapters/dune` (extract, cache, urls, types).
195
- - Ports and models live in `src/spice_mcp/core`; services consume ports and are exercised by FastMCP tools.
196
- - Query history and SQL artefacts are always-on (see `src/spice_mcp/logging/query_history.py`).
197
- - To bypass dot-env loading during tests/CI, export `SPICE_MCP_SKIP_DOTENV=1`.
198
- - LazyFrames everywhere: eager `.collect()` or `pl.DataFrame` usage outside dedicated helpers is blocked by `tests/style/test_polars_lazy.py`; materialization helpers live in `src/spice_mcp/polars_utils.py`.