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
| Name | Required | Description | Default |
|---|---|---|---|
| project_name | Yes | ||
| sprint_name | Yes | ||
| include_closed | No |
Implementation Reference
- goodday_mcp/main.py:864-940 (handler)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}"
- goodday_mcp/main.py:185-219 (helper)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
- goodday_mcp/main.py:220-269 (helper)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
- goodday_mcp/main.py:165-173 (helper)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
- goodday_mcp/main.py:15-57 (helper)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)}")