Skip to main content
Glama
geoffwhittington

SD Elements MCP Server

update_countermeasure

Modify the status or details of a countermeasure in the SD Elements MCP Server by specifying its ID, ensuring accurate tracking and management of security measures.

Instructions

Update a countermeasure status or details

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
countermeasure_idYesThe ID of the countermeasure to update
notesNoNotes about the countermeasure
statusNoNew status for the countermeasure

Implementation Reference

  • Main execution logic for the update_countermeasure MCP tool. Handles input normalization, status resolution, API call, error handling, and JSON response formatting. The docstring defines the tool schema including parameters and usage instructions.
    @mcp.tool()
    async def update_countermeasure(ctx: Context, project_id: int, countermeasure_id: Union[int, str], status: Optional[str] = None, notes: Optional[str] = None) -> str:
        """Update a countermeasure (status or notes). Use when user says 'update status', 'mark as complete', or 'change status'. Do NOT use for 'add note', 'document', or 'note' - use add_countermeasure_note instead. Accepts countermeasure ID as integer (e.g., 21) or string (e.g., "T21" or "31244-T21").
        
        Status can be provided as name (e.g., 'Complete', 'Not Applicable'), slug (e.g., 'DONE', 'NA'), or ID (e.g., 'TS1'). The tool will automatically resolve names/slugs to the correct status ID required by the API.
        
        IMPORTANT: The 'notes' parameter sets a status_note, which is only saved when the status actually changes. If the countermeasure already has the target status, use add_countermeasure_note instead to add a note, or change the status to a different value first, then back to the target status to trigger saving the status_note."""
        global api_client
        if api_client is None:
            api_client = init_api_client()
        normalized_id = normalize_countermeasure_id(project_id, countermeasure_id)
        data = {}
        if status is not None:
            # Resolve status name/slug to ID (API requires status IDs like "TS1", not names like "Complete")
            status_id = resolve_status_to_id(status, api_client)
            
            # Validate that we got a proper status ID (should start with "TS")
            # If the resolved status doesn't look like an ID and doesn't match the original input,
            # it means the conversion might have failed
            if not status_id.upper().startswith('TS') and status_id.lower() == status.strip().lower():
                # The status wasn't converted - this means we couldn't find a match
                # Try to get available statuses to provide a helpful error message
                try:
                    statuses_response = api_client.get_task_status_choices()
                    status_choices = statuses_response.get('status_choices', [])
                    available_statuses = [s.get('name', '') for s in status_choices if s.get('name')]
                    return json.dumps({
                        "error": f"Could not resolve status '{status}' to a status ID. The API requires status IDs (e.g., 'TS1', 'TS2'), not names.",
                        "provided_status": status,
                        "available_status_names": available_statuses[:10],  # Show first 10
                        "suggestion": "Use get_task_status_choices to see all available statuses and their IDs."
                    }, indent=2)
                except Exception:
                    return json.dumps({
                        "error": f"Could not resolve status '{status}' to a status ID. The API requires status IDs (e.g., 'TS1', 'TS2'), not names like '{status}'.",
                        "provided_status": status,
                        "suggestion": "Use get_task_status_choices to see all available statuses and their IDs."
                    }, indent=2)
            
            data["status"] = status_id
        if notes is not None:
            data["status_note"] = notes
        
        if not data:
            return json.dumps({"error": "No update data provided. Specify either 'status' or 'notes'."}, indent=2)
        
        result = api_client.update_countermeasure(project_id, normalized_id, data)
        return json.dumps(result, indent=2)
  • Helper function used by update_countermeasure to normalize countermeasure_id to the full 'project_id-T{n}' format expected by the API.
    def normalize_countermeasure_id(project_id: int, countermeasure_id: Union[int, str]) -> str:
        """
        Normalize countermeasure ID to full format (project_id-task_id).
        
        Accepts:
        - Integer: 21 -> "T21" -> "{project_id}-T21"
        - String starting with "T": "T21" -> "{project_id}-T21"
        - String in full format: "31244-T21" -> "31244-T21" (as-is)
        
        Args:
            project_id: The project ID
            countermeasure_id: Countermeasure ID as int or str
            
        Returns:
            Full task ID format: "{project_id}-T{number}" or existing full format
        """
        # If integer, convert to "T{number}" format
        if isinstance(countermeasure_id, int):
            task_id = f"T{countermeasure_id}"
        else:
            # Already a string
            task_id = countermeasure_id
        
        # If already in full format (contains project_id), return as-is
        if task_id.startswith(f"{project_id}-"):
            return task_id
        
        # Otherwise, construct full format
        return f"{project_id}-{task_id}"
  • Helper function used by update_countermeasure to resolve human-readable status names or slugs (e.g., 'Complete', 'DONE') to API-required status IDs (e.g., 'TS1'). Fetches status choices from API and performs exact/partial matching.
    def resolve_status_to_id(status: str, api_client) -> str:
        """
        Resolve a status name or slug to its ID.
        
        The API requires status IDs (e.g., "TS1", "TS2") not names (e.g., "Complete").
        This function looks up the status ID from the task-statuses endpoint.
        
        Args:
            status: Status name (e.g., "Complete"), slug (e.g., "DONE"), or ID (e.g., "TS1")
            api_client: The API client instance
            
        Returns:
            Status ID (e.g., "TS1") or the original value if not found
        """
        if not status or not status.strip():
            return status
        
        try:
            # Get all available statuses
            statuses_response = api_client.get_task_status_choices()
            status_choices = statuses_response.get('status_choices', [])
            
            if not status_choices:
                # If we can't get statuses, return original (might already be an ID)
                return status
            
            # Normalize input for comparison
            status_normalized = status.strip()
            status_lower = status_normalized.lower()
            
            # Check if it's already an ID (starts with "TS")
            if status_normalized.upper().startswith('TS'):
                # Verify it's a valid ID
                for s in status_choices:
                    if s.get('id', '').upper() == status_normalized.upper():
                        return s['id']
                return status_normalized  # Return as-is if not found
            
            # Try to match by exact name, slug, or meaning first (most reliable)
            for status_obj in status_choices:
                name = status_obj.get('name', '')
                slug = status_obj.get('slug', '')
                meaning = status_obj.get('meaning', '')
                status_id = status_obj.get('id', '')
                
                # Exact matches (case-insensitive)
                if (status_lower == name.lower() or 
                    status_lower == slug.lower() or 
                    status_lower == meaning.lower()):
                    return status_id
            
            # Try partial/fuzzy matching as fallback (e.g., "complete" -> "Complete")
            # Only match if the input is a substring of the name/slug (not the other way around)
            # This avoids false positives like "complete" matching "incomplete"
            for status_obj in status_choices:
                name = status_obj.get('name', '').lower()
                slug = status_obj.get('slug', '').lower()
                meaning = status_obj.get('meaning', '').lower()
                status_id = status_obj.get('id', '')
                
                # Check if normalized input matches the start of name/slug/meaning
                # or if it's a common variation
                if (name.startswith(status_lower) or 
                    slug.startswith(status_lower) or
                    meaning.startswith(status_lower) or
                    # Handle common variations like "completed" -> "complete"
                    (status_lower in ['completed', 'done', 'finished'] and 'complete' in name) or
                    (status_lower in ['completed', 'done', 'finished'] and 'done' in slug)):
                    return status_id
            
            # If no match found, return original (might be a valid ID we don't know about)
            return status_normalized
            
        except Exception as e:
            # If lookup fails, log the error but still return original value
            # In production, you might want to log this for debugging
            # For now, we'll return the original value
            return status.strip()

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/geoffwhittington/sde-mcp'

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