spice-mcp 0.1.2__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.
- spice_mcp/adapters/dune/__init__.py +4 -2
- spice_mcp/adapters/dune/cache.py +2 -34
- spice_mcp/adapters/dune/client.py +9 -4
- spice_mcp/adapters/dune/extract.py +79 -633
- spice_mcp/adapters/dune/query_wrapper.py +86 -0
- spice_mcp/adapters/dune/user_agent.py +9 -0
- spice_mcp/adapters/spellbook/__init__.py +6 -0
- spice_mcp/adapters/spellbook/explorer.py +313 -0
- spice_mcp/config.py +1 -1
- spice_mcp/core/models.py +0 -8
- spice_mcp/core/ports.py +0 -15
- spice_mcp/mcp/server.py +407 -137
- spice_mcp/mcp/tools/base.py +1 -1
- spice_mcp/mcp/tools/execute_query.py +32 -63
- spice_mcp-0.1.4.dist-info/METADATA +121 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.4.dist-info}/RECORD +19 -17
- spice_mcp/mcp/tools/sui_package_overview.py +0 -56
- spice_mcp/service_layer/sui_service.py +0 -131
- spice_mcp-0.1.2.dist-info/METADATA +0 -193
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.4.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.4.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.4.dist-info}/licenses/LICENSE +0 -0
spice_mcp/mcp/tools/base.py
CHANGED
|
@@ -20,7 +20,7 @@ class MCPTool(ABC):
|
|
|
20
20
|
raise NotImplementedError
|
|
21
21
|
|
|
22
22
|
@abstractmethod
|
|
23
|
-
|
|
23
|
+
def execute(self, **kwargs) -> dict[str, Any]:
|
|
24
24
|
"""Execute tool logic and return result dictionary."""
|
|
25
25
|
raise NotImplementedError
|
|
26
26
|
|
|
@@ -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
|
|
@@ -80,7 +82,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
80
82
|
"additionalProperties": False,
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
def execute(
|
|
84
86
|
self,
|
|
85
87
|
*,
|
|
86
88
|
query: str,
|
|
@@ -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,
|
|
@@ -149,74 +151,41 @@ class ExecuteQueryTool(MCPTool):
|
|
|
149
151
|
return {"type": "execution", "execution_id": execution_id, "status": "submitted"}
|
|
150
152
|
|
|
151
153
|
if format == "metadata":
|
|
152
|
-
|
|
153
|
-
meta = self.query_service.fetch_metadata(
|
|
154
|
-
query=q_use,
|
|
155
|
-
parameters=parameters,
|
|
156
|
-
max_age=max_age,
|
|
157
|
-
limit=limit,
|
|
158
|
-
offset=offset,
|
|
159
|
-
sample_count=sample_count,
|
|
160
|
-
sort_by=sort_by,
|
|
161
|
-
columns=columns,
|
|
162
|
-
extras=extras,
|
|
163
|
-
performance=performance,
|
|
164
|
-
)
|
|
165
|
-
except TypeError:
|
|
166
|
-
# Back-compat: older services without 'extras'
|
|
167
|
-
meta = self.query_service.fetch_metadata(
|
|
168
|
-
query=q_use,
|
|
169
|
-
parameters=parameters,
|
|
170
|
-
max_age=max_age,
|
|
171
|
-
limit=limit,
|
|
172
|
-
offset=offset,
|
|
173
|
-
sample_count=sample_count,
|
|
174
|
-
sort_by=sort_by,
|
|
175
|
-
columns=columns,
|
|
176
|
-
performance=performance,
|
|
177
|
-
)
|
|
178
|
-
return {
|
|
179
|
-
"type": "metadata",
|
|
180
|
-
"metadata": meta.get("metadata"),
|
|
181
|
-
"next_uri": meta.get("next_uri"),
|
|
182
|
-
"next_offset": meta.get("next_offset"),
|
|
183
|
-
}
|
|
184
|
-
try:
|
|
185
|
-
result = self.query_service.execute(
|
|
154
|
+
meta = self.query_service.fetch_metadata(
|
|
186
155
|
query=q_use,
|
|
187
156
|
parameters=parameters,
|
|
188
|
-
refresh=refresh,
|
|
189
157
|
max_age=max_age,
|
|
190
|
-
poll=True,
|
|
191
|
-
timeout_seconds=timeout_seconds,
|
|
192
158
|
limit=limit,
|
|
193
159
|
offset=offset,
|
|
194
160
|
sample_count=sample_count,
|
|
195
161
|
sort_by=sort_by,
|
|
196
162
|
columns=columns,
|
|
197
163
|
extras=extras,
|
|
198
|
-
include_execution=True,
|
|
199
164
|
performance=performance,
|
|
200
|
-
return_raw=format == "raw",
|
|
201
|
-
)
|
|
202
|
-
except TypeError:
|
|
203
|
-
# Back-compat: older services without 'extras'
|
|
204
|
-
result = self.query_service.execute(
|
|
205
|
-
query=q_use,
|
|
206
|
-
parameters=parameters,
|
|
207
|
-
refresh=refresh,
|
|
208
|
-
max_age=max_age,
|
|
209
|
-
poll=True,
|
|
210
|
-
timeout_seconds=timeout_seconds,
|
|
211
|
-
limit=limit,
|
|
212
|
-
offset=offset,
|
|
213
|
-
sample_count=sample_count,
|
|
214
|
-
sort_by=sort_by,
|
|
215
|
-
columns=columns,
|
|
216
|
-
include_execution=True,
|
|
217
|
-
performance=performance,
|
|
218
|
-
return_raw=format == "raw",
|
|
219
165
|
)
|
|
166
|
+
return {
|
|
167
|
+
"type": "metadata",
|
|
168
|
+
"metadata": meta.get("metadata"),
|
|
169
|
+
"next_uri": meta.get("next_uri"),
|
|
170
|
+
"next_offset": meta.get("next_offset"),
|
|
171
|
+
}
|
|
172
|
+
result = self.query_service.execute(
|
|
173
|
+
query=q_use,
|
|
174
|
+
parameters=parameters,
|
|
175
|
+
refresh=refresh,
|
|
176
|
+
max_age=max_age,
|
|
177
|
+
poll=True,
|
|
178
|
+
timeout_seconds=timeout_seconds,
|
|
179
|
+
limit=limit,
|
|
180
|
+
offset=offset,
|
|
181
|
+
sample_count=sample_count,
|
|
182
|
+
sort_by=sort_by,
|
|
183
|
+
columns=columns,
|
|
184
|
+
extras=extras,
|
|
185
|
+
include_execution=True,
|
|
186
|
+
performance=performance,
|
|
187
|
+
return_raw=format == "raw",
|
|
188
|
+
)
|
|
220
189
|
|
|
221
190
|
duration_ms = int((time.time() - t0) * 1000)
|
|
222
191
|
execution = result.get("execution", {})
|
|
@@ -340,7 +309,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
340
309
|
# Add debugging information for raw SQL failures
|
|
341
310
|
if q_type == "raw_sql" and "could not determine execution" in str(exc):
|
|
342
311
|
context.update({
|
|
343
|
-
"debug_info": "Raw SQL execution failed -
|
|
312
|
+
"debug_info": "Raw SQL execution failed - check template query configuration and API key",
|
|
344
313
|
"template_query_id": template_id_value,
|
|
345
314
|
"environment_vars": {
|
|
346
315
|
"SPICE_RAW_SQL_QUERY_ID": os.getenv("SPICE_RAW_SQL_QUERY_ID"),
|
|
@@ -366,7 +335,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
366
335
|
query_id = dune_urls.get_query_id(query)
|
|
367
336
|
headers = {
|
|
368
337
|
"X-Dune-API-Key": dune_urls.get_api_key(),
|
|
369
|
-
"User-Agent":
|
|
338
|
+
"User-Agent": get_dune_user_agent(),
|
|
370
339
|
}
|
|
371
340
|
resp = self._http.request(
|
|
372
341
|
"GET",
|
|
@@ -395,7 +364,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
395
364
|
url = dune_urls.get_execution_status_url(execution_id)
|
|
396
365
|
headers = {
|
|
397
366
|
"X-Dune-API-Key": dune_urls.get_api_key(),
|
|
398
|
-
"User-Agent":
|
|
367
|
+
"User-Agent": get_dune_user_agent(),
|
|
399
368
|
}
|
|
400
369
|
resp = self._http.request("GET", url, headers=headers, timeout=10.0)
|
|
401
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
|
+
[](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.
|
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
spice_mcp/__init__.py,sha256=SpDcj8yU1fpT2YZCqal1R3KLZpBty3w--6bkmrmWU6o,55
|
|
2
|
-
spice_mcp/config.py,sha256=
|
|
2
|
+
spice_mcp/config.py,sha256=QX-c3veCFEoKjXIHsEX1_Kt4kjrd7tmrdTiOLFS5pSw,2766
|
|
3
3
|
spice_mcp/polars_utils.py,sha256=hCw3M_iZhTFdBnfmcx2SN8J8jpoucfJnpX_ZrXz6JwY,442
|
|
4
4
|
spice_mcp/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
5
|
spice_mcp/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
spice_mcp/adapters/http_client.py,sha256=CYgSKAsx-5c-uxaNIBCBTgQdaoBe5J3dJvnw8iqda34,5033
|
|
7
|
-
spice_mcp/adapters/dune/__init__.py,sha256=
|
|
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
|
-
spice_mcp/adapters/dune/cache.py,sha256=
|
|
10
|
-
spice_mcp/adapters/dune/client.py,sha256=
|
|
11
|
-
spice_mcp/adapters/dune/extract.py,sha256=
|
|
9
|
+
spice_mcp/adapters/dune/cache.py,sha256=7ykT58WN1yHGIN2uV3t7fWOqGb1VJdCvf3I-xZwsv74,4304
|
|
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
|
|
19
|
+
spice_mcp/adapters/spellbook/__init__.py,sha256=D2cdVtSUbmAJdbPRvAyKxYS4-wUQ3unXyX4ZFYxenuk,150
|
|
20
|
+
spice_mcp/adapters/spellbook/explorer.py,sha256=Q3UfEGlALizCDeeW_ZZVRRedzwMXiknmxwSkeOnxxgc,11515
|
|
17
21
|
spice_mcp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
22
|
spice_mcp/core/errors.py,sha256=jlfTuyRaAaA_oU07KUk-1pDAAa43KG0BbZc5CINXtoE,3256
|
|
19
|
-
spice_mcp/core/models.py,sha256=
|
|
20
|
-
spice_mcp/core/ports.py,sha256
|
|
23
|
+
spice_mcp/core/models.py,sha256=i0C_-UE16OWyyZo_liooEJeYvbChE5lpK80aN2OF4lk,1795
|
|
24
|
+
spice_mcp/core/ports.py,sha256=nEdeA3UH7v0kB_hbguMrpDljb9EhSxUAO0SdhjpoijQ,1618
|
|
21
25
|
spice_mcp/logging/query_history.py,sha256=doE9lod64uzJxlA2XzHH2-VAmC6WstYAkQ0taEAxiIM,4315
|
|
22
26
|
spice_mcp/mcp/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
23
|
-
spice_mcp/mcp/server.py,sha256=
|
|
27
|
+
spice_mcp/mcp/server.py,sha256=lHYEI76oQpPcpPT9pEoGXpUmAjDFXlOXuUrlOWV8s2c,28729
|
|
24
28
|
spice_mcp/mcp/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
25
|
-
spice_mcp/mcp/tools/base.py,sha256=
|
|
26
|
-
spice_mcp/mcp/tools/execute_query.py,sha256=
|
|
27
|
-
spice_mcp/mcp/tools/sui_package_overview.py,sha256=a1CcpfBzEFzScjXgxLu9urxdtSbXjF98ugWPOX167rc,1736
|
|
29
|
+
spice_mcp/mcp/tools/base.py,sha256=zJkVxLgXR48iZcJeng8cZ2rXvbyicagoGlMN7BK7Img,1041
|
|
30
|
+
spice_mcp/mcp/tools/execute_query.py,sha256=CJtoNKpRY6pCkyqjwFy_cTYqTIg9-EsZA8GrH-2MFjk,16262
|
|
28
31
|
spice_mcp/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
32
|
spice_mcp/observability/logging.py,sha256=ceJUEpKGpf5PAgPBmpB49zjqhdGCAESfLemFUhDSmI8,529
|
|
30
33
|
spice_mcp/service_layer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
34
|
spice_mcp/service_layer/discovery_service.py,sha256=202O0SzCZGQukd9kb2JYfarLygZHgiXlHqp_nTAdrWA,730
|
|
32
35
|
spice_mcp/service_layer/query_admin_service.py,sha256=4q1NAAuTui7cm83Aq2rFDLIzKTHX17yzbSoSJyCmLbI,1356
|
|
33
36
|
spice_mcp/service_layer/query_service.py,sha256=q0eAVW5I3sUxm29DgzPN_cH3rZEzmKwmdE3Xj4qP9lI,3878
|
|
34
|
-
spice_mcp/
|
|
35
|
-
spice_mcp-0.1.
|
|
36
|
-
spice_mcp-0.1.
|
|
37
|
-
spice_mcp-0.1.
|
|
38
|
-
spice_mcp-0.1.
|
|
39
|
-
spice_mcp-0.1.2.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,56 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from ...core.errors import error_response
|
|
6
|
-
from ...service_layer.sui_service import SuiService
|
|
7
|
-
from .base import MCPTool
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SuiPackageOverviewTool(MCPTool):
|
|
11
|
-
"""Overview of Sui packages: events, transactions, objects (small preview)."""
|
|
12
|
-
|
|
13
|
-
def __init__(self, sui_service: SuiService):
|
|
14
|
-
self.sui_service = sui_service
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def name(self) -> str:
|
|
18
|
-
return "sui_package_overview"
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def description(self) -> str:
|
|
22
|
-
return (
|
|
23
|
-
"Return a compact overview (counts + previews) of Sui package activity "
|
|
24
|
-
"over a time window, with polling timeouts."
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
def get_parameter_schema(self) -> dict[str, Any]:
|
|
28
|
-
return {
|
|
29
|
-
"type": "object",
|
|
30
|
-
"properties": {
|
|
31
|
-
"packages": {"type": "array", "items": {"type": "string"}},
|
|
32
|
-
"hours": {"type": "integer", "default": 72},
|
|
33
|
-
"timeout_seconds": {"type": "number", "default": 30},
|
|
34
|
-
},
|
|
35
|
-
"required": ["packages"],
|
|
36
|
-
"additionalProperties": False,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async def execute(
|
|
40
|
-
self, *, packages: list[str], hours: int = 72, timeout_seconds: float | None = 30
|
|
41
|
-
) -> dict[str, Any]:
|
|
42
|
-
try:
|
|
43
|
-
return self.sui_service.package_overview(
|
|
44
|
-
packages,
|
|
45
|
-
hours=hours,
|
|
46
|
-
timeout_seconds=timeout_seconds,
|
|
47
|
-
)
|
|
48
|
-
except Exception as exc:
|
|
49
|
-
return error_response(
|
|
50
|
-
exc,
|
|
51
|
-
context={
|
|
52
|
-
"tool": "sui_package_overview",
|
|
53
|
-
"packages": packages,
|
|
54
|
-
"hours": hours,
|
|
55
|
-
},
|
|
56
|
-
)
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from .query_service import QueryService
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SuiService:
|
|
9
|
-
"""Opinionated helpers for Sui package exploration."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, query_service: QueryService):
|
|
12
|
-
self.query_service = query_service
|
|
13
|
-
|
|
14
|
-
def events_preview(
|
|
15
|
-
self, packages: list[str], *, hours: int = 72, limit: int = 50, performance: str = "large"
|
|
16
|
-
) -> dict[str, object]:
|
|
17
|
-
now_ms = int(time.time() * 1000)
|
|
18
|
-
start_ms = now_ms - hours * 3600 * 1000
|
|
19
|
-
|
|
20
|
-
norms = _normalize_packages(packages)
|
|
21
|
-
if norms:
|
|
22
|
-
preds = [f"lower(event_type) LIKE '{pkg}::%'" for pkg in norms]
|
|
23
|
-
where = " OR ".join(preds)
|
|
24
|
-
sql = (
|
|
25
|
-
"SELECT timestamp_ms, package, module, event_type FROM sui.events "
|
|
26
|
-
f"WHERE ({where}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms} "
|
|
27
|
-
"ORDER BY timestamp_ms DESC "
|
|
28
|
-
f"LIMIT {limit}"
|
|
29
|
-
)
|
|
30
|
-
else:
|
|
31
|
-
sql = (
|
|
32
|
-
"SELECT timestamp_ms, package, module, event_type FROM sui.events "
|
|
33
|
-
f"WHERE timestamp_ms BETWEEN {start_ms} AND {now_ms} "
|
|
34
|
-
"ORDER BY timestamp_ms DESC LIMIT {limit}"
|
|
35
|
-
).format(limit=limit)
|
|
36
|
-
|
|
37
|
-
result = self.query_service.execute(
|
|
38
|
-
sql,
|
|
39
|
-
performance=performance,
|
|
40
|
-
timeout_seconds=60,
|
|
41
|
-
limit=limit,
|
|
42
|
-
)
|
|
43
|
-
return {
|
|
44
|
-
"rowcount": result["rowcount"],
|
|
45
|
-
"columns": result["columns"],
|
|
46
|
-
"data_preview": result["data_preview"],
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
def package_overview(
|
|
50
|
-
self,
|
|
51
|
-
packages: list[str],
|
|
52
|
-
*,
|
|
53
|
-
hours: int = 72,
|
|
54
|
-
timeout_seconds: float | None = 30,
|
|
55
|
-
performance: str = "large",
|
|
56
|
-
) -> dict[str, object]:
|
|
57
|
-
now_ms = int(time.time() * 1000)
|
|
58
|
-
start_ms = now_ms - hours * 3600 * 1000
|
|
59
|
-
norms = _normalize_packages(packages)
|
|
60
|
-
in_clause = ",".join(f"'{pkg}'" for pkg in norms)
|
|
61
|
-
|
|
62
|
-
out: dict[str, object] = {"ok": True}
|
|
63
|
-
|
|
64
|
-
def _run(sql: str, limit: int) -> dict[str, object]:
|
|
65
|
-
return self.query_service.execute(
|
|
66
|
-
sql,
|
|
67
|
-
performance=performance,
|
|
68
|
-
timeout_seconds=timeout_seconds,
|
|
69
|
-
limit=limit,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Events
|
|
73
|
-
try:
|
|
74
|
-
sql_ev = (
|
|
75
|
-
"SELECT timestamp_ms, package, module, event_type, event_json FROM sui.events "
|
|
76
|
-
f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms} "
|
|
77
|
-
"ORDER BY timestamp_ms DESC LIMIT 200"
|
|
78
|
-
)
|
|
79
|
-
ev = _run(sql_ev, 200)
|
|
80
|
-
out["events_preview"] = ev.get("data_preview")
|
|
81
|
-
out["events_count"] = ev.get("rowcount")
|
|
82
|
-
except TimeoutError:
|
|
83
|
-
out["events_timeout"] = True
|
|
84
|
-
except Exception as exc:
|
|
85
|
-
out["events_error"] = str(exc)
|
|
86
|
-
|
|
87
|
-
# Transactions
|
|
88
|
-
try:
|
|
89
|
-
sql_tx = (
|
|
90
|
-
"WITH txs AS (SELECT DISTINCT transaction_digest FROM sui.events "
|
|
91
|
-
f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms}) "
|
|
92
|
-
"SELECT t.* FROM sui.transactions t "
|
|
93
|
-
"JOIN txs ON t.transaction_digest = txs.transaction_digest "
|
|
94
|
-
"ORDER BY t.timestamp_ms DESC LIMIT 200"
|
|
95
|
-
)
|
|
96
|
-
tx = _run(sql_tx, 200)
|
|
97
|
-
out["transactions_preview"] = tx.get("data_preview")
|
|
98
|
-
out["transactions_count"] = tx.get("rowcount")
|
|
99
|
-
except TimeoutError:
|
|
100
|
-
out["transactions_timeout"] = True
|
|
101
|
-
except Exception as exc:
|
|
102
|
-
out["transactions_error"] = str(exc)
|
|
103
|
-
|
|
104
|
-
# Objects
|
|
105
|
-
try:
|
|
106
|
-
sql_ob = (
|
|
107
|
-
"WITH txs AS (SELECT DISTINCT transaction_digest FROM sui.events "
|
|
108
|
-
f"WHERE lower(package) IN ({in_clause}) AND timestamp_ms BETWEEN {start_ms} AND {now_ms}) "
|
|
109
|
-
"SELECT o.* FROM sui.objects o "
|
|
110
|
-
"WHERE o.previous_transaction IN (SELECT transaction_digest FROM txs) "
|
|
111
|
-
"ORDER BY o.timestamp_ms DESC LIMIT 200"
|
|
112
|
-
)
|
|
113
|
-
ob = _run(sql_ob, 200)
|
|
114
|
-
out["objects_preview"] = ob.get("data_preview")
|
|
115
|
-
out["objects_count"] = ob.get("rowcount")
|
|
116
|
-
except TimeoutError:
|
|
117
|
-
out["objects_timeout"] = True
|
|
118
|
-
except Exception as exc:
|
|
119
|
-
out["objects_error"] = str(exc)
|
|
120
|
-
|
|
121
|
-
return out
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _normalize_packages(packages: list[str]) -> list[str]:
|
|
125
|
-
norms: list[str] = []
|
|
126
|
-
for p in packages:
|
|
127
|
-
value = p.lower()
|
|
128
|
-
if not value.startswith("0x"):
|
|
129
|
-
value = "0x" + value
|
|
130
|
-
norms.append(value)
|
|
131
|
-
return norms
|