spice-mcp 0.1.0__tar.gz → 0.1.1__tar.gz

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

Potentially problematic release.


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

Files changed (112) hide show
  1. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.github/workflows/ci.yml +6 -6
  2. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/PKG-INFO +75 -15
  3. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/README.md +74 -14
  4. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/pyproject.toml +18 -1
  5. spice_mcp-0.1.1/scripts/bridgez/make_circle_comparison.py +114 -0
  6. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/extract.py +12 -7
  7. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/typing_utils.py +3 -1
  8. spice_mcp-0.1.1/tests/offline/test_typing_utils.py +62 -0
  9. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/uv.lock +408 -2
  10. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/agentic-documentation-systems-droid.md +0 -0
  11. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/development-workflow-optimization-droid.md +0 -0
  12. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/production-python-development-droid.md +0 -0
  13. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.factory/droids/temp-smoke-droid.md +0 -0
  14. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.gitignore +0 -0
  15. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/.python-version +0 -0
  16. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/CONTRIBUTING.md +0 -0
  17. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/LICENSE +0 -0
  18. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/bin/bat +0 -0
  19. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/bin/fd +0 -0
  20. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/debug_api.py +0 -0
  21. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/architecture.md +0 -0
  22. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/codex_cli.md +0 -0
  23. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/codex_cli_tools.md +0 -0
  24. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/commands.md +0 -0
  25. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/config.md +0 -0
  26. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/development.md +0 -0
  27. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/discovery.md +0 -0
  28. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/dune_api.md +0 -0
  29. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/index.md +0 -0
  30. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/installation.md +0 -0
  31. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/docs/tools.md +0 -0
  32. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/pytest.ini +0 -0
  33. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/scripts/codex_tools_doctor.sh +0 -0
  34. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/__init__.py +0 -0
  35. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/__init__.py +0 -0
  36. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/__init__.py +0 -0
  37. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/admin.py +0 -0
  38. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/cache.py +0 -0
  39. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/client.py +0 -0
  40. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/helpers.py +0 -0
  41. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/transport.py +0 -0
  42. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/types.py +0 -0
  43. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/dune/urls.py +0 -0
  44. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/adapters/http_client.py +0 -0
  45. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/config.py +0 -0
  46. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/__init__.py +0 -0
  47. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/errors.py +0 -0
  48. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/models.py +0 -0
  49. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/core/ports.py +0 -0
  50. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/logging/query_history.py +0 -0
  51. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/__init__.py +0 -0
  52. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/server.py +0 -0
  53. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/__init__.py +0 -0
  54. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/base.py +0 -0
  55. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/execute_query.py +0 -0
  56. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/mcp/tools/sui_package_overview.py +0 -0
  57. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/observability/__init__.py +0 -0
  58. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/observability/logging.py +0 -0
  59. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/polars_utils.py +0 -0
  60. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/py.typed +0 -0
  61. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/__init__.py +0 -0
  62. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/discovery_service.py +0 -0
  63. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/query_admin_service.py +0 -0
  64. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/query_service.py +0 -0
  65. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/src/spice_mcp/service_layer/sui_service.py +0 -0
  66. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/cassettes/.gitkeep +0 -0
  67. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/config/environments.yaml +0 -0
  68. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/config/test_queries.yaml +0 -0
  69. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/conftest.py +0 -0
  70. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_resources_and_validation.py +0 -0
  71. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_server_fastmcp.py +0 -0
  72. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/fastmcp/test_server_mcp_extras.py +0 -0
  73. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_age.py +0 -0
  74. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_errors.py +0 -0
  75. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/http_stubbed/test_pagination.py +0 -0
  76. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/live/test_live_basic.py +0 -0
  77. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/live/test_live_sui.py +0 -0
  78. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/conftest.py +0 -0
  79. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/test_resources.py +0 -0
  80. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/mcp/test_tool_contracts.py +0 -0
  81. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_cache.py +0 -0
  82. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_discovery.py +0 -0
  83. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_dune_adapter.py +0 -0
  84. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_parsing.py +0 -0
  85. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_query_history.py +0 -0
  86. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_show_rewrite.py +0 -0
  87. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_timeout.py +0 -0
  88. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/offline/test_urls.py +0 -0
  89. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/comprehensive_test_runner.py +0 -0
  90. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/run_tests.py +0 -0
  91. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_api_health.py +0 -0
  92. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_cache_functionality.py +0 -0
  93. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_data_types.py +0 -0
  94. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_dune_connectivity.py +0 -0
  95. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_dune_query_execution.py +0 -0
  96. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_error_handling.py +0 -0
  97. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_mcp_simulation.py +0 -0
  98. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_mcp_tools.py +0 -0
  99. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_performance.py +0 -0
  100. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_query_lifecycle.py +0 -0
  101. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_resilience.py +0 -0
  102. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/scripts/test_resource_management.py +0 -0
  103. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/style/test_polars_lazy.py +0 -0
  104. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/api_client.py +0 -0
  105. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/helpers.py +0 -0
  106. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/query_factory.py +0 -0
  107. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/support/test_data.py +0 -0
  108. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_additional_mcp_tools.py +0 -0
  109. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_execute_query_tool.py +0 -0
  110. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_health_tool.py +0 -0
  111. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_query_service.py +0 -0
  112. {spice_mcp-0.1.0 → spice_mcp-0.1.1}/tests/tools/test_schemas.py +0 -0
@@ -1,8 +1,8 @@
1
- name: CI
1
+ name: CI (disabled by default)
2
2
 
3
3
  on:
4
- push:
5
- pull_request:
4
+ # Disable automatic triggers; allow manual runs only
5
+ workflow_dispatch:
6
6
 
7
7
  permissions:
8
8
  contents: read
@@ -48,7 +48,7 @@ jobs:
48
48
  - name: Install dependencies (uv)
49
49
  run: |
50
50
  uv --version
51
- uv sync --frozen --dev
51
+ uv sync --frozen --group dev
52
52
 
53
53
  - name: Ruff lint
54
54
  run: uv run ruff check --output-format=github --color always .
@@ -93,7 +93,7 @@ jobs:
93
93
  - name: Install dependencies (uv)
94
94
  run: |
95
95
  uv --version
96
- uv sync --frozen --dev
96
+ uv sync --frozen --group dev
97
97
 
98
98
  - name: mypy
99
99
  env:
@@ -132,7 +132,7 @@ jobs:
132
132
  - name: Install dependencies (uv)
133
133
  run: |
134
134
  uv --version
135
- uv sync --frozen --dev
135
+ uv sync --frozen --group dev
136
136
 
137
137
  - name: Run tests (skip live) with coverage
138
138
  env:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spice-mcp
3
- Version: 0.1.0
3
+ Version: 0.1.1
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,27 +21,87 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  # spice-mcp
23
23
 
24
- spice-mcp is an MCP server for Dune 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 and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
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 and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
25
25
 
26
26
  Requirements: Python 3.13+
27
27
 
28
28
  This project uses FastMCP for typed, decorator-registered tools and resources.
29
29
 
30
- Highlights
30
+ ## Highlights
31
31
  - Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
32
- - Ports/adapters layering for maintainable integrations (`docs/architecture.md`)
32
+ - Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
33
33
  - Discovery utilities (find schemas/tables, describe columns)
34
34
  - Sui package workflows (events/transactions/objects) with safe defaults
35
35
  - JSONL query history + SQL artifacts (SHA-256) for reproducibility
36
36
  - Rich MCP surface: query info/run, discovery, health, Sui, and Dune admin (create/update/fork)
37
37
 
38
- Quick Start
38
+ ## What is Dune?
39
+ [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
+
41
+ ## Quick Start
39
42
  - 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).
40
43
  - Install dependencies (`uv sync` or `pip install -e .`).
41
44
  - Start the FastMCP stdio server:
42
45
  - `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
43
46
  - or install the console script via `uv tool install .` and run `spice-mcp`.
44
47
 
48
+ ## Cursor IDE Setup
49
+
50
+ To use spice-mcp with Cursor IDE:
51
+
52
+ 1. **Install the MCP Server**:
53
+ ```bash
54
+ # Install dependencies and package
55
+ uv sync
56
+ pip install -e .
57
+
58
+ # Or install via uv tool (creates console script)
59
+ uv tool install .
60
+ ```
61
+
62
+ 2. **Configure Cursor**:
63
+ - Open Cursor Settings → MCP Servers
64
+ - Add new MCP server configuration:
65
+ ```json
66
+ {
67
+ "name": "spice-mcp",
68
+ "command": "spice-mcp",
69
+ "env": {
70
+ "DUNE_API_KEY": "your-dune-api-key-here"
71
+ },
72
+ "disabled": false
73
+ }
74
+ ```
75
+ Alternatively, if you prefer running from source:
76
+ ```json
77
+ {
78
+ "name": "spice-mcp",
79
+ "command": "python",
80
+ "args": ["-m", "spice_mcp.mcp.server"],
81
+ "env": {
82
+ "PYTHONPATH": "/path/to/your/spice-mcp/src",
83
+ "DUNE_API_KEY": "your-dune-api-key-here"
84
+ },
85
+ "disabled": false
86
+ }
87
+ ```
88
+
89
+ 3. **Restart Cursor** to load the MCP server
90
+
91
+ 4. **Verify Connection**:
92
+ - Open Cursor and use the command palette (Cmd/Ctrl + Shift + P)
93
+ - Search for "MCP" or "spice" commands
94
+ - Test with `dune_health_check` to verify the connection
95
+
96
+ 5. **Available Tools in Cursor**:
97
+ - `dune_query`: Run Dune queries by ID, URL, or raw SQL
98
+ - `dune_find_tables`: Search schemas and list tables
99
+ - `dune_describe_table`: Get column metadata
100
+ - `sui_package_overview`: Analyze Sui packages
101
+ - `dune_health_check`: Verify API connection
102
+
103
+ **Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
104
+
45
105
  ## MCP Tools and Features
46
106
 
47
107
  All tools expose typed parameters, titles, and tags; failures return a consistent error envelope.
@@ -114,16 +174,16 @@ Core Tools (with parameters)
114
174
  - Use: Verify API key presence and logging paths
115
175
  - Output: `api_key_present`, `query_history_path`, `logging_enabled`, `status`
116
176
 
117
- Docs
118
- - See `docs/index.md` for full documentation:
119
- - Dune API structure and capabilities: `docs/dune_api.md`
120
- - Discovery patterns and examples: `docs/discovery.md`
121
- - Sui package workflows: `docs/sui_packages.md`
122
- - Tool reference and schemas: `docs/tools.md`
123
- - Codex CLI + tooling integration: `docs/codex_cli.md`, `docs/codex_cli_tools.md`
124
- - Architecture overview: `docs/architecture.md`
125
- - Installation and configuration: `docs/installation.md`, `docs/config.md`
126
- - Development and linting: `docs/development.md`
177
+ ## Docs
178
+ - See [docs/index.md](docs/index.md) for full documentation:
179
+ - Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
180
+ - Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
181
+ - Sui package workflows: [docs/sui_packages.md](docs/sui_packages.md)
182
+ - Tool reference and schemas: [docs/tools.md](docs/tools.md)
183
+ - Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
184
+ - Architecture overview: [docs/architecture.md](docs/architecture.md)
185
+ - Installation and configuration: [docs/installation.md](docs/installation.md), [docs/config.md](docs/config.md)
186
+ - Development and linting: [docs/development.md](docs/development.md)
127
187
 
128
188
  Notes
129
189
  - Legacy Spice code now lives under `src/spice_mcp/adapters/dune` (extract, cache, urls, types).
@@ -1,26 +1,86 @@
1
1
  # spice-mcp
2
2
 
3
- spice-mcp is an MCP server for Dune 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 and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
3
+ 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 and Sui package exploration. Results are Polars-first in Python and compact, token-efficient in MCP responses.
4
4
 
5
5
  Requirements: Python 3.13+
6
6
 
7
7
  This project uses FastMCP for typed, decorator-registered tools and resources.
8
8
 
9
- Highlights
9
+ ## Highlights
10
10
  - Polars LazyFrame-first pipeline: results stay lazy until explicitly materialized
11
- - Ports/adapters layering for maintainable integrations (`docs/architecture.md`)
11
+ - Ports/adapters layering for maintainable integrations ([docs/architecture.md](docs/architecture.md))
12
12
  - Discovery utilities (find schemas/tables, describe columns)
13
13
  - Sui package workflows (events/transactions/objects) with safe defaults
14
14
  - JSONL query history + SQL artifacts (SHA-256) for reproducibility
15
15
  - Rich MCP surface: query info/run, discovery, health, Sui, and Dune admin (create/update/fork)
16
16
 
17
- Quick Start
17
+ ## What is Dune?
18
+ [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.
19
+
20
+ ## Quick Start
18
21
  - 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).
19
22
  - Install dependencies (`uv sync` or `pip install -e .`).
20
23
  - Start the FastMCP stdio server:
21
24
  - `python -m spice_mcp.mcp.server --env PYTHONPATH=$(pwd)/src`
22
25
  - or install the console script via `uv tool install .` and run `spice-mcp`.
23
26
 
27
+ ## Cursor IDE Setup
28
+
29
+ To use spice-mcp with Cursor IDE:
30
+
31
+ 1. **Install the MCP Server**:
32
+ ```bash
33
+ # Install dependencies and package
34
+ uv sync
35
+ pip install -e .
36
+
37
+ # Or install via uv tool (creates console script)
38
+ uv tool install .
39
+ ```
40
+
41
+ 2. **Configure Cursor**:
42
+ - Open Cursor Settings → MCP Servers
43
+ - Add new MCP server configuration:
44
+ ```json
45
+ {
46
+ "name": "spice-mcp",
47
+ "command": "spice-mcp",
48
+ "env": {
49
+ "DUNE_API_KEY": "your-dune-api-key-here"
50
+ },
51
+ "disabled": false
52
+ }
53
+ ```
54
+ Alternatively, if you prefer running from source:
55
+ ```json
56
+ {
57
+ "name": "spice-mcp",
58
+ "command": "python",
59
+ "args": ["-m", "spice_mcp.mcp.server"],
60
+ "env": {
61
+ "PYTHONPATH": "/path/to/your/spice-mcp/src",
62
+ "DUNE_API_KEY": "your-dune-api-key-here"
63
+ },
64
+ "disabled": false
65
+ }
66
+ ```
67
+
68
+ 3. **Restart Cursor** to load the MCP server
69
+
70
+ 4. **Verify Connection**:
71
+ - Open Cursor and use the command palette (Cmd/Ctrl + Shift + P)
72
+ - Search for "MCP" or "spice" commands
73
+ - Test with `dune_health_check` to verify the connection
74
+
75
+ 5. **Available Tools in Cursor**:
76
+ - `dune_query`: Run Dune queries by ID, URL, or raw SQL
77
+ - `dune_find_tables`: Search schemas and list tables
78
+ - `dune_describe_table`: Get column metadata
79
+ - `sui_package_overview`: Analyze Sui packages
80
+ - `dune_health_check`: Verify API connection
81
+
82
+ **Tip**: Create a `.env` file in your project root with `DUNE_API_KEY=your-key-here` for easier configuration.
83
+
24
84
  ## MCP Tools and Features
25
85
 
26
86
  All tools expose typed parameters, titles, and tags; failures return a consistent error envelope.
@@ -93,16 +153,16 @@ Core Tools (with parameters)
93
153
  - Use: Verify API key presence and logging paths
94
154
  - Output: `api_key_present`, `query_history_path`, `logging_enabled`, `status`
95
155
 
96
- Docs
97
- - See `docs/index.md` for full documentation:
98
- - Dune API structure and capabilities: `docs/dune_api.md`
99
- - Discovery patterns and examples: `docs/discovery.md`
100
- - Sui package workflows: `docs/sui_packages.md`
101
- - Tool reference and schemas: `docs/tools.md`
102
- - Codex CLI + tooling integration: `docs/codex_cli.md`, `docs/codex_cli_tools.md`
103
- - Architecture overview: `docs/architecture.md`
104
- - Installation and configuration: `docs/installation.md`, `docs/config.md`
105
- - Development and linting: `docs/development.md`
156
+ ## Docs
157
+ - See [docs/index.md](docs/index.md) for full documentation:
158
+ - Dune API structure and capabilities: [docs/dune_api.md](docs/dune_api.md)
159
+ - Discovery patterns and examples: [docs/discovery.md](docs/discovery.md)
160
+ - Sui package workflows: [docs/sui_packages.md](docs/sui_packages.md)
161
+ - Tool reference and schemas: [docs/tools.md](docs/tools.md)
162
+ - Codex CLI + tooling integration: [docs/codex_cli.md](docs/codex_cli.md), [docs/codex_cli_tools.md](docs/codex_cli_tools.md)
163
+ - Architecture overview: [docs/architecture.md](docs/architecture.md)
164
+ - Installation and configuration: [docs/installation.md](docs/installation.md), [docs/config.md](docs/config.md)
165
+ - Development and linting: [docs/development.md](docs/development.md)
106
166
 
107
167
  Notes
108
168
  - Legacy Spice code now lives under `src/spice_mcp/adapters/dune` (extract, cache, urls, types).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spice-mcp"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "MCP server for Dune Analytics data access"
5
5
  authors = [
6
6
  { name = "Evan-Kim2028", email = "ekcopersonal@gmail.com" }
@@ -36,6 +36,23 @@ managed = true
36
36
  dev-dependencies = [
37
37
  "pytest>=8.0.0",
38
38
  "pytest-asyncio>=0.23.0",
39
+ "pytest-cov>=5.0.0",
40
+ "freezegun>=1.4.0",
41
+ "jsonschema>=4.21.1",
42
+ "responses>=0.25.0",
43
+ "aioresponses>=0.7.6",
44
+ "vcrpy>=6.0.1",
45
+ "hypothesis>=6.100.0",
46
+ "ruff>=0.6.9",
47
+ "mypy>=1.11.2",
48
+ ]
49
+
50
+ # uv dependency groups for CI and local dev
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8.0.0",
54
+ "pytest-asyncio>=0.23.0",
55
+ "pytest-cov>=5.0.0",
39
56
  "freezegun>=1.4.0",
40
57
  "jsonschema>=4.21.1",
41
58
  "responses>=0.25.0",
@@ -0,0 +1,114 @@
1
+ import csv
2
+ from pathlib import Path
3
+
4
+
5
+ BASE = Path("notes/bridgez")
6
+ CIRCLE_DIR = BASE / "circle"
7
+
8
+
9
+ def read_single_row_metrics(path: Path):
10
+ with path.open() as f:
11
+ reader = csv.DictReader(f)
12
+ row = next(reader)
13
+ return {
14
+ "tx_count": float(row.get("tx_count", 0) or 0),
15
+ "total_usd": float(row.get("total_usd", 0) or 0),
16
+ "avg_usd": float(row.get("avg_usd", 0) or 0),
17
+ }
18
+
19
+
20
+ def read_circle_totals(path: Path):
21
+ rows = []
22
+ with path.open() as f:
23
+ reader = csv.DictReader(f)
24
+ for r in reader:
25
+ rows.append(
26
+ {
27
+ "schema": r["schema"],
28
+ "usd_out_30d": float(r["usd_out_30d"]) if r["usd_out_30d"] else 0.0,
29
+ "usd_out_ytd": float(r["usd_out_ytd"]) if r["usd_out_ytd"] else 0.0,
30
+ }
31
+ )
32
+ return rows
33
+
34
+
35
+ def write_text(path: Path, content: str):
36
+ path.write_text(content)
37
+
38
+
39
+ def update_readme(readme_path: Path, section: str):
40
+ # Append or replace a Circle vs LayerZero section
41
+ marker_start = "\n## Circle vs LayerZero (Summary)\n"
42
+ content = readme_path.read_text() if readme_path.exists() else ""
43
+ if "## Circle vs LayerZero (Summary)" in content:
44
+ # replace from marker to end or next header
45
+ parts = content.split("## Circle vs LayerZero (Summary)")
46
+ head = parts[0]
47
+ tail = "## Circle vs LayerZero (Summary)" + parts[1]
48
+ # cut tail at the next header if present
49
+ tail_lines = tail.split("\n## ")
50
+ new_tail = "## Circle vs LayerZero (Summary)\n" + section
51
+ if len(tail_lines) > 1:
52
+ # keep next headers
53
+ new_content = head + new_tail + "\n## " + "\n## ".join(tail_lines[1:])
54
+ else:
55
+ new_content = head + new_tail
56
+ readme_path.write_text(new_content)
57
+ else:
58
+ readme_path.write_text(content.rstrip() + marker_start + section)
59
+
60
+
61
+ def fmt_usd(x: float) -> str:
62
+ return f"${x:,.2f}"
63
+
64
+
65
+ def main():
66
+ # LayerZero metrics
67
+ lz_30d = read_single_row_metrics(BASE / "metrics_last_30d.csv")
68
+ lz_ytd = read_single_row_metrics(BASE / "metrics_ytd.csv")
69
+
70
+ # Circle per-schema summary
71
+ circle_rows = read_circle_totals(CIRCLE_DIR / "flows_outbound_summary.csv")
72
+ c_30d_total = sum(r["usd_out_30d"] for r in circle_rows)
73
+ c_ytd_total = sum(r["usd_out_ytd"] for r in circle_rows)
74
+
75
+ # Build comparison text
76
+ pct_30d = (c_30d_total / lz_30d["total_usd"] * 100.0) if lz_30d["total_usd"] else 0.0
77
+ pct_ytd = (c_ytd_total / lz_ytd["total_usd"] * 100.0) if lz_ytd["total_usd"] else 0.0
78
+
79
+ top_circle_30d = sorted(circle_rows, key=lambda r: r["usd_out_30d"], reverse=True)[:5]
80
+ top_circle_ytd = sorted(circle_rows, key=lambda r: r["usd_out_ytd"], reverse=True)[:5]
81
+
82
+ lines = []
83
+ lines.append("Circle vs LayerZero (USD volumes)\n")
84
+ lines.append(f"- LayerZero — 30d: {fmt_usd(lz_30d['total_usd'])}; YTD: {fmt_usd(lz_ytd['total_usd'])}")
85
+ lines.append(f"- Circle (CCTP outbound) — 30d: {fmt_usd(c_30d_total)} ({pct_30d:.1f}% of LZ); YTD: {fmt_usd(c_ytd_total)} ({pct_ytd:.1f}% of LZ)\n")
86
+ lines.append("Top Circle schemas (30d):")
87
+ for r in top_circle_30d:
88
+ lines.append(f"- {r['schema']}: {fmt_usd(r['usd_out_30d'])}")
89
+ lines.append("")
90
+ lines.append("Top Circle schemas (YTD):")
91
+ for r in top_circle_ytd:
92
+ lines.append(f"- {r['schema']}: {fmt_usd(r['usd_out_ytd'])}")
93
+ lines.append("")
94
+ lines.append("Note: circle_bnb and circle_zksync may be missing if their tables/columns differ; totals reflect included schemas only.")
95
+
96
+ text = "\n".join(lines) + "\n"
97
+
98
+ # Write dedicated comparison under circle
99
+ write_text(CIRCLE_DIR / "COMPARISON.txt", text)
100
+
101
+ # Update top-level README with a concise section
102
+ section = (
103
+ f"\n- LayerZero — 30d: {fmt_usd(lz_30d['total_usd'])}; YTD: {fmt_usd(lz_ytd['total_usd'])}\n"
104
+ f"- Circle (CCTP outbound) — 30d: {fmt_usd(c_30d_total)} ({pct_30d:.1f}% of LZ); YTD: {fmt_usd(c_ytd_total)} ({pct_ytd:.1f}% of LZ)\n"
105
+ "- Top Circle schemas (30d): "
106
+ + ", ".join([f"{r['schema']} {fmt_usd(r['usd_out_30d'])}" for r in top_circle_30d])
107
+ + "\n"
108
+ )
109
+ update_readme(BASE / "README.md", section)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
114
+
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import io
4
4
  import time
5
5
  from typing import TYPE_CHECKING, overload
6
+ import os
6
7
 
7
8
  from ..http_client import HttpClient
8
9
  from . import cache as _cache
@@ -32,6 +33,10 @@ from .typing_utils import resolve_raw_sql_template_id
32
33
 
33
34
  ADAPTER_VERSION = "spice-mcp-adapter"
34
35
 
36
+ # Runtime-configurable HTTP timeouts (helps avoid host-level timeouts)
37
+ _GET_TIMEOUT: float = float(os.getenv("SPICE_DUNE_GET_TIMEOUT", os.getenv("SPICE_HTTP_TIMEOUT", "30")))
38
+ _POST_TIMEOUT: float = float(os.getenv("SPICE_DUNE_POST_TIMEOUT", os.getenv("SPICE_HTTP_TIMEOUT", "30")))
39
+
35
40
  if TYPE_CHECKING:
36
41
  from collections.abc import Mapping, Sequence
37
42
  from typing import Any, Literal
@@ -749,7 +754,7 @@ def _execute(
749
754
  print('executing query, query_id = ' + str(query_id))
750
755
 
751
756
  # perform request
752
- response = _transport_post(url, headers=headers, json=data, timeout=15.0)
757
+ response = _transport_post(url, headers=headers, json=data, timeout=_POST_TIMEOUT)
753
758
  result: Mapping[str, Any] = response.json()
754
759
 
755
760
  # check for errors
@@ -786,7 +791,7 @@ async def _async_execute(
786
791
  print('executing query, query_id = ' + str(query_id))
787
792
 
788
793
  # perform request
789
- timeout = aiohttp.ClientTimeout(total=30)
794
+ timeout = aiohttp.ClientTimeout(total=_POST_TIMEOUT)
790
795
  async with aiohttp.ClientSession(timeout=timeout) as session:
791
796
  async with session.post(url, headers=headers, json=data) as response:
792
797
  result: Mapping[str, Any] = await response.json()
@@ -862,12 +867,12 @@ def _get_results(
862
867
  def _get_with_retries(u: str):
863
868
  client = current_http_client()
864
869
  if client is not None:
865
- return client.request("GET", u, headers=headers, timeout=30.0)
870
+ return client.request("GET", u, headers=headers, timeout=_GET_TIMEOUT)
866
871
 
867
872
  attempts = 0
868
873
  backoff = 0.5
869
874
  while True:
870
- resp = _transport_get(u, headers=headers, timeout=30.0)
875
+ resp = _transport_get(u, headers=headers, timeout=_GET_TIMEOUT)
871
876
  if resp.status_code in (429, 502):
872
877
  attempts += 1
873
878
  if attempts >= 3:
@@ -963,7 +968,7 @@ async def _async_get_results(
963
968
  print('getting results, execution_id = ' + str(execution['execution_id']))
964
969
 
965
970
  # perform request
966
- timeout = aiohttp.ClientTimeout(total=30)
971
+ timeout = aiohttp.ClientTimeout(total=_GET_TIMEOUT)
967
972
  async with aiohttp.ClientSession(timeout=timeout) as session:
968
973
  # GET with simple retry/backoff for 429/502
969
974
  attempts = 0
@@ -1169,7 +1174,7 @@ def _poll_execution(
1169
1174
  )
1170
1175
 
1171
1176
  # poll
1172
- response = _http_get(url, headers=headers, timeout=15.0)
1177
+ response = _http_get(url, headers=headers, timeout=_GET_TIMEOUT)
1173
1178
  result = response.json()
1174
1179
  if (
1175
1180
  'is_execution_finished' not in result
@@ -1243,7 +1248,7 @@ async def _async_poll_execution(
1243
1248
  t_start = time.time()
1244
1249
 
1245
1250
  # poll until completion
1246
- timeout = aiohttp.ClientTimeout(total=30)
1251
+ timeout = aiohttp.ClientTimeout(total=_GET_TIMEOUT)
1247
1252
  async with aiohttp.ClientSession(timeout=timeout) as session:
1248
1253
  sleep_amount = poll_interval
1249
1254
  while True:
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
4
+
3
5
 
4
6
  def resolve_raw_sql_template_id() -> int:
5
7
  """Return a stable template ID used for executing raw SQL text.
@@ -7,4 +9,4 @@ def resolve_raw_sql_template_id() -> int:
7
9
  Tests stub HTTP boundaries and only require a consistent integer. This
8
10
  placeholder can be adjusted if upstream semantics change.
9
11
  """
10
- return 0
12
+ return int(os.getenv("SPICE_RAW_SQL_QUERY_ID", "4060379"))
@@ -0,0 +1,62 @@
1
+ """Tests for typing utilities, especially environment variable handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from unittest.mock import patch
7
+
8
+ from spice_mcp.adapters.dune.typing_utils import resolve_raw_sql_template_id
9
+
10
+
11
+ def test_resolve_raw_sql_template_id_default():
12
+ """Test that the function returns the default template ID when no env var is set."""
13
+ # Ensure env var is not set
14
+ with patch.dict(os.environ, {}, clear=True):
15
+ result = resolve_raw_sql_template_id()
16
+ assert result == 4060379
17
+ assert isinstance(result, int)
18
+
19
+
20
+ def test_resolve_raw_sql_template_id_from_env():
21
+ """Test that the function reads from the SPICE_RAW_SQL_QUERY_ID environment variable."""
22
+ custom_id = "12345"
23
+ with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": custom_id}):
24
+ result = resolve_raw_sql_template_id()
25
+ assert result == int(custom_id)
26
+
27
+
28
+ def test_resolve_raw_sql_template_id_invalid_env():
29
+ """Test that the function handles invalid environment variable values gracefully."""
30
+ with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "invalid_number"}):
31
+ # This should raise a ValueError when trying to convert to int
32
+ try:
33
+ resolve_raw_sql_template_id()
34
+ assert False, "Expected ValueError for invalid number"
35
+ except ValueError:
36
+ pass # Expected
37
+
38
+
39
+ def test_resolve_raw_sql_template_id_zero_env():
40
+ """Test that the function handles zero value correctly."""
41
+ with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "0"}):
42
+ result = resolve_raw_sql_template_id()
43
+ assert result == 0
44
+
45
+
46
+ def test_determine_input_type_raw_sql_uses_template_id(monkeypatch):
47
+ """Test that determine_input_type correctly resolves raw SQL using the template ID."""
48
+ from spice_mcp.adapters.dune.extract import determine_input_type
49
+
50
+ # Test with default template ID
51
+ with patch.dict(os.environ, {}, clear=True):
52
+ query_id, execution, parameters = determine_input_type("SELECT 1 as test")
53
+ assert query_id == 4060379
54
+ assert execution is None
55
+ assert parameters == {"query": "SELECT 1 as test"}
56
+
57
+ # Test with custom template ID
58
+ with patch.dict(os.environ, {"SPICE_RAW_SQL_QUERY_ID": "12345"}):
59
+ query_id, execution, parameters = determine_input_type("SELECT count(*) as total")
60
+ assert query_id == 12345
61
+ assert execution is None
62
+ assert parameters == {"query": "SELECT count(*) as total"}