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()
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions generating a 'comprehensive' summary but doesn't specify output format (e.g., structured data vs. text report), data sources, permissions required, rate limits, or whether it's a read-only operation. This is inadequate for a tool that presumably queries and aggregates data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is efficiently structured: a clear purpose statement followed by an 'Args' section with parameter explanations. Every sentence adds value, with no redundant information. The two-sentence format is appropriately front-loaded with the core functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of generating a 'comprehensive' summary with metrics, no annotations, no output schema, and 0% schema coverage, the description is incomplete. It doesn't explain what 'comprehensive' includes (e.g., specific metrics like velocity or burndown), output format, or behavioral characteristics like error handling or data freshness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It provides clear examples for both parameters ('project_name' with 'ASTRA', 'sprint_name' with 'Sprint 233' or '233'), adding practical meaning beyond the bare schema. However, it doesn't explain constraints like valid project/sprint names or format requirements.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Generate a comprehensive sprint summary with task details, status distribution, and key metrics.' This specifies the verb ('generate'), resource ('sprint summary'), and scope ('comprehensive'), though it doesn't explicitly differentiate from sibling tools like 'get_goodday_sprint_tasks' or 'get_goodday_smart_query'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention sibling tools like 'get_goodday_sprint_tasks' (which might list tasks without summary metrics) or 'get_goodday_smart_query' (which could potentially generate similar reports), leaving the agent without context for tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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