Skip to main content
Glama
test_spellbook_discovery.py11.8 kB
""" Integration test for spellbook model discovery through MCP tools. This test verifies that the spellbook tools can actually discover dbt models from the Spellbook GitHub repository (https://github.com/duneanalytics/spellbook) through the full MCP stack. """ from __future__ import annotations import os from pathlib import Path import pytest from spice_mcp.config import Config, DuneConfig from spice_mcp.mcp import server def _should_run_live(): """Check if live tests should run.""" return bool(os.getenv("SPICE_TEST_LIVE") == "1" and os.getenv("DUNE_API_KEY")) @pytest.mark.mcp def test_spellbook_discovery_through_dune_discover(monkeypatch, tmp_path): """ Test spellbook discovery through dune_discover tool. This verifies the full stack: 1. dune_discover with source="spellbook" 2. Uses _spellbook_find_models_impl internally 3. SpellbookExplorer.find_schemas() which parses GitHub repo 4. Returns schema/subproject names from Spellbook dbt models """ monkeypatch.setenv("DUNE_API_KEY", "test-key") monkeypatch.setenv("SPICE_QUERY_HISTORY", str(tmp_path / "history.jsonl")) # Initialize server to set up services server._ensure_initialized() # Verify the dune_discover tool exists (via FastMCP wrapper) assert hasattr(server.dune_discover, 'fn') assert callable(server.dune_discover.fn) # Test with a stub first to verify the tool interface works from spice_mcp.core.models import SchemaMatch, TableSummary, TableColumn, TableDescription class StubSpellbookExplorer: """Explorer stub that simulates parsing Spellbook GitHub repo.""" def find_schemas(self, keyword: str): # Simulate finding subprojects like "dex", "nft", "tokens" from repo if "dex" in keyword.lower(): return [SchemaMatch(schema="dex")] if "nft" in keyword.lower(): return [SchemaMatch(schema="nft")] if "token" in keyword.lower(): return [SchemaMatch(schema="tokens")] if "spellbook" in keyword.lower(): return [ SchemaMatch(schema="dex"), SchemaMatch(schema="nft"), SchemaMatch(schema="tokens"), ] return [] def list_tables(self, schema: str, limit: int | None = None): # Simulate listing dbt models from repo if schema == "dex": tables = ["trades", "pools", "liquidity"] elif schema == "nft": tables = ["transfers", "mints", "trades"] elif schema == "tokens": tables = ["erc20_transfers", "erc20_balances", "prices"] else: tables = [] summaries = [TableSummary(schema=schema, table=t) for t in tables] if limit: return summaries[:limit] return summaries def describe_table(self, schema: str, table: str): # Simulate parsing schema.yml or SQL from repo if schema == "dex" and table == "trades": return TableDescription( fully_qualified_name=f"{schema}.{table}", columns=[ TableColumn(name="block_time", dune_type="TIMESTAMP", polars_dtype="Datetime"), TableColumn(name="tx_hash", dune_type="VARCHAR", polars_dtype="Utf8"), TableColumn(name="amount_usd", dune_type="DECIMAL", polars_dtype="Float64"), ], ) raise ValueError(f"Table {schema}.{table} not found in Spellbook") def _load_models(self): """Return mock models cache matching real SpellbookExplorer structure.""" return { "dex": [ { "name": "trades", "schema": "dex", "dune_schema": "dex", "dune_alias": "trades", "dune_table": "dex.trades", }, { "name": "pools", "schema": "dex", "dune_schema": "dex", "dune_alias": "pools", "dune_table": "dex.pools", }, ], "nft": [ { "name": "transfers", "schema": "nft", "dune_schema": "nft", "dune_alias": "transfers", "dune_table": "nft.transfers", }, ], } # Replace spellbook explorer with stub server.SPELLBOOK_EXPLORER = StubSpellbookExplorer() # Create a stub verification service that always returns True (skip verification for stub test) from spice_mcp.service_layer.verification_service import VerificationService from unittest.mock import MagicMock from pathlib import Path import tempfile stub_adapter = MagicMock() stub_verification = VerificationService( cache_path=Path(tempfile.gettempdir()) / "test_verification_cache.json", dune_adapter=stub_adapter, ) # Mock verify_tables_batch to always return True for stub tables stub_verification.verify_tables_batch = lambda tables: {f"{s}.{t}": True for s, t in tables} server.VERIFICATION_SERVICE = stub_verification # Test 1: Find spellbook schemas/subprojects via dune_discover result = server._unified_discover_impl(keyword="dex", source="spellbook") assert "schemas" in result schemas = result["schemas"] assert len(schemas) > 0 assert "dex" in schemas # Test 2: List tables/models in a spellbook schema via dune_discover result = server._unified_discover_impl(schema="dex", source="spellbook", limit=10) assert "tables" in result tables = result["tables"] assert len(tables) > 0 table_names = [t["table"] for t in tables] assert "trades" in table_names # Test 3: Verify table includes column details trades_table = next(t for t in tables if t["table"] == "trades") assert "columns" in trades_table assert len(trades_table["columns"]) > 0 column_names = [c["name"] for c in trades_table["columns"]] assert "block_time" in column_names or "tx_hash" in column_names # Test 4: Test with multiple keywords via dune_discover result = server._unified_discover_impl(keyword=["dex", "nft"], source="spellbook", include_columns=False) assert "schemas" in result assert "tables" in result assert len(result["schemas"]) >= 2 # Should find both dex and nft schemas @pytest.mark.skipif(not _should_run_live(), reason="live tests disabled by default") @pytest.mark.live def test_spellbook_discovery_live(): """ Live test: Actually clone and parse Spellbook GitHub repository. This requires: - SPICE_TEST_LIVE=1 - Git available on system This verifies that: 1. The explorer can clone the Spellbook GitHub repo 2. Can parse dbt models from the repo structure 3. Can find schemas/subprojects and list tables/models 4. Can describe models by parsing SQL/schema.yml """ server._ensure_initialized() # Create stub verification service for live tests from spice_mcp.service_layer.verification_service import VerificationService from unittest.mock import MagicMock from pathlib import Path import tempfile stub_adapter = MagicMock() stub_verification = VerificationService( cache_path=Path(tempfile.gettempdir()) / "test_verification_cache.json", dune_adapter=stub_adapter, ) stub_verification.verify_tables_batch = lambda tables: {f"{s}.{t}": True for s, t in tables} server.VERIFICATION_SERVICE = stub_verification # Test 1: Find spellbook schemas/subprojects via dune_discover (parses GitHub repo) print("\n🔍 Searching Spellbook GitHub repo for schemas...") result = server._unified_discover_impl(keyword="dex", source="spellbook") assert "schemas" in result, "Result should contain 'schemas' key" schemas = result.get("schemas", []) print(f" Found {len(schemas)} schemas: {schemas[:5]}...") if not schemas: pytest.skip("No schemas found - may need to check git availability or repo access") # Test 2: Search for models matching keyword (includes column details) via dune_discover print(f"\n📊 Searching for models matching 'dex' with column details...") result = server._unified_discover_impl(keyword="dex", source="spellbook", limit=5, include_columns=True) assert "tables" in result tables = result.get("tables", []) print(f" Found {len(tables)} tables") if not tables: pytest.skip("No tables found - may need to check git availability or repo access") # Test 3: Verify table structure includes columns test_table = tables[0] print(f"\n📋 Table: {test_table.get('fully_qualified_name')}") columns = test_table.get("columns", []) print(f" Columns ({len(columns)}): {[c['name'] for c in columns[:5]]}...") assert "schema" in test_table assert "table" in test_table assert "fully_qualified_name" in test_table assert len(columns) >= 0, "Table should have columns list (may be empty if parsing fails)" @pytest.mark.skipif(not _should_run_live(), reason="live tests disabled by default") @pytest.mark.live def test_spellbook_workflow_end_to_end(): """ End-to-end workflow: Discover spellbook → List tables → Describe → Query. This tests the complete user journey with actual Dune API calls. """ server._ensure_initialized() # Create stub verification service for live tests from spice_mcp.service_layer.verification_service import VerificationService from unittest.mock import MagicMock from pathlib import Path import tempfile stub_adapter = MagicMock() stub_verification = VerificationService( cache_path=Path(tempfile.gettempdir()) / "test_verification_cache.json", dune_adapter=stub_adapter, ) stub_verification.verify_tables_batch = lambda tables: {f"{s}.{t}": True for s, t in tables} server.VERIFICATION_SERVICE = stub_verification # Step 1: Discover spellbook schemas and tables via dune_discover result = server._unified_discover_impl(keyword="dex", source="spellbook", limit=5, include_columns=True) schemas = result.get("schemas", []) tables = result.get("tables", []) assert len(schemas) > 0 assert len(tables) > 0 # Step 2: Verify table structure includes schema, table, and columns test_table = tables[0] assert "schema" in test_table assert "table" in test_table assert "fully_qualified_name" in test_table assert "columns" in test_table columns = test_table.get("columns", []) assert isinstance(columns, list) # Step 3: Use discovered info to query (if query tool is available) if server.EXECUTE_QUERY_TOOL: # Construct a simple query using discovered table # Note: For verified tables, use dune_table field if available table_name = test_table.get("dune_table") or test_table["fully_qualified_name"] query_sql = f"SELECT * FROM {table_name} LIMIT 5" print(f"\n🔍 Querying: {query_sql}") query_result = server.EXECUTE_QUERY_TOOL.execute(query=query_sql, format="preview") assert query_result["type"] == "preview" assert "rowcount" in query_result

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Evan-Kim2028/spice-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server