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()
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It states this is an update operation, implying mutation, but doesn't cover critical aspects like required permissions, whether changes are reversible, error handling, or rate limits. The description is minimal and lacks behavioral context beyond the basic action.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise—a single sentence with zero wasted words. It's front-loaded with the core action and resource, making it easy to parse. Every word earns its place, though this conciseness comes at the cost of completeness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no annotations and no output schema, the description is inadequate. It doesn't explain what 'update' entails (e.g., partial vs. full updates), what happens to unspecified fields, or what the return value might be. Given the complexity of an update operation and lack of structured data, more context is needed to make this tool usable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, with all parameters well-documented in the schema. The description adds minimal value by mentioning 'status or details', which aligns with the 'status' and 'notes' parameters but doesn't provide additional syntax, format details, or constraints beyond what the schema already specifies. Baseline 3 is appropriate given high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('Update') and resource ('countermeasure'), specifying what can be updated ('status or details'). It distinguishes from sibling 'get_countermeasure' by indicating a write operation rather than a read. However, it doesn't explicitly differentiate from other update tools like 'update_application' or 'update_project' in terms of resource type.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., needing a countermeasure ID), exclusions, or comparisons to similar tools like 'update_application' or 'update_project'. Usage is implied by the action but not explicitly contextualized.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

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