Skip to main content
Glama
PulkitXChadha

Databricks MCP Server

lakeview_dashboard.py69.4 kB
r"""Lakeview Dashboard MCP Tools. This module provides comprehensive MCP tools for Lakeview dashboard management with native Lakeview implementation. Provides parameter validation, widget creation, and encoding management for .lvdash.json file generation. QueryLines Format (based on actual Lakeview dashboard JSON structure): - Always uses 'queryLines' field (array format) - Single-line queries: ["SELECT * FROM table"] - Multi-line queries: ["SELECT \\n", " col1,\\n", "FROM table"] (preserves line breaks) """ # Standard library imports for JSON handling, file operations, and type hints import json import os import uuid from pathlib import Path from typing import Any, Dict, List # Import widget specification creation function # Try relative import first (when used as module), fallback to direct import try: from .widget_specs import create_widget_spec except ImportError: from widget_specs import create_widget_spec def generate_id() -> str: """Generate 8-character hex ID for Lakeview objects. Lakeview dashboards use short hex IDs for internal object identification. This function creates a unique 8-character identifier by truncating a UUID. Returns: str: 8-character hexadecimal string (e.g., 'a1b2c3d4') """ # Generate a full UUID and take the first 8 characters for brevity return str(uuid.uuid4())[:8] def query_to_querylines(query: str) -> List[str]: r"""Convert SQL query string to Lakeview queryLines format. Based on actual Lakeview dashboard JSON format analysis: - Always returns an array for the queryLines field - Short simple queries: ["SELECT * FROM table"] - Complex queries: Intelligently split into readable lines with proper formatting - Multi-line format: ["SELECT \n", " col1,\n", " col2\n", "FROM table"] Each line except typically the last has \n appended Args: query: SQL query as string (single or multiline) Returns: List of strings in proper Lakeview queryLines array format """ # Remove leading/trailing whitespace from the input query query = query.strip() # If query already contains newlines, split and preserve formatting # This handles multi-line queries that are already formatted if '\n' in query: result = [] lines = query.split('\n') # Process each line to maintain Lakeview queryLines format for i in range(len(lines)): line = lines[i] if i < len(lines) - 1: # Add \n to all lines except the last to preserve line breaks result.append(line + '\n') else: # Last line: only add if it's not empty (avoid trailing empty strings) if line.strip(): result.append(line) return result # For single-line queries, check if they should be formatted as multi-line # Threshold: queries longer than 120 characters or containing multiple clauses # This improves readability for complex queries in the dashboard should_format_multiline = ( len(query) > 120 # Long queries benefit from multi-line formatting or query.upper().count(' FROM ') >= 1 # Has FROM clause and ( query.upper().count(' WHERE ') >= 1 # Plus WHERE or query.upper().count(' GROUP BY ') >= 1 # Or GROUP BY or query.upper().count(' ORDER BY ') >= 1 # Or ORDER BY or query.upper().count(' HAVING ') >= 1 # Or HAVING or query.upper().count(' JOIN ') >= 1 # Or JOIN or query.upper().count(',') >= 3 # Or many columns ) ) # Simple queries stay as single-line for cleaner queryLines format if not should_format_multiline: return [query] # Format complex single-line queries into readable multi-line format import re # Simplified approach: split on major SQL keywords and format columns # This creates a more readable queryLines array for complex queries result = [] # Split on major SQL clauses while preserving them # Uses regex to identify SQL keywords as clause boundaries pattern = r'\b(SELECT|FROM|WHERE|GROUP BY|ORDER BY|HAVING|UNION)\b' parts = re.split(pattern, query, flags=re.IGNORECASE) parts = [p.strip() for p in parts if p.strip()] current_clause = '' # Process each part to build formatted clauses for i, part in enumerate(parts): part_upper = part.upper() if part_upper in ['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'ORDER BY', 'HAVING', 'UNION']: # Start new clause - format the previous one first if current_clause.strip(): result.extend(_format_clause_content(current_clause)) current_clause = part + ' ' else: # Continue building current clause current_clause += part # Add the final clause if current_clause.strip(): result.extend(_format_clause_content(current_clause)) # Return formatted result or fallback to original query return result if result else [query] def _format_clause_content(clause: str) -> List[str]: """Format the content of a SQL clause into properly formatted lines. This function handles special formatting for SELECT clauses with multiple columns, creating indented, comma-separated lines for better readability. Args: clause: SQL clause string to format Returns: List of formatted lines with proper indentation and line breaks """ clause = clause.strip() # Check if this is a SELECT clause with multiple columns # Multi-column SELECT statements benefit from line-by-line formatting if clause.upper().startswith('SELECT ') and ',' in clause: # Extract the SELECT keyword and column list select_part = clause[:6] # "SELECT" columns_part = clause[6:].strip() # Split columns more carefully, respecting parentheses and CASE statements # This ensures we don't break on commas inside functions or expressions columns = _split_columns_safely(columns_part) if len(columns) > 1: # Format as multi-line with proper indentation lines = [select_part + ' \n'] # SELECT keyword on its own line for j, col in enumerate(columns): col = col.strip() if j < len(columns) - 1: # Add comma after each column except the last lines.append(' ' + col + ',\n') else: # Last column without comma lines.append(' ' + col + '\n') return lines # For non-SELECT clauses or single-column SELECT, return as single line return [clause + '\n'] def _split_columns_safely(columns_text: str) -> List[str]: """Split column list while respecting parentheses, CASE statements, etc. This function intelligently splits a comma-separated column list without breaking on commas that are inside function calls, CASE statements, or other nested expressions. Args: columns_text: String containing comma-separated column expressions Returns: List of individual column expressions as strings """ columns = [] current_col = '' paren_depth = 0 # Track nested parentheses case_depth = 0 # Track nested CASE statements i = 0 while i < len(columns_text): char = columns_text[i] if char == '(': # Entering a nested expression (function call, subquery, etc.) paren_depth += 1 current_col += char elif char == ')': # Exiting a nested expression paren_depth -= 1 current_col += char elif char == ',' and paren_depth == 0 and case_depth == 0: # This is a real column separator (not inside parentheses or CASE) columns.append(current_col.strip()) current_col = '' else: current_col += char # Check for CASE/END keywords to track CASE statement nesting if i >= 3: last_4 = columns_text[i - 3 : i + 1].upper() if last_4 == 'CASE': case_depth += 1 elif last_4.endswith('END') and case_depth > 0: case_depth -= 1 i += 1 # Add the last column if it exists if current_col.strip(): columns.append(current_col.strip()) return columns def create_dashboard_json( name: str, warehouse_id: str, datasets: List[Dict[str, Any]], widgets: List[Dict[str, Any]] = None ) -> Dict[str, Any]: """Create complete Lakeview dashboard JSON with proper queryLines format. Generates Lakeview dashboard JSON with queryLines array format: - Input accepts 'query' as a string for convenience - Output uses 'queryLines' array as required by Lakeview format Args: name: Dashboard display name warehouse_id: SQL warehouse ID datasets: List of dataset configurations with structure: { "name": str, # Dataset display name "query": str, # SQL query string (converted to queryLines array) "parameters": List[Dict] # Optional query parameters } widgets: List of widget configurations with advanced features Returns: Complete dashboard JSON ready for .lvdash.json file with proper queryLines array format """ # Initialize widgets list if not provided if widgets is None: widgets = [] # Generate dashboard ID - Lakeview uses 32-character hex IDs # Concatenate four 8-character IDs to match the expected format dashboard_id = ( generate_id() + generate_id() + generate_id() + generate_id() ) # 32 character ID like real examples # Convert datasets to Lakeview format with parameter support lv_datasets = [] for ds in datasets: # Convert query to proper Lakeview queryLines format using simplified function # This handles both single-line and multi-line queries appropriately query_lines = query_to_querylines(ds['query']) # Create dataset object with generated ID and user-provided display name dataset = {'name': generate_id(), 'displayName': ds['name'], 'queryLines': query_lines} # Add parameters if provided (for parameterized queries) if 'parameters' in ds and ds['parameters']: dataset['parameters'] = ds['parameters'] lv_datasets.append(dataset) # Convert widgets to layout items with custom positioning support # Lakeview uses a grid-based layout system (12 columns wide) layout = [] for i, widget in enumerate(widgets): # Check if custom position is provided by the user if 'position' in widget: position = widget['position'] else: # Default grid layout: 2 columns, auto-flow vertically # Each widget takes 6 columns (half width) and 4 rows height x = (i % 2) * 6 # Alternate between columns 0 and 6 y = (i // 2) * 4 # Move down every 2 widgets position = {'x': x, 'y': y, 'width': 6, 'height': 4} # Create layout item with position and widget specification layout.append( {'position': position, 'widget': create_widget_spec(widget, lv_datasets, dashboard_id)} ) # Return complete Lakeview dashboard JSON structure return { 'dashboard_id': dashboard_id, # Unique dashboard identifier 'displayName': name, # Dashboard title shown in UI 'warehouseId': warehouse_id, # SQL warehouse for query execution 'datasets': lv_datasets, # Data sources with queryLines format 'pages': [{'name': generate_id(), 'displayName': name, 'layout': layout}], # Single page } def create_optimized_dashboard_json( name: str, warehouse_id: str, datasets: List[Dict[str, Any]], widgets: List[Dict[str, Any]] = None, enable_optimization: bool = True, ) -> Dict[str, Any]: """Create dashboard with automatic layout optimization. This function enhances the core dashboard creation with intelligent layout optimization. It analyzes widget data to determine optimal sizing and positioning automatically. Args: name: Dashboard display name warehouse_id: SQL warehouse ID datasets: List of datasets widgets: List of widget configurations enable_optimization: Whether to apply layout optimization (default: True) Returns: Optimized dashboard JSON with intelligent layout """ # Initialize widgets list if not provided if widgets is None: widgets = [] # Apply layout optimization if enabled if enable_optimization: try: # Import layout optimization functions (optional dependency) from .layout_optimization import optimize_dashboard_layout # Optimize widget layout based on data characteristics and best practices optimized_widgets = optimize_dashboard_layout(widgets, warehouse_id, datasets) # Use the core function with optimized widgets return create_dashboard_json(name, warehouse_id, datasets, optimized_widgets) except ImportError: # Fallback if optimization module not available print('Layout optimization module not found, using default layout') return create_dashboard_json(name, warehouse_id, datasets, widgets) except Exception as e: # Fallback on any error - use default layout to ensure dashboard creation succeeds print(f'Layout optimization failed, using default layout: {str(e)}') return create_dashboard_json(name, warehouse_id, datasets, widgets) else: # Optimization disabled, use default layout algorithm return create_dashboard_json(name, warehouse_id, datasets, widgets) def prepare_dashboard_for_client(dashboard_json: Dict[str, Any], file_path: str) -> Dict[str, Any]: """Create dashboard JSON file on the filesystem. Saves the dashboard JSON to the specified file path and returns both the file path and content for verification. """ import os try: # Ensure the directory exists - create parent directories if needed file_path_obj = Path(file_path) file_path_obj.parent.mkdir(parents=True, exist_ok=True) # Format JSON content with proper indentation for readability json_content = json.dumps(dashboard_json, indent=2) # Write the file to the filesystem with UTF-8 encoding with open(file_path, 'w', encoding='utf-8') as f: f.write(json_content) # Verify file was created successfully and get file size for confirmation if os.path.exists(file_path): file_size = os.path.getsize(file_path) return { 'success': True, 'file_path': file_path, 'content': json_content, 'file_size': file_size, 'message': f'Dashboard file successfully created at {file_path} ({file_size} bytes)', } else: return { 'success': False, 'error': f'File creation failed: {file_path} was not created', 'file_path': file_path, } except PermissionError as e: return { 'success': False, 'error': f'Permission denied: Cannot write to {file_path}. {str(e)}', 'file_path': file_path, } except OSError as e: return {'success': False, 'error': f'File system error: {str(e)}', 'file_path': file_path} except Exception as e: return { 'success': False, 'error': f'Unexpected error creating file: {str(e)}', 'file_path': file_path, } def find_dataset_id(dataset_name: str, datasets: List[Dict[str, Any]]) -> str: """Find dataset ID by display name. This helper function maps user-friendly dataset names to internal Lakeview IDs. Widgets reference datasets by display name, but Lakeview uses internal IDs. Args: dataset_name: User-provided dataset display name datasets: List of dataset objects with 'name' (ID) and 'displayName' fields Returns: str: Internal dataset ID for use in widget specifications """ # Search for matching display name in datasets for ds in datasets: if ds['displayName'] == dataset_name: return ds['name'] # If not found, return the first dataset or generate new ID as fallback return datasets[0]['name'] if datasets else generate_id() def validate_sql_query( query: str, warehouse_id: str, catalog: str = None, schema: str = None ) -> Dict[str, Any]: """Validate SQL query by executing it with LIMIT 0 to check syntax and table references. This function works with both single-line and multi-line SQL queries. It normalizes the query format and validates against the database to ensure proper syntax and table/column existence before dashboard creation. Args: query: SQL query to validate (single-line or multi-line string format) warehouse_id: SQL warehouse ID for execution catalog: Optional catalog to use for three-part table names schema: Optional schema to use for three-part table names Returns: { "valid": bool, # True if query is valid "error": str, # Error message if invalid, None if valid "columns": list, # List of column names returned by query "message": str # Success message with column info } """ try: # Import here to avoid circular dependencies from databricks.sdk import WorkspaceClient # Initialize Databricks SDK with environment credentials w = WorkspaceClient( host=os.environ.get('DATABRICKS_HOST'), token=os.environ.get('DATABRICKS_TOKEN') ) # Clean query for validation (remove trailing semicolons and whitespace) clean_query = str(query).strip().rstrip(';').strip() # Create validation query - use LIMIT 0 to check syntax without returning data # This approach validates the query structure and gets column metadata efficiently validation_query = f'SELECT * FROM ({clean_query}) AS validation_subquery LIMIT 0' # Build the full query with catalog/schema context if provided # This ensures validation happens in the correct database context full_query = validation_query if catalog and schema: full_query = f'USE CATALOG {catalog}; USE SCHEMA {schema}; {validation_query}' print(f'🔍 Validating SQL query: {clean_query[:100]}...') # Execute the validation query with short timeout result = w.statement_execution.execute_statement( warehouse_id=warehouse_id, statement=full_query, wait_timeout='10s', # Shorter timeout for validation (not data processing) ) # Extract column information for widget field validation # This metadata is crucial for validating widget field references columns = [] if result.manifest and result.manifest.schema and result.manifest.schema.columns: columns = [col.name for col in result.manifest.schema.columns] return { 'valid': True, 'error': None, 'columns': columns, 'message': ( f'Query validated successfully. Found {len(columns)} columns: ' f'{", ".join(columns[:5])}{"..." if len(columns) > 5 else ""}' ), } except Exception as e: error_msg = str(e) print(f'❌ SQL validation failed: {error_msg}') # Parse common SQL errors to provide helpful feedback to users # This helps developers understand and fix common issues quickly if 'TABLE_OR_VIEW_NOT_FOUND' in error_msg: return { 'valid': False, 'error': ( f'Table or view not found. Please check table names and ensure ' f'they exist in the specified catalog/schema. Error: {error_msg}' ), 'columns': [], } elif 'PARSE_SYNTAX_ERROR' in error_msg: return { 'valid': False, 'error': f'SQL syntax error. Please check your query syntax. Error: {error_msg}', 'columns': [], } elif 'PERMISSION_DENIED' in error_msg: return { 'valid': False, 'error': ( f'Permission denied. Please ensure you have access to the tables ' f'and warehouse. Error: {error_msg}' ), 'columns': [], } else: # Generic error fallback for unexpected issues return {'valid': False, 'error': f'Query validation failed: {error_msg}', 'columns': []} def validate_widget_fields( widget_config: Dict[str, Any], available_columns: List[str] ) -> Dict[str, Any]: """Validate that widget field references exist in the dataset columns. Args: widget_config: Widget configuration with field references available_columns: List of available column names from the dataset Returns: {"valid": bool, "error": str, "warnings": list} - validation result """ # Extract widget configuration and type for validation config = widget_config.get('config', {}) widget_type = widget_config.get('type', '') warnings = [] missing_fields = [] # Check fields based on widget type - each widget type has specific field requirements if widget_type in ['bar', 'line', 'area', 'scatter']: # Chart widgets with x/y axes - validate axis field references if 'x_field' in config and config['x_field'] not in available_columns: missing_fields.append(f"x_field: '{config['x_field']}'") if 'y_field' in config and config['y_field'] not in available_columns: missing_fields.append(f"y_field: '{config['y_field']}'") if 'color_field' in config and config['color_field'] not in available_columns: missing_fields.append(f"color_field: '{config['color_field']}'") elif widget_type == 'pie': # Pie chart with category/value - requires both fields for proper rendering if 'category_field' in config and config['category_field'] not in available_columns: missing_fields.append(f"category_field: '{config['category_field']}'") if 'value_field' in config and config['value_field'] not in available_columns: missing_fields.append(f"value_field: '{config['value_field']}'") elif widget_type == 'counter': # Counter with value field - single numeric display widget if 'value_field' in config and config['value_field'] not in available_columns: missing_fields.append(f"value_field: '{config['value_field']}'") elif widget_type == 'funnel': # Funnel with stage and value fields - conversion analysis widget if 'stage_field' in config and config['stage_field'] not in available_columns: missing_fields.append(f"stage_field: '{config['stage_field']}'") if 'value_field' in config and config['value_field'] not in available_columns: missing_fields.append(f"value_field: '{config['value_field']}'") # Check for fallback categorical fields if stage_field is missing # Funnel widgets can use alternative categorical fields for stages if 'stage_field' not in config: fallback_found = False for field_key in ['category_field', 'x_field', 'color_field']: if field_key in config and config[field_key] in available_columns: fallback_found = True break elif field_key in config and config[field_key] not in available_columns: missing_fields.append(f"{field_key} (used as stage_field): '{config[field_key]}'") if not fallback_found and any( key in config for key in ['category_field', 'x_field', 'color_field'] ): warnings.append('Funnel widget: no valid categorical field found for stage dimension') elif widget_type == 'histogram': # Histogram with x field - distribution analysis widget if 'x_field' in config and config['x_field'] not in available_columns: missing_fields.append(f"x_field: '{config['x_field']}'") elif widget_type == 'table': # Table with specific columns - validate each column exists if 'columns' in config: for col in config['columns']: if col not in available_columns: missing_fields.append(f"table column: '{col}'") elif widget_type == 'choropleth-map': # Choropleth map with location and color fields - geographic visualization if 'location_field' in config and config['location_field'] not in available_columns: missing_fields.append(f"location_field: '{config['location_field']}'") if 'color_field' in config and config['color_field'] not in available_columns: missing_fields.append(f"color_field: '{config['color_field']}'") elif widget_type == 'symbol-map': # Symbol map with lat/lng and optional color/size fields - point-based geographic visualization if 'latitude_field' in config and config['latitude_field'] not in available_columns: missing_fields.append(f"latitude_field: '{config['latitude_field']}'") if 'longitude_field' in config and config['longitude_field'] not in available_columns: missing_fields.append(f"longitude_field: '{config['longitude_field']}'") if 'color_field' in config and config['color_field'] not in available_columns: missing_fields.append(f"color_field: '{config['color_field']}'") if 'size_field' in config and config['size_field'] not in available_columns: missing_fields.append(f"size_field: '{config['size_field']}'") # Generate validation result with detailed error information if missing_fields: return { 'valid': False, 'error': ( f"Widget '{widget_type}' references fields that don't exist in the " f'dataset: {", ".join(missing_fields)}. Available columns: ' f'{", ".join(available_columns)}' ), 'warnings': warnings, } # All field references are valid return {'valid': True, 'error': None, 'warnings': warnings} def load_dashboard_tools(mcp_server): """Register simplified dashboard tools with MCP server. This function registers three main MCP tools for Lakeview dashboard management: 1. create_dashboard_file - Creates complete dashboard files with validation 2. validate_dashboard_sql - Validates SQL queries and widget field references 3. get_widget_configuration_guide - Provides widget configuration documentation Args: mcp_server: MCP server instance to register tools with """ @mcp_server.tool() def create_dashboard_file( name: str, warehouse_id: str, datasets: List[Dict[str, str]], file_path: str, widgets: List[Dict[str, Any]] = None, validate_sql: bool = True, catalog: str = None, schema: str = None, ) -> Dict[str, Any]: r"""Creates a complete .lvdash.json file compatible with Databricks Lakeview dashboards. IMPORTANT: This tool creates the dashboard file directly on the filesystem at the specified path. The file will be saved automatically and you'll receive confirmation of successful creation. COMPREHENSIVE WIDGET SUPPORT: This tool supports all Lakeview widget types with full schema compliance and advanced configuration options. All widgets support positioning, styling, and interactive features according to Lakeview specifications. DATASET OPTIMIZATION GUIDANCE: Prefer fewer raw datasets with widget-level transformations over multiple pre-aggregated datasets. This approach: - Supports more widgets with fewer datasets - Improves performance through Lakeview's native aggregation - Simplifies maintenance and modifications - Provides single source of truth per data source Example optimization: Instead of: Multiple datasets (raw_sales, monthly_sales, product_sales) Use: Single raw dataset + widget expressions for aggregations Widget Expression Support: All widgets support field transformations using {field_key}_expression pattern: - Direct SQL: "y_expression": "SUM(`revenue`)" - Date functions: "x_expression": "DATE_TRUNC('MONTH', `date`)" - Binning: "x_expression": "BIN_FLOOR(`score`, 10)" - Helper functions available: get_aggregation_expression(), get_date_trunc_expression(), etc. Args: name: Dashboard display name (appears at the top of the dashboard) warehouse_id: SQL warehouse ID for running queries datasets: List of data sources, each with structure: { "name": "Human readable name", # Display name for the dataset "query": "SELECT col1, col2 FROM table" # SQL query (single or multi-line) OR # Examples: ''' # Simple single-line query SELECT product_name, SUM(revenue) as total_revenue FROM sales_data WHERE date >= '2024-01-01' GROUP BY product_name ORDER BY total_revenue DESC ''', # Complex multi-line query "parameters": [ # Optional: Query parameters { "displayName": "param_name", # Parameter display name "keyword": "param_name", # Parameter keyword in query "dataType": "STRING|DATE|NUMBER", # Parameter data type "defaultSelection": { # Default parameter value "values": {"dataType": "STRING", "values": [{"value": "default"}]} } } ] } widgets: List of widgets, each with structure: { "type": "bar|line|counter|table|pie|...", # Widget type (16 types supported) "dataset": "Dataset Name", # Must match dataset name "config": { # Widget-specific configuration "x_field": "column_name", # X-axis field (charts) "y_field": "column_name", # Y-axis field (charts) "color_field": "column_name", # Color grouping (optional) "value_field": "column_name", # Value field (counters) "category_field": "column_name", # Category field (pie charts) "columns": ["col1", "col2"], # Table columns (tables) "title": "Widget Title", # Custom widget title "show_title": true # Show/hide title }, "position": { # Optional: Custom positioning "x": 0, "y": 0, # Grid coordinates "width": 6, "height": 4 # Size in grid units (12-column grid) } } file_path: Path to save the dashboard JSON file validate_sql: Whether to validate SQL queries before creating dashboard (default: True) - Checks SQL syntax using LIMIT 0 queries - Validates table/view existence and permissions - Verifies widget field references against actual columns - Provides detailed error messages for debugging catalog: Optional catalog name for SQL execution context Used for validation and three-part table names schema: Optional schema name for SQL execution context Used for validation and three-part table names Widget Types Supported: Charts: bar, line, area, scatter, pie, histogram, heatmap, box Display: counter, table, pivot, text Maps: choropleth-map Advanced: sankey (flow diagrams) Filters: filter-single-select, filter-multi-select, filter-date-range Returns: { "success": true, "file_path": "path/to/dashboard.lvdash.json", "content": "...complete JSON content as string...", "file_size": 1234, "message": ("Dashboard file successfully created at " "path/to/dashboard.lvdash.json (1234 bytes)"), "validation_results": { # If validate_sql=True "queries_validated": [ # SQL validation results { "dataset": "Dataset Name", "valid": true, "error": null, "columns": ["col1", "col2", "col3"], "message": "Query validated successfully. Found 3 columns: col1, col2, col3" } ], "widget_validations": [ # Widget field validation results { "widget_type": "bar", "dataset": "Dataset Name", "valid": true, "error": null, "warnings": [] } ], "warnings": [] # Any warnings encountered } } Examples: # Single-line query dashboard (generates queryLines: ["SELECT..."]) result = create_dashboard_file( name="Simple Sales Dashboard", warehouse_id="abc123", datasets=[{ "name": "Sales Data", "query": "SELECT product, revenue FROM sales_transactions WHERE revenue > 100" }], widgets=[{ "type": "bar", "dataset": "Sales Data", "config": {"x_field": "product", "y_field": "revenue"} }], file_path="path/simple_sales_dashboard.lvdash.json" ) # File is automatically saved! Check result["success"] for confirmation # Multi-line query dashboard (generates queryLines: ["SELECT \\n", # " product, \\n", ...]) result = create_dashboard_file( name="Advanced Sales Dashboard", warehouse_id="abc123", datasets=[{ "name": "Sales Analysis", "query": \"\"\" SELECT product, SUM(revenue) as total_revenue, COUNT(*) as sales_count FROM sales_transactions WHERE sales_date >= '2024-01-01' GROUP BY product ORDER BY total_revenue DESC \"\"\" }], widgets=[{ "type": "bar", "dataset": "Sales Analysis", "config": {"x_field": "product", "y_field": "total_revenue"} }], file_path="path/advanced_sales_dashboard.lvdash.json" ) # File is automatically saved! Check result["success"] for confirmation # Mixed format dashboard with parameters result = create_dashboard_file( name="Executive Dashboard", warehouse_id="analytics_warehouse", datasets=[ { "name": "Revenue Metrics", # Multi-line query generates proper queryLines array "query": \"\"\" SELECT region, SUM(revenue) as total_revenue, COUNT(DISTINCT customer_id) as unique_customers FROM sales WHERE date >= :start_date GROUP BY region ORDER BY total_revenue DESC \"\"\", "parameters": [{ "displayName": "Start Date", "keyword": "start_date", "dataType": "DATE", "defaultSelection": { "values": {"dataType": "DATE", "values": [{"value": "2024-01-01"}]} } }] }, { "name": "Customer Count", # Single-line query generates queryLines: ["SELECT COUNT(*)..."] "query": "SELECT COUNT(*) as total_customers FROM customers WHERE active = true" } ], widgets=[ {"type": "counter", "dataset": "Revenue Metrics", "config": {"value_field": "total_revenue", "title": "Total Revenue"}}, {"type": "bar", "dataset": "Revenue Metrics", "config": {"x_field": "region", "y_field": "total_revenue"}}, {"type": "counter", "dataset": "Customer Count", "config": {"value_field": "total_customers", "title": "Total Customers"}} ], file_path="path/executive_dashboard.lvdash.json", catalog="production", schema="analytics" ) # File is automatically saved! Check result["success"] for confirmation # DATASET OPTIMIZATION EXAMPLE - Single raw dataset with widget-level transformations result = create_dashboard_file( name="Optimized Sales Dashboard", warehouse_id="analytics_warehouse", datasets=[ { "name": "Raw Sales Data", "query": \"\"\" SELECT product, region, revenue, order_date, customer_id FROM sales_transactions WHERE order_date >= '2024-01-01' \"\"\" } ], widgets=[ # Monthly revenue trend using date truncation { "type": "line", "dataset": "Raw Sales Data", "config": { "x_field": "month", "x_expression": "DATE_TRUNC('MONTH', `order_date`)", "y_field": "monthly_revenue", "y_expression": "SUM(`revenue`)", "title": "Monthly Revenue Trend" } }, # Product performance using aggregation { "type": "bar", "dataset": "Raw Sales Data", "config": { "x_field": "product", "y_field": "total_sales", "y_expression": "SUM(`revenue`)", "title": "Product Sales Performance" } }, # Customer count by region using count distinct { "type": "bar", "dataset": "Raw Sales Data", "config": { "x_field": "region", "y_field": "unique_customers", "y_expression": "COUNT(DISTINCT `customer_id`)", "title": "Unique Customers by Region" } } ], file_path="path/optimized_sales_dashboard.lvdash.json" ) # This approach uses 1 dataset to support 3 different aggregated visualizations! """ try: # Initialize widgets list if not provided if widgets is None: widgets = [] # Validate basic requirements before proceeding if not name or not warehouse_id or not file_path: return { 'success': False, 'error': 'Missing required parameters: name, warehouse_id, file_path', } if not datasets: return {'success': False, 'error': 'At least one dataset is required'} # Ensure file path has correct extension for Lakeview dashboards if not file_path.endswith('.lvdash.json'): file_path += '.lvdash.json' # Initialize validation results structure # This tracks all validation steps for comprehensive error reporting validation_results = {'queries_validated': [], 'widget_validations': [], 'warnings': []} # SQL Validation Phase - validates queries and widget field references if validate_sql: print('🔍 Starting SQL validation for dashboard datasets...') # Validate each dataset query against the Databricks warehouse for i, dataset in enumerate(datasets): query = dataset['query'] dataset_name = dataset['name'] print(f"🔍 Validating dataset '{dataset_name}' query...") validation_result = validate_sql_query(query, warehouse_id, catalog, schema) # Record validation result for this dataset validation_results['queries_validated'].append( { 'dataset': dataset_name, 'valid': validation_result['valid'], 'error': validation_result['error'], 'columns': validation_result['columns'], 'message': validation_result.get('message', ''), } ) # If query is invalid, return error immediately to prevent dashboard creation if not validation_result['valid']: return { 'success': False, 'error': ( f"Dataset '{dataset_name}' has invalid SQL query: {validation_result['error']}" ), 'validation_results': validation_results, } # Validate widgets that reference this dataset # This ensures widget field references match actual query columns dataset_columns = validation_result['columns'] for widget in widgets: if widget.get('dataset') == dataset_name: print( f"🔍 Validating widget '{widget.get('type', 'unknown')}' " f"fields against dataset '{dataset_name}'..." ) widget_validation = validate_widget_fields(widget, dataset_columns) # Record widget validation result validation_results['widget_validations'].append( { 'widget_type': widget.get('type', 'unknown'), 'dataset': dataset_name, 'valid': widget_validation['valid'], 'error': widget_validation['error'], 'warnings': widget_validation['warnings'], } ) # If widget field validation fails, return error to prevent dashboard creation if not widget_validation['valid']: return { 'success': False, 'error': f'Widget validation failed: {widget_validation["error"]}', 'validation_results': validation_results, } # Collect warnings for user awareness (non-blocking issues) validation_results['warnings'].extend(widget_validation['warnings']) print('✅ All SQL queries and widget fields validated successfully!') else: # Validation was skipped - note this for transparency validation_results['warnings'].append('SQL validation was skipped (validate_sql=False)') # Dashboard Creation Phase - generate the complete Lakeview JSON structure # Uses optimized layout algorithm for better widget positioning dashboard_json = create_optimized_dashboard_json( name, warehouse_id, datasets, widgets, enable_optimization=True ) # File Creation Phase - write dashboard JSON to filesystem result = prepare_dashboard_for_client(dashboard_json, file_path) # Include validation results in response for transparency result['validation_results'] = validation_results return result except Exception as e: # Catch-all error handler for unexpected issues during dashboard creation return {'success': False, 'error': f'Failed to create dashboard: {str(e)}'} @mcp_server.tool() def validate_dashboard_sql( datasets: List[Dict[str, str]], warehouse_id: str, widgets: List[Dict[str, Any]] = None, catalog: str = None, schema: str = None, ) -> Dict[str, Any]: """Validate SQL queries and widget field references for dashboard datasets. Comprehensive validation tool that checks SQL syntax, table existence, permissions, and widget field references before dashboard creation. Automatically handles proper queryLines format conversion during validation process. Args: datasets: List of data sources with structure matching create_dashboard_file: [ { "name": "Human readable name", "query": "SQL query (single-line or multi-line format supported)", "parameters": [ # Optional query parameters { "displayName": "param_name", "keyword": "param_name", "dataType": "STRING|DATE|NUMBER", "defaultSelection": { "values": {"dataType": "STRING", "values": [{"value": "default"}]} } } ] } ] warehouse_id: SQL warehouse ID for validation execution widgets: List of widgets with structure matching create_dashboard_file: [ { "type": "bar|line|counter|table|pie|...", "dataset": "Dataset Name", # Must match dataset name "config": { "x_field": "column_name", # Will be validated "y_field": "column_name", "color_field": "column_name", "value_field": "column_name", "category_field": "column_name", "columns": ["col1", "col2"] } } ] catalog: Optional catalog name for SQL execution context Used for three-part table names (catalog.schema.table) schema: Optional schema name for SQL execution context Used for three-part table names (catalog.schema.table) Validation Checks Performed: SQL Validation: - Syntax validation using LIMIT 0 queries - Table/view existence verification - Permission checks (SELECT access) - Column discovery for widget field validation - Parameter validation (if parameters provided) Widget Validation: - Field existence in dataset columns - Widget-type specific field requirements - Field name case sensitivity checks - Missing field detection with suggestions Returns: { "success": true|false, "message": "Validation summary message", "error": "Error description (if success=false)", "validation_results": { "queries_validated": [ { "dataset": "Dataset Name", "valid": true|false, "error": "Error message or null", "columns": ["col1", "col2", "col3"], # Available columns "message": "Validation success message" } ], "widget_validations": [ { "widget_type": "bar", "dataset": "Dataset Name", "valid": true|false, "error": "Missing field details or null", "warnings": ["Warning messages"] } ], "warnings": ["General warnings"] } } Common Error Types: SQL Errors: - "TABLE_OR_VIEW_NOT_FOUND": Table doesn't exist or wrong name - "PARSE_SYNTAX_ERROR": SQL syntax issues - "PERMISSION_DENIED": No SELECT access to table/warehouse Widget Errors: - Field reference errors: "Widget 'bar' references fields that don't exist" - Available columns listed in error message for easy fixing - Type-specific requirements (e.g., pie charts need category_field + value_field) Examples: # Validate single-line query (would generate queryLines: ["SELECT..."]) validate_dashboard_sql( datasets=[{ "name": "Simple Sales", "query": "SELECT product, revenue FROM sales_transactions WHERE revenue > 100" }], warehouse_id="abc123", widgets=[{ "type": "bar", "dataset": "Simple Sales", "config": {"x_field": "product", "y_field": "revenue"} }] ) # Validate multi-line query (would generate queryLines array with line breaks preserved) validate_dashboard_sql( datasets=[{ "name": "Complex Sales", "query": \"\"\" SELECT product, SUM(revenue) as total_revenue, COUNT(*) as sales_count FROM sales_transactions WHERE revenue > 0 GROUP BY product ORDER BY total_revenue DESC \"\"\" }], warehouse_id="abc123", widgets=[{ "type": "bar", "dataset": "Complex Sales", "config": {"x_field": "product", "y_field": "total_revenue"} }] ) # Validate complex CTE query with parameters validate_dashboard_sql( datasets=[{ "name": "Regional Analysis", "query": \"\"\" WITH regional_sales AS ( SELECT region, revenue, customer_id, date FROM sales WHERE date >= :start_date ) SELECT region, SUM(revenue) as total, COUNT(DISTINCT customer_id) as unique_customers FROM regional_sales GROUP BY region ORDER BY total DESC \"\"\", "parameters": [{ "displayName": "Start Date", "keyword": "start_date", "dataType": "DATE", "defaultSelection": { "values": {"dataType": "DATE", "values": [{"value": "2024-01-01"}]} } }] }], warehouse_id="analytics_warehouse", widgets=[{ "type": "bar", "dataset": "Regional Analysis", "config": {"x_field": "region", "y_field": "total"} }], catalog="production", schema="analytics" ) """ try: # Initialize widgets list if not provided if widgets is None: widgets = [] # Initialize validation results structure validation_results = {'queries_validated': [], 'widget_validations': [], 'warnings': []} print('🔍 Starting SQL validation for dashboard datasets...') # Validate each dataset query - this is the standalone validation tool # Unlike create_dashboard_file, this continues validation even if errors are found for dataset in datasets: query = dataset['query'] dataset_name = dataset['name'] print(f"🔍 Validating dataset '{dataset_name}' query...") validation_result = validate_sql_query(query, warehouse_id, catalog, schema) validation_results['queries_validated'].append( { 'dataset': dataset_name, 'valid': validation_result['valid'], 'error': validation_result['error'], 'columns': validation_result['columns'], 'message': validation_result.get('message', ''), } ) # Continue validation even if one query fails (collect all errors) if validation_result['valid']: # Validate widgets that reference this dataset dataset_columns = validation_result['columns'] for widget in widgets: if widget.get('dataset') == dataset_name: print( f"🔍 Validating widget '{widget.get('type', 'unknown')}' " f"fields against dataset '{dataset_name}'..." ) widget_validation = validate_widget_fields(widget, dataset_columns) validation_results['widget_validations'].append( { 'widget_type': widget.get('type', 'unknown'), 'dataset': dataset_name, 'valid': widget_validation['valid'], 'error': widget_validation['error'], 'warnings': widget_validation['warnings'], } ) # Collect warnings validation_results['warnings'].extend(widget_validation['warnings']) # Check if any validation failed query_failures = [q for q in validation_results['queries_validated'] if not q['valid']] widget_failures = [w for w in validation_results['widget_validations'] if not w['valid']] if query_failures or widget_failures: error_messages = [] if query_failures: error_messages.extend([f"Dataset '{q['dataset']}': {q['error']}" for q in query_failures]) if widget_failures: error_messages.extend( [f"Widget '{w['widget_type']}': {w['error']}" for w in widget_failures] ) return { 'success': False, 'error': 'Validation failed. Issues found: ' + '; '.join(error_messages), 'validation_results': validation_results, } print('✅ All SQL queries and widget fields validated successfully!') return { 'success': True, 'message': 'All SQL queries and widget field references are valid', 'validation_results': validation_results, } except Exception as e: return {'success': False, 'error': f'Validation failed with error: {str(e)}'} @mcp_server.tool() def get_widget_configuration_guide(widget_type: str = None) -> Dict[str, Any]: """Get comprehensive configuration guide for Lakeview dashboard widgets. This is a documentation and reference tool that provides detailed information about widget configuration options, field requirements, and best practices. Use this tool to understand available widget types and their configuration before creating dashboards. Provides detailed configuration options, required fields, examples, and best practices for creating dashboard widgets. Use this to understand available options before creating dashboards with create_dashboard_file. Args: widget_type: Optional specific widget type to get detailed info for. If not provided, returns overview of all widget types. Supported values: - Chart widgets: "bar", "line", "area", "scatter", "pie", "histogram", "heatmap", "box", "funnel", "combo" - Map widgets: "choropleth-map", "symbol-map" - Display widgets: "counter", "table", "pivot", "text" - Advanced widgets: "sankey" - Filter widgets: "filter-single-select", "filter-multi-select", "filter-date-range-picker", "range-slider" Returns: Comprehensive widget configuration guide with examples and best practices. """ # Widget categories for overview - organized by functional purpose # This categorization helps users understand widget types and their use cases widget_categories = { 'chart': [ 'bar', # Bar charts for categorical comparisons 'line', # Line charts for trends over time 'area', # Area charts for cumulative values 'scatter', # Scatter plots for correlation analysis 'pie', # Pie charts for part-to-whole relationships 'histogram', # Histograms for distribution analysis 'heatmap', # Heatmaps for matrix/correlation visualization 'box', # Box plots for statistical distribution 'funnel', # Funnel charts for conversion analysis 'combo', # Combination charts (multiple chart types) ], 'map': ['choropleth-map', 'symbol-map'], # Geographic visualizations 'display': ['counter', 'table', 'pivot', 'text'], # Data display widgets 'advanced': ['sankey'], # Advanced flow/relationship visualizations 'filter': [ # Interactive filter controls for dashboard interactivity 'filter-single-select', 'filter-multi-select', 'filter-date-range-picker', 'range-slider', ], } if widget_type is None: return { 'widget_categories': widget_categories, 'quick_reference': { 'common_fields': [ 'x_field', 'y_field', 'color_field', 'size_field', 'value_field', 'category_field', ], 'scale_types': ['categorical', 'quantitative', 'temporal'], 'color_schemes': ['redblue', 'viridis', 'plasma', 'inferno', 'magma'], 'positioning': {'grid_columns': 12, 'auto_layout': '2-column'}, 'table_column_types': ['string', 'integer', 'float', 'date', 'boolean'], 'table_display_types': ['string', 'number', 'datetime', 'link', 'image'], }, 'transformation_examples': { 'description': 'Use {field_key}_expression for custom SQL transformations', 'examples': [ { 'config': { 'x_field': 'revenue', 'x_expression': 'SUM(`revenue`)', 'y_field': 'date', 'y_expression': "DATE_TRUNC('MONTH', `date`)", }, 'description': 'Monthly revenue aggregation', }, { 'config': { 'x_field': 'score', 'x_expression': 'BIN_FLOOR(`score`, 10)', 'y_field': 'count', 'y_expression': 'COUNT(`*`)', }, 'description': 'Score distribution histogram', }, ], }, 'common_patterns': { 'aggregations': [ 'SUM(`field`)', 'AVG(`field`)', 'COUNT(`field`)', 'COUNT(DISTINCT `field`)', ], 'date_functions': [ "DATE_TRUNC('MONTH', `date`)", "DATE_TRUNC('DAY', `timestamp`)", "DATE_TRUNC('YEAR', `created_at`)", ], 'binning': [ 'BIN_FLOOR(`value`, 10)', 'BIN_FLOOR(`score`, 5)', 'BIN_FLOOR(`amount`, 100)', ], 'helper_functions': { "get_aggregation_expression('revenue', 'sum')": 'SUM(`revenue`)', "get_date_trunc_expression('date', 'month')": "DATE_TRUNC('MONTH', `date`)", "get_bin_expression('score', 10)": 'BIN_FLOOR(`score`, 10)', 'get_count_star_expression()': 'COUNT(`*`)', }, }, 'dataset_optimization': { 'description': 'Prefer widget-level transformations over multiple datasets', 'best_practices': [ 'Use one raw dataset per data source', 'Apply aggregations at widget level with expressions', 'Avoid creating pre-aggregated datasets for each visualization', 'Let Lakeview handle widget-level aggregations efficiently', ], 'example': { 'recommended': 'Single dataset + widget expressions', 'avoid': 'Multiple pre-aggregated datasets', }, 'benefits': [ 'Fewer datasets to manage', "Better performance through Lakeview's native aggregation", 'More flexible - easy to change aggregations', 'Single source of truth per data source', ], }, 'usage': ( 'Call this function with a specific widget_type parameter to get ' 'detailed configuration options for that widget type.' ), } # Detailed configurations for specific widget types widget_configs = { 'bar': { 'description': 'Bar charts for categorical data visualization', 'version': 3, 'required_fields': ['x_field', 'y_field'], 'optional_fields': { 'color_field': 'Field for color grouping/series', 'x_scale_type': 'Scale type for x-axis (categorical, quantitative, temporal)', 'y_scale_type': 'Scale type for y-axis', 'show_labels': 'Show data labels on bars', 'colors': 'Custom color palette array', 'title': 'Widget title', 'x_axis_title': 'Custom x-axis title', 'y_axis_title': 'Custom y-axis title', 'x_expression': ( 'Custom SQL expression for x-axis (e.g., \'DATE_TRUNC("MONTH", `date`)\')' ), 'y_expression': "Custom SQL expression for y-axis (e.g., 'SUM(`revenue`)')", }, 'examples': [ { 'name': 'Simple bar chart', 'config': {'x_field': 'region', 'y_field': 'sales', 'title': 'Sales by Region'}, }, { 'name': 'Grouped bar chart', 'config': { 'x_field': 'region', 'y_field': 'sales', 'color_field': 'product_category', 'colors': ['#1f77b4', '#ff7f0e', '#2ca02c'], 'show_labels': True, }, }, { 'name': 'Monthly revenue aggregation (with expressions)', 'config': { 'x_field': 'month', 'x_expression': "DATE_TRUNC('MONTH', `order_date`)", 'y_field': 'total_revenue', 'y_expression': 'SUM(`revenue`)', 'title': 'Monthly Revenue Totals', }, }, { 'name': 'Product sales with count aggregation', 'config': { 'x_field': 'product', 'y_field': 'order_count', 'y_expression': 'COUNT(`order_id`)', 'title': 'Orders by Product', }, }, ], }, 'funnel': { 'description': 'Funnel charts for conversion and step-wise data analysis', 'version': 3, 'required_fields': ['value_field'], 'preferred_fields': ['stage_field', 'value_field'], 'fallback_fields': ['category_field', 'x_field', 'color_field'], 'field_notes': ( 'If stage_field is not provided, the system will attempt ' 'to use category_field, x_field, or color_field as the ' 'categorical dimension' ), 'optional_fields': { 'stage_display_name': 'Display name for stage field', 'value_display_name': 'Display name for value field', 'title': 'Widget title', }, 'examples': [ { 'name': 'Customer conversion funnel', 'config': { 'stage_field': 'tier', 'value_field': 'customers', 'title': 'Customer Loyalty Funnel', }, }, { 'name': 'Sales pipeline funnel', 'config': { 'stage_field': 'stage', 'value_field': 'deals', 'stage_display_name': 'Sales Stage', 'value_display_name': 'Deal Count', }, }, { 'name': 'Flight volume funnel (using fallback)', 'config': { 'value_field': 'total_flights', 'category_field': 'UniqueCarrier', 'title': 'Flight Volume Funnel by Carrier', }, }, ], }, 'symbol-map': { 'description': 'Point-based geographic visualizations with latitude/longitude coordinates', 'version': 3, 'required_fields': ['latitude_field', 'longitude_field'], 'optional_fields': { 'size_field': 'Field for point size encoding', 'color_field': 'Field for color encoding', 'color_scale_type': 'Scale type for color (categorical or quantitative)', 'title': 'Widget title', }, 'examples': [ { 'name': 'Store locations with revenue', 'config': { 'latitude_field': 'lat', 'longitude_field': 'lng', 'size_field': 'store_size', 'color_field': 'revenue', 'title': 'Store Performance Map', }, } ], }, 'table': { 'description': 'Data tables with advanced formatting and interactive features', 'version': 1, 'required_fields': ['columns'], 'optional_fields': { 'items_per_page': 'Number of items per page', 'condensed': 'Use condensed table layout', 'with_row_number': 'Show row numbers', 'title': 'Widget title', }, 'examples': [ {'name': 'Simple table', 'config': {'columns': ['name', 'revenue', 'date']}}, { 'name': 'Advanced formatted table', 'config': { 'columns': [ {'field': 'name', 'title': 'Customer', 'type': 'string'}, { 'field': 'revenue', 'title': 'Revenue', 'type': 'float', 'display_as': 'number', 'number_format': '$,.0f', }, { 'field': 'date', 'title': 'Date', 'type': 'date', 'display_as': 'datetime', 'date_format': 'MMM DD, YYYY', }, ], 'items_per_page': 25, 'title': 'Customer Revenue Table', }, }, ], }, 'filter-single-select': { 'description': 'Single-select dropdown filter for dashboard interactivity', 'version': 2, 'required_fields': ['field'], 'optional_fields': { 'display_name': 'Display name for the filter field', 'title': 'Widget title', 'default_field': 'Default field if none specified', }, 'examples': [ { 'name': 'State filter', 'config': {'field': 'state', 'display_name': 'State', 'title': 'Filter by State'}, }, { 'name': 'Category filter', 'config': { 'field': 'category', 'display_name': 'Product Category', 'title': 'Filter by Category', }, }, ], }, 'filter-multi-select': { 'description': 'Multi-select dropdown filter for dashboard interactivity', 'version': 2, 'required_fields': ['field'], 'optional_fields': { 'display_name': 'Display name for the filter field', 'title': 'Widget title', }, 'examples': [ { 'name': 'Regions filter', 'config': {'field': 'region', 'display_name': 'Region', 'title': 'Select Regions'}, } ], }, 'filter-date-range-picker': { 'description': 'Date range picker filter for temporal data filtering', 'version': 2, 'required_fields': ['field'], 'optional_fields': { 'display_name': 'Display name for the date field', 'title': 'Widget title', }, 'examples': [ { 'name': 'Date range filter', 'config': {'field': 'date', 'display_name': 'Date Range', 'title': 'Select Date Range'}, } ], }, } if widget_type in widget_configs: return { 'widget_type': widget_type, **widget_configs[widget_type], 'transformation_support': { 'description': 'All widgets support field expressions for custom SQL transformations', 'pattern': ( "Use {field_key}_expression for custom SQL (e.g., 'y_expression': 'SUM(`revenue`)')" ), 'helper_functions': [ 'get_aggregation_expression(field, func) - Generate aggregation expressions', 'get_date_trunc_expression(field, interval) - Generate date truncation expressions', 'get_bin_expression(field, width) - Generate binning expressions', 'get_count_star_expression() - Generate count(*) expression', ], }, 'best_practices': [ 'Ensure field names match exactly with dataset columns', 'Use appropriate scale types for data types', 'Consider widget positioning in 12-column grid', 'Add meaningful titles for better dashboard readability', 'Prefer widget-level transformations over multiple datasets', 'Use helper functions for common SQL patterns', 'Always wrap field names in backticks in expressions', ], } else: return { 'error': f"Widget type '{widget_type}' not recognized", 'supported_types': [item for sublist in widget_categories.values() for item in sublist], }

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/PulkitXChadha/awesome-databricks-mcp'

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