Skip to main content
Glama

MCP Azure DevOps Server

by Vortiago
tools.py15.8 kB
""" Team tools for Azure DevOps. This module provides MCP tools for working with Azure DevOps teams. """ from typing import Optional from azure.devops.v7_1.core import CoreClient from azure.devops.v7_1.core.models import WebApiTeam from azure.devops.v7_1.work.models import TeamContext from mcp_azure_devops.features.teams.common import ( AzureDevOpsClientError, get_core_client, get_work_client, ) def _format_team(team: WebApiTeam) -> str: """ Format team information. Args: team: Team object to format Returns: String with team details """ # Basic information that should always be available formatted_info = [f"# Team: {team.name}"] formatted_info.append(f"ID: {team.id}") # Add description if available if hasattr(team, "description") and team.description: formatted_info.append(f"Description: {team.description}") # Add project information if available if hasattr(team, "project_name") and team.project_name: formatted_info.append(f"Project: {team.project_name}") if hasattr(team, "project_id") and team.project_id: formatted_info.append(f"Project ID: {team.project_id}") return "\n".join(formatted_info) def _format_team_member(team_member) -> str: """ Format team member information. Args: team_member: Team member object to format Returns: String with team member details """ formatted_info = [] # Get identity information if hasattr(team_member, "identity") and team_member.identity: identity = team_member.identity # Use display name if available, otherwise use ID if hasattr(identity, "display_name") and identity.display_name: formatted_info.append(f"# Member: {identity.display_name}") else: formatted_info.append(f"# Member ID: {identity.id}") # Add ID if hasattr(identity, "id") and identity.id: formatted_info.append(f"ID: {identity.id}") # Add descriptor if hasattr(identity, "descriptor") and identity.descriptor: formatted_info.append(f"Descriptor: {identity.descriptor}") # Add unique name (email/username) if hasattr(identity, "unique_name") and identity.unique_name: formatted_info.append(f"Email/Username: {identity.unique_name}") else: formatted_info.append("# Unknown Member") # Add team admin status if hasattr(team_member, "is_team_admin"): is_admin = "Yes" if team_member.is_team_admin else "No" formatted_info.append(f"Team Administrator: {is_admin}") return "\n".join(formatted_info) def _format_team_area_path(team_field_values) -> str: """ Format team area path information. Args: team_field_values: Team field values object to format Returns: String with team area path details """ formatted_info = ["# Team Area Paths"] # Add default area path if (hasattr(team_field_values, "default_value") and team_field_values.default_value): formatted_info.append( f"Default Area Path: {team_field_values.default_value}") # Add all area paths if hasattr(team_field_values, "values") and team_field_values.values: formatted_info.append("\n## All Area Paths:") for area_path in team_field_values.values: value_str = f"- {area_path.value}" if (hasattr(area_path, "include_children") and area_path.include_children): value_str += " (Including sub-areas)" formatted_info.append(value_str) return "\n".join(formatted_info) def _format_team_iteration(iteration) -> str: """ Format team iteration information. Args: iteration: Team iteration object to format Returns: String with team iteration details """ formatted_info = [f"# Iteration: {iteration.name}"] # Add ID if hasattr(iteration, "id") and iteration.id: formatted_info.append(f"ID: {iteration.id}") # Add path if hasattr(iteration, "path") and iteration.path: formatted_info.append(f"Path: {iteration.path}") # Add attributes if available if hasattr(iteration, "attributes") and iteration.attributes: attributes = iteration.attributes # Add start date if hasattr(attributes, "start_date") and attributes.start_date: formatted_info.append(f"Start Date: {attributes.start_date}") # Add finish date if hasattr(attributes, "finish_date") and attributes.finish_date: formatted_info.append(f"Finish Date: {attributes.finish_date}") # Add time frame if hasattr(attributes, "time_frame") and attributes.time_frame: formatted_info.append(f"Time Frame: {attributes.time_frame}") return "\n".join(formatted_info) def _get_all_teams_impl( core_client: CoreClient, user_is_member_of: Optional[bool] = None, top: Optional[int] = None, skip: Optional[int] = None, expand_identity: Optional[bool] = None ) -> str: """ Implementation of teams retrieval. Args: core_client: Core client user_is_member_of: If true, then return all teams requesting user is member. Otherwise return all teams user has read access. top: Maximum number of teams to return skip: Number of teams to skip Returns: Formatted string containing team information """ try: # Call the SDK function - note we're mapping user_is_member_of to mine # param teams = core_client.get_all_teams( mine=user_is_member_of, top=top, skip=skip ) if not teams: return "No teams found." formatted_teams = [] for team in teams: formatted_teams.append(_format_team(team)) return "\n\n".join(formatted_teams) except Exception as e: return f"Error retrieving teams: {str(e)}" def _get_team_members_impl( core_client: CoreClient, project_id: str, team_id: str, top: Optional[int] = None, skip: Optional[int] = None ) -> str: """ Implementation of team members retrieval. Args: core_client: Core client project_id: The name or ID (GUID) of the team project the team belongs to team_id: The name or ID (GUID) of the team top: Maximum number of members to return skip: Number of members to skip Returns: Formatted string containing team members information """ try: team_members = core_client.get_team_members_with_extended_properties( project_id=project_id, team_id=team_id, top=top, skip=skip ) if not team_members: return (f"No members found for team {team_id} in " f"project {project_id}.") formatted_members = [] for member in team_members: formatted_members.append(_format_team_member(member)) return "\n\n".join(formatted_members) except Exception as e: return f"Error retrieving team members: {str(e)}" def _get_team_area_paths_impl( work_client, project_name_or_id: str, team_name_or_id: str ) -> str: """ Implementation of team area paths retrieval. Args: work_client: Work client project_name_or_id: The name or ID of the team project team_name_or_id: The name or ID of the team Returns: Formatted string containing team area path information """ try: # Create a TeamContext object team_context = TeamContext( project=project_name_or_id, team=team_name_or_id ) # Get the team field values team_field_values = work_client.get_team_field_values(team_context) if not team_field_values: return (f"No area paths found for team {team_name_or_id} " f"in project {project_name_or_id}.") return _format_team_area_path(team_field_values) except Exception as e: return f"Error retrieving team area paths: {str(e)}" def _get_team_iterations_impl( work_client, project_name_or_id: str, team_name_or_id: str, current: Optional[bool] = None ) -> str: """ Implementation of team iterations retrieval. Args: work_client: Work client project_name_or_id: The name or ID of the team project team_name_or_id: The name or ID of the team current: If True, return only the current iteration Returns: Formatted string containing team iteration information """ try: # Create a TeamContext object team_context = TeamContext( project=project_name_or_id, team=team_name_or_id ) # Set timeframe parameter if current is True timeframe = "Current" if current else None # Get the team iterations team_iterations = work_client.get_team_iterations( team_context=team_context, timeframe=timeframe ) if not team_iterations: return (f"No iterations found for team {team_name_or_id} " f"in project {project_name_or_id}.") formatted_iterations = [] for iteration in team_iterations: formatted_iterations.append(_format_team_iteration(iteration)) return "\n\n".join(formatted_iterations) except Exception as e: return f"Error retrieving team iterations: {str(e)}" def register_tools(mcp) -> None: """ Register team tools with the MCP server. Args: mcp: The FastMCP server instance """ @mcp.tool() def get_all_teams( user_is_member_of: Optional[bool] = None, top: Optional[int] = None, skip: Optional[int] = None ) -> str: """ Retrieves all teams in the Azure DevOps organization. Use this tool when you need to: - Get an overview of all teams across projects - Find team IDs for use in other operations - Determine which teams exist in the organization - Locate specific teams by name Args: user_is_member_of: If true, return only teams where the current user is a member. Otherwise return all teams the user has read access to. top: Maximum number of teams to return skip: Number of teams to skip Returns: Formatted string containing team information including names, IDs, descriptions, and associated projects, formatted as markdown with each team clearly separated """ try: core_client = get_core_client() return _get_all_teams_impl( core_client, user_is_member_of, top, skip ) except AzureDevOpsClientError as e: return f"Error: {str(e)}" @mcp.tool() def get_team_members( project_id: str, team_id: str, top: Optional[int] = None, skip: Optional[int] = None ) -> str: """ Retrieves the membership roster for a specific team. Use this tool when you need to: - See who belongs to a particular team - Find team administrators - Check user assignments across teams - Determine team size and composition Args: project_id: The name or ID (GUID) of the team project the team belongs to team_id: The name or ID (GUID) of the team top: Maximum number of members to return skip: Number of members to skip Returns: Formatted string containing team members information including display names, emails, IDs, and administrator status, formatted as markdown with each member clearly separated """ try: core_client = get_core_client() return _get_team_members_impl( core_client, project_id, team_id, top, skip ) except AzureDevOpsClientError as e: return f"Error: {str(e)}" @mcp.tool() def get_team_area_paths( project_name_or_id: str, team_name_or_id: str ) -> str: """ Retrieves the area paths assigned to a specific team. Use this tool when you need to: - Understand a team's areas of responsibility - Check default area path assignments - Determine how work is classified and routed to teams - Set up board and backlog configurations IMPORTANT: Area paths in Azure DevOps determine which work items appear on a team's backlogs and boards. The default area path is used when creating new work items through a team's interface. Args: project_name_or_id: The name or ID of the team project team_name_or_id: The name or ID of the team Returns: Formatted string containing team area path information including the default area path and all assigned paths, with indicators for paths that include sub-areas """ try: work_client = get_work_client() return _get_team_area_paths_impl( work_client, project_name_or_id, team_name_or_id ) except AzureDevOpsClientError as e: return f"Error: {str(e)}" @mcp.tool() def get_team_iterations( project_name_or_id: str, team_name_or_id: str, current: Optional[bool] = None ) -> str: """ Retrieves the iterations (sprints) assigned to a specific team. Use this tool when you need to: - View a team's sprint schedule - Find date ranges for iterations - Determine which iteration is currently active - Plan work based on team's iteration calendar IMPORTANT: Iterations in Azure DevOps define time periods for planning and tracking work. They determine sprint dates and are used for capacity planning, burndown charts, and velocity calculations. Args: project_name_or_id: The name or ID of the team project team_name_or_id: The name or ID of the team current: If True, return only the current iteration Returns: Formatted string containing team iteration information including names, date ranges, and time frames (past/current/future), formatted as markdown """ try: work_client = get_work_client() return _get_team_iterations_impl( work_client, project_name_or_id, team_name_or_id, current ) except AzureDevOpsClientError as e: return f"Error: {str(e)}"

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/Vortiago/mcp-azure-devops'

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