Skip to main content
Glama

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