spice-mcp 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spice_mcp/adapters/dune/__init__.py +4 -2
- spice_mcp/adapters/dune/cache.py +2 -34
- spice_mcp/adapters/dune/extract.py +33 -631
- 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 +321 -116
- spice_mcp/mcp/tools/base.py +1 -1
- spice_mcp/mcp/tools/execute_query.py +26 -59
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/METADATA +18 -13
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/RECORD +16 -16
- 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 → spice_mcp-0.1.3.dist-info}/WHEEL +0 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {spice_mcp-0.1.2.dist-info → spice_mcp-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -80,7 +80,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
80
80
|
"additionalProperties": False,
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
def execute(
|
|
84
84
|
self,
|
|
85
85
|
*,
|
|
86
86
|
query: str,
|
|
@@ -149,74 +149,41 @@ class ExecuteQueryTool(MCPTool):
|
|
|
149
149
|
return {"type": "execution", "execution_id": execution_id, "status": "submitted"}
|
|
150
150
|
|
|
151
151
|
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(
|
|
152
|
+
meta = self.query_service.fetch_metadata(
|
|
186
153
|
query=q_use,
|
|
187
154
|
parameters=parameters,
|
|
188
|
-
refresh=refresh,
|
|
189
155
|
max_age=max_age,
|
|
190
|
-
poll=True,
|
|
191
|
-
timeout_seconds=timeout_seconds,
|
|
192
156
|
limit=limit,
|
|
193
157
|
offset=offset,
|
|
194
158
|
sample_count=sample_count,
|
|
195
159
|
sort_by=sort_by,
|
|
196
160
|
columns=columns,
|
|
197
161
|
extras=extras,
|
|
198
|
-
include_execution=True,
|
|
199
162
|
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
163
|
)
|
|
164
|
+
return {
|
|
165
|
+
"type": "metadata",
|
|
166
|
+
"metadata": meta.get("metadata"),
|
|
167
|
+
"next_uri": meta.get("next_uri"),
|
|
168
|
+
"next_offset": meta.get("next_offset"),
|
|
169
|
+
}
|
|
170
|
+
result = self.query_service.execute(
|
|
171
|
+
query=q_use,
|
|
172
|
+
parameters=parameters,
|
|
173
|
+
refresh=refresh,
|
|
174
|
+
max_age=max_age,
|
|
175
|
+
poll=True,
|
|
176
|
+
timeout_seconds=timeout_seconds,
|
|
177
|
+
limit=limit,
|
|
178
|
+
offset=offset,
|
|
179
|
+
sample_count=sample_count,
|
|
180
|
+
sort_by=sort_by,
|
|
181
|
+
columns=columns,
|
|
182
|
+
extras=extras,
|
|
183
|
+
include_execution=True,
|
|
184
|
+
performance=performance,
|
|
185
|
+
return_raw=format == "raw",
|
|
186
|
+
)
|
|
220
187
|
|
|
221
188
|
duration_ms = int((time.time() - t0) * 1000)
|
|
222
189
|
execution = result.get("execution", {})
|
|
@@ -340,7 +307,7 @@ class ExecuteQueryTool(MCPTool):
|
|
|
340
307
|
# Add debugging information for raw SQL failures
|
|
341
308
|
if q_type == "raw_sql" and "could not determine execution" in str(exc):
|
|
342
309
|
context.update({
|
|
343
|
-
"debug_info": "Raw SQL execution failed -
|
|
310
|
+
"debug_info": "Raw SQL execution failed - check template query configuration and API key",
|
|
344
311
|
"template_query_id": template_id_value,
|
|
345
312
|
"environment_vars": {
|
|
346
313
|
"SPICE_RAW_SQL_QUERY_ID": os.getenv("SPICE_RAW_SQL_QUERY_ID"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spice-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: MCP server for Dune Analytics data access
|
|
5
5
|
Author-email: Evan-Kim2028 <ekcopersonal@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -21,7 +21,12 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
# spice-mcp
|
|
23
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
|
|
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
|
+
[](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>
|
|
25
30
|
|
|
26
31
|
Requirements: Python 3.13+
|
|
27
32
|
|
|
@@ -31,16 +36,17 @@ This project uses FastMCP for typed, decorator-registered tools and resources.
|
|
|
31
36
|
- Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
|
|
32
37
|
- Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
|
|
33
38
|
- Discovery utilities (find schemas/tables, describe columns)
|
|
34
|
-
- Sui package workflows (events/transactions/objects) with safe defaults
|
|
35
39
|
- JSONL query history + SQL artifacts (SHA-256) for reproducibility
|
|
36
|
-
- Rich MCP surface: query info/run, discovery, health,
|
|
40
|
+
- Rich MCP surface: query info/run, discovery, health, and Dune admin (create/update/fork)
|
|
37
41
|
|
|
38
42
|
## What is Dune?
|
|
39
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.
|
|
40
44
|
|
|
41
45
|
## Quick Start
|
|
42
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).
|
|
43
|
-
- Install
|
|
47
|
+
- Install from PyPI: `uv pip install spice-mcp`
|
|
48
|
+
- Or install from source:
|
|
49
|
+
- `uv sync` then `uv pip install -e .`
|
|
44
50
|
- Start the FastMCP stdio server:
|
|
45
51
|
- `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
|
|
46
52
|
- or install the console script via `uv tool install .` and run `spice-mcp`.
|
|
@@ -51,9 +57,12 @@ To use spice-mcp with Cursor IDE:
|
|
|
51
57
|
|
|
52
58
|
1. **Install the MCP Server**:
|
|
53
59
|
```bash
|
|
54
|
-
# Install
|
|
60
|
+
# Install from PyPI (recommended)
|
|
61
|
+
uv pip install spice-mcp
|
|
62
|
+
|
|
63
|
+
# Or install from source
|
|
55
64
|
uv sync
|
|
56
|
-
pip install -e .
|
|
65
|
+
uv pip install -e .
|
|
57
66
|
|
|
58
67
|
# Or install via uv tool (creates console script)
|
|
59
68
|
uv tool install .
|
|
@@ -97,7 +106,6 @@ To use spice-mcp with Cursor IDE:
|
|
|
97
106
|
- `dune_query`: Run Dune queries by ID, URL, or raw SQL
|
|
98
107
|
- `dune_find_tables`: Search schemas and list tables
|
|
99
108
|
- `dune_describe_table`: Get column metadata
|
|
100
|
-
- `sui_package_overview`: Analyze Sui packages
|
|
101
109
|
- `dune_health_check`: Verify API connection
|
|
102
110
|
|
|
103
111
|
**Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
|
|
@@ -121,9 +129,6 @@ All tools expose typed parameters, titles, and tags; failures return a consisten
|
|
|
121
129
|
- `dune_describe_table` (Describe Table, tags: dune, schema)
|
|
122
130
|
- Column metadata for `schema.table` (Dune types + Polars inferred dtypes when available).
|
|
123
131
|
|
|
124
|
-
- `sui_package_overview` (Sui Package Overview, tag: sui)
|
|
125
|
-
- Compact Sui activity overview for `packages[]` with `hours` (default 72) and `timeout_seconds`.
|
|
126
|
-
|
|
127
132
|
- Dune Admin tools (tags: dune, admin)
|
|
128
133
|
- `dune_query_create(name, query_sql, description?, tags?, parameters?)`
|
|
129
134
|
- `dune_query_update(query_id, name?, query_sql?, description?, tags?, parameters?)`
|
|
@@ -178,7 +183,7 @@ Core Tools (with parameters)
|
|
|
178
183
|
- See [docs/index.md](docs/index.md) for full documentation:
|
|
179
184
|
- Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
|
|
180
185
|
- Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
|
|
181
|
-
|
|
186
|
+
|
|
182
187
|
- Tool reference and schemas: [docs/tools.md](docs/tools.md)
|
|
183
188
|
- Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
|
|
184
189
|
- Architecture overview: [docs/architecture.md](docs/architecture.md)
|
|
@@ -190,4 +195,4 @@ Notes
|
|
|
190
195
|
- Ports and models live in `src/spice_mcp/core`; services consume ports and are exercised by FastMCP tools.
|
|
191
196
|
- Query history and SQL artefacts are always-on (see `src/spice_mcp/logging/query_history.py`).
|
|
192
197
|
- To bypass dot-env loading during tests/CI, export `SPICE_MCP_SKIP_DOTENV=1`.
|
|
193
|
-
- 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`.
|
|
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`.
|
|
@@ -1,39 +1,39 @@
|
|
|
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=
|
|
9
|
+
spice_mcp/adapters/dune/cache.py,sha256=7ykT58WN1yHGIN2uV3t7fWOqGb1VJdCvf3I-xZwsv74,4304
|
|
10
10
|
spice_mcp/adapters/dune/client.py,sha256=JPrQZ_dtaPcGf6lYUguDxGweOtxG-8qMqOiXuhWL9QA,9122
|
|
11
|
-
spice_mcp/adapters/dune/extract.py,sha256=
|
|
11
|
+
spice_mcp/adapters/dune/extract.py,sha256=3jrXqlak4Kpl11y7C3L1AhFv09lQ-2pBqmrh-0Mwlm0,28836
|
|
12
12
|
spice_mcp/adapters/dune/helpers.py,sha256=BgDKr_g-UqmU2hoMb0ejQZHta_NbKwR1eDJp33sJYNk,227
|
|
13
13
|
spice_mcp/adapters/dune/transport.py,sha256=eRP-jPY2ZXxvTX9HSjIFqFUlbIzXspgH95jBFoTlpaQ,1436
|
|
14
14
|
spice_mcp/adapters/dune/types.py,sha256=57TMX07u-Gq4BYwRAuZV0xI81nVXgtpp7KBID9YbKyQ,1195
|
|
15
15
|
spice_mcp/adapters/dune/typing_utils.py,sha256=EpWneGDn-eQdo6lkLuESR09KXkDj9OqGz8bEF3JaFkM,574
|
|
16
16
|
spice_mcp/adapters/dune/urls.py,sha256=bcuPERkFQduRTT2BrgzVhoFrMn-Lkvw9NmktcBZYEig,3902
|
|
17
|
+
spice_mcp/adapters/spellbook/__init__.py,sha256=D2cdVtSUbmAJdbPRvAyKxYS4-wUQ3unXyX4ZFYxenuk,150
|
|
18
|
+
spice_mcp/adapters/spellbook/explorer.py,sha256=Q3UfEGlALizCDeeW_ZZVRRedzwMXiknmxwSkeOnxxgc,11515
|
|
17
19
|
spice_mcp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
20
|
spice_mcp/core/errors.py,sha256=jlfTuyRaAaA_oU07KUk-1pDAAa43KG0BbZc5CINXtoE,3256
|
|
19
|
-
spice_mcp/core/models.py,sha256=
|
|
20
|
-
spice_mcp/core/ports.py,sha256
|
|
21
|
+
spice_mcp/core/models.py,sha256=i0C_-UE16OWyyZo_liooEJeYvbChE5lpK80aN2OF4lk,1795
|
|
22
|
+
spice_mcp/core/ports.py,sha256=nEdeA3UH7v0kB_hbguMrpDljb9EhSxUAO0SdhjpoijQ,1618
|
|
21
23
|
spice_mcp/logging/query_history.py,sha256=doE9lod64uzJxlA2XzHH2-VAmC6WstYAkQ0taEAxiIM,4315
|
|
22
24
|
spice_mcp/mcp/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
23
|
-
spice_mcp/mcp/server.py,sha256=
|
|
25
|
+
spice_mcp/mcp/server.py,sha256=NNTjn5f_OLNd8zSnNsx73WhHzNGW_SBJoCrdzkgOGP4,26275
|
|
24
26
|
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
|
|
27
|
+
spice_mcp/mcp/tools/base.py,sha256=zJkVxLgXR48iZcJeng8cZ2rXvbyicagoGlMN7BK7Img,1041
|
|
28
|
+
spice_mcp/mcp/tools/execute_query.py,sha256=CucjxoBT22VUS-QJV1JrDjoywu2nd13fL3Lfs1qytUg,16093
|
|
28
29
|
spice_mcp/observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
30
|
spice_mcp/observability/logging.py,sha256=ceJUEpKGpf5PAgPBmpB49zjqhdGCAESfLemFUhDSmI8,529
|
|
30
31
|
spice_mcp/service_layer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
32
|
spice_mcp/service_layer/discovery_service.py,sha256=202O0SzCZGQukd9kb2JYfarLygZHgiXlHqp_nTAdrWA,730
|
|
32
33
|
spice_mcp/service_layer/query_admin_service.py,sha256=4q1NAAuTui7cm83Aq2rFDLIzKTHX17yzbSoSJyCmLbI,1356
|
|
33
34
|
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,,
|
|
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,,
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|