#
tokens: 24759/50000 5/92 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 4. Use http://codebase.md/severity1/terraform-cloud-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── .python-version
├── CLAUDE.md
├── docs
│   ├── API_REFERENCES.md
│   ├── CLAUDE.md
│   ├── CONTRIBUTING.md
│   ├── conversations
│   │   ├── account.md
│   │   ├── apply-management-conversation.md
│   │   ├── assessment-results-conversation.md
│   │   ├── cost-estimate-conversation.md
│   │   ├── organization-entitlements-conversation.md
│   │   ├── organizations-management-conversation.md
│   │   ├── plan-management-conversation.md
│   │   ├── project-management-conversation.md
│   │   ├── runs-management-conversation.md
│   │   ├── state_management.md
│   │   ├── variables-conversation.md
│   │   └── workspace-management-conversation.md
│   ├── DEVELOPMENT.md
│   ├── FILTERING_SYSTEM.md
│   ├── models
│   │   ├── account.md
│   │   ├── apply.md
│   │   ├── assessment_result.md
│   │   ├── cost_estimate.md
│   │   ├── organization.md
│   │   ├── plan.md
│   │   ├── project.md
│   │   ├── run.md
│   │   ├── state_version_outputs.md
│   │   ├── state_versions.md
│   │   ├── variables.md
│   │   └── workspace.md
│   ├── README.md
│   └── tools
│       ├── account.md
│       ├── apply.md
│       ├── assessment_results.md
│       ├── cost_estimate.md
│       ├── organization.md
│       ├── plan.md
│       ├── project.md
│       ├── run.md
│       ├── state_version_outputs.md
│       ├── state_versions.md
│       ├── variables.md
│       └── workspace.md
├── env.example
├── LICENSE
├── mypy.ini
├── pyproject.toml
├── README.md
├── terraform_cloud_mcp
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── CLAUDE.md
│   │   └── client.py
│   ├── configs
│   │   ├── __init__.py
│   │   ├── CLAUDE.md
│   │   └── filter_configs.py
│   ├── models
│   │   ├── __init__.py
│   │   ├── account.py
│   │   ├── applies.py
│   │   ├── assessment_results.py
│   │   ├── base.py
│   │   ├── CLAUDE.md
│   │   ├── cost_estimates.py
│   │   ├── filters.py
│   │   ├── organizations.py
│   │   ├── plans.py
│   │   ├── projects.py
│   │   ├── runs.py
│   │   ├── state_version_outputs.py
│   │   ├── state_versions.py
│   │   ├── variables.py
│   │   └── workspaces.py
│   ├── server.py
│   ├── tools
│   │   ├── __init__.py
│   │   ├── account.py
│   │   ├── applies.py
│   │   ├── assessment_results.py
│   │   ├── CLAUDE.md
│   │   ├── cost_estimates.py
│   │   ├── organizations.py
│   │   ├── plans.py
│   │   ├── projects.py
│   │   ├── runs.py
│   │   ├── state_version_outputs.py
│   │   ├── state_versions.py
│   │   ├── variables.py
│   │   └── workspaces.py
│   └── utils
│       ├── __init__.py
│       ├── CLAUDE.md
│       ├── decorators.py
│       ├── env.py
│       ├── filters.py
│       ├── payload.py
│       └── request.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/terraform_cloud_mcp/models/runs.py:
--------------------------------------------------------------------------------

```python
  1 | """Run models for Terraform Cloud API
  2 | 
  3 | This module contains models for Terraform Cloud run-related requests.
  4 | Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run
  5 | """
  6 | 
  7 | from enum import Enum
  8 | from typing import List, Optional
  9 | 
 10 | from pydantic import Field
 11 | 
 12 | from .base import APIRequest
 13 | 
 14 | 
 15 | class RunOperation(str, Enum):
 16 |     """Operation options for runs in Terraform Cloud.
 17 | 
 18 |     Defines the different types of operations a run can perform:
 19 |     - PLAN_ONLY: Create a plan without applying changes
 20 |     - PLAN_AND_APPLY: Create a plan and apply if approved
 21 |     - SAVE_PLAN: Save the plan for later use
 22 |     - REFRESH_ONLY: Only refresh state without planning changes
 23 |     - DESTROY: Destroy all resources
 24 |     - EMPTY_APPLY: Apply even with no changes detected
 25 | 
 26 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-a-workspace
 27 | 
 28 |     See:
 29 |         docs/models/run.md for reference
 30 |     """
 31 | 
 32 |     PLAN_ONLY = "plan_only"
 33 |     PLAN_AND_APPLY = "plan_and_apply"
 34 |     SAVE_PLAN = "save_plan"
 35 |     REFRESH_ONLY = "refresh_only"
 36 |     DESTROY = "destroy"
 37 |     EMPTY_APPLY = "empty_apply"
 38 | 
 39 | 
 40 | class RunStatus(str, Enum):
 41 |     """Status options for runs in Terraform Cloud.
 42 | 
 43 |     Defines the various states a run can be in during its lifecycle,
 44 |     from initial creation through planning, policy checks, application,
 45 |     and completion or cancellation.
 46 | 
 47 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-a-workspace
 48 | 
 49 |     See:
 50 |         docs/models/run.md for reference
 51 |     """
 52 | 
 53 |     PENDING = "pending"
 54 |     FETCHING = "fetching"
 55 |     FETCHING_COMPLETED = "fetching_completed"
 56 |     PRE_PLAN_RUNNING = "pre_plan_running"
 57 |     PRE_PLAN_COMPLETED = "pre_plan_completed"
 58 |     QUEUING = "queuing"
 59 |     PLAN_QUEUED = "plan_queued"
 60 |     PLANNING = "planning"
 61 |     PLANNED = "planned"
 62 |     COST_ESTIMATING = "cost_estimating"
 63 |     COST_ESTIMATED = "cost_estimated"
 64 |     POLICY_CHECKING = "policy_checking"
 65 |     POLICY_OVERRIDE = "policy_override"
 66 |     POLICY_SOFT_FAILED = "policy_soft_failed"
 67 |     POLICY_CHECKED = "policy_checked"
 68 |     CONFIRMED = "confirmed"
 69 |     POST_PLAN_RUNNING = "post_plan_running"
 70 |     POST_PLAN_COMPLETED = "post_plan_completed"
 71 |     PLANNED_AND_FINISHED = "planned_and_finished"
 72 |     PLANNED_AND_SAVED = "planned_and_saved"
 73 |     APPLY_QUEUED = "apply_queued"
 74 |     APPLYING = "applying"
 75 |     APPLIED = "applied"
 76 |     DISCARDED = "discarded"
 77 |     ERRORED = "errored"
 78 |     CANCELED = "canceled"
 79 |     FORCE_CANCELED = "force_canceled"
 80 | 
 81 | 
 82 | class RunSource(str, Enum):
 83 |     """Source options for runs in Terraform Cloud.
 84 | 
 85 |     Identifies the origin of a run:
 86 |     - TFE_UI: Created through the Terraform Cloud web interface
 87 |     - TFE_API: Created through the API
 88 |     - TFE_CONFIGURATION_VERSION: Created by uploading a configuration version
 89 | 
 90 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-a-workspace
 91 | 
 92 |     See:
 93 |         docs/models/run.md for reference
 94 |     """
 95 | 
 96 |     TFE_UI = "tfe-ui"
 97 |     TFE_API = "tfe-api"
 98 |     TFE_CONFIGURATION_VERSION = "tfe-configuration-version"
 99 | 
100 | 
101 | class RunStatusGroup(str, Enum):
102 |     """Status group options for categorizing runs.
103 | 
104 |     Groups run statuses into categories for filtering:
105 |     - NON_FINAL: Runs that are still in progress
106 |     - FINAL: Runs that have reached a terminal state
107 |     - DISCARDABLE: Runs that can be discarded
108 | 
109 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-a-workspace
110 | 
111 |     See:
112 |         docs/models/run.md for reference
113 |     """
114 | 
115 |     NON_FINAL = "non_final"
116 |     FINAL = "final"
117 |     DISCARDABLE = "discardable"
118 | 
119 | 
120 | class RunVariable(APIRequest):
121 |     """Model for run-specific variables.
122 | 
123 |     Run variables are used to provide input values for a specific run,
124 |     which override any workspace variables for that run only.
125 | 
126 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#create-a-run
127 | 
128 |     See:
129 |         docs/models/run.md for reference
130 |     """
131 | 
132 |     key: str = Field(
133 |         ...,
134 |         # No alias needed as field name matches API field name
135 |         description="Variable key",
136 |         min_length=1,
137 |         max_length=128,
138 |     )
139 |     value: str = Field(
140 |         ...,
141 |         # No alias needed as field name matches API field name
142 |         description="Variable value",
143 |         max_length=256,
144 |     )
145 | 
146 | 
147 | class RunListInWorkspaceRequest(APIRequest):
148 |     """Request parameters for listing runs in a workspace.
149 | 
150 |     Used with the GET /workspaces/{workspace_id}/runs endpoint to retrieve
151 |     and filter run data for a specific workspace.
152 | 
153 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-a-workspace
154 | 
155 |     See:
156 |         docs/models/run.md for reference
157 |     """
158 | 
159 |     workspace_id: str = Field(
160 |         ...,
161 |         description="The workspace ID to list runs for",
162 |         pattern=r"^ws-[a-zA-Z0-9]{16}$",  # Standardized workspace ID pattern
163 |     )
164 |     page_number: Optional[int] = Field(1, ge=1, description="Page number to fetch")
165 |     page_size: Optional[int] = Field(
166 |         20, ge=1, le=100, description="Number of results per page"
167 |     )
168 |     filter_operation: Optional[str] = Field(
169 |         None,
170 |         description="Filter runs by operation type, comma-separated",
171 |         max_length=100,
172 |     )
173 |     filter_status: Optional[str] = Field(
174 |         None, description="Filter runs by status, comma-separated", max_length=100
175 |     )
176 |     filter_source: Optional[str] = Field(
177 |         None, description="Filter runs by source, comma-separated", max_length=100
178 |     )
179 |     filter_status_group: Optional[str] = Field(
180 |         None, description="Filter runs by status group", max_length=50
181 |     )
182 |     filter_timeframe: Optional[str] = Field(
183 |         None, description="Filter runs by timeframe", max_length=50
184 |     )
185 |     filter_agent_pool_names: Optional[str] = Field(
186 |         None,
187 |         description="Filter runs by agent pool names, comma-separated",
188 |         max_length=100,
189 |     )
190 |     search_user: Optional[str] = Field(
191 |         None, description="Search for runs by VCS username", max_length=100
192 |     )
193 |     search_commit: Optional[str] = Field(
194 |         None, description="Search for runs by commit SHA", max_length=40
195 |     )
196 |     search_basic: Optional[str] = Field(
197 |         None,
198 |         description="Basic search across run ID, message, commit SHA, and username",
199 |         max_length=100,
200 |     )
201 | 
202 | 
203 | class RunListInOrganizationRequest(APIRequest):
204 |     """Request parameters for listing runs in an organization.
205 | 
206 |     These parameters map to the query parameters in the runs API.
207 |     The endpoint returns a paginated list of runs across all workspaces in an organization,
208 |     with options for filtering by workspace name, status, and other criteria.
209 | 
210 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#list-runs-in-an-organization
211 | 
212 |     See:
213 |         docs/models/run.md for reference
214 |     """
215 | 
216 |     organization: str = Field(
217 |         ...,
218 |         description="The organization name",
219 |         min_length=3,
220 |         pattern=r"^[a-z0-9][-a-z0-9_]*[a-z0-9]$",
221 |     )
222 |     page_number: Optional[int] = Field(1, ge=1, description="Page number to fetch")
223 |     page_size: Optional[int] = Field(
224 |         20, ge=1, le=100, description="Number of results per page"
225 |     )
226 |     filter_operation: Optional[str] = Field(
227 |         None,
228 |         description="Filter runs by operation type, comma-separated",
229 |         max_length=100,
230 |     )
231 |     filter_status: Optional[str] = Field(
232 |         None, description="Filter runs by status, comma-separated", max_length=100
233 |     )
234 |     filter_source: Optional[str] = Field(
235 |         None, description="Filter runs by source, comma-separated", max_length=100
236 |     )
237 |     filter_status_group: Optional[str] = Field(
238 |         None, description="Filter runs by status group", max_length=50
239 |     )
240 |     filter_timeframe: Optional[str] = Field(
241 |         None, description="Filter runs by timeframe", max_length=50
242 |     )
243 |     filter_agent_pool_names: Optional[str] = Field(
244 |         None,
245 |         description="Filter runs by agent pool names, comma-separated",
246 |         max_length=100,
247 |     )
248 |     filter_workspace_names: Optional[str] = Field(
249 |         None,
250 |         description="Filter runs by workspace names, comma-separated",
251 |         max_length=250,
252 |     )
253 |     search_user: Optional[str] = Field(
254 |         None, description="Search for runs by VCS username", max_length=100
255 |     )
256 |     search_commit: Optional[str] = Field(
257 |         None, description="Search for runs by commit SHA", max_length=40
258 |     )
259 |     search_basic: Optional[str] = Field(
260 |         None,
261 |         description="Basic search across run ID, message, commit SHA, and username",
262 |         max_length=100,
263 |     )
264 | 
265 | 
266 | class BaseRunRequest(APIRequest):
267 |     """Base class for run requests with common fields.
268 | 
269 |     Common fields shared across run creation and management APIs.
270 |     Provides field definitions and validation rules for run operations.
271 | 
272 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run
273 | 
274 |     Note:
275 |         Consolidates common parameters for consistency across endpoints
276 | 
277 |     See:
278 |         docs/models/run.md for reference
279 |     """
280 | 
281 |     # Inherits model_config from APIRequest -> BaseModelConfig
282 | 
283 |     # Optional fields with their defaults
284 |     message: Optional[str] = Field(None, description="Message to include with the run")
285 |     auto_apply: Optional[bool] = Field(
286 |         None,
287 |         alias="auto-apply",
288 |         description="Whether to auto-apply the run when planned (defaults to workspace setting)",
289 |     )
290 |     is_destroy: Optional[bool] = Field(
291 |         False,
292 |         alias="is-destroy",
293 |         description="Whether this run should destroy all resources",
294 |     )
295 |     refresh: Optional[bool] = Field(
296 |         True, description="Whether to refresh state before plan"
297 |     )
298 |     refresh_only: Optional[bool] = Field(
299 |         False, alias="refresh-only", description="Whether this is a refresh-only run"
300 |     )
301 |     plan_only: Optional[bool] = Field(
302 |         False,
303 |         alias="plan-only",
304 |         description="Whether this is a speculative, plan-only run",
305 |     )
306 |     allow_empty_apply: Optional[bool] = Field(
307 |         False,
308 |         alias="allow-empty-apply",
309 |         description="Whether to allow apply when there are no changes",
310 |     )
311 |     allow_config_generation: Optional[bool] = Field(
312 |         False,
313 |         alias="allow-config-generation",
314 |         description="Whether to allow generating config for imports",
315 |     )
316 |     target_addrs: Optional[List[str]] = Field(
317 |         None, alias="target-addrs", description="Resource addresses to target"
318 |     )
319 |     replace_addrs: Optional[List[str]] = Field(
320 |         None, alias="replace-addrs", description="Resource addresses to replace"
321 |     )
322 |     variables: Optional[List[RunVariable]] = Field(
323 |         None, description="Run-specific variables"
324 |     )
325 |     terraform_version: Optional[str] = Field(
326 |         None,
327 |         alias="terraform-version",
328 |         description="Specific Terraform version (only valid for plan-only runs)",
329 |     )
330 |     save_plan: Optional[bool] = Field(
331 |         False,
332 |         alias="save-plan",
333 |         description="Whether to save the plan without becoming the current run",
334 |     )
335 |     debugging_mode: Optional[bool] = Field(
336 |         False, alias="debugging-mode", description="Enable debug logging for this run"
337 |     )
338 | 
339 | 
340 | class RunCreateRequest(BaseRunRequest):
341 |     """Request model for creating a Terraform Cloud run.
342 | 
343 |     Validates and structures the request according to the Terraform Cloud API
344 |     requirements for creating runs. The model inherits common run attributes from
345 |     BaseRunRequest and adds workspace_id as a required parameter.
346 | 
347 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#create-a-run
348 | 
349 |     Note:
350 |         This inherits all configuration fields from BaseRunRequest
351 |         and adds workspace_id as a required parameter.
352 |         This model is typically used internally by the create_run tool function,
353 |         which accepts parameters directly and constructs the request object.
354 | 
355 |     See:
356 |         docs/models/run.md for reference
357 |     """
358 | 
359 |     # Required fields
360 |     workspace_id: str = Field(
361 |         ...,
362 |         # No alias needed as field name matches API field name
363 |         description="The workspace ID to execute the run in (required)",
364 |         pattern=r"^ws-[a-zA-Z0-9]{16}$",  # Standardized workspace ID pattern
365 |     )
366 | 
367 |     # Optional fields specific to run creation
368 |     configuration_version_id: Optional[str] = Field(
369 |         None,
370 |         alias="configuration-version-id",
371 |         description="The configuration version ID to use",
372 |         pattern=r"^cv-[a-zA-Z0-9]{16}$",
373 |     )
374 | 
375 | 
376 | class RunActionRequest(APIRequest):
377 |     """Base request model for run actions like apply, discard, cancel, etc.
378 | 
379 |     This model provides common fields used in run action requests such as
380 |     applying, discarding, or canceling runs. It includes the run ID and
381 |     an optional comment field that can be included with the action.
382 | 
383 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#apply-a-run
384 | 
385 |     Note:
386 |         This model is used for multiple run action endpoints that share the
387 |         same basic structure but perform different operations on the run.
388 | 
389 |     See:
390 |         docs/models/run.md for reference
391 |     """
392 | 
393 |     run_id: str = Field(
394 |         ...,
395 |         # No alias needed as field name matches API field name
396 |         description="The ID of the run to perform an action on",
397 |         pattern=r"^run-[a-zA-Z0-9]{16}$",
398 |     )
399 |     comment: Optional[str] = Field(
400 |         None,
401 |         # No alias needed as field name matches API field name
402 |         description="An optional comment about the run",
403 |     )
404 | 
405 | 
406 | class RunParams(BaseRunRequest):
407 |     """Parameters for run operations without routing fields.
408 | 
409 |     This model provides all optional parameters that can be used when creating runs,
410 |     reusing the field definitions from BaseRunRequest.
411 | 
412 |     Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run#create-a-run
413 | 
414 |     Note:
415 |         All fields are inherited from BaseRunRequest.
416 | 
417 |     See:
418 |         docs/models/run.md for reference
419 |     """
420 | 
421 |     # Inherits model_config and all fields from BaseRunRequest
422 | 
423 | 
424 | # Response handling is implemented through raw dictionaries
425 | 
```

--------------------------------------------------------------------------------
/terraform_cloud_mcp/tools/runs.py:
--------------------------------------------------------------------------------

```python
  1 | """Run management tools for Terraform Cloud API.
  2 | 
  3 | This module provides tools for managing runs in Terraform Cloud.
  4 | Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run
  5 | """
  6 | 
  7 | from typing import Optional
  8 | 
  9 | from ..api.client import api_request
 10 | from ..utils.decorators import handle_api_errors
 11 | from ..utils.payload import create_api_payload, add_relationship
 12 | from ..utils.request import query_params
 13 | from ..models.base import APIResponse
 14 | from ..models.runs import (
 15 |     RunListInWorkspaceRequest,
 16 |     RunListInOrganizationRequest,
 17 |     RunCreateRequest,
 18 |     RunActionRequest,
 19 |     RunParams,
 20 | )
 21 | 
 22 | 
 23 | @handle_api_errors
 24 | async def create_run(
 25 |     workspace_id: str,
 26 |     params: Optional[RunParams] = None,
 27 | ) -> APIResponse:
 28 |     """Create a run in a workspace
 29 | 
 30 |     Creates a new Terraform run to trigger infrastructure changes through Terraform Cloud,
 31 |     representing a single execution of plan and apply operations. The run queues in the
 32 |     workspace and executes based on the workspace's execution mode and settings. Use this
 33 |     to deploy new infrastructure, apply configuration changes, or destroy resources.
 34 | 
 35 |     API endpoint: POST /runs
 36 | 
 37 |     Args:
 38 |         workspace_id: The workspace ID to execute the run in (format: "ws-xxxxxxxx")
 39 |         params: Optional run configuration with:
 40 |             - message: Description of the run's purpose
 41 |             - is_destroy: Whether to destroy all resources managed by the workspace
 42 |             - auto_apply: Whether to auto-apply after a successful plan
 43 |             - refresh: Whether to refresh Terraform state before planning
 44 |             - refresh_only: Only refresh the state without planning changes
 45 |             - plan_only: Create a speculative plan without applying
 46 |             - allow_empty_apply: Allow applying when there are no changes
 47 |             - target_addrs: List of resource addresses to specifically target
 48 |             - replace_addrs: List of resource addresses to force replacement
 49 |             - variables: Run-specific variables that override workspace variables
 50 |             - terraform_version: Specific Terraform version to use for this run
 51 |             - save_plan: Save the plan for later execution
 52 |             - debugging_mode: Enable extended debug logging
 53 | 
 54 |     Returns:
 55 |         The created run details with ID, status, configuration information,
 56 |         workspace relationship, and links to associated resources
 57 | 
 58 |     See:
 59 |         docs/tools/run.md for reference documentation
 60 |     """
 61 |     # Convert optional params to dictionary
 62 |     param_dict = params.model_dump(exclude_none=True) if params else {}
 63 | 
 64 |     # Create validated request object
 65 |     request = RunCreateRequest(workspace_id=workspace_id, **param_dict)
 66 | 
 67 |     # Extract variables for special handling
 68 |     variables = request.variables
 69 | 
 70 |     # Create API payload using utility function
 71 |     payload = create_api_payload(
 72 |         resource_type="runs",
 73 |         model=request,
 74 |         exclude_fields={"workspace_id", "variables"},  # Fields handled separately
 75 |     )
 76 | 
 77 |     # Add workspace relationship
 78 |     add_relationship(
 79 |         payload=payload,
 80 |         relation_name="workspace",
 81 |         resource_type="workspaces",
 82 |         resource_id=workspace_id,
 83 |     )
 84 | 
 85 |     # Add optional configuration version relationship
 86 |     if request.configuration_version_id:
 87 |         add_relationship(
 88 |             payload=payload,
 89 |             relation_name="configuration-version",
 90 |             resource_type="configuration-versions",
 91 |             resource_id=request.configuration_version_id,
 92 |         )
 93 | 
 94 |     # Transform variables to key-value format required by API
 95 |     if variables:
 96 |         payload["data"]["attributes"]["variables"] = [
 97 |             {"key": var.key, "value": var.value} for var in variables
 98 |         ]
 99 | 
100 |     return await api_request("runs", method="POST", data=payload)
101 | 
102 | 
103 | @handle_api_errors
104 | async def list_runs_in_workspace(
105 |     workspace_id: str,
106 |     page_number: int = 1,
107 |     page_size: int = 20,
108 |     filter_operation: Optional[str] = None,
109 |     filter_status: Optional[str] = None,
110 |     filter_source: Optional[str] = None,
111 |     filter_status_group: Optional[str] = None,
112 |     filter_timeframe: Optional[str] = None,
113 |     filter_agent_pool_names: Optional[str] = None,
114 |     search_user: Optional[str] = None,
115 |     search_commit: Optional[str] = None,
116 |     search_basic: Optional[str] = None,
117 | ) -> APIResponse:
118 |     """List runs in a workspace with filtering and pagination
119 | 
120 |     Retrieves run history for a specific workspace with options to filter by status,
121 |     operation type, source, and other criteria. Useful for auditing changes, troubleshooting,
122 |     or monitoring deployment history.
123 | 
124 |     API endpoint: GET /workspaces/{workspace_id}/runs
125 | 
126 |     Args:
127 |         workspace_id: The workspace ID to list runs for (format: "ws-xxxxxxxx")
128 |         page_number: Page number to fetch (default: 1)
129 |         page_size: Number of results per page (default: 20)
130 |         filter_operation: Filter by operation type
131 |         filter_status: Filter by status
132 |         filter_source: Filter by source
133 |         filter_status_group: Filter by status group
134 |         filter_timeframe: Filter by timeframe
135 |         filter_agent_pool_names: Filter by agent pool names
136 |         search_user: Search by VCS username
137 |         search_commit: Search by commit SHA
138 |         search_basic: Search across run ID, message, commit SHA, and username
139 | 
140 |     Returns:
141 |         List of runs with metadata, status info, and pagination details
142 | 
143 |     See:
144 |         docs/tools/run.md for reference documentation
145 |     """
146 |     # Create request using Pydantic model for validation
147 |     request = RunListInWorkspaceRequest(
148 |         workspace_id=workspace_id,
149 |         page_number=page_number,
150 |         page_size=page_size,
151 |         filter_operation=filter_operation,
152 |         filter_status=filter_status,
153 |         filter_source=filter_source,
154 |         filter_status_group=filter_status_group,
155 |         filter_timeframe=filter_timeframe,
156 |         filter_agent_pool_names=filter_agent_pool_names,
157 |         search_user=search_user,
158 |         search_commit=search_commit,
159 |         search_basic=search_basic,
160 |     )
161 | 
162 |     # Use the unified query params utility function
163 |     params = query_params(request)
164 | 
165 |     # Make API request
166 |     return await api_request(
167 |         f"workspaces/{workspace_id}/runs", method="GET", params=params
168 |     )
169 | 
170 | 
171 | @handle_api_errors
172 | async def list_runs_in_organization(
173 |     organization: str,
174 |     page_number: int = 1,
175 |     page_size: int = 20,
176 |     filter_operation: Optional[str] = None,
177 |     filter_status: Optional[str] = None,
178 |     filter_source: Optional[str] = None,
179 |     filter_status_group: Optional[str] = None,
180 |     filter_timeframe: Optional[str] = None,
181 |     filter_agent_pool_names: Optional[str] = None,
182 |     filter_workspace_names: Optional[str] = None,
183 |     search_user: Optional[str] = None,
184 |     search_commit: Optional[str] = None,
185 |     search_basic: Optional[str] = None,
186 | ) -> APIResponse:
187 |     """List runs across all workspaces in an organization
188 | 
189 |     Retrieves run history across all workspaces in an organization with powerful filtering.
190 |     Useful for organization-wide auditing, monitoring deployments across teams, or finding
191 |     specific runs by commit or author.
192 | 
193 |     API endpoint: GET /organizations/{organization}/runs
194 | 
195 |     Args:
196 |         organization: The organization name
197 |         page_number: Page number to fetch (default: 1)
198 |         page_size: Number of results per page (default: 20)
199 |         filter_operation: Filter by operation type
200 |         filter_status: Filter by status
201 |         filter_source: Filter by source
202 |         filter_status_group: Filter by status group
203 |         filter_timeframe: Filter by timeframe
204 |         filter_agent_pool_names: Filter by agent pool names
205 |         filter_workspace_names: Filter by workspace names
206 |         search_user: Search by VCS username
207 |         search_commit: Search by commit SHA
208 |         search_basic: Basic search across run attributes
209 | 
210 |     Returns:
211 |         List of runs across workspaces with metadata and pagination details
212 | 
213 |     See:
214 |         docs/tools/run.md for reference documentation
215 |     """
216 |     # Create request using Pydantic model for validation
217 |     request = RunListInOrganizationRequest(
218 |         organization=organization,
219 |         page_number=page_number,
220 |         page_size=page_size,
221 |         filter_operation=filter_operation,
222 |         filter_status=filter_status,
223 |         filter_source=filter_source,
224 |         filter_status_group=filter_status_group,
225 |         filter_timeframe=filter_timeframe,
226 |         filter_agent_pool_names=filter_agent_pool_names,
227 |         filter_workspace_names=filter_workspace_names,
228 |         search_user=search_user,
229 |         search_commit=search_commit,
230 |         search_basic=search_basic,
231 |     )
232 | 
233 |     # Use the unified query params utility function
234 |     params = query_params(request)
235 | 
236 |     # Make API request
237 |     return await api_request(
238 |         f"organizations/{organization}/runs", method="GET", params=params
239 |     )
240 | 
241 | 
242 | @handle_api_errors
243 | async def get_run_details(run_id: str) -> APIResponse:
244 |     """Get detailed information about a specific run
245 | 
246 |     Retrieves comprehensive information about a run including its current status,
247 |     plan output, and relationship to other resources. Use to check run progress or results.
248 | 
249 |     API endpoint: GET /runs/{run_id}
250 | 
251 |     Args:
252 |         run_id: The ID of the run to retrieve details for (format: "run-xxxxxxxx")
253 | 
254 |     Returns:
255 |         Complete run details including status, plan, and relationships
256 | 
257 |     See:
258 |         docs/tools/run.md for reference documentation
259 |     """
260 |     # Make API request
261 |     return await api_request(f"runs/{run_id}", method="GET")
262 | 
263 | 
264 | @handle_api_errors
265 | async def apply_run(run_id: str, comment: str = "") -> APIResponse:
266 |     """Apply a run that is paused waiting for confirmation after a plan
267 | 
268 |     Confirms and executes the apply phase for a run that has completed planning and is
269 |     waiting for approval. Use this when you've reviewed the plan output and want to
270 |     apply the proposed changes to your infrastructure.
271 | 
272 |     API endpoint: POST /runs/{run_id}/actions/apply
273 | 
274 |     Args:
275 |         run_id: The ID of the run to apply (format: "run-xxxxxxxx")
276 |         comment: An optional comment explaining the reason for applying the run
277 | 
278 |     Returns:
279 |         Run details with updated status information and confirmation of the apply action
280 |         including timestamp information and any comment provided
281 | 
282 |     See:
283 |         docs/tools/run.md for reference documentation
284 |     """
285 |     request = RunActionRequest(run_id=run_id, comment=comment)
286 | 
287 |     # Create payload if comment is provided
288 |     payload = {}
289 |     if request.comment:
290 |         payload = {"comment": request.comment}
291 | 
292 |     # Make API request
293 |     return await api_request(
294 |         f"runs/{run_id}/actions/apply", method="POST", data=payload
295 |     )
296 | 
297 | 
298 | @handle_api_errors
299 | async def discard_run(run_id: str, comment: str = "") -> APIResponse:
300 |     """Discard a run that is paused waiting for confirmation
301 | 
302 |     Cancels a run without applying its changes, typically used when the plan
303 |     shows undesired changes or after reviewing and rejecting a plan. This action
304 |     removes the run from the queue and unlocks the workspace for new runs.
305 | 
306 |     API endpoint: POST /runs/{run_id}/actions/discard
307 | 
308 |     Args:
309 |         run_id: The ID of the run to discard (format: "run-xxxxxxxx")
310 |         comment: An optional explanation for why the run was discarded
311 | 
312 |     Returns:
313 |         Run status update with discarded state information, timestamp of the
314 |         discard action, and user information
315 | 
316 |     See:
317 |         docs/tools/run.md for reference documentation
318 |     """
319 |     request = RunActionRequest(run_id=run_id, comment=comment)
320 | 
321 |     # Create payload if comment is provided
322 |     payload = {}
323 |     if request.comment:
324 |         payload = {"comment": request.comment}
325 | 
326 |     # Make API request
327 |     return await api_request(
328 |         f"runs/{run_id}/actions/discard", method="POST", data=payload
329 |     )
330 | 
331 | 
332 | @handle_api_errors
333 | async def cancel_run(run_id: str, comment: str = "") -> APIResponse:
334 |     """Cancel a run that is currently planning or applying
335 | 
336 |     Gracefully stops an in-progress run during planning or applying phases. Use this
337 |     when you need to stop a run that's taking too long, consuming too many resources,
338 |     or needs to be stopped for any reason. The operation attempts to cleanly terminate
339 |     the run by sending an interrupt signal.
340 | 
341 |     API endpoint: POST /runs/{run_id}/actions/cancel
342 | 
343 |     Args:
344 |         run_id: The ID of the run to cancel (format: "run-xxxxxxxx")
345 |         comment: An optional explanation for why the run was canceled
346 | 
347 |     Returns:
348 |         Run status update with canceled state, timestamp of cancellation,
349 |         and any provided comment in the response metadata
350 | 
351 |     See:
352 |         docs/tools/run.md for reference documentation
353 |     """
354 |     request = RunActionRequest(run_id=run_id, comment=comment)
355 | 
356 |     # Create payload if comment is provided
357 |     payload = {}
358 |     if request.comment:
359 |         payload = {"comment": request.comment}
360 | 
361 |     # Make API request
362 |     return await api_request(
363 |         f"runs/{run_id}/actions/cancel", method="POST", data=payload
364 |     )
365 | 
366 | 
367 | @handle_api_errors
368 | async def force_cancel_run(run_id: str, comment: str = "") -> APIResponse:
369 |     """Forcefully cancel a run immediately
370 | 
371 |     Immediately terminates a run that hasn't responded to a normal cancel request.
372 |     Use this as a last resort when a run is stuck and not responding to regular
373 |     cancellation. This action bypasses the graceful shutdown process and forces
374 |     the workspace to be unlocked.
375 | 
376 |     API endpoint: POST /runs/{run_id}/actions/force-cancel
377 | 
378 |     Args:
379 |         run_id: The ID of the run to force cancel (format: "run-xxxxxxxx")
380 |         comment: An optional explanation for why the run was force canceled
381 | 
382 |     Returns:
383 |         Run status update confirming forced cancellation with timestamp,
384 |         user information, and workspace unlock status
385 | 
386 |     See:
387 |         docs/tools/run.md for reference documentation
388 |     """
389 |     request = RunActionRequest(run_id=run_id, comment=comment)
390 | 
391 |     # Create payload if comment is provided
392 |     payload = {}
393 |     if request.comment:
394 |         payload = {"comment": request.comment}
395 | 
396 |     # Make API request
397 |     return await api_request(
398 |         f"runs/{run_id}/actions/force-cancel", method="POST", data=payload
399 |     )
400 | 
401 | 
402 | @handle_api_errors
403 | async def force_execute_run(run_id: str) -> APIResponse:
404 |     """Forcefully execute a run by canceling all prior runs
405 | 
406 |     Prioritizes a specific run by canceling other queued runs to unlock the workspace,
407 |     equivalent to clicking "Run this plan now" in the UI. Use this when a run is
408 |     stuck in the pending queue but needs immediate execution due to urgency or
409 |     priority over other queued runs.
410 | 
411 |     API endpoint: POST /runs/{run_id}/actions/force-execute
412 | 
413 |     Args:
414 |         run_id: The ID of the run to execute (format: "run-xxxxxxxx")
415 | 
416 |     Returns:
417 |         Status update confirming the run has been promoted to active status,
418 |         with information about which runs were canceled to allow execution
419 | 
420 |     See:
421 |         docs/tools/run.md for reference documentation
422 |     """
423 |     # Make API request
424 |     return await api_request(f"runs/{run_id}/actions/force-execute", method="POST")
425 | 
```

--------------------------------------------------------------------------------
/terraform_cloud_mcp/tools/workspaces.py:
--------------------------------------------------------------------------------

```python
  1 | """Workspace management tools for Terraform Cloud MCP
  2 | 
  3 | This module implements the workspace-related endpoints of the Terraform Cloud API.
  4 | Reference: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces
  5 | """
  6 | 
  7 | import logging
  8 | from typing import Optional
  9 | 
 10 | from ..api.client import api_request
 11 | from ..utils.decorators import handle_api_errors
 12 | from ..utils.payload import create_api_payload
 13 | from ..utils.request import query_params
 14 | from ..models.base import APIResponse
 15 | from ..models.workspaces import (
 16 |     WorkspaceCreateRequest,
 17 |     WorkspaceUpdateRequest,
 18 |     WorkspaceListRequest,
 19 |     WorkspaceParams,
 20 |     DataRetentionPolicyRequest,
 21 | )
 22 | 
 23 | 
 24 | @handle_api_errors
 25 | async def create_workspace(
 26 |     organization: str, name: str, params: Optional[WorkspaceParams] = None
 27 | ) -> APIResponse:
 28 |     """Create a new workspace in an organization.
 29 | 
 30 |     Creates a new Terraform Cloud workspace which serves as an isolated environment
 31 |     for managing infrastructure. Workspaces contain variables, state files, and run
 32 |     histories for a specific infrastructure configuration.
 33 | 
 34 |     API endpoint: POST /organizations/{organization}/workspaces
 35 | 
 36 |     Args:
 37 |         organization: The name of the organization
 38 |         name: The name to give the workspace
 39 | 
 40 |         params: Additional workspace parameters (optional):
 41 |             - description: Human-readable description of the workspace
 42 |             - execution_mode: How Terraform runs are executed (remote, local, agent)
 43 |             - terraform_version: Version of Terraform to use (default: latest)
 44 |             - working_directory: Subdirectory to use when running Terraform
 45 |             - vcs_repo: Version control repository configuration
 46 |             - auto_apply: Whether to automatically apply successful plans
 47 |             - file_triggers_enabled: Whether file changes trigger runs
 48 |             - trigger_prefixes: Directories that trigger runs when changed
 49 |             - trigger_patterns: Glob patterns that trigger runs when files match
 50 |             - allow_destroy_plan: Whether to allow destruction plans
 51 |             - auto_apply_run_trigger: Whether to auto-apply changes from run triggers
 52 | 
 53 |     Returns:
 54 |         The created workspace data including configuration, settings and metadata
 55 | 
 56 |     See:
 57 |         docs/tools/workspace.md for reference documentation
 58 |     """
 59 |     param_dict = params.model_dump(exclude_none=True) if params else {}
 60 |     request = WorkspaceCreateRequest(organization=organization, name=name, **param_dict)
 61 | 
 62 |     payload = create_api_payload(
 63 |         resource_type="workspaces", model=request, exclude_fields={"organization"}
 64 |     )
 65 | 
 66 |     return await api_request(
 67 |         f"organizations/{organization}/workspaces", method="POST", data=payload
 68 |     )
 69 | 
 70 | 
 71 | @handle_api_errors
 72 | async def update_workspace(
 73 |     organization: str, workspace_name: str, params: Optional[WorkspaceParams] = None
 74 | ) -> APIResponse:
 75 |     """Update an existing workspace.
 76 | 
 77 |     Modifies the settings of a Terraform Cloud workspace. This can be used to change
 78 |     attributes like execution mode, VCS repository settings, description, or any other
 79 |     workspace configuration options. Only specified attributes will be updated;
 80 |     unspecified attributes remain unchanged.
 81 | 
 82 |     API endpoint: PATCH /organizations/{organization}/workspaces/{workspace_name}
 83 | 
 84 |     Args:
 85 |         organization: The name of the organization that owns the workspace
 86 |         workspace_name: The name of the workspace to update
 87 | 
 88 |         params: Workspace parameters to update (optional):
 89 |             - name: New name for the workspace (if renaming)
 90 |             - description: Human-readable description of the workspace
 91 |             - execution_mode: How Terraform runs are executed (remote, local, agent)
 92 |             - terraform_version: Version of Terraform to use
 93 |             - working_directory: Subdirectory to use when running Terraform
 94 |             - vcs_repo: Version control repository configuration (oauth-token-id, identifier)
 95 |             - auto_apply: Whether to automatically apply successful plans
 96 |             - file_triggers_enabled: Whether file changes trigger runs
 97 |             - trigger_prefixes: Directories that trigger runs when changed
 98 |             - trigger_patterns: Glob patterns that trigger runs when files match
 99 |             - allow_destroy_plan: Whether to allow destruction plans
100 |             - auto_apply_run_trigger: Whether to auto-apply changes from run triggers
101 | 
102 |     Returns:
103 |         The updated workspace with all current settings and configuration
104 | 
105 |     See:
106 |         docs/tools/workspace.md for reference documentation
107 |     """
108 |     # Extract parameters from the params object if provided
109 |     param_dict = params.model_dump(exclude_none=True) if params else {}
110 | 
111 |     # Create request using Pydantic model
112 |     request = WorkspaceUpdateRequest(
113 |         organization=organization, workspace_name=workspace_name, **param_dict
114 |     )
115 | 
116 |     # Create API payload using utility function
117 |     payload = create_api_payload(
118 |         resource_type="workspaces",
119 |         model=request,
120 |         exclude_fields={"organization", "workspace_name"},
121 |     )
122 | 
123 |     # Log payload for debugging
124 |     logger = logging.getLogger(__name__)
125 |     logger.debug(f"Update workspace payload: {payload}")
126 | 
127 |     # Make API request
128 |     response = await api_request(
129 |         f"organizations/{organization}/workspaces/{workspace_name}",
130 |         method="PATCH",
131 |         data=payload,
132 |     )
133 | 
134 |     # Log response for debugging
135 |     logger.debug(f"Update workspace response: {response}")
136 | 
137 |     return response
138 | 
139 | 
140 | @handle_api_errors
141 | async def list_workspaces(
142 |     organization: str,
143 |     page_number: int = 1,
144 |     page_size: int = 20,
145 |     search: Optional[str] = None,
146 | ) -> APIResponse:
147 |     """List workspaces in an organization.
148 | 
149 |     Retrieves a paginated list of all workspaces in a Terraform Cloud organization.
150 |     Results can be filtered using a search string to find specific workspaces by name.
151 |     Use this tool to discover existing workspaces, check workspace configurations,
152 |     or find specific workspaces by partial name match.
153 | 
154 |     API endpoint: GET /organizations/{organization}/workspaces
155 | 
156 |     Args:
157 |         organization: The name of the organization to list workspaces from
158 |         page_number: The page number to return (default: 1)
159 |         page_size: The number of items per page (default: 20, max: 100)
160 |         search: Optional search string to filter workspaces by name
161 | 
162 |     Returns:
163 |         Paginated list of workspaces with their configuration settings and metadata
164 | 
165 |     See:
166 |         docs/tools/workspace.md for reference documentation
167 |     """
168 |     # Create request using Pydantic model for validation
169 |     request = WorkspaceListRequest(
170 |         organization=organization,
171 |         page_number=page_number,
172 |         page_size=page_size,
173 |         search=search,
174 |     )
175 | 
176 |     params = query_params(request)
177 | 
178 |     return await api_request(
179 |         f"organizations/{organization}/workspaces", method="GET", params=params
180 |     )
181 | 
182 | 
183 | @handle_api_errors
184 | async def delete_workspace(organization: str, workspace_name: str) -> APIResponse:
185 |     """Delete a workspace.
186 | 
187 |     Permanently deletes a Terraform Cloud workspace and all its resources including
188 |     state versions, run history, and configuration versions. This action cannot be undone.
189 | 
190 |     WARNING: This is a destructive operation. For workspaces that have active resources,
191 |     consider running a destroy plan first or use safe_delete_workspace instead.
192 | 
193 |     API endpoint: DELETE /organizations/{organization}/workspaces/{workspace_name}
194 | 
195 |     Args:
196 |         organization: The name of the organization that owns the workspace
197 |         workspace_name: The name of the workspace to delete
198 | 
199 |     Returns:
200 |         Success message with no content (HTTP 204) if successful
201 |         Error response with explanation if the workspace cannot be deleted
202 | 
203 |     See:
204 |         docs/tools/workspace.md for reference documentation
205 |     """
206 |     # Make API request
207 |     return await api_request(
208 |         f"organizations/{organization}/workspaces/{workspace_name}",
209 |         method="DELETE",
210 |     )
211 | 
212 | 
213 | @handle_api_errors
214 | async def safe_delete_workspace(organization: str, workspace_name: str) -> APIResponse:
215 |     """Safely delete a workspace by first checking if it can be deleted.
216 | 
217 |     Initiates a safe delete operation which checks if the workspace has resources
218 |     before deleting it. This is a safer alternative to delete_workspace as it prevents
219 |     accidental deletion of workspaces with active infrastructure.
220 | 
221 |     The operation follows these steps:
222 |     1. Checks if the workspace has any resources
223 |     2. If no resources exist, deletes the workspace
224 |     3. If resources exist, returns an error indicating the workspace cannot be safely deleted
225 | 
226 |     API endpoint: POST /organizations/{organization}/workspaces/{workspace_name}/actions/safe-delete
227 | 
228 |     Args:
229 |         organization: The name of the organization that owns the workspace
230 |         workspace_name: The name of the workspace to delete
231 | 
232 |     Returns:
233 |         Status of the safe delete operation including:
234 |         - Success response if deletion was completed
235 |         - Error with details if workspace has resources and cannot be safely deleted
236 |         - List of resources that would be affected by deletion (if applicable)
237 | 
238 |     See:
239 |         docs/tools/workspace.md for reference documentation
240 |     """
241 |     # Make API request
242 |     return await api_request(
243 |         f"organizations/{organization}/workspaces/{workspace_name}/actions/safe-delete",
244 |         method="POST",
245 |     )
246 | 
247 | 
248 | @handle_api_errors
249 | async def lock_workspace(workspace_id: str, reason: str = "") -> APIResponse:
250 |     """Lock a workspace.
251 | 
252 |     Locks a workspace to prevent runs from being queued. This is useful when you want
253 |     to prevent changes to infrastructure while performing maintenance or making manual
254 |     adjustments. Locking a workspace does not affect currently running plans or applies.
255 | 
256 |     API endpoint: POST /workspaces/{workspace_id}/actions/lock
257 | 
258 |     Args:
259 |         workspace_id: The ID of the workspace to lock (format: "ws-xxxxxxxx")
260 |         reason: Optional reason for locking
261 | 
262 |     Returns:
263 |         The workspace with updated lock status and related metadata
264 | 
265 |     See:
266 |         docs/tools/workspace.md for reference documentation
267 |     """
268 |     payload = {}
269 |     if reason:
270 |         payload = {"reason": reason}
271 |     return await api_request(
272 |         f"workspaces/{workspace_id}/actions/lock", method="POST", data=payload
273 |     )
274 | 
275 | 
276 | @handle_api_errors
277 | async def unlock_workspace(workspace_id: str) -> APIResponse:
278 |     """Unlock a workspace.
279 | 
280 |     Removes the lock from a workspace, allowing runs to be queued. This enables
281 |     normal operation of the workspace after it was previously locked.
282 | 
283 |     API endpoint: POST /workspaces/{workspace_id}/actions/unlock
284 | 
285 |     Args:
286 |         workspace_id: The ID of the workspace to unlock (format: "ws-xxxxxxxx")
287 | 
288 |     Returns:
289 |         The workspace with updated lock status and related metadata
290 | 
291 |     See:
292 |         docs/tools/workspace.md for reference documentation
293 |     """
294 |     return await api_request(f"workspaces/{workspace_id}/actions/unlock", method="POST")
295 | 
296 | 
297 | @handle_api_errors
298 | async def force_unlock_workspace(workspace_id: str) -> APIResponse:
299 |     """Force unlock a workspace. This should be used with caution.
300 | 
301 |     Forces a workspace to unlock even when the normal unlock process isn't possible.
302 |     This is typically needed when a run has orphaned a lock or when the user who locked
303 |     the workspace is unavailable. This operation requires admin privileges on the workspace.
304 | 
305 |     WARNING: Forcing an unlock can be dangerous if the workspace is legitimately locked
306 |     for active operations. Only use this when you are certain it's safe to unlock.
307 | 
308 |     API endpoint: POST /workspaces/{workspace_id}/actions/force-unlock
309 | 
310 |     Args:
311 |         workspace_id: The ID of the workspace to force unlock (format: "ws-xxxxxxxx")
312 | 
313 |     Returns:
314 |         The workspace with updated lock status and related metadata
315 | 
316 |     See:
317 |         docs/tools/workspace.md for reference documentation
318 |     """
319 |     # Make API request
320 |     return await api_request(
321 |         f"workspaces/{workspace_id}/actions/force-unlock", method="POST"
322 |     )
323 | 
324 | 
325 | @handle_api_errors
326 | async def set_data_retention_policy(workspace_id: str, days: int) -> APIResponse:
327 |     """Set a data retention policy for a workspace.
328 | 
329 |     Creates or updates a data retention policy that determines how long Terraform Cloud
330 |     keeps run history and state files for a workspace. This can be used to comply with
331 |     data retention requirements or to reduce resource usage.
332 | 
333 |     API endpoint: POST /workspaces/{workspace_id}/relationships/data-retention-policy
334 | 
335 |     Args:
336 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
337 |         days: Number of days to retain data
338 | 
339 |     Returns:
340 |         The created data retention policy with configuration details and timestamps
341 | 
342 |     See:
343 |         docs/tools/workspace.md for reference documentation
344 |     """
345 |     # Create request using Pydantic model
346 |     request = DataRetentionPolicyRequest(workspace_id=workspace_id, days=days)
347 | 
348 |     # Create API payload using utility function
349 |     payload = create_api_payload(
350 |         resource_type="data-retention-policy",
351 |         model=request,
352 |         exclude_fields={"workspace_id"},
353 |     )
354 | 
355 |     # Make API request
356 |     return await api_request(
357 |         f"workspaces/{workspace_id}/relationships/data-retention-policy",
358 |         method="POST",
359 |         data=payload,
360 |     )
361 | 
362 | 
363 | @handle_api_errors
364 | async def get_data_retention_policy(workspace_id: str) -> APIResponse:
365 |     """Get the data retention policy for a workspace.
366 | 
367 |     Retrieves the current data retention policy for a workspace, which defines how long
368 |     Terraform Cloud keeps run history and state files before automatic removal.
369 | 
370 |     API endpoint: GET /workspaces/{workspace_id}/relationships/data-retention-policy
371 | 
372 |     Args:
373 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
374 | 
375 |     Returns:
376 |         The data retention policy with configuration details and timestamps
377 | 
378 |     See:
379 |         docs/tools/workspace.md for reference documentation
380 |     """
381 |     # Make API request
382 |     return await api_request(
383 |         f"workspaces/{workspace_id}/relationships/data-retention-policy", method="GET"
384 |     )
385 | 
386 | 
387 | @handle_api_errors
388 | async def delete_data_retention_policy(workspace_id: str) -> APIResponse:
389 |     """Delete the data retention policy for a workspace.
390 | 
391 |     Removes the data retention policy from a workspace, reverting to the default behavior
392 |     of retaining all data indefinitely. This is useful when you no longer want to automatically
393 |     remove historical data after a certain period.
394 | 
395 |     API endpoint: DELETE /workspaces/{workspace_id}/relationships/data-retention-policy
396 | 
397 |     Args:
398 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
399 | 
400 |     Returns:
401 |         Empty response with HTTP 204 status code indicating successful deletion
402 | 
403 |     See:
404 |         docs/tools/workspace.md for reference documentation
405 |     """
406 |     # Make API request
407 |     return await api_request(
408 |         f"workspaces/{workspace_id}/relationships/data-retention-policy",
409 |         method="DELETE",
410 |     )
411 | 
412 | 
413 | @handle_api_errors
414 | async def get_workspace_details(
415 |     workspace_id: str = "", organization: str = "", workspace_name: str = ""
416 | ) -> APIResponse:
417 |     """Get details for a specific workspace, identified either by ID or by org name and workspace name.
418 | 
419 |     Retrieves comprehensive information about a workspace including its configuration,
420 |     VCS settings, execution mode, and other attributes. This is useful for checking
421 |     workspace settings before operations or determining the current state of a workspace.
422 | 
423 |     The workspace can be identified either by its ID directly, or by the combination
424 |     of organization name and workspace name.
425 | 
426 |     API endpoint:
427 |     - GET /workspaces/{workspace_id} (when using workspace_id)
428 |     - GET /organizations/{organization}/workspaces/{workspace_name} (when using org+name)
429 | 
430 |     Args:
431 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
432 |         organization: The name of the organization (required if workspace_id not provided)
433 |         workspace_name: The name of the workspace (required if workspace_id not provided)
434 | 
435 |     Returns:
436 |         Comprehensive workspace details including settings, configuration and status
437 | 
438 |     See:
439 |         docs/tools/workspace.md for reference documentation
440 |     """
441 |     # Ensure we have either workspace_id OR both organization and workspace_name
442 |     if not workspace_id and not (organization and workspace_name):
443 |         raise ValueError(
444 |             "Either workspace_id OR both organization and workspace_name must be provided"
445 |         )
446 | 
447 |     # Determine API path based on provided parameters
448 |     if workspace_id:
449 |         path = f"workspaces/{workspace_id}"
450 |     else:
451 |         path = f"organizations/{organization}/workspaces/{workspace_name}"
452 | 
453 |     # Make API request
454 |     return await api_request(path, method="GET")
455 | 
```

--------------------------------------------------------------------------------
/docs/DEVELOPMENT.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Terraform Cloud MCP Development Guide
  2 | 
  3 | This document outlines the development guidelines, code standards, and best practices for the Terraform Cloud MCP project.
  4 | 
  5 | ## Getting Started
  6 | 
  7 | ### Development Setup
  8 | 
  9 | ```bash
 10 | # Clone the repository
 11 | git clone https://github.com/severity1/terraform-cloud-mcp.git
 12 | cd terraform-cloud-mcp
 13 | 
 14 | # Create virtual environment and activate it
 15 | uv venv
 16 | source .venv/bin/activate
 17 | 
 18 | # Install in development mode (editable)
 19 | uv pip install -e .
 20 | 
 21 | # Install development dependencies
 22 | uv pip install black mypy pydantic ruff
 23 | ```
 24 | 
 25 | ### Build & Run Commands
 26 | 
 27 | - Setup: `uv pip install -e .` (install with latest changes)
 28 | - Install dev deps: `uv pip install black mypy pydantic ruff`
 29 | - Format: `uv pip install black && uv run -m black .` 
 30 | - Type check: `uv pip install mypy && uv run -m mypy .`
 31 | - Lint: `uv pip install ruff && uv run -m ruff check .`
 32 | - Fix lint issues: `uv pip install ruff && uv run -m ruff check --fix .`
 33 | 
 34 | ### Development With Claude Integrations
 35 | 
 36 | #### Adding to Claude Code (Development Mode)
 37 | 
 38 | ```bash
 39 | # Add to Claude Code with your development path and token
 40 | claude mcp add -e TFC_TOKEN=YOUR_TF_TOKEN -s user terraform-cloud-mcp -- "$(pwd)/terraform_cloud_mcp/server.py"
 41 | 
 42 | # To use a self-hosted Terraform Enterprise instance:
 43 | # claude mcp add -e TFC_TOKEN=YOUR_TF_TOKEN -e TFC_ADDRESS=https://terraform.example.com -s user terraform-cloud-mcp -- "$(pwd)/terraform_cloud_mcp/server.py"
 44 | ```
 45 | 
 46 | #### Adding to Claude Desktop (Development Mode)
 47 | 
 48 | Create a `claude_desktop_config.json` configuration file:
 49 | - mac: ~/Library/Application Support/Claude/claude_desktop_config.json
 50 | - win: %APPDATA%\Claude\claude_desktop_config.json
 51 | 
 52 | ```json
 53 | {
 54 |   "mcpServers": {
 55 |     "terraform-cloud-mcp": {
 56 |       "command": "/path/to/uv", # Get this by running: `which uv`
 57 |       "args": [
 58 |         "--directory",
 59 |         "/path/to/your/terraform-cloud-mcp", # Full path to this project
 60 |         "run",
 61 |         "terraform_cloud_mcp/server.py"
 62 |       ],
 63 |       "env": {
 64 |         "TFC_TOKEN": "your_terraform_cloud_token", # Your actual TF Cloud token
 65 |         "TFC_ADDRESS": "https://app.terraform.io" # Optional, change for self-hosted TFE
 66 |       }
 67 |     }
 68 |   }
 69 | }
 70 | ```
 71 | 
 72 | ## Core Principles
 73 | 
 74 | - **KISS (Keep It Simple, Stupid)**: Favor simple, maintainable solutions over complex ones
 75 | - **DRY (Don't Repeat Yourself)**: Use utility functions for common patterns
 76 | - **Consistency**: Follow established patterns throughout the codebase
 77 | - **Type Safety**: Use proper typing and validation everywhere
 78 | - **Documentation**: All code should be well-documented with standardized formats
 79 | - **Testability**: Write code that can be easily tested
 80 | - **Modularity**: Keep components focused and decoupled
 81 | 
 82 | ## Code Organization
 83 | 
 84 | ```
 85 | terraform_cloud_mcp/
 86 | ├── api/                # API client and core request handling
 87 | │   ├── __init__.py
 88 | │   └── client.py       # Core API client with error handling
 89 | ├── models/             # Pydantic data models for validation
 90 | │   ├── __init__.py
 91 | │   ├── account.py      # Account-related models
 92 | │   ├── applies.py      # Apply-related models
 93 | │   ├── assessment_results.py # Assessment results models
 94 | │   ├── base.py         # Base model classes and shared types
 95 | │   ├── cost_estimates.py # Cost estimation models
 96 | │   ├── organizations.py # Organization models
 97 | │   ├── plans.py        # Plan-related models
 98 | │   ├── projects.py     # Project management models
 99 | │   ├── runs.py         # Run management models
100 | │   └── workspaces.py   # Workspace management models
101 | ├── tools/              # Tool implementations exposed via MCP
102 | │   ├── __init__.py
103 | │   ├── account.py      # Account management tools
104 | │   ├── applies.py      # Apply management tools
105 | │   ├── assessment_results.py # Assessment results tools
106 | │   ├── cost_estimates.py # Cost estimation tools
107 | │   ├── organizations.py # Organization management tools
108 | │   ├── plans.py        # Plan management tools
109 | │   ├── projects.py     # Project management tools
110 | │   ├── runs.py         # Run management tools
111 | │   └── workspaces.py   # Workspace management tools
112 | ├── utils/              # Shared utilities
113 | │   ├── __init__.py
114 | │   ├── decorators.py   # Error handling decorators
115 | │   ├── filters.py      # Response filtering for token optimization
116 | │   ├── payload.py      # JSON:API payload utilities
117 | │   └── request.py      # Request parameter utilities
118 | └── server.py           # MCP server entry point
119 | ```
120 | 
121 | ## Code Style Guidelines
122 | 
123 | ### General Guidelines
124 | 
125 | - **Imports**: stdlib → third-party → local, alphabetically within groups
126 | - **Formatting**: Black, 100 char line limit
127 | - **Types**: Type hints everywhere, Pydantic models for validation
128 | - **Naming**: snake_case (functions/vars), PascalCase (classes), UPPER_CASE (constants)
129 | - **Error handling**: Use `handle_api_errors` decorator from `terraform_cloud_mcp/utils/decorators.py`
130 | - **Audit-safe filtering**: Automatic filtering system uses conservative 5-15% token reduction while preserving 100% audit compliance - all user accountability, security, and change tracking data preserved
131 | - **Async pattern**: All API functions should be async, using httpx
132 | - **Security**: Never log tokens, validate all inputs, redact sensitive data
133 | - **MCP Tool Registration**: Follow minimal pattern in `server.py`:
134 |   - Use simple `mcp.tool()(function_name)` for standard operations
135 |   - Use `mcp.tool(enabled=False)(function_name)` for dangerous delete operations
136 | 
137 | ## Pydantic Model Standards
138 | 
139 | ### Pattern Principles
140 | - **Validation First**: Use Pydantic to validate all input parameters
141 | - **Explicit Aliases**: Use field aliases for API compatibility (`kebab-case` to `snake_case`)
142 | - **Base Models**: Extend from common base classes
143 | - **No Response Models**: Use `APIResponse` type alias (`Dict[str, Any]`) for responses
144 | 
145 | ### Model Structure
146 | 
147 | 1. **Base Classes**:
148 |    See the base classes defined in `terraform_cloud_mcp/models/base.py`:
149 |    - `BaseModelConfig` - Core configuration for all models
150 |    - `APIRequest` - Base class for all API requests
151 |    - `APIResponse` type alias
152 | 
153 | 2. **Request Models**:
154 |    For examples of request models with field aliases, see:
155 |    - `VcsRepoConfig` in `terraform_cloud_mcp/models/workspaces.py`
156 |    - `WorkspaceCreateRequest` in `terraform_cloud_mcp/models/workspaces.py`
157 |    - `WorkspaceListRequest` in `terraform_cloud_mcp/models/workspaces.py`
158 | 
159 | 3. **Enum Classes**:
160 |    For enum implementation examples, see:
161 |    - `ExecutionMode` in `terraform_cloud_mcp/models/base.py`
162 |    - Status enums in `terraform_cloud_mcp/models/runs.py`
163 | 
164 | ### Implementation Pattern
165 | 
166 | 1. **Tool Implementation**:
167 |    See the implementation pattern in `terraform_cloud_mcp/tools/workspaces.py`:
168 |    - `create_workspace` function for a complete implementation example
169 |    - `update_workspace` function for updating existing resources
170 |    - `list_workspaces` function for listing resources with pagination
171 |    - Other CRUD operations in workspace management tools
172 | 
173 | ### Pydantic Best Practices
174 | 
175 | 1. **Use Field Aliases** for API compatibility:
176 |    See the `execution_mode` field in `terraform_cloud_mcp/models/workspaces.py` (class `BaseWorkspaceRequest`)
177 | 
178 | 2. **Use Field Validators** for constraints:
179 |    See the `page_size` field in `terraform_cloud_mcp/models/workspaces.py` (class `WorkspaceListRequest`) 
180 | 
181 | 3. **Use Description** for clarity:
182 |    See the `description` field in `terraform_cloud_mcp/models/workspaces.py` (class `BaseWorkspaceRequest`)
183 | 
184 | 4. **Use Enums** for constrained choices:
185 |    See `ExecutionMode` enum in `terraform_cloud_mcp/models/base.py`
186 | 
187 | 5. **Parameter Inheritance** for common parameters:
188 |    See the class hierarchy in `terraform_cloud_mcp/models/workspaces.py`:
189 |    - `BaseWorkspaceRequest` defines common fields
190 |    - `WorkspaceCreateRequest` extends it with required fields
191 |    - `WorkspaceUpdateRequest` adds routing fields
192 |    - `WorkspaceParams` provides a parameter object without routing fields
193 | 
194 | ## Utility Functions
195 | 
196 | ### JSON:API Payload Utilities
197 | To ensure consistent handling of API payloads, use the utility functions from `terraform_cloud_mcp/utils/payload.py`:
198 | 
199 | 1. **create_api_payload**:
200 |    See implementation in `terraform_cloud_mcp/utils/payload.py` and example usage in `create_workspace` function in `terraform_cloud_mcp/tools/workspaces.py`
201 | 
202 | 2. **add_relationship**:
203 |    See implementation in `terraform_cloud_mcp/utils/payload.py` and usage examples in `terraform_cloud_mcp/tools/runs.py`
204 | 
205 | ### Request Parameter Utilities
206 | For handling pagination and request parameters, see `terraform_cloud_mcp/utils/request.py` and its usage in list operations like `list_workspaces` in `terraform_cloud_mcp/tools/workspaces.py`
207 | 
208 | ## Documentation Standards
209 | 
210 | ### Documentation Principles
211 | - **Essential Information**: Focus on what's needed without verbosity
212 | - **Completeness**: Provide enough context to understand usage
213 | - **External References**: Move examples to dedicated documentation files
214 | - **Agent-Friendly**: Include sufficient context for AI agents/LLMs
215 | 
216 | ### Model Classes
217 | 
218 | See `VcsRepoConfig` class in `terraform_cloud_mcp/models/workspaces.py` for proper model documentation
219 | 
220 | For derived classes that inherit from a base class, see `WorkspaceCreateRequest` in `terraform_cloud_mcp/models/workspaces.py` as an example of how to document inheritance
221 | 
222 | ### Tool Functions
223 | 
224 | See `create_workspace` function in `terraform_cloud_mcp/tools/workspaces.py` for proper tool function documentation including:
225 | - Purpose description
226 | - API endpoint reference
227 | - Parameter documentation
228 | - Return value description
229 | - External documentation references
230 | 
231 | ### Utility Functions
232 | 
233 | See utility functions in `terraform_cloud_mcp/utils/payload.py` and `terraform_cloud_mcp/utils/request.py` for examples of properly documented helper functions
234 | 
235 | ### Documentation Structure
236 | 
237 | Documentation is organized in dedicated markdown files:
238 | 
239 | ```
240 | docs/
241 |   models/             # Documentation for Pydantic models
242 |     account.md
243 |     apply.md
244 |     assessment_result.md
245 |     cost_estimate.md
246 |     organization.md
247 |     plan.md
248 |     project.md
249 |     run.md
250 |     workspace.md
251 |   tools/              # Reference documentation for MCP tools
252 |     account.md
253 |     apply.md
254 |     assessment_results.md
255 |     cost_estimate.md
256 |     organization.md
257 |     plan.md
258 |     project.md
259 |     run.md
260 |     workspace.md
261 |   conversations/      # Example conversations using the tools
262 |     account.md
263 |     apply-management-conversation.md
264 |     assessment-results-conversation.md
265 |     cost-estimate-conversation.md
266 |     organization-entitlements-conversation.md
267 |     organizations-management-conversation.md
268 |     plan-management-conversation.md
269 |     project-management-conversation.md
270 |     runs-management-conversation.md
271 |     workspace-management-conversation.md
272 | ```
273 | 
274 | #### Tool Documentation Format
275 | 
276 | Each tool documentation file should follow this structure:
277 | 
278 | ```markdown
279 | # Module Name Tools
280 | 
281 | Brief introduction about the module's purpose.
282 | 
283 | ## Overview
284 | 
285 | Detailed explanation of the functionality and concepts.
286 | 
287 | ## API Reference
288 | 
289 | Links to relevant Terraform Cloud API documentation:
290 | - [API Section 1](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/section)
291 | - [API Section 2](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/section)
292 | 
293 | ## Tools Reference
294 | 
295 | ### function_name
296 | 
297 | **Function:** `function_name(param1: type, param2: type) -> ReturnType`
298 | 
299 | **Description:** Explanation of what the function does.
300 | 
301 | **Parameters:**
302 | - `param1` (type): Parameter description
303 | - `param2` (type): Parameter description
304 | 
305 | **Returns:** Description of return value structure
306 | 
307 | **Notes:**
308 | - Important usage information
309 | - Related functionality
310 | - Permissions required
311 | 
312 | **Common Error Scenarios:**
313 | 
314 | | Error | Cause | Solution |
315 | |-------|-------|----------|
316 | | 404   | Resource not found | Verify ID and permissions |
317 | | 422   | Invalid parameters | Ensure values match required format |
318 | ```
319 | 
320 | ## Code Commenting Standards
321 | 
322 | The KISS (Keep It Simple, Stupid) principle applies to comments as much as code. Comments should be minimal, precise, and focused only on what's not obvious from the code itself.
323 | 
324 | ### Comment Only When Necessary
325 | 
326 | Add comments only in these essential situations:
327 | 
328 | 1. **Non-obvious "Why"**: Explain reasoning that isn't evident from reading the code
329 | 2. **Complex Logic**: Brief explanation of multi-step transformations or algorithms
330 | 3. **Edge Cases**: Why special handling is needed for boundary conditions
331 | 4. **Security Considerations**: Rationale for security measures (without exposing vulnerabilities)
332 | 5. **API Requirements**: Explanations of why code conforms to specific API requirements
333 | 
334 | ### Avoid Unnecessary Comments
335 | 
336 | 1. **No "What" Comments**: Don't describe what the code does when it's self-evident
337 | 2. **No Redundant Information**: Don't repeat documentation that exists in docstrings
338 | 3. **No Commented-Out Code**: Delete unused code rather than commenting it out
339 | 4. **No Obvious Comments**: Don't state the obvious (e.g., "Increment counter")
340 | 
341 | ### Effective Comment Examples
342 | 
343 | #### For Complex Transformations
344 | ```python
345 | # Transform variables array to required API format with key-value pairs
346 | variables_array = [{"key": var.key, "value": var.value} for var in variables]
347 | ```
348 | 
349 | #### For Error Handling
350 | ```python
351 | # Return standardized success response for 204 No Content to ensure consistent interface
352 | if response.status_code == 204:
353 |     return {"status": "success"}
354 | ```
355 | 
356 | #### For Security Measures
357 | ```python
358 | # Redact token from error message to prevent credential exposure
359 | error_message = error_message.replace(token, "[REDACTED]")
360 | ```
361 | 
362 | #### For Performance Considerations
363 | ```python
364 | # Use exclude_unset to prevent default values from overriding server defaults
365 | request_data = data.model_dump(exclude_unset=True)
366 | ```
367 | 
368 | ## Quality Assurance Protocol
369 | 
370 | ### Mandatory Quality Check Sequence
371 | 
372 | After ANY code changes, run these commands in exact order:
373 | 
374 | 1. **`uv run -m ruff check --fix .`** - Fix linting issues automatically
375 | 2. **`uv run -m black .`** - Format code consistently  
376 | 3. **`uv run -m mypy .`** - Verify type safety
377 | 4. **Manual Testing** - Test basic tool functionality (see methodology below)
378 | 5. **Documentation Completeness** - Verify using checklist below
379 | 
380 | **IMPORTANT**: Fix all issues at each step before proceeding to the next step.
381 | 
382 | ### Manual Testing Methodology
383 | 
384 | For each new tool, test these scenarios in order:
385 | 
386 | 1. **Happy Path**: Test with valid, typical parameters
387 | 2. **Edge Cases**: Test with boundary values, empty strings, None values  
388 | 3. **Error Cases**: Test with invalid IDs, missing permissions, malformed data
389 | 4. **Integration**: Test with related tools in realistic workflows
390 | 
391 | ### Documentation Completeness Checklist
392 | 
393 | - [ ] Function docstring includes API endpoint reference
394 | - [ ] Parameter descriptions include formats and constraints
395 | - [ ] Return value description explains structure and key fields
396 | - [ ] docs/tools/ entry created with function signature and examples
397 | - [ ] docs/models/ entry created if new models added
398 | - [ ] docs/conversations/ updated with realistic usage scenario
399 | - [ ] Cross-references between all documentation layers verified
400 | - [ ] All links in documentation are valid and accessible
401 | 
402 | ### Quality Standards
403 | 
404 | - **Code Coverage**: All new functions must have comprehensive docstrings
405 | - **Type Safety**: All parameters and return values must have type hints
406 | - **Error Handling**: All tools must use @handle_api_errors decorator
407 | - **Security**: No tokens or sensitive data in logs or error messages
408 | - **Consistency**: New code must follow established patterns in existing codebase
409 | 
410 | ## Enhanced Code Style Guidelines
411 | 
412 | ### Core Principles
413 | - **KISS Principle**: Keep It Simple, Stupid. Favor simple, maintainable solutions over complex ones.
414 | - **DRY Principle**: Don't Repeat Yourself. Use utility functions for common patterns.
415 | - **Imports**: stdlib → third-party → local, alphabetically within groups
416 | - **Formatting**: Black, 100 char line limit
417 | - **Types**: Type hints everywhere, Pydantic models for validation
418 | - **Naming**: snake_case (functions/vars), PascalCase (classes), UPPER_CASE (constants)
419 | - **Error handling**: Use `handle_api_errors` decorator from `terraform_cloud_mcp/utils/decorators.py`
420 | - **Async pattern**: All API functions should be async, using httpx
421 | - **Security**: Never log tokens, validate all inputs, redact sensitive data
422 | 
423 | ### Pydantic Patterns
424 | See `terraform_cloud_mcp/models/workspaces.py` for reference implementation:
425 | - Use `BaseModelConfig` base class for common configuration
426 | - Use `APIRequest` for request validation
427 | - Define explicit model classes for parameter objects (e.g., `WorkspaceParams`)
428 | - Use `params` parameter instead of `**kwargs` in tool functions
429 | - Use explicit field aliases (e.g., `alias="kebab-case-name"`) for API field mapping
430 | - Type API responses as `APIResponse` (alias for `Dict[str, Any]`)
431 | 
432 | ### Utility Functions
433 | Use common utilities for repetitive patterns:
434 | - `create_api_payload()` from `utils/payload.py` for JSON:API payload creation
435 | - `add_relationship()` from `utils/payload.py` for relationship management
436 | - `query_params()` from `utils/request.py` for converting model to API parameters
437 | 
438 | ### API Response Handling
439 | - Handle 204 No Content responses properly, returning `{"status": "success", "status_code": 204}`
440 | - Implement custom redirect handling for pre-signed URLs
441 | - Use proper error handling for JSON parsing failures
442 | 
443 | ### Documentation Standards
444 | - Docstrings with Args/Returns for all functions
445 | - Reference specific code implementations in docs rather than code snippets
446 | - See cost_estimates.py for latest documentation patterns
447 | 
448 | ### Comments Guidelines
449 | Follow KISS principles for comments:
450 | - Only explain the non-obvious "why" behind code choices, not the "what"
451 | - Add comments for complex logic, edge cases, security measures, or API-specific requirements
452 | - Avoid redundant, unnecessary, or self-explanatory comments
453 | - Keep comments concise and directly relevant
454 | 
455 | ## Contributing
456 | 
457 | For details on how to extend the server, contribute code, and the release process, see our [Contributing Guide](CONTRIBUTING.md).
```

--------------------------------------------------------------------------------
/terraform_cloud_mcp/tools/variables.py:
--------------------------------------------------------------------------------

```python
  1 | """Variable management tools for Terraform Cloud MCP
  2 | 
  3 | This module implements workspace variables and variable sets endpoints
  4 | of the Terraform Cloud API.
  5 | 
  6 | Reference:
  7 | - https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspace-variables
  8 | - https://developer.hashicorp.com/terraform/cloud-docs/api-docs/variable-sets
  9 | """
 10 | 
 11 | from typing import List, Optional
 12 | 
 13 | from ..api.client import api_request
 14 | from ..utils.decorators import handle_api_errors
 15 | from ..utils.payload import create_api_payload
 16 | from ..utils.request import query_params
 17 | from ..models.base import APIResponse
 18 | from ..models.variables import (
 19 |     WorkspaceVariableCreateRequest,
 20 |     WorkspaceVariableUpdateRequest,
 21 |     WorkspaceVariableParams,
 22 |     VariableSetCreateRequest,
 23 |     VariableSetUpdateRequest,
 24 |     VariableSetParams,
 25 |     VariableSetVariableParams,
 26 |     VariableSetListRequest,
 27 |     VariableCategory,
 28 | )
 29 | 
 30 | 
 31 | # Workspace Variables Tools
 32 | 
 33 | 
 34 | @handle_api_errors
 35 | async def list_workspace_variables(workspace_id: str) -> APIResponse:
 36 |     """List all variables for a workspace.
 37 | 
 38 |     Retrieves all variables (both Terraform and environment) configured
 39 |     for a specific workspace.
 40 | 
 41 |     API endpoint: GET /workspaces/{workspace_id}/vars
 42 | 
 43 |     Args:
 44 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
 45 | 
 46 |     Returns:
 47 |         List of workspace variables with their configuration and values
 48 | 
 49 |     See:
 50 |         docs/tools/variables.md#list-workspace-variables for reference documentation
 51 |     """
 52 |     endpoint = f"workspaces/{workspace_id}/vars"
 53 |     return await api_request(endpoint, method="GET")
 54 | 
 55 | 
 56 | @handle_api_errors
 57 | async def create_workspace_variable(
 58 |     workspace_id: str,
 59 |     key: str,
 60 |     category: str,
 61 |     params: Optional[WorkspaceVariableParams] = None,
 62 | ) -> APIResponse:
 63 |     """Create a new variable in a workspace.
 64 | 
 65 |     Creates a new Terraform or environment variable within a workspace.
 66 |     Variables can be marked as sensitive to hide their values.
 67 | 
 68 |     API endpoint: POST /workspaces/{workspace_id}/vars
 69 | 
 70 |     Args:
 71 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
 72 |         key: The variable name/key
 73 |         category: Variable category ("terraform" or "env")
 74 | 
 75 |         params: Additional variable parameters (optional):
 76 |             - value: Variable value
 77 |             - description: Description of the variable
 78 |             - hcl: Whether the value is HCL code (terraform variables only)
 79 |             - sensitive: Whether the variable value is sensitive
 80 | 
 81 |     Returns:
 82 |         The created variable with its configuration and metadata
 83 | 
 84 |     See:
 85 |         docs/tools/variables.md#create-workspace-variable for reference documentation
 86 |     """
 87 |     param_dict = params.model_dump(exclude_none=True) if params else {}
 88 |     request = WorkspaceVariableCreateRequest(
 89 |         workspace_id=workspace_id,
 90 |         key=key,
 91 |         category=VariableCategory(category),
 92 |         **param_dict,
 93 |     )
 94 | 
 95 |     payload = create_api_payload(
 96 |         resource_type="vars", model=request, exclude_fields={"workspace_id"}
 97 |     )
 98 | 
 99 |     return await api_request(
100 |         f"workspaces/{workspace_id}/vars", method="POST", data=payload
101 |     )
102 | 
103 | 
104 | @handle_api_errors
105 | async def update_workspace_variable(
106 |     workspace_id: str,
107 |     variable_id: str,
108 |     params: Optional[WorkspaceVariableParams] = None,
109 | ) -> APIResponse:
110 |     """Update an existing workspace variable.
111 | 
112 |     Modifies the configuration of an existing workspace variable. Only
113 |     specified attributes will be updated; unspecified attributes remain unchanged.
114 | 
115 |     API endpoint: PATCH /workspaces/{workspace_id}/vars/{variable_id}
116 | 
117 |     Args:
118 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
119 |         variable_id: The ID of the variable (format: "var-xxxxxxxx")
120 | 
121 |         params: Variable parameters to update (optional):
122 |             - key: New variable name/key
123 |             - value: New variable value
124 |             - description: New description of the variable
125 |             - category: New variable category ("terraform" or "env")
126 |             - hcl: Whether the value is HCL code (terraform variables only)
127 |             - sensitive: Whether the variable value is sensitive
128 | 
129 |     Returns:
130 |         The updated variable with all current settings and configuration
131 | 
132 |     See:
133 |         docs/tools/variables.md#update-workspace-variable for reference documentation
134 |     """
135 |     param_dict = params.model_dump(exclude_none=True) if params else {}
136 |     request = WorkspaceVariableUpdateRequest(
137 |         workspace_id=workspace_id, variable_id=variable_id, **param_dict
138 |     )
139 | 
140 |     payload = create_api_payload(
141 |         resource_type="vars",
142 |         model=request,
143 |         exclude_fields={"workspace_id", "variable_id"},
144 |     )
145 | 
146 |     return await api_request(
147 |         f"workspaces/{workspace_id}/vars/{variable_id}", method="PATCH", data=payload
148 |     )
149 | 
150 | 
151 | @handle_api_errors
152 | async def delete_workspace_variable(workspace_id: str, variable_id: str) -> APIResponse:
153 |     """Delete a workspace variable.
154 | 
155 |     Permanently removes a variable from a workspace. This action cannot be undone.
156 | 
157 |     API endpoint: DELETE /workspaces/{workspace_id}/vars/{variable_id}
158 | 
159 |     Args:
160 |         workspace_id: The ID of the workspace (format: "ws-xxxxxxxx")
161 |         variable_id: The ID of the variable (format: "var-xxxxxxxx")
162 | 
163 |     Returns:
164 |         Empty response with HTTP 204 status code if successful
165 | 
166 |     See:
167 |         docs/tools/variables.md#delete-workspace-variable for reference documentation
168 |     """
169 |     endpoint = f"workspaces/{workspace_id}/vars/{variable_id}"
170 |     return await api_request(endpoint, method="DELETE")
171 | 
172 | 
173 | # Variable Sets Tools
174 | 
175 | 
176 | @handle_api_errors
177 | async def list_variable_sets(
178 |     organization: str,
179 |     page_number: int = 1,
180 |     page_size: int = 20,
181 | ) -> APIResponse:
182 |     """List variable sets in an organization.
183 | 
184 |     Retrieves a paginated list of all variable sets in a Terraform Cloud organization.
185 |     Variable sets allow you to reuse variables across multiple workspaces.
186 | 
187 |     API endpoint: GET /organizations/{organization}/varsets
188 | 
189 |     Args:
190 |         organization: The name of the organization
191 |         page_number: The page number to return (default: 1)
192 |         page_size: The number of items per page (default: 20, max: 100)
193 | 
194 |     Returns:
195 |         Paginated list of variable sets with their configuration and metadata
196 | 
197 |     See:
198 |         docs/tools/variables.md#list-variable-sets for reference documentation
199 |     """
200 |     request = VariableSetListRequest(
201 |         organization=organization,
202 |         page_number=page_number,
203 |         page_size=page_size,
204 |     )
205 | 
206 |     params = query_params(request)
207 | 
208 |     return await api_request(
209 |         f"organizations/{organization}/varsets", method="GET", params=params
210 |     )
211 | 
212 | 
213 | @handle_api_errors
214 | async def get_variable_set(varset_id: str) -> APIResponse:
215 |     """Get details for a specific variable set.
216 | 
217 |     Retrieves comprehensive information about a variable set including its
218 |     variables, workspace assignments, and configuration.
219 | 
220 |     API endpoint: GET /varsets/{varset_id}
221 | 
222 |     Args:
223 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
224 | 
225 |     Returns:
226 |         Variable set details including configuration and relationships
227 | 
228 |     See:
229 |         docs/tools/variables.md#get-variable-set for reference documentation
230 |     """
231 |     endpoint = f"varsets/{varset_id}"
232 |     return await api_request(endpoint, method="GET")
233 | 
234 | 
235 | @handle_api_errors
236 | async def create_variable_set(
237 |     organization: str,
238 |     name: str,
239 |     params: Optional[VariableSetParams] = None,
240 | ) -> APIResponse:
241 |     """Create a new variable set in an organization.
242 | 
243 |     Creates a new variable set which can be used to manage variables across
244 |     multiple workspaces and projects.
245 | 
246 |     API endpoint: POST /organizations/{organization}/varsets
247 | 
248 |     Args:
249 |         organization: The name of the organization
250 |         name: The name to give the variable set
251 | 
252 |         params: Additional variable set parameters (optional):
253 |             - description: Description of the variable set
254 |             - global: Whether this is a global variable set
255 |             - priority: Whether this variable set takes priority over workspace variables
256 | 
257 |     Returns:
258 |         The created variable set with its configuration and metadata
259 | 
260 |     See:
261 |         docs/tools/variables.md#create-variable-set for reference documentation
262 |     """
263 |     param_dict = params.model_dump(exclude_none=True) if params else {}
264 |     request = VariableSetCreateRequest(
265 |         organization=organization, name=name, **param_dict
266 |     )
267 | 
268 |     payload = create_api_payload(
269 |         resource_type="varsets", model=request, exclude_fields={"organization"}
270 |     )
271 | 
272 |     return await api_request(
273 |         f"organizations/{organization}/varsets", method="POST", data=payload
274 |     )
275 | 
276 | 
277 | @handle_api_errors
278 | async def update_variable_set(
279 |     varset_id: str,
280 |     params: Optional[VariableSetParams] = None,
281 | ) -> APIResponse:
282 |     """Update an existing variable set.
283 | 
284 |     Modifies the settings of a variable set. Only specified attributes will be
285 |     updated; unspecified attributes remain unchanged.
286 | 
287 |     API endpoint: PATCH /varsets/{varset_id}
288 | 
289 |     Args:
290 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
291 | 
292 |         params: Variable set parameters to update (optional):
293 |             - name: New name for the variable set
294 |             - description: New description of the variable set
295 |             - global: Whether this is a global variable set
296 |             - priority: Whether this variable set takes priority over workspace variables
297 | 
298 |     Returns:
299 |         The updated variable set with all current settings and configuration
300 | 
301 |     See:
302 |         docs/tools/variables.md#update-variable-set for reference documentation
303 |     """
304 |     param_dict = params.model_dump(exclude_none=True) if params else {}
305 |     request = VariableSetUpdateRequest(varset_id=varset_id, **param_dict)
306 | 
307 |     payload = create_api_payload(
308 |         resource_type="varsets", model=request, exclude_fields={"varset_id"}
309 |     )
310 | 
311 |     return await api_request(f"varsets/{varset_id}", method="PATCH", data=payload)
312 | 
313 | 
314 | @handle_api_errors
315 | async def delete_variable_set(varset_id: str) -> APIResponse:
316 |     """Delete a variable set.
317 | 
318 |     Permanently removes a variable set and all its variables. This action cannot be undone.
319 |     The variable set will be unassigned from all workspaces and projects.
320 | 
321 |     API endpoint: DELETE /varsets/{varset_id}
322 | 
323 |     Args:
324 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
325 | 
326 |     Returns:
327 |         Empty response with HTTP 204 status code if successful
328 | 
329 |     See:
330 |         docs/tools/variables.md#delete-variable-set for reference documentation
331 |     """
332 |     endpoint = f"varsets/{varset_id}"
333 |     return await api_request(endpoint, method="DELETE")
334 | 
335 | 
336 | @handle_api_errors
337 | async def assign_variable_set_to_workspaces(
338 |     varset_id: str, workspace_ids: List[str]
339 | ) -> APIResponse:
340 |     """Assign a variable set to one or more workspaces.
341 | 
342 |     Makes the variables in a variable set available to the specified workspaces.
343 |     Variables from variable sets take precedence over workspace variables if
344 |     the variable set has priority enabled.
345 | 
346 |     API endpoint: POST /varsets/{varset_id}/relationships/workspaces
347 | 
348 |     Args:
349 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
350 |         workspace_ids: List of workspace IDs (format: ["ws-xxxxxxxx", ...])
351 | 
352 |     Returns:
353 |         Empty response with HTTP 204 status code if successful
354 | 
355 |     See:
356 |         docs/tools/variables.md#assign-variable-set-to-workspaces for reference documentation
357 |     """
358 |     # Build relationships payload
359 |     relationships_data = []
360 |     for workspace_id in workspace_ids:
361 |         relationships_data.append({"id": workspace_id, "type": "workspaces"})
362 | 
363 |     payload = {"data": relationships_data}
364 |     endpoint = f"varsets/{varset_id}/relationships/workspaces"
365 |     return await api_request(endpoint, method="POST", data=payload)
366 | 
367 | 
368 | @handle_api_errors
369 | async def unassign_variable_set_from_workspaces(
370 |     varset_id: str, workspace_ids: List[str]
371 | ) -> APIResponse:
372 |     """Remove a variable set from one or more workspaces.
373 | 
374 |     Removes the variable set assignment from the specified workspaces. The variables
375 |     will no longer be available in those workspaces.
376 | 
377 |     API endpoint: DELETE /varsets/{varset_id}/relationships/workspaces
378 | 
379 |     Args:
380 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
381 |         workspace_ids: List of workspace IDs (format: ["ws-xxxxxxxx", ...])
382 | 
383 |     Returns:
384 |         Empty response with HTTP 204 status code if successful
385 | 
386 |     See:
387 |         docs/tools/variables.md#unassign-variable-set-from-workspaces for reference documentation
388 |     """
389 |     # Build relationships payload
390 |     relationships_data = []
391 |     for workspace_id in workspace_ids:
392 |         relationships_data.append({"type": "workspaces", "id": workspace_id})
393 | 
394 |     payload = {"data": relationships_data}
395 |     endpoint = f"varsets/{varset_id}/relationships/workspaces"
396 |     return await api_request(endpoint, method="DELETE", data=payload)
397 | 
398 | 
399 | @handle_api_errors
400 | async def assign_variable_set_to_projects(
401 |     varset_id: str, project_ids: List[str]
402 | ) -> APIResponse:
403 |     """Assign a variable set to one or more projects.
404 | 
405 |     Makes the variables in a variable set available to all workspaces within
406 |     the specified projects.
407 | 
408 |     API endpoint: POST /varsets/{varset_id}/relationships/projects
409 | 
410 |     Args:
411 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
412 |         project_ids: List of project IDs (format: ["prj-xxxxxxxx", ...])
413 | 
414 |     Returns:
415 |         Empty response with HTTP 204 status code if successful
416 | 
417 |     See:
418 |         docs/tools/variables.md#assign-variable-set-to-projects for reference documentation
419 |     """
420 |     # Build relationships payload
421 |     relationships_data = []
422 |     for project_id in project_ids:
423 |         relationships_data.append({"id": project_id, "type": "projects"})
424 | 
425 |     payload = {"data": relationships_data}
426 |     endpoint = f"varsets/{varset_id}/relationships/projects"
427 |     return await api_request(endpoint, method="POST", data=payload)
428 | 
429 | 
430 | @handle_api_errors
431 | async def unassign_variable_set_from_projects(
432 |     varset_id: str, project_ids: List[str]
433 | ) -> APIResponse:
434 |     """Remove a variable set from one or more projects.
435 | 
436 |     Removes the variable set assignment from the specified projects. The variables
437 |     will no longer be available in workspaces within those projects.
438 | 
439 |     API endpoint: DELETE /varsets/{varset_id}/relationships/projects
440 | 
441 |     Args:
442 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
443 |         project_ids: List of project IDs (format: ["prj-xxxxxxxx", ...])
444 | 
445 |     Returns:
446 |         Empty response with HTTP 204 status code if successful
447 | 
448 |     See:
449 |         docs/tools/variables.md#unassign-variable-set-from-projects for reference documentation
450 |     """
451 |     # Build relationships payload
452 |     relationships_data = []
453 |     for project_id in project_ids:
454 |         relationships_data.append({"type": "projects", "id": project_id})
455 | 
456 |     payload = {"data": relationships_data}
457 |     endpoint = f"varsets/{varset_id}/relationships/projects"
458 |     return await api_request(endpoint, method="DELETE", data=payload)
459 | 
460 | 
461 | # Variable Set Variables Tools
462 | 
463 | 
464 | @handle_api_errors
465 | async def list_variables_in_variable_set(varset_id: str) -> APIResponse:
466 |     """List all variables in a variable set.
467 | 
468 |     Retrieves all variables that belong to a specific variable set,
469 |     including their configuration and values.
470 | 
471 |     API endpoint: GET /varsets/{varset_id}/relationships/vars
472 | 
473 |     Args:
474 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
475 | 
476 |     Returns:
477 |         List of variables in the variable set with their configuration
478 | 
479 |     See:
480 |         docs/tools/variables.md#list-variables-in-variable-set for reference documentation
481 |     """
482 |     endpoint = f"varsets/{varset_id}/relationships/vars"
483 |     return await api_request(endpoint, method="GET")
484 | 
485 | 
486 | @handle_api_errors
487 | async def create_variable_in_variable_set(
488 |     varset_id: str,
489 |     key: str,
490 |     category: str,
491 |     params: Optional[VariableSetVariableParams] = None,
492 | ) -> APIResponse:
493 |     """Create a new variable in a variable set.
494 | 
495 |     Creates a new Terraform or environment variable within a variable set.
496 |     Variables can be marked as sensitive to hide their values.
497 | 
498 |     API endpoint: POST /varsets/{varset_id}/relationships/vars
499 | 
500 |     Args:
501 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
502 |         key: The variable name/key
503 |         category: Variable category ("terraform" or "env")
504 | 
505 |         params: Additional variable parameters (optional):
506 |             - value: Variable value
507 |             - description: Description of the variable
508 |             - hcl: Whether the value is HCL code (terraform variables only)
509 |             - sensitive: Whether the variable value is sensitive
510 | 
511 |     Returns:
512 |         The created variable with its configuration and metadata
513 | 
514 |     See:
515 |         docs/tools/variables.md#create-variable-in-variable-set for reference documentation
516 |     """
517 |     # Create a temporary request-like structure for the variable
518 |     # Note: We don't have specific models for variable set variables yet
519 |     var_data = {
520 |         "key": key,
521 |         "category": VariableCategory(category).value,
522 |     }
523 | 
524 |     if params:
525 |         param_dict = params.model_dump(exclude_none=True)
526 |         var_data.update(param_dict)
527 | 
528 |     payload = {"data": {"type": "vars", "attributes": var_data}}
529 | 
530 |     return await api_request(
531 |         f"varsets/{varset_id}/relationships/vars", method="POST", data=payload
532 |     )
533 | 
534 | 
535 | @handle_api_errors
536 | async def update_variable_in_variable_set(
537 |     varset_id: str,
538 |     var_id: str,
539 |     params: Optional[VariableSetVariableParams] = None,
540 | ) -> APIResponse:
541 |     """Update an existing variable in a variable set.
542 | 
543 |     Modifies the configuration of an existing variable within a variable set. Only
544 |     specified attributes will be updated; unspecified attributes remain unchanged.
545 | 
546 |     API endpoint: PATCH /varsets/{varset_id}/relationships/vars/{var_id}
547 | 
548 |     Args:
549 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
550 |         var_id: The ID of the variable (format: "var-xxxxxxxx")
551 | 
552 |         params: Variable parameters to update (optional):
553 |             - key: New variable name/key
554 |             - value: New variable value
555 |             - description: New description of the variable
556 |             - category: New variable category ("terraform" or "env")
557 |             - hcl: Whether the value is HCL code (terraform variables only)
558 |             - sensitive: Whether the variable value is sensitive
559 | 
560 |     Returns:
561 |         The updated variable with all current settings and configuration
562 | 
563 |     See:
564 |         docs/tools/variables.md#update-variable-in-variable-set for reference documentation
565 |     """
566 |     param_dict = params.model_dump(exclude_none=True) if params else {}
567 | 
568 |     payload = {"data": {"type": "vars", "attributes": param_dict}}
569 | 
570 |     return await api_request(
571 |         f"varsets/{varset_id}/relationships/vars/{var_id}", method="PATCH", data=payload
572 |     )
573 | 
574 | 
575 | @handle_api_errors
576 | async def delete_variable_from_variable_set(varset_id: str, var_id: str) -> APIResponse:
577 |     """Delete a variable from a variable set.
578 | 
579 |     Permanently removes a variable from a variable set. This action cannot be undone.
580 | 
581 |     API endpoint: DELETE /varsets/{varset_id}/relationships/vars/{var_id}
582 | 
583 |     Args:
584 |         varset_id: The ID of the variable set (format: "varset-xxxxxxxx")
585 |         var_id: The ID of the variable (format: "var-xxxxxxxx")
586 | 
587 |     Returns:
588 |         Empty response with HTTP 204 status code if successful
589 | 
590 |     See:
591 |         docs/tools/variables.md#delete-variable-from-variable-set for reference documentation
592 |     """
593 |     endpoint = f"varsets/{varset_id}/relationships/vars/{var_id}"
594 |     return await api_request(endpoint, method="DELETE")
595 | 
```
Page 4/4FirstPrevNextLast