Source code for src.integrations.label_helper

#!/usr/bin/env python3
"""
Helper module for managing labels in kanban-mcp.

This module provides utility functions for working with labels, handling the
specific requirements of the kanban-mcp label manager including:
- Using proper color names from the allowed enum
- Creating labels before adding them to cards
- Managing label IDs for card operations
"""

import json
import logging
import sys
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


[docs] class LabelManagerHelper: """Helper class for managing kanban labels.""" # Valid colors from kanban-mcp schema VALID_COLORS = [ "berry-red", "pumpkin-orange", "lagoon-blue", "pink-tulip", "light-mud", "orange-peel", "bright-moss", "antique-blue", "dark-granite", "lagune-blue", "sunny-grass", "morning-sky", "light-orange", "midnight-blue", "tank-green", "gun-metal", "wet-moss", "red-burgundy", "light-concrete", "apricot-red", "desert-sand", "navy-blue", "egg-yellow", "coral-green", "light-cocoa", ] # Mapping of common label names to appropriate colors DEFAULT_LABEL_COLORS = { # Skills/Technologies "backend": "berry-red", "frontend": "lagoon-blue", "database": "pumpkin-orange", "api": "berry-red", "ui": "lagune-blue", "ux": "lagune-blue", "devops": "tank-green", "fullstack": "midnight-blue", "react": "lagoon-blue", "django": "bright-moss", "python": "bright-moss", "nodejs": "sunny-grass", "javascript": "egg-yellow", # Task types "testing": "sunny-grass", "bug": "midnight-blue", "feature": "pink-tulip", "documentation": "sunny-grass", "refactor": "light-concrete", "enhancement": "bright-moss", "setup": "pink-tulip", "deployment": "pumpkin-orange", "design": "lagune-blue", "implementation": "berry-red", # Priorities "high": "berry-red", "medium": "egg-yellow", "low": "bright-moss", "urgent": "red-burgundy", "high-priority": "red-burgundy", # Complexity "simple": "bright-moss", "moderate": "egg-yellow", "complex": "berry-red", # Other "security": "midnight-blue", "performance": "orange-peel", "authentication": "midnight-blue", "infrastructure": "tank-green", }
[docs] def __init__(self, session: Any, board_id: str) -> None: """ Initialize the label manager helper. Parameters ---------- session : ClientSession Active MCP client session board_id : str ID of the board to manage labels for """ self.session = session self.board_id = board_id self._label_cache: Dict[str, Any] = {} # Cache of label name -> label data
[docs] async def refresh_labels(self) -> List[Dict[str, Any]]: """ Get all labels for the board and refresh the cache. Returns ------- List[Dict[str, Any]] List of all labels on the board """ result = await self.session.call_tool( "mcp_kanban_label_manager", {"action": "get_all", "boardId": self.board_id} ) labels = [] if result and hasattr(result, "content") and result.content: labels_data = json.loads(result.content[0].text) labels = labels_data if isinstance(labels_data, list) else [] # Update cache self._label_cache.clear() for label in labels: name = label.get("name", "").lower() if name: self._label_cache[name] = label return labels
[docs] async def ensure_label_exists(self, name: str, color: Optional[str] = None) -> str: """ Ensure a label exists, creating it if necessary. Parameters ---------- name : str Name of the label color : Optional[str] Color for the label. If not provided, uses default mapping or picks one. Returns ------- str ID of the label (existing or newly created) Raises ------ ValueError If the color is not in the valid colors list """ # Normalize name normalized_name = name.lower() # Check cache first if normalized_name in self._label_cache: cached_label = self._label_cache[normalized_name] # Verify the color is correct expected_color = self.get_color_for_label(name) if cached_label["color"] != expected_color: # Update the label color logger.info( f"Updating label '{name}' color from " f"{cached_label['color']} to {expected_color}" ) try: update_result = await self.session.call_tool( "mcp_kanban_label_manager", { "action": "update", "id": cached_label["id"], "boardId": self.board_id, "name": name, "color": expected_color, "position": cached_label.get("position", 65536), }, ) if ( update_result and hasattr(update_result, "content") and update_result.content ): updated_label = json.loads(update_result.content[0].text) self._label_cache[normalized_name] = updated_label except Exception as e: logger.error(f"Failed to update label color: {e}") return str(cached_label["id"]) # Refresh cache and check again await self.refresh_labels() if normalized_name in self._label_cache: cached_label = self._label_cache[normalized_name] # Verify the color is correct expected_color = self.get_color_for_label(name) if cached_label["color"] != expected_color: # Update the label color logger.info( f"Updating label '{name}' color from " f"{cached_label['color']} to {expected_color}" ) try: update_result = await self.session.call_tool( "mcp_kanban_label_manager", { "action": "update", "id": cached_label["id"], "boardId": self.board_id, "name": name, "color": expected_color, "position": cached_label.get("position", 65536), }, ) if ( update_result and hasattr(update_result, "content") and update_result.content ): updated_label = json.loads(update_result.content[0].text) self._label_cache[normalized_name] = updated_label except Exception as e: logger.error(f"Failed to update label color: {e}") return str(cached_label["id"]) # Need to create the label if color is None: # Use the class method for consistency color = self.get_color_for_label(name) # Validate color if color not in self.VALID_COLORS: raise ValueError( f"Invalid color '{color}'. Must be one of: " f"{', '.join(self.VALID_COLORS)}" ) # Create the label with required position parameter result = await self.session.call_tool( "mcp_kanban_label_manager", { "action": "create", "boardId": self.board_id, "name": name, # Use original name (preserves case) "color": color, "position": 65536, # Required parameter for label creation }, ) if result and hasattr(result, "content") and result.content: created_label = json.loads(result.content[0].text) # Update cache self._label_cache[normalized_name] = created_label return str(created_label["id"]) else: raise Exception(f"Failed to create label '{name}'")
[docs] async def add_labels_to_card( self, card_id: str, label_names: List[str] ) -> List[str]: """ Add multiple labels to a card, creating them if necessary. Parameters ---------- card_id : str ID of the card to add labels to label_names : List[str] Names of labels to add Returns ------- List[str] List of label IDs that were successfully added """ added_label_ids = [] for label_name in label_names: try: # Ensure label exists and get its ID label_id = await self.ensure_label_exists(label_name) # Add label to card await self.session.call_tool( "mcp_kanban_label_manager", {"action": "add_to_card", "cardId": card_id, "labelId": label_id}, ) added_label_ids.append(label_id) except Exception as e: logger.warning(f"Failed to add label '{label_name}' to card: {e}") return added_label_ids
[docs] @classmethod def get_color_for_label(cls, label_name: str) -> str: """ Get the recommended color for a label name. Parameters ---------- label_name : str Name of the label Returns ------- str Recommended color from the valid colors list """ normalized = label_name.lower() # Handle prefixed labels (e.g., "component:frontend" -> check "frontend") if ":" in normalized: prefix, suffix = normalized.split(":", 1) # Try exact suffix match first color = cls.DEFAULT_LABEL_COLORS.get(suffix, None) # If not found, try to find a matching key in suffix # (e.g., "python" in "python-255887") if not color: for key in cls.DEFAULT_LABEL_COLORS: if key in suffix: color = cls.DEFAULT_LABEL_COLORS[key] break # If not found, try prefix if not color: color = cls.DEFAULT_LABEL_COLORS.get(prefix, None) # If still not found, use a color based on prefix type if not color: prefix_colors = { "component": "bright-moss", "type": "pumpkin-orange", "priority": "berry-red", "skill": "lagoon-blue", "complexity": "light-concrete", } color = prefix_colors.get(prefix, "lagoon-blue") return color else: # Simple label without prefix return cls.DEFAULT_LABEL_COLORS.get(normalized, "lagoon-blue")
[docs] @classmethod def map_hex_to_valid_color(cls, hex_color: str) -> str: """ Map a hex color to the closest valid kanban-mcp color. This is a simple mapping for common colors. Parameters ---------- hex_color : str Hex color code (e.g., "#4CAF50") Returns ------- str Valid color name from the allowed list """ hex_mappings = { "#4CAF50": "sunny-grass", # Green "#2196F3": "lagoon-blue", # Blue "#FF9800": "pumpkin-orange", # Orange "#9C27B0": "pink-tulip", # Purple "#F44336": "berry-red", # Red "#795548": "light-mud", # Brown "#607D8B": "dark-granite", # Blue Grey "#FFEB3B": "egg-yellow", # Yellow "#00BCD4": "lagune-blue", # Cyan "#E91E63": "pink-tulip", # Pink } return hex_mappings.get(hex_color.upper(), "lagoon-blue")
# Example usage function
[docs] async def example_usage(session: Any, board_id: str, card_id: str) -> None: """ Demonstrate how to use the LabelManagerHelper. Parameters ---------- session : ClientSession Active MCP session board_id : str Board ID card_id : str Card ID to add labels to """ # Create helper helper = LabelManagerHelper(session, board_id) # Add labels to a card (creates them if they don't exist) labels_to_add = ["backend", "python", "high-priority"] added_ids = await helper.add_labels_to_card(card_id, labels_to_add) # Example usage - should use logger in real code print(f"Added {len(added_ids)} labels to card", file=sys.stderr) # Get all labels on the board all_labels = await helper.refresh_labels() print(f"Board has {len(all_labels)} total labels", file=sys.stderr) # Get color for a label type color = LabelManagerHelper.get_color_for_label("frontend") print(f"Recommended color for 'frontend': {color}", file=sys.stderr)