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
| Name | Required | Description | Default |
|---|---|---|---|
| countermeasure_id | Yes | The ID of the countermeasure to update | |
| notes | No | Notes about the countermeasure | |
| status | No | New 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()