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

get_goodday_sprint_tasks

Retrieve tasks from a specific sprint in Goodday projects by providing project and sprint details, with options to include closed items.

Instructions

Get tasks from a specific sprint by project name and sprint name/number.

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") include_closed: Whether to include closed tasks (default: True)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_nameYes
sprint_nameYes
include_closedNo

Implementation Reference

  • Primary handler for the 'get_goodday_sprint_tasks' MCP tool. Locates sprint by project and sprint names, fetches tasks via API, formats with user mappings, returns structured output. Includes registration via @mcp.tool() decorator.
    @mcp.tool()
    async def get_goodday_sprint_tasks(project_name: str, sprint_name: str, include_closed: bool = True) -> str:
        """Get tasks from a specific sprint by project name and sprint name/number.
    
        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")
            include_closed: Whether to include closed tasks (default: True)
        """
        # 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 tasks from sprint
        params = []
        if include_closed:
            params.append("closed=true")
        endpoint = f"project/{sprint_id}/tasks"
        if params:
            endpoint += "?" + "&".join(params)
    
        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}"
    
        # Format tasks
        formatted_tasks = []
        for task in tasks_data:
            if not isinstance(task, dict):
                continue
    
            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)
    
            formatted_task = f"""
    **{task.get('shortId', 'N/A')}**: {task.get('name', 'No title')}
    - **Status**: {status_name}
    - **Assigned To**: {assigned_user}
    - **Priority**: {task.get('priority', 'N/A')}
    """.strip()
            formatted_tasks.append(formatted_task)
    
        result = "\n---\n".join(formatted_tasks)
        return f"**Tasks in Sprint '{actual_sprint_name}' (Project: '{actual_project_name}') - {len(tasks_data)} tasks:**\n\n{result}"
  • Helper function to find the main project by name using fuzzy matching, filters out sprint projects, returns project data and list of available projects. Used by the tool to locate the parent project.
    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
  • Key helper to locate sprint sub-project by name or number under parent project using number matching and fuzzy search. Returns sprint data and available sprints list. Critical for tool's sprint identification.
    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 to fetch and cache user ID to name mapping from API, used for displaying assignee names in task outputs.
    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
  • Core API client helper for all Goodday requests, handles auth, auto-subfolders param, error handling, used by tool to fetch projects, sprints, tasks.
    async def make_goodday_request(endpoint: str, method: str = "GET", data: dict = None, subfolders: bool = True) -> dict[str, Any] | list[Any] | None:
        """Make a request to the Goodday API with proper error handling."""
        api_token = os.getenv("GOODDAY_API_TOKEN")
        if not api_token:
            raise ValueError("GOODDAY_API_TOKEN environment variable is required")
        
        headers = {
            "User-Agent": USER_AGENT,
            "gd-api-token": api_token,
            "Content-Type": "application/json"
        }
        
        # Automatically add subfolders=true for project task and document endpoints if not already present
        if subfolders and endpoint.startswith("project/") and ("/tasks" in endpoint or "/documents" in endpoint):
            if "?" in endpoint:
                if "subfolders=" not in endpoint:
                    endpoint += "&subfolders=true"
            else:
                endpoint += "?subfolders=true"
        
        url = f"{GOODDAY_API_BASE}/{endpoint.lstrip('/')}"
        
        async with httpx.AsyncClient() as client:
            try:
                if method.upper() == "POST":
                    response = await client.post(url, headers=headers, json=data, timeout=30.0)
                elif method.upper() == "PUT":
                    response = await client.put(url, headers=headers, json=data, timeout=30.0)
                elif method.upper() == "DELETE":
                    response = await client.delete(url, headers=headers, timeout=30.0)
                else:
                    response = await client.get(url, headers=headers, timeout=30.0)
    
                response.raise_for_status()
                return response.json()
    
            except httpx.HTTPStatusError as e:
                raise Exception(f"HTTP error {e.response.status_code}: {e.response.text}")
            except httpx.RequestError as e:
                raise Exception(f"Request error: {str(e)}")
            except Exception as e:
                raise Exception(f"Unexpected error: {str(e)}")

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