Skip to main content
Glama
sdelements

SD Elements MCP Server

Official
by sdelements
reports.py15.2 kB
"""Advanced report tools""" import json from typing import Any, Dict, Optional, Union from fastmcp import Context from pydantic import BaseModel, field_validator from ..server import mcp, api_client, init_api_client class CubeQuery(BaseModel): """Cube API query structure""" schema: str # Field name for Cube API dimensions: list[str] measures: list[str] filters: Optional[list[Dict[str, Any]]] = None order: Optional[list[list[str]]] = None limit: Optional[int] = None time_dimensions: Optional[list[Dict[str, Any]]] = None @classmethod def parse_obj_or_str(cls, obj: Union[str, Dict[str, Any]]) -> "CubeQuery": """Parse from either a dict or JSON string""" if isinstance(obj, str): try: obj = json.loads(obj) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON: {e}") return cls(**obj) class Config: # Allow 'schema' as a field name even though it's a Python keyword populate_by_name = True class ChartMeta(BaseModel): """Chart metadata structure""" columnOrder: Optional[list[str]] = None def suggest_json_fix(json_str: str, error: json.JSONDecodeError) -> str: """Suggest fixes for common JSON typos based on error position""" suggestions = [] pos = error.pos if hasattr(error, 'pos') else None if pos is not None and pos < len(json_str): # Check for common issues around the error position context_start = max(0, pos - 20) context_end = min(len(json_str), pos + 20) context = json_str[context_start:context_end] # Check for missing quotes if 'member' in context and '"member' not in context and "'member" not in context: suggestions.append("Missing quotes around 'member' property") if 'values' in context and '"values' not in context and "'values" not in context: suggestions.append("Missing quotes around 'values' property") if 'operator' in context and '"operator' not in context and "'operator" not in context: suggestions.append("Missing quotes around 'operator' property") # Check for missing commas if '][' in context or '}{' in context: suggestions.append("Missing comma between array/object elements") # Check for concatenated strings if ',pleteCount' in context or ',teCount' in context: suggestions.append("Strings appear concatenated - ensure each string is separate: [\"Task.count\",\"Task.completeCount\"]") # Check for missing brackets if context.count('[') != context.count(']'): suggestions.append("Mismatched square brackets") if context.count('{') != context.count('}'): suggestions.append("Mismatched curly braces") if suggestions: return " Possible issues: " + "; ".join(suggestions) return "" def parse_query_param(query: Union[str, Dict[str, Any], Any]) -> tuple[Dict[str, Any], Optional[str]]: """ Parse query parameter from string or dict, handling FastMCP serialization. Returns (parsed_query_dict, error_message) Handles multiple encoding scenarios: 1. Direct dict (already parsed) 2. JSON string (needs parsing) 3. Double-encoded JSON string (FastMCP may encode twice) 4. Other types (attempts to convert) """ # If it's already a dict, use it directly if isinstance(query, dict): return query, None # Convert to string if it's not already query_str = str(query) if not isinstance(query, str) else query # Try to parse as JSON, handling multiple encoding layers max_attempts = 3 parsed = query_str for attempt in range(max_attempts): try: if isinstance(parsed, str): parsed = json.loads(parsed) else: break # Already parsed to a non-string except json.JSONDecodeError as e: if attempt == max_attempts - 1: # Final attempt failed - return detailed error with suggestions suggestion_text = suggest_json_fix(query_str, e) error_msg = { "error": "Invalid JSON in query parameter", "json_error": str(e), "position": f"Line {e.lineno}, Column {e.colno}" if hasattr(e, 'lineno') else f"Position {e.pos}" if hasattr(e, 'pos') else "Unknown", "received_type": type(query).__name__, "received_value_preview": query_str[:500] if len(query_str) > 500 else query_str, "attempt": attempt + 1, "suggestion": "Ensure the query is valid JSON. Check for missing quotes, commas, or brackets." + suggestion_text } return None, json.dumps(error_msg, indent=2) # Try again with the parsed result (might be double-encoded) continue # Validate that we got a dict if not isinstance(parsed, dict): return None, json.dumps({ "error": "Query parameter must be a JSON object/dictionary", "received_type": type(query).__name__, "parsed_type": type(parsed).__name__, "parsed_value_preview": str(parsed)[:200], "suggestion": "The query must be a JSON object, not an array or primitive value." }, indent=2) return parsed, None @mcp.tool() async def list_advanced_reports(ctx: Context) -> str: """List all advanced reports""" global api_client if api_client is None: api_client = init_api_client() result = api_client.list_advanced_reports() return json.dumps(result, indent=2) @mcp.tool() async def get_advanced_report(ctx: Context, report_id: int) -> str: """Get details of a specific advanced report""" global api_client if api_client is None: api_client = init_api_client() result = api_client.get_advanced_report(report_id) return json.dumps(result, indent=2) @mcp.tool() async def update_advanced_report( ctx: Context, report_id: int, title: Optional[str] = None, chart: Optional[str] = None, query: Optional[Union[str, Dict[str, Any]]] = None, description: Optional[str] = None, chart_meta: Optional[Union[str, Dict[str, Any]]] = None, type: Optional[str] = None, ) -> str: """Update an existing advanced report. Provide only the fields you want to update. The query and chart_meta parameters can be JSON strings or dicts.""" global api_client if api_client is None: api_client = init_api_client() data = {} if title is not None: data["title"] = title if chart is not None: data["chart"] = chart if query is not None: query_dict, error = parse_query_param(query) if error: return error try: cube_query = CubeQuery(**query_dict) data["query"] = cube_query.model_dump() except ValueError as e: return json.dumps({ "error": "Invalid query parameter", "details": str(e), "suggestion": "Ensure query has required fields: schema, dimensions, measures" }, indent=2) if description is not None: data["description"] = description if chart_meta is not None: if isinstance(chart_meta, dict): chart_meta_dict = chart_meta else: chart_meta_str = str(chart_meta) parsed_meta = chart_meta_str for _ in range(3): try: if isinstance(parsed_meta, str): parsed_meta = json.loads(parsed_meta) else: break except json.JSONDecodeError as e: return json.dumps({ "error": "Invalid chart_meta parameter", "json_error": str(e), "position": f"Line {e.lineno}, Column {e.colno}" if hasattr(e, 'lineno') else "Unknown" }, indent=2) if not isinstance(parsed_meta, dict): return json.dumps({ "error": "Invalid chart_meta parameter", "details": f"Expected dict, got {type(parsed_meta).__name__}" }, indent=2) chart_meta_dict = parsed_meta try: chart_meta_obj = ChartMeta(**chart_meta_dict) data["chart_meta"] = chart_meta_obj.model_dump() except ValueError as e: return json.dumps({ "error": "Invalid chart_meta parameter", "details": str(e) }, indent=2) if type is not None: data["type"] = type if not data: return json.dumps({"error": "No update data provided. Specify at least one field to update."}, indent=2) result = api_client.update_advanced_report(report_id, data) return json.dumps(result, indent=2) @mcp.tool() async def run_advanced_report(ctx: Context, report_id: int, format: Optional[str] = None) -> str: """Run an advanced report""" global api_client if api_client is None: api_client = init_api_client() params = {} if format is not None: params["format"] = format result = api_client.run_advanced_report(report_id, params) return json.dumps(result, indent=2) @mcp.tool() async def create_advanced_report( ctx: Context, title: str, chart: str, query: Union[str, Dict[str, Any]], description: Optional[str] = None, chart_meta: Optional[Union[str, Dict[str, Any]]] = None, type: Optional[str] = None, ) -> str: """Create a new advanced report. The query parameter can be a JSON string or dict with schema, dimensions, measures, filters, order, and limit. The chart_meta parameter can be a JSON string or dict if provided. Example query: {"schema": "application", "dimensions": ["Project.name"], "measures": ["Task.count"]} Example chart_meta: {"columnOrder": ["Project.name", "Task.count"]} """ global api_client if api_client is None: api_client = init_api_client() # Parse query (handles both dict and JSON string, including FastMCP double-encoding) query_dict, error = parse_query_param(query) if error: return error try: # Validate using Pydantic model cube_query = CubeQuery(**query_dict) query_dict = cube_query.model_dump() except ValueError as e: return json.dumps({ "error": "Invalid query parameter", "details": str(e), "suggestion": "Ensure query is valid JSON with schema, dimensions, and measures fields" }, indent=2) # Parse chart_meta if provided (handles FastMCP double-encoding) chart_meta_dict = None if chart_meta is not None: if isinstance(chart_meta, dict): chart_meta_dict = chart_meta else: # Parse as JSON string, handling multiple encoding layers chart_meta_str = str(chart_meta) parsed_meta = chart_meta_str for _ in range(3): # Try up to 3 layers of encoding try: if isinstance(parsed_meta, str): parsed_meta = json.loads(parsed_meta) else: break except json.JSONDecodeError as e: return json.dumps({ "error": "Invalid chart_meta parameter", "json_error": str(e), "position": f"Line {e.lineno}, Column {e.colno}" if hasattr(e, 'lineno') else "Unknown", "received_preview": chart_meta_str[:200] }, indent=2) if not isinstance(parsed_meta, dict): return json.dumps({ "error": "Invalid chart_meta parameter", "details": f"Expected dict, got {type(parsed_meta).__name__}" }, indent=2) chart_meta_dict = parsed_meta try: chart_meta_obj = ChartMeta(**chart_meta_dict) chart_meta_dict = chart_meta_obj.model_dump() except ValueError as e: return json.dumps({ "error": "Invalid chart_meta parameter", "details": str(e) }, indent=2) data = {"title": title, "chart": chart, "query": query_dict} if description: data["description"] = description if chart_meta_dict: data["chart_meta"] = chart_meta_dict if type is not None: data["type"] = type result = api_client.create_advanced_report(data) return json.dumps(result, indent=2) @mcp.tool() async def execute_cube_query(ctx: Context, query: Union[str, Dict[str, Any]]) -> str: """Execute a Cube API query for advanced analytics. The query parameter can be a JSON string or dict. Query structure (see https://docs.sdelements.com/master/cubeapi/): - schema: Required. One of: activity, application, countermeasure, integration, library, project_survey_answers, training, trend_application, trend_projects, trend_tasks, user - dimensions: Required. Array like ["Application.name", "Project.id"] - measures: Required. Array like ["Project.count", "Task.completeCount"] - filters: Optional. Array of objects with member, operator (equals/contains/gt/etc), values - order: Optional. 2D array like [["Application.name", "asc"], ["Project.count", "desc"]] - limit: Optional. Number to limit results - time_dimensions: Optional. For Trend Reports only (trend_application, trend_projects, trend_tasks) Example: {"schema": "application", "dimensions": ["Application.name"], "measures": ["Project.count"], "limit": 10}""" global api_client if api_client is None: api_client = init_api_client() # Parse query (handles both dict and JSON string) try: if isinstance(query, str): query_dict = json.loads(query) else: query_dict = query # Validate using Pydantic model cube_query = CubeQuery(**query_dict) parsed_query = cube_query.model_dump() except (json.JSONDecodeError, ValueError) as e: return json.dumps({ "error": "Invalid query parameter", "details": str(e), "suggestion": "Ensure query is valid JSON with schema, dimensions, and measures fields" }, indent=2) # Basic validation if "schema" not in parsed_query: return json.dumps({"error": "Query must include 'schema' field (e.g., 'application', 'countermeasure', 'user')"}, indent=2) if "dimensions" not in parsed_query and "measures" not in parsed_query: return json.dumps({"error": "Query must include at least one of 'dimensions' or 'measures'"}, indent=2) result = api_client.execute_cube_query(parsed_query) return json.dumps(result, indent=2)

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/sdelements/sde-mcp'

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