lucius-mcp 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. lucius_mcp-0.4.0.dist-info/METADATA +124 -0
  2. {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/RECORD +38 -29
  3. src/client/__init__.py +6 -0
  4. src/client/client.py +271 -7
  5. src/client/exceptions.py +23 -0
  6. src/client/generated/README.md +18 -0
  7. src/client/generated/__init__.py +2 -0
  8. src/client/generated/api/__init__.py +1 -0
  9. src/client/generated/api/integration_controller_api.py +5285 -0
  10. src/client/generated/docs/IntegrationControllerApi.md +1224 -0
  11. src/services/__init__.py +9 -1
  12. src/services/custom_field_value_service.py +301 -0
  13. src/services/integration_service.py +205 -0
  14. src/services/launch_service.py +29 -1
  15. src/services/shared_step_service.py +34 -17
  16. src/services/test_case_service.py +269 -37
  17. src/services/test_layer_service.py +11 -0
  18. src/tools/__init__.py +19 -1
  19. src/tools/create_custom_field_value.py +38 -0
  20. src/tools/create_test_case.py +32 -2
  21. src/tools/delete_custom_field_value.py +57 -0
  22. src/tools/delete_test_case.py +5 -4
  23. src/tools/delete_test_layer.py +17 -4
  24. src/tools/delete_test_layer_schema.py +17 -4
  25. src/tools/launches.py +86 -0
  26. src/tools/link_shared_step.py +18 -12
  27. src/tools/list_custom_field_values.py +72 -0
  28. src/tools/list_integrations.py +77 -0
  29. src/tools/search.py +3 -3
  30. src/tools/shared_steps.py +23 -8
  31. src/tools/unlink_shared_step.py +19 -5
  32. src/tools/update_custom_field_value.py +55 -0
  33. src/tools/update_test_case.py +67 -2
  34. src/tools/update_test_layer.py +15 -4
  35. src/tools/update_test_layer_schema.py +16 -5
  36. lucius_mcp-0.3.0.dist-info/METADATA +0 -297
  37. {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/WHEEL +0 -0
  38. {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/entry_points.txt +0 -0
  39. {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/licenses/LICENSE +0 -0
src/tools/search.py CHANGED
@@ -122,12 +122,12 @@ async def search_test_cases(
122
122
  Examples:
123
123
  search_test_cases(query="login")
124
124
  → "Found 5 test cases matching 'login':
125
- - [TC-1] User Login Flow (tags: smoke, auth)
126
- - [TC-2] Admin Login Test (tags: admin)"
125
+ - [#1] User Login Flow (tags: smoke, auth)
126
+ - [#2] Admin Login Test (tags: admin)"
127
127
 
128
128
  search_test_cases(aql='status="failed" and tag="regression"')
129
129
  → "Found 12 test cases matching 'status=\"failed\" and tag=\"regression\"':
130
- - [TC-5] Critical Path Test ..."
130
+ - [#5] Critical Path Test ..."
131
131
  """
132
132
  # Validate that at least one search parameter is provided
133
133
  if not aql and not query:
src/tools/shared_steps.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Annotated, Any
1
+ from typing import Annotated
2
2
 
3
3
  from pydantic import Field
4
4
 
@@ -9,7 +9,7 @@ from src.services.shared_step_service import SharedStepService
9
9
  async def create_shared_step(
10
10
  name: Annotated[str, Field(description='The name of the shared step (e.g., "Login as Admin").')],
11
11
  steps: Annotated[
12
- list[dict[str, Any]] | None,
12
+ list[dict[str, object]] | None,
13
13
  Field(
14
14
  description="Optional list of steps. Each step is a dictionary with:"
15
15
  ' - action (str): The step description (e.g., "Enter username").'
@@ -86,9 +86,11 @@ async def update_shared_step(
86
86
  step_id: Annotated[int, Field(description="The shared step ID to update (required).")],
87
87
  name: Annotated[str | None, Field(description="New name for the shared step (optional).")] = None,
88
88
  description: Annotated[str | None, Field(description="New description (optional).")] = None,
89
+ confirm: Annotated[bool, Field(description="Must be set to True to proceed with update. Safety measure.")] = False,
89
90
  project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
90
91
  ) -> str:
91
92
  """Update an existing shared step.
93
+ ⚠️ CAUTION: Destructive.
92
94
 
93
95
  ⚠️ IMPORTANT: Changes propagate to ALL test cases using this shared step.
94
96
 
@@ -100,6 +102,8 @@ async def update_shared_step(
100
102
  Found via list_shared_steps or in the Allure URL.
101
103
  name: New name for the shared step (optional).
102
104
  description: New description (optional).
105
+ confirm: Must be set to True to proceed with update.
106
+ This is a safety measure to prevent accidental updates.
103
107
  project_id: Optional override for the default Project ID.
104
108
 
105
109
  Returns:
@@ -108,9 +112,16 @@ async def update_shared_step(
108
112
  Example:
109
113
  update_shared_step(
110
114
  step_id=789,
111
- name="Login as Admin (Updated)"
115
+ name="Login as Admin (Updated)",
116
+ confirm=True
112
117
  )
113
118
  """
119
+ if not confirm:
120
+ return (
121
+ "⚠️ Update requires confirmation.\n\n"
122
+ "Changes propagate to ALL test cases using this shared step. "
123
+ f"Please call again with confirm=True to proceed with updating shared step {step_id}."
124
+ )
114
125
 
115
126
  async with AllureClient.from_env(project=project_id) as client:
116
127
  service = SharedStepService(client=client)
@@ -136,6 +147,7 @@ async def delete_shared_step(
136
147
  project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
137
148
  ) -> str:
138
149
  """Delete a shared step from the library.
150
+ ⚠️ CAUTION: Destructive.
139
151
 
140
152
  ⚠️ CAUTION: If this shared step is used by test cases, deleting it
141
153
  will break those references.
@@ -157,13 +169,16 @@ async def delete_shared_step(
157
169
  """
158
170
  if not confirm:
159
171
  return (
160
- "⚠️ Delete confirmation required\n\n"
161
- "To delete this shared step, set confirm=True.\n\n"
162
- "WARNING: Deleting a shared step used by test cases will break those references."
172
+ "⚠️ Deletion requires confirmation.\n\n"
173
+ "Deleting a shared step used by test cases will be breaking those references. "
174
+ f"Please call again with confirm=True to proceed with archiving shared step {step_id}."
163
175
  )
164
176
 
165
177
  async with AllureClient.from_env(project=project_id) as client:
166
178
  service = SharedStepService(client=client)
167
- await service.delete_shared_step(step_id=step_id)
179
+ deleted = await service.delete_shared_step(step_id=step_id)
168
180
 
169
- return f"🗑️ Archived Shared Step {step_id}\n\nThe shared step has been successfully archived."
181
+ if deleted:
182
+ return f"✅ Archived Shared Step {step_id}\n\nThe shared step has been successfully archived."
183
+ else:
184
+ return f"ℹ️ Shared Step {step_id} was already archived or doesn't exist." # noqa: RUF001
@@ -1,6 +1,6 @@
1
1
  """Tool for unlinking Shared Steps from Test Cases."""
2
2
 
3
- from typing import Annotated, Any
3
+ from typing import Annotated
4
4
 
5
5
  from pydantic import Field
6
6
 
@@ -9,13 +9,14 @@ from src.client.generated.models.shared_step_step_dto import SharedStepStepDto
9
9
  from src.services.test_case_service import TestCaseService
10
10
 
11
11
 
12
- def _format_steps(scenario: Any) -> str:
12
+ def _format_steps(scenario: object) -> str:
13
13
  """Format steps for display."""
14
- if not scenario or not scenario.steps:
14
+ steps = getattr(scenario, "steps", None)
15
+ if not isinstance(steps, list) or not steps:
15
16
  return "No steps."
16
17
 
17
18
  output = []
18
- for i, step in enumerate(scenario.steps):
19
+ for i, step in enumerate(steps):
19
20
  if step.actual_instance and isinstance(step.actual_instance, SharedStepStepDto):
20
21
  output.append(f"{i + 1}. [Shared Step] ID: {step.actual_instance.shared_step_id}")
21
22
  else:
@@ -30,8 +31,12 @@ async def unlink_shared_step(
30
31
  test_case_id: Annotated[int, Field(description="The test case to modify.")],
31
32
  shared_step_id: Annotated[int, Field(description="The shared step to unlink.")],
32
33
  project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
34
+ confirm: Annotated[
35
+ bool, Field(description="Must be set to True to proceed with unlinking. Safety measure.")
36
+ ] = False,
33
37
  ) -> str:
34
38
  """Remove a shared step reference from a test case.
39
+ ⚠️ CAUTION: Destructive.
35
40
 
36
41
  Removes the link to the shared step. The test case will no longer
37
42
  include those steps at execution time.
@@ -40,6 +45,8 @@ async def unlink_shared_step(
40
45
  test_case_id: The test case to modify.
41
46
  shared_step_id: The shared step to unlink.
42
47
  project_id: Optional override for the default Project ID.
48
+ confirm: Must be set to True to proceed with unlinking.
49
+ This is a safety measure to prevent accidental unlinking.
43
50
 
44
51
  Returns:
45
52
  Confirmation with updated step list.
@@ -48,8 +55,15 @@ async def unlink_shared_step(
48
55
  remains in the library and other test cases are unaffected.
49
56
 
50
57
  Example:
51
- unlink_shared_step(test_case_id=12345, shared_step_id=789)
58
+ unlink_shared_step(test_case_id=12345, shared_step_id=789, confirm=True)
52
59
  """
60
+ if not confirm:
61
+ return (
62
+ "⚠️ Unlinking requires confirmation.\n\n"
63
+ "This will remove a shared step from the test case scenario. "
64
+ f"Please call again with confirm=True to proceed with unlinking "
65
+ f"shared step {shared_step_id} from test case {test_case_id}."
66
+ )
53
67
 
54
68
  async with AllureClient.from_env(project=project_id) as client:
55
69
  service = TestCaseService(client=client)
@@ -0,0 +1,55 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field
4
+
5
+ from src.client import AllureClient
6
+ from src.services.custom_field_value_service import CustomFieldValueService
7
+
8
+
9
+ async def update_custom_field_value(
10
+ cfv_id: Annotated[int, Field(description="Custom field value ID to update.")],
11
+ name: Annotated[str, Field(description="New name for the custom field value.")],
12
+ custom_field_id: Annotated[
13
+ int | None,
14
+ Field(description="Project-scoped custom field ID (optional, resolves by name if missing)."),
15
+ ] = None,
16
+ custom_field_name: Annotated[
17
+ str | None,
18
+ Field(description="Custom field name to resolve when custom_field_id is not provided."),
19
+ ] = None,
20
+ project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
21
+ confirm: Annotated[bool, Field(description="Must be set to True to proceed with update. Safety measure.")] = False,
22
+ ) -> str:
23
+ """Update an existing custom field value.
24
+ ⚠️ CAUTION: Destructive.
25
+
26
+ Args:
27
+ cfv_id: Custom field value ID to update.
28
+ name: New name for the custom field value.
29
+ custom_field_id: Project-scoped custom field ID (optional, resolves by name if missing).
30
+ custom_field_name: Custom field name to resolve when custom_field_id is not provided.
31
+ project_id: Optional Allure TestOps project ID override.
32
+ confirm: Must be set to True to proceed with update.
33
+ This is a safety measure to prevent accidental updates.
34
+
35
+ Returns:
36
+ Confirmation message indicating whether changes were made.
37
+ """
38
+ if not confirm:
39
+ return (
40
+ "⚠️ Update requires confirmation.\n\n"
41
+ "Updating a custom field value affects all test cases using it. "
42
+ f"Please call again with confirm=True to proceed with updating custom field value {cfv_id}."
43
+ )
44
+
45
+ async with AllureClient.from_env(project=project_id) as client:
46
+ service = CustomFieldValueService(client=client)
47
+ await service.update_custom_field_value(
48
+ project_id=project_id,
49
+ cfv_id=cfv_id,
50
+ custom_field_id=custom_field_id,
51
+ custom_field_name=custom_field_name,
52
+ name=name,
53
+ )
54
+
55
+ return f"✅ Custom field value {cfv_id} updated successfully!\nNew name: {name}"
@@ -33,14 +33,48 @@ async def update_test_case( # noqa: C901
33
33
  expected_result: Annotated[str | None, Field(description="Global expected result for the test case")] = None,
34
34
  status_id: Annotated[int | None, Field(description="ID of the test case status")] = None,
35
35
  test_layer_id: Annotated[int | None, Field(description="ID of the test layer")] = None,
36
+ test_layer_name: Annotated[str | None, Field(description="Name of the test layer")] = None,
36
37
  workflow_id: Annotated[int | None, Field(description="ID of the workflow")] = None,
37
38
  links: Annotated[
38
39
  list[dict[str, str]] | None,
39
40
  Field(description="New list of external links. Each dict has 'name', 'url', and optional 'type'."),
40
41
  ] = None,
42
+ # Issue Linking
43
+ issues: Annotated[
44
+ list[str] | None,
45
+ Field(description="List of issue keys to ADD (e.g. ['PROJ-123'])."),
46
+ ] = None,
47
+ remove_issues: Annotated[
48
+ list[str] | None,
49
+ Field(description="List of issue keys to REMOVE."),
50
+ ] = None,
51
+ clear_issues: Annotated[
52
+ bool | None,
53
+ Field(description="If True, remove ALL issues from the test case."),
54
+ ] = None,
55
+ integration_id: Annotated[
56
+ int | None,
57
+ Field(
58
+ description=(
59
+ "Optional integration ID for issue linking (use list_integrations to find IDs). "
60
+ "Required when multiple integrations exist and adding issues. Mutually exclusive with integration_name."
61
+ )
62
+ ),
63
+ ] = None,
64
+ integration_name: Annotated[
65
+ str | None,
66
+ Field(
67
+ description=(
68
+ "Optional integration name for issue linking (exact case-sensitive match). "
69
+ "Required when multiple integrations exist and adding issues. Mutually exclusive with integration_id."
70
+ )
71
+ ),
72
+ ] = None,
41
73
  project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
74
+ confirm: Annotated[bool, Field(description="Must be set to True to proceed with update. Safety measure.")] = False,
42
75
  ) -> str:
43
76
  """Update an existing test case in Allure TestOps.
77
+ ⚠️ CAUTION: Destructive.
44
78
 
45
79
  Performs a partial update: only supplied fields are sent to the API. When
46
80
  provided, ``steps`` replace all existing steps, and ``attachments`` replace
@@ -61,10 +95,18 @@ async def update_test_case( # noqa: C901
61
95
  expected_result: Global expected result for the test case.
62
96
  status_id: ID of the test case status.
63
97
  test_layer_id: ID of the test layer.
98
+ test_layer_name: Name of the test layer.
64
99
  workflow_id: ID of the workflow.
65
100
  links: New list of external links. Each dict has ``name``, ``url``,
66
101
  and optional ``type``.
102
+ issues: List of issue keys to ADD (e.g. ['PROJ-123']).
103
+ remove_issues: List of issue keys to REMOVE.
104
+ clear_issues: If True, remove ALL issues from the test case.
105
+ integration_id: Optional integration ID for issue linking.
106
+ integration_name: Optional integration name for issue linking.
67
107
  project_id: Optional override for the default Project ID.
108
+ confirm: Must be set to True to proceed with update.
109
+ This is a safety measure to prevent accidental updates.
68
110
 
69
111
  Returns:
70
112
  A confirmation message summarizing the update.
@@ -73,6 +115,12 @@ async def update_test_case( # noqa: C901
73
115
  AuthenticationError: If no API token available from environment or
74
116
  arguments.
75
117
  """
118
+ if not confirm:
119
+ return (
120
+ "⚠️ Update requires confirmation.\n\n"
121
+ "This will modify test case properties and may overwrite existing data. "
122
+ f"Please call again with confirm=True to proceed with updating test case {test_case_id}."
123
+ )
76
124
 
77
125
  async with AllureClient.from_env(project=project_id) as client:
78
126
  service = TestCaseService(client=client)
@@ -89,8 +137,14 @@ async def update_test_case( # noqa: C901
89
137
  expected_result=expected_result,
90
138
  status_id=status_id,
91
139
  test_layer_id=test_layer_id,
140
+ test_layer_name=test_layer_name,
92
141
  workflow_id=workflow_id,
93
142
  links=links,
143
+ issues=issues,
144
+ remove_issues=remove_issues,
145
+ clear_issues=clear_issues,
146
+ integration_id=integration_id,
147
+ integration_name=integration_name,
94
148
  )
95
149
 
96
150
  updated_case = await service.update_test_case(test_case_id, update_data)
@@ -130,10 +184,14 @@ async def update_test_case( # noqa: C901
130
184
  current_status_id = getattr(current_status, "id", None) if current_status else None
131
185
  if status_id != current_status_id:
132
186
  changes.append("status updated")
133
- if test_layer_id is not None:
187
+ if test_layer_id is not None or test_layer_name is not None:
134
188
  current_test_layer = getattr(current_case, "test_layer", None)
135
189
  current_test_layer_id = getattr(current_test_layer, "id", None) if current_test_layer else None
136
- if test_layer_id != current_test_layer_id:
190
+
191
+ updated_test_layer = getattr(updated_case, "test_layer", None)
192
+ updated_test_layer_id = getattr(updated_test_layer, "id", None) if updated_test_layer else None
193
+
194
+ if updated_test_layer_id != current_test_layer_id:
137
195
  changes.append("test layer updated")
138
196
  if workflow_id is not None:
139
197
  current_workflow = getattr(current_case, "workflow", None)
@@ -145,6 +203,13 @@ async def update_test_case( # noqa: C901
145
203
  if normalize_links(current_links) != normalize_links(links):
146
204
  changes.append("links updated")
147
205
 
206
+ if issues:
207
+ changes.append(f"added {len(issues)} issues ({', '.join(issues)})")
208
+ if remove_issues:
209
+ changes.append(f"removed {len(remove_issues)} issues ({', '.join(remove_issues)})")
210
+ if clear_issues:
211
+ changes.append("cleared all issues")
212
+
148
213
  summary = ", ".join(changes) if changes else "No changes made (idempotent)"
149
214
 
150
215
  return f"Test Case {updated_case.id} updated successfully. Changes: {summary}"
@@ -12,17 +12,28 @@ async def update_test_layer(
12
12
  layer_id: Annotated[int, Field(description="ID of the test layer to update.")],
13
13
  name: Annotated[str, Field(description="New name for the test layer.")],
14
14
  project_id: Annotated[int | None, Field(description="Allure TestOps project ID.")] = None,
15
+ confirm: Annotated[bool, Field(description="Must be set to True to proceed with update. Safety measure.")] = False,
15
16
  ) -> str:
16
17
  """Update an existing test layer's name.
18
+ ⚠️ CAUTION: Destructive.
17
19
 
18
20
  Args:
19
- layer_id: ID of the test layer to update
20
- name: New name for the test layer
21
- project_id: Optional project ID override
21
+ layer_id: ID of the test layer to update.
22
+ name: New name for the test layer.
23
+ project_id: Optional Allure TestOps project ID override.
24
+ confirm: Must be set to True to proceed with update.
25
+ This is a safety measure to prevent accidental updates.
22
26
 
23
27
  Returns:
24
- Confirmation message indicating whether changes were made
28
+ Confirmation message indicating whether changes were made.
25
29
  """
30
+ if not confirm:
31
+ return (
32
+ "⚠️ Update requires confirmation.\n\n"
33
+ "Renaming a test layer affects all associated test cases. "
34
+ f"Please call again with confirm=True to proceed with updating test layer {layer_id}."
35
+ )
36
+
26
37
  async with AllureClient.from_env(project=project_id) as client:
27
38
  service = TestLayerService(client)
28
39
  updated, changed = await service.update_test_layer(layer_id=layer_id, name=name)
@@ -13,18 +13,29 @@ async def update_test_layer_schema(
13
13
  test_layer_id: Annotated[int | None, Field(description="New test layer ID to link to.")] = None,
14
14
  key: Annotated[str | None, Field(description="New schema key.")] = None,
15
15
  project_id: Annotated[int | None, Field(description="Allure TestOps project ID.")] = None,
16
+ confirm: Annotated[bool, Field(description="Must be set to True to proceed with update. Safety measure.")] = False,
16
17
  ) -> str:
17
18
  """Update an existing test layer schema.
19
+ ⚠️ CAUTION: Destructive.
18
20
 
19
21
  Args:
20
- schema_id: ID of the schema to update
21
- test_layer_id: Optional new test layer ID
22
- key: Optional new schema key
23
- project_id: Optional project ID override
22
+ schema_id: ID of the schema to update.
23
+ test_layer_id: Optional new test layer ID to link to.
24
+ key: Optional new schema key.
25
+ project_id: Optional Allure TestOps project ID override.
26
+ confirm: Must be set to True to proceed with update.
27
+ This is a safety measure to prevent accidental updates.
24
28
 
25
29
  Returns:
26
- Confirmation message indicating whether changes were made
30
+ Confirmation message indicating whether changes were made.
27
31
  """
32
+ if not confirm:
33
+ return (
34
+ "⚠️ Update requires confirmation.\n\n"
35
+ "Modifying a schema affects how test layers are automatically assigned. "
36
+ f"Please call again with confirm=True to proceed with updating test layer schema {schema_id}."
37
+ )
38
+
28
39
  async with AllureClient.from_env(project=project_id) as client:
29
40
  service = TestLayerService(client)
30
41
  updated, changed = await service.update_test_layer_schema(