Skip to main content
Glama
cdmx-in
by cdmx-in

get_goodday_sprint_summary

Generate sprint summaries with task details, status distribution, and key metrics from Goodday project management data.

Instructions

Generate a comprehensive sprint summary with task details, status distribution, and key metrics.

Args: project_name: The name of the main project (e.g., "ASTRA") sprint_name: The name or number of the sprint (e.g., "Sprint 233", "233")

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_nameYes
sprint_nameYes

Implementation Reference

  • Core handler function implementing the get_goodday_sprint_summary tool. It finds the project and sprint, fetches tasks, analyzes status distribution, task assignments, fetches descriptions, and generates a formatted summary report.
    @mcp.tool()
    async def get_goodday_sprint_summary(project_name: str, sprint_name: str) -> str:
        """Generate a comprehensive sprint summary with task details, status distribution, and key metrics.
    
        Args:
            project_name: The name of the main project (e.g., "ASTRA")
            sprint_name: The name or number of the sprint (e.g., "Sprint 233", "233")
        """
        # Find main project
        matched_project, available_projects = await find_project_by_name(project_name)
        if not matched_project:
            return f"Project '{project_name}' not found. Available projects: {', '.join(available_projects[:10])}{'...' if len(available_projects) > 10 else ''}"
    
        main_project_id = matched_project.get("id")
        actual_project_name = matched_project.get("name")
    
        # Find sprint project
        sprint_project, available_sprints = await find_sprint_by_name(main_project_id, sprint_name)
        if not sprint_project:
            if available_sprints:
                return f"Sprint '{sprint_name}' not found. Available sprints: {', '.join(available_sprints)}"
            else:
                return f"No sprints found under project '{actual_project_name}'."
    
        sprint_id = sprint_project.get("id")
        actual_sprint_name = sprint_project.get("name")
    
        # Get all tasks with closed tasks included
        endpoint = f"project/{sprint_id}/tasks?closed=true"
        tasks_data = await make_goodday_request(endpoint)
        if not tasks_data:
            return f"No tasks found in sprint '{actual_sprint_name}'."
        
        if isinstance(tasks_data, dict) and "error" in tasks_data:
            return f"Unable to fetch sprint tasks: {tasks_data.get('error', 'Unknown error')}"
        
        if not isinstance(tasks_data, list):
            return f"Unexpected response format: {str(tasks_data)}"
    
        # Get user mapping
        user_id_to_name = await get_user_mapping()
        
        def user_display(user_id):
            if not user_id:
                return "Unassigned"
            name = user_id_to_name.get(user_id)
            return name if name else f"User {user_id}"
    
        # Analyze tasks
        status_counts = {}
        user_task_counts = {}
        task_summaries = []
    
        for task in tasks_data:
            if not isinstance(task, dict):
                continue
    
            task_short_id = task.get("shortId", "N/A")
            task_name = task.get("name", "No title")
            status = task.get("status", {}) if isinstance(task.get("status"), dict) else {}
            status_name = status.get("name", "Unknown Status")
            assigned_user_id = task.get("assignedToUserId")
            assigned_user = user_display(assigned_user_id)
    
            # Count statistics
            status_counts[status_name] = status_counts.get(status_name, 0) + 1
            user_task_counts[assigned_user] = user_task_counts.get(assigned_user, 0) + 1
    
            # Get task description
            task_description = "No description available"
            task_id = task.get("id")
            if task_id:
                try:
                    messages_endpoint = f"task/{task_id}/messages"
                    messages_data = await make_goodday_request(messages_endpoint)
                    if messages_data and isinstance(messages_data, list) and len(messages_data) > 0:
                        first_msg = messages_data[0]
                        if isinstance(first_msg, dict):
                            task_description = first_msg.get("message", "No description available")
                except Exception:
                    pass
    
            task_summary = f"""
    **{task_short_id}**: {task_name}
    - **Status**: {status_name}
    - **Assigned To**: {assigned_user}
    - **Description**: {task_description}
    """.strip()
            task_summaries.append(task_summary)
    
        # Build summary
        summary_parts = []
        summary_parts.append(f"**Sprint Overview:**\n- **Sprint**: {actual_sprint_name}\n- **Project**: {actual_project_name}\n- **Total Tasks**: {len(tasks_data)}")
    
        if status_counts:
            status_list = [f"  - {status}: {count}" for status, count in sorted(status_counts.items())]
            summary_parts.append(f"**Status Distribution:**\n{chr(10).join(status_list)}")
    
        if user_task_counts:
            user_list = [f"  - {user}: {count} tasks" for user, count in sorted(user_task_counts.items(), key=lambda x: x[1], reverse=True)]
            summary_parts.append(f"**Task Assignment:**\n{chr(10).join(user_list)}")
    
        if task_summaries:
            summary_parts.append(f"**Task Details:**\n{chr(10).join(['---'] + task_summaries)}")
    
        result = "\n\n".join(summary_parts)
        return f"**Sprint Summary for '{actual_sprint_name}' in '{actual_project_name}':**\n\n{result}"
  • Helper function to locate the specific sprint project under a parent project by normalizing and matching sprint names/numbers.
    async def find_sprint_by_name(parent_project_id: str, sprint_name: str) -> tuple[Optional[dict], List[str]]:
        """Find sprint project by name within a parent project."""
        projects_data = await make_goodday_request("projects")
        if not projects_data or not isinstance(projects_data, list):
            return None, []
        
        normalized_sprint_name = sprint_name.lower().strip()
        if not normalized_sprint_name.startswith("sprint"):
            normalized_sprint_name = f"sprint {normalized_sprint_name}"
    
        available_sprints = []
        search_number = re.search(r"(\d+)", normalized_sprint_name)
        exact_match = None
        substring_match = None
    
        for proj in projects_data:
            if isinstance(proj, dict) and proj.get("systemType") == "PROJECT":
                sprint_proj_name = proj.get("name", "").lower().strip()
                if sprint_proj_name.startswith("sprint"):
                    available_sprints.append(proj.get("name", ""))
                    project_number = re.search(r"(\d+)", sprint_proj_name)
                    # Prefer exact number match
                    if (
                        search_number
                        and project_number
                        and search_number.group(1) == project_number.group(1)
                    ):
                        exact_match = proj
                    # Fallback: search number as substring anywhere in the sprint name
                    elif (
                        search_number and search_number.group(1) in sprint_proj_name
                    ):
                        if not substring_match:
                            substring_match = proj
                    elif normalized_sprint_name == sprint_proj_name:
                        if not exact_match:
                            exact_match = proj
                    elif (
                        normalized_sprint_name in sprint_proj_name
                        or sprint_proj_name in normalized_sprint_name
                    ):
                        if not substring_match:
                            substring_match = proj
    
        if exact_match:
            return exact_match, available_sprints
        if substring_match:
            return substring_match, available_sprints
        return None, available_sprints
  • Helper function to find the main project by case-insensitive name matching.
    async def find_project_by_name(project_name: str) -> tuple[Optional[dict], List[str]]:
        """Find project by name (case-insensitive)."""
        projects_data = await make_goodday_request("projects")
        if not projects_data or not isinstance(projects_data, list):
            return None, []
        
        # Filter out system projects (like sprints) to avoid overwhelming the AI
        filtered_projects = [
            proj for proj in projects_data 
            if isinstance(proj, dict) and proj.get("systemType") != "PROJECT"
        ]
        
        project_name_lower = project_name.lower().strip()
        matched_project = None
        for proj in filtered_projects:
            if not isinstance(proj, dict):
                continue
            current_project_name = proj.get("name", "").lower().strip()
            if current_project_name == project_name_lower:
                matched_project = proj
                break
            if (
                project_name_lower in current_project_name
                or current_project_name in project_name_lower
            ):
                matched_project = proj
                break
        
        available_projects = [
            p.get("name", "Unknown")
            for p in projects_data
            if isinstance(p, dict)
        ]
        return matched_project, available_projects
  • Helper to fetch and map user IDs to names, used for displaying assignee names in summaries.
    async def get_user_mapping() -> dict:
        """Get mapping of user IDs to names."""
        data = await make_goodday_request("users")
        user_id_to_name = {}
        if isinstance(data, list):
            for u in data:
                if isinstance(u, dict):
                    user_id_to_name[u.get("id")] = u.get("name", "Unknown")
        return user_id_to_name
  • MCP tool registration decorator applied to the handler function.
    @mcp.tool()

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/cdmx-in/goodday-mcp'

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