mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,532 +1,942 @@
1
- """Hierarchy management tools for Epic/Issue/Task structure.
1
+ """Hierarchy management tools for Epic/Issue/Task structure (v2.0.0).
2
2
 
3
3
  This module implements tools for managing the three-level ticket hierarchy:
4
4
  - Epic: Strategic level containers
5
5
  - Issue: Standard work items
6
6
  - Task: Sub-work items
7
+
8
+ Version 2.0.0 changes:
9
+ - Removed all deprecated functions (epic_create, epic_get, epic_list, etc.)
10
+ - Single `hierarchy()` function provides all hierarchy operations
11
+ - All deprecated function logic has been inlined into the unified interface
7
12
  """
8
13
 
9
14
  from datetime import datetime
10
15
  from pathlib import Path
11
- from typing import Any
16
+ from typing import Any, Literal
12
17
 
18
+ from ....core.adapter import BaseAdapter
13
19
  from ....core.models import Epic, Priority, Task, TicketType
14
20
  from ....core.project_config import ConfigResolver, TicketerConfig
15
21
  from ..server_sdk import get_adapter, mcp
16
22
  from .ticket_tools import detect_and_apply_labels
17
23
 
24
+ # Sentinel value to distinguish between "parameter not provided" and "explicitly None"
25
+ _UNSET = object()
18
26
 
19
- @mcp.tool()
20
- async def epic_create(
21
- title: str,
22
- description: str = "",
23
- target_date: str | None = None,
24
- lead_id: str | None = None,
25
- child_issues: list[str] | None = None,
26
- ) -> dict[str, Any]:
27
- """Create a new epic (strategic level container).
28
-
29
- Args:
30
- title: Epic title (required)
31
- description: Detailed description of the epic
32
- target_date: Target completion date in ISO format (YYYY-MM-DD)
33
- lead_id: User ID or email of the epic lead
34
- child_issues: List of existing issue IDs to link to this epic
35
-
36
- Returns:
37
- Created epic details including ID and metadata, or error information
38
-
39
- """
40
- try:
41
- adapter = get_adapter()
42
-
43
- # Parse target date if provided
44
- target_datetime = None
45
- if target_date:
46
- try:
47
- target_datetime = datetime.fromisoformat(target_date)
48
- except ValueError:
49
- return {
50
- "status": "error",
51
- "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
52
- }
53
-
54
- # Create epic object
55
- epic = Epic(
56
- title=title,
57
- description=description or "",
58
- due_date=target_datetime,
59
- assignee=lead_id,
60
- child_issues=child_issues or [],
61
- )
62
-
63
- # Create via adapter
64
- created = await adapter.create(epic)
65
27
 
66
- return {
67
- "status": "completed",
68
- "epic": created.model_dump(),
69
- }
70
- except Exception as e:
71
- return {
72
- "status": "error",
73
- "error": f"Failed to create epic: {str(e)}",
74
- }
75
-
76
-
77
- @mcp.tool()
78
- async def epic_list(
79
- limit: int = 10,
80
- offset: int = 0,
28
+ def _build_adapter_metadata(
29
+ adapter: BaseAdapter,
30
+ ticket_id: str | None = None,
81
31
  ) -> dict[str, Any]:
82
- """List all epics with pagination.
32
+ """Build adapter metadata for MCP responses.
83
33
 
84
34
  Args:
85
- limit: Maximum number of epics to return (default: 10)
86
- offset: Number of epics to skip for pagination (default: 0)
35
+ adapter: The adapter that handled the operation
36
+ ticket_id: Optional ticket ID to include in metadata
87
37
 
88
38
  Returns:
89
- List of epics, or error information
39
+ Dictionary with adapter metadata fields
90
40
 
91
41
  """
92
- try:
93
- adapter = get_adapter()
94
-
95
- # List with epic filter
96
- filters = {"ticket_type": TicketType.EPIC}
97
- epics = await adapter.list(limit=limit, offset=offset, filters=filters)
98
-
99
- return {
100
- "status": "completed",
101
- "epics": [epic.model_dump() for epic in epics],
102
- "count": len(epics),
103
- "limit": limit,
104
- "offset": offset,
105
- }
106
- except Exception as e:
107
- return {
108
- "status": "error",
109
- "error": f"Failed to list epics: {str(e)}",
110
- }
111
-
112
-
113
- @mcp.tool()
114
- async def epic_issues(epic_id: str) -> dict[str, Any]:
115
- """Get all issues belonging to an epic.
116
-
117
- Args:
118
- epic_id: Unique identifier of the epic
119
-
120
- Returns:
121
- List of issues in the epic, or error information
122
-
123
- """
124
- try:
125
- adapter = get_adapter()
126
-
127
- # Read the epic to get child issue IDs
128
- epic = await adapter.read(epic_id)
129
- if epic is None:
130
- return {
131
- "status": "error",
132
- "error": f"Epic {epic_id} not found",
133
- }
134
-
135
- # If epic has no child_issues attribute, use empty list
136
- child_issue_ids = getattr(epic, "child_issues", [])
42
+ metadata = {
43
+ "adapter": adapter.adapter_type,
44
+ "adapter_name": adapter.adapter_display_name,
45
+ }
137
46
 
138
- # Fetch each child issue
139
- issues = []
140
- for issue_id in child_issue_ids:
141
- issue = await adapter.read(issue_id)
142
- if issue:
143
- issues.append(issue.model_dump())
47
+ if ticket_id:
48
+ metadata["ticket_id"] = ticket_id
144
49
 
145
- return {
146
- "status": "completed",
147
- "epic_id": epic_id,
148
- "issues": issues,
149
- "count": len(issues),
150
- }
151
- except Exception as e:
152
- return {
153
- "status": "error",
154
- "error": f"Failed to get epic issues: {str(e)}",
155
- }
50
+ return metadata
156
51
 
157
52
 
158
53
  @mcp.tool()
159
- async def issue_create(
160
- title: str,
161
- description: str = "",
54
+ async def hierarchy(
55
+ entity_type: Literal["epic", "issue", "task"],
56
+ action: Literal[
57
+ "create",
58
+ "get",
59
+ "list",
60
+ "update",
61
+ "delete",
62
+ "get_children",
63
+ "get_parent",
64
+ "get_tree",
65
+ ],
66
+ # Entity identification
67
+ entity_id: str | None = None,
162
68
  epic_id: str | None = None,
69
+ issue_id: str | None = None,
70
+ # Creation/Update parameters
71
+ title: str | None = None,
72
+ description: str = "",
73
+ # Epic-specific
74
+ target_date: str | None = None,
75
+ lead_id: str | None = None,
76
+ child_issues: list[str] | None = None,
77
+ # List parameters
78
+ project_id: str | None = None,
79
+ state: str | None = None,
80
+ limit: int = 10,
81
+ offset: int = 0,
82
+ include_completed: bool = False,
83
+ # Tree parameters
84
+ max_depth: int = 3,
85
+ # Task/Issue parameters
163
86
  assignee: str | None = None,
164
87
  priority: str = "medium",
165
88
  tags: list[str] | None = None,
166
89
  auto_detect_labels: bool = True,
167
90
  ) -> dict[str, Any]:
168
- """Create a new issue (standard work item) with automatic label detection.
91
+ """Unified hierarchy management tool for epics, issues, and tasks.
169
92
 
170
- This tool automatically scans available labels/tags and intelligently
171
- applies relevant ones based on the issue title and description.
93
+ Consolidates 11 separate hierarchy tools into a single interface for
94
+ all CRUD operations and hierarchical relationships across the three-tier
95
+ structure: Epic → Issue → Task.
172
96
 
173
- Args:
174
- title: Issue title (required)
175
- description: Detailed description of the issue
176
- epic_id: Parent epic ID to link this issue to
177
- assignee: User ID or email to assign the issue to
178
- priority: Priority level - must be one of: low, medium, high, critical
179
- tags: List of tags to categorize the issue (auto-detection adds to these)
180
- auto_detect_labels: Automatically detect and apply relevant labels (default: True)
181
-
182
- Returns:
183
- Created issue details including ID and metadata, or error information
184
-
185
- """
186
- try:
187
- adapter = get_adapter()
188
-
189
- # Validate and convert priority
190
- try:
191
- priority_enum = Priority(priority.lower())
192
- except ValueError:
193
- return {
194
- "status": "error",
195
- "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
196
- }
197
-
198
- # Use default_user if no assignee specified
199
- final_assignee = assignee
200
- if final_assignee is None:
201
- resolver = ConfigResolver(project_path=Path.cwd())
202
- config = resolver.load_project_config() or TicketerConfig()
203
- if config.default_user:
204
- final_assignee = config.default_user
205
-
206
- # Use default_project if no epic_id specified
207
- final_epic_id = epic_id
208
- if final_epic_id is None:
209
- resolver = ConfigResolver(project_path=Path.cwd())
210
- config = resolver.load_project_config() or TicketerConfig()
211
- # Try default_project first, fall back to default_epic
212
- if config.default_project:
213
- final_epic_id = config.default_project
214
- elif config.default_epic:
215
- final_epic_id = config.default_epic
216
-
217
- # Auto-detect labels if enabled
218
- final_tags = tags
219
- if auto_detect_labels:
220
- final_tags = await detect_and_apply_labels(
221
- adapter, title, description or "", tags
222
- )
223
-
224
- # Create issue (Task with ISSUE type)
225
- issue = Task(
226
- title=title,
227
- description=description or "",
228
- ticket_type=TicketType.ISSUE,
229
- parent_epic=final_epic_id,
230
- assignee=final_assignee,
231
- priority=priority_enum,
232
- tags=final_tags or [],
233
- )
234
-
235
- # Create via adapter
236
- created = await adapter.create(issue)
237
-
238
- return {
239
- "status": "completed",
240
- "issue": created.model_dump(),
241
- "labels_applied": created.tags or [],
242
- "auto_detected": auto_detect_labels,
243
- }
244
- except Exception as e:
245
- return {
246
- "status": "error",
247
- "error": f"Failed to create issue: {str(e)}",
248
- }
249
-
250
-
251
- @mcp.tool()
252
- async def issue_tasks(issue_id: str) -> dict[str, Any]:
253
- """Get all tasks (sub-items) belonging to an issue.
97
+ This tool replaces:
98
+ - epic_create, epic_get, epic_list, epic_update, epic_delete, epic_issues
99
+ - issue_create, issue_get_parent, issue_tasks
100
+ - task_create
101
+ - hierarchy_tree
254
102
 
255
103
  Args:
256
- issue_id: Unique identifier of the issue
104
+ entity_type: Type of entity - "epic", "issue", or "task"
105
+ action: Operation to perform - create, get, list, update, delete,
106
+ get_children, get_parent, or get_tree
107
+ entity_id: ID for get/update/delete operations
108
+ epic_id: Parent epic ID (for issues/tasks/get_children)
109
+ issue_id: Parent issue ID (for tasks/get_parent/get_children)
110
+ title: Title for create/update operations
111
+ description: Description for create/update operations
112
+ target_date: Target date for epics (ISO YYYY-MM-DD format)
113
+ lead_id: Lead user ID for epics
114
+ child_issues: List of child issue IDs for epics
115
+ project_id: Project filter for list operations
116
+ state: State filter for list operations
117
+ limit: Maximum results for list operations (default: 10)
118
+ offset: Pagination offset for list operations (default: 0)
119
+ include_completed: Include completed items in epic lists (default: False)
120
+ max_depth: Maximum depth for tree operations (1-3, default: 3)
121
+ assignee: Assigned user for issues/tasks
122
+ priority: Priority level - low, medium, high, critical (default: medium)
123
+ tags: Tags/labels for issues/tasks
124
+ auto_detect_labels: Auto-detect labels from title/description (default: True)
257
125
 
258
126
  Returns:
259
- List of tasks in the issue, or error information
260
-
261
- """
262
- try:
263
- adapter = get_adapter()
264
-
265
- # Read the issue to get child task IDs
266
- issue = await adapter.read(issue_id)
267
- if issue is None:
268
- return {
269
- "status": "error",
270
- "error": f"Issue {issue_id} not found",
271
- }
272
-
273
- # Get child task IDs
274
- child_task_ids = getattr(issue, "children", [])
275
-
276
- # Fetch each child task
277
- tasks = []
278
- for task_id in child_task_ids:
279
- task = await adapter.read(task_id)
280
- if task:
281
- tasks.append(task.model_dump())
282
-
283
- return {
284
- "status": "completed",
285
- "issue_id": issue_id,
286
- "tasks": tasks,
287
- "count": len(tasks),
288
- }
289
- except Exception as e:
290
- return {
291
- "status": "error",
292
- "error": f"Failed to get issue tasks: {str(e)}",
293
- }
294
-
295
-
296
- @mcp.tool()
297
- async def task_create(
298
- title: str,
299
- description: str = "",
300
- issue_id: str | None = None,
301
- assignee: str | None = None,
302
- priority: str = "medium",
303
- tags: list[str] | None = None,
304
- auto_detect_labels: bool = True,
305
- ) -> dict[str, Any]:
306
- """Create a new task (sub-work item) with automatic label detection.
307
-
308
- This tool automatically scans available labels/tags and intelligently
309
- applies relevant ones based on the task title and description.
310
-
311
- Args:
312
- title: Task title (required)
313
- description: Detailed description of the task
314
- issue_id: Parent issue ID to link this task to
315
- assignee: User ID or email to assign the task to
316
- priority: Priority level - must be one of: low, medium, high, critical
317
- tags: List of tags to categorize the task (auto-detection adds to these)
318
- auto_detect_labels: Automatically detect and apply relevant labels (default: True)
127
+ Operation results in standard format with status, data, and metadata
128
+
129
+ Raises:
130
+ ValueError: If action/entity_type combination is invalid
131
+
132
+ Examples:
133
+ # Create epic
134
+ await hierarchy(
135
+ entity_type="epic",
136
+ action="create",
137
+ title="Q4 Features",
138
+ description="New features for Q4",
139
+ target_date="2025-12-31"
140
+ )
319
141
 
320
- Returns:
321
- Created task details including ID and metadata, or error information
142
+ # Get epic details
143
+ await hierarchy(
144
+ entity_type="epic",
145
+ action="get",
146
+ entity_id="EPIC-123"
147
+ )
322
148
 
323
- """
324
- try:
325
- adapter = get_adapter()
149
+ # List epics in project
150
+ await hierarchy(
151
+ entity_type="epic",
152
+ action="list",
153
+ project_id="PROJECT-1",
154
+ limit=20
155
+ )
326
156
 
327
- # Validate and convert priority
328
- try:
329
- priority_enum = Priority(priority.lower())
330
- except ValueError:
331
- return {
332
- "status": "error",
333
- "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
334
- }
157
+ # Get epic's child issues
158
+ await hierarchy(
159
+ entity_type="epic",
160
+ action="get_children",
161
+ entity_id="EPIC-123"
162
+ )
335
163
 
336
- # Use default_user if no assignee specified
337
- final_assignee = assignee
338
- if final_assignee is None:
339
- resolver = ConfigResolver(project_path=Path.cwd())
340
- config = resolver.load_project_config() or TicketerConfig()
341
- if config.default_user:
342
- final_assignee = config.default_user
343
-
344
- # Auto-detect labels if enabled
345
- final_tags = tags
346
- if auto_detect_labels:
347
- final_tags = await detect_and_apply_labels(
348
- adapter, title, description or "", tags
349
- )
350
-
351
- # Create task (Task with TASK type)
352
- task = Task(
353
- title=title,
354
- description=description or "",
355
- ticket_type=TicketType.TASK,
356
- parent_issue=issue_id,
357
- assignee=final_assignee,
358
- priority=priority_enum,
359
- tags=final_tags or [],
164
+ # Create issue under epic
165
+ await hierarchy(
166
+ entity_type="issue",
167
+ action="create",
168
+ title="User authentication",
169
+ description="Implement OAuth2 flow",
170
+ epic_id="EPIC-123",
171
+ priority="high"
360
172
  )
361
173
 
362
- # Create via adapter
363
- created = await adapter.create(task)
174
+ # Get issue's parent
175
+ await hierarchy(
176
+ entity_type="issue",
177
+ action="get_parent",
178
+ entity_id="ISSUE-456"
179
+ )
364
180
 
365
- return {
366
- "status": "completed",
367
- "task": created.model_dump(),
368
- "labels_applied": created.tags or [],
369
- "auto_detected": auto_detect_labels,
370
- }
371
- except Exception as e:
372
- return {
373
- "status": "error",
374
- "error": f"Failed to create task: {str(e)}",
375
- }
181
+ # Get issue's child tasks
182
+ await hierarchy(
183
+ entity_type="issue",
184
+ action="get_children",
185
+ entity_id="ISSUE-456",
186
+ state="open"
187
+ )
376
188
 
189
+ # Create task under issue
190
+ await hierarchy(
191
+ entity_type="task",
192
+ action="create",
193
+ title="Write tests",
194
+ issue_id="ISSUE-456",
195
+ priority="medium"
196
+ )
377
197
 
378
- @mcp.tool()
379
- async def epic_update(
380
- epic_id: str,
381
- title: str | None = None,
382
- description: str | None = None,
383
- state: str | None = None,
384
- target_date: str | None = None,
385
- ) -> dict[str, Any]:
386
- """Update an existing epic's metadata and description.
198
+ # Get full hierarchy tree
199
+ await hierarchy(
200
+ entity_type="epic",
201
+ action="get_tree",
202
+ entity_id="EPIC-123",
203
+ max_depth=3
204
+ )
387
205
 
388
- Args:
389
- epic_id: Epic identifier (required)
390
- title: New title for the epic
391
- description: New description for the epic
392
- state: New state (open, in_progress, done, closed)
393
- target_date: Target completion date in ISO format (YYYY-MM-DD)
206
+ # Update epic
207
+ await hierarchy(
208
+ entity_type="epic",
209
+ action="update",
210
+ entity_id="EPIC-123",
211
+ title="Updated Title",
212
+ state="in_progress"
213
+ )
394
214
 
395
- Returns:
396
- Updated epic details, or error information
215
+ # Delete epic
216
+ await hierarchy(
217
+ entity_type="epic",
218
+ action="delete",
219
+ entity_id="EPIC-123"
220
+ )
397
221
 
222
+ Migration from old tools:
223
+ epic_create(...) → hierarchy(entity_type="epic", action="create", ...)
224
+ epic_get(epic_id) → hierarchy(entity_type="epic", action="get", entity_id=epic_id)
225
+ epic_list(...) → hierarchy(entity_type="epic", action="list", ...)
226
+ epic_update(...) → hierarchy(entity_type="epic", action="update", ...)
227
+ epic_delete(epic_id) → hierarchy(entity_type="epic", action="delete", entity_id=epic_id)
228
+ epic_issues(epic_id) → hierarchy(entity_type="epic", action="get_children", entity_id=epic_id)
229
+ issue_create(...) → hierarchy(entity_type="issue", action="create", ...)
230
+ issue_get_parent(issue_id) → hierarchy(entity_type="issue", action="get_parent", entity_id=issue_id)
231
+ issue_tasks(issue_id) → hierarchy(entity_type="issue", action="get_children", entity_id=issue_id)
232
+ task_create(...) → hierarchy(entity_type="task", action="create", ...)
233
+ hierarchy_tree(epic_id) → hierarchy(entity_type="epic", action="get_tree", entity_id=epic_id)
234
+
235
+ See: docs/mcp-api-reference.md for detailed response formats
398
236
  """
237
+ # Normalize entity_type and action to lowercase for case-insensitive matching
238
+ entity_type_lower = entity_type.lower()
239
+ action_lower = action.lower()
240
+
241
+ # Route to appropriate handler based on entity_type + action
399
242
  try:
400
243
  adapter = get_adapter()
401
244
 
402
- # Check if adapter supports epic updates
403
- if not hasattr(adapter, "update_epic"):
404
- return {
405
- "status": "error",
406
- "error": f"Epic updates not supported by {type(adapter).__name__} adapter",
407
- "epic_id": epic_id,
408
- "note": "Use ticket_update instead for basic field updates",
409
- }
410
-
411
- # Build updates dictionary
412
- updates = {}
413
- if title is not None:
414
- updates["title"] = title
415
- if description is not None:
416
- updates["description"] = description
417
- if state is not None:
418
- updates["state"] = state
419
- if target_date is not None:
420
- # Parse target date if provided
421
- try:
422
- target_datetime = datetime.fromisoformat(target_date)
423
- updates["target_date"] = target_datetime
424
- except ValueError:
245
+ if entity_type_lower == "epic":
246
+ if action_lower == "create":
247
+ # Inline implementation of epic_create
248
+ try:
249
+ # Parse target date if provided
250
+ target_datetime = None
251
+ if target_date:
252
+ try:
253
+ target_datetime = datetime.fromisoformat(target_date)
254
+ except ValueError:
255
+ return {
256
+ "status": "error",
257
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
258
+ }
259
+
260
+ # Create epic object
261
+ epic = Epic(
262
+ title=title or "",
263
+ description=description or "",
264
+ due_date=target_datetime,
265
+ assignee=lead_id,
266
+ child_issues=child_issues or [],
267
+ )
268
+
269
+ # Create via adapter
270
+ created = await adapter.create(epic)
271
+
272
+ return {
273
+ "status": "completed",
274
+ **_build_adapter_metadata(adapter, created.id),
275
+ "epic": created.model_dump(),
276
+ }
277
+ except Exception as e:
278
+ return {
279
+ "status": "error",
280
+ "error": f"Failed to create epic: {str(e)}",
281
+ }
282
+
283
+ elif action_lower == "get":
284
+ # Inline implementation of epic_get
285
+ if not entity_id and not epic_id:
286
+ return {
287
+ "status": "error",
288
+ "error": "entity_id or epic_id required for get operation",
289
+ }
290
+ try:
291
+ final_epic_id = entity_id or epic_id or ""
292
+
293
+ # Use adapter's get_epic method if available (optimized for some adapters)
294
+ if hasattr(adapter, "get_epic"):
295
+ epic = await adapter.get_epic(final_epic_id)
296
+ else:
297
+ # Fallback to generic read method
298
+ epic = await adapter.read(final_epic_id)
299
+
300
+ if epic is None:
301
+ return {
302
+ "status": "error",
303
+ "error": f"Epic {final_epic_id} not found",
304
+ **_build_adapter_metadata(adapter, final_epic_id),
305
+ }
306
+
307
+ return {
308
+ "status": "completed",
309
+ **_build_adapter_metadata(adapter, final_epic_id),
310
+ "epic": epic.model_dump(),
311
+ }
312
+ except Exception as e:
313
+ return {
314
+ "status": "error",
315
+ "error": f"Failed to get epic: {str(e)}",
316
+ }
317
+
318
+ elif action_lower == "list":
319
+ # Inline implementation of epic_list
320
+ try:
321
+ # Validate project context (Required for list operations)
322
+ resolver = ConfigResolver(project_path=Path.cwd())
323
+ config = resolver.load_project_config()
324
+ final_project = project_id or (
325
+ config.default_project if config else None
326
+ )
327
+
328
+ if not final_project:
329
+ return {
330
+ "status": "error",
331
+ "error": "project_id required. Provide project_id parameter or configure default_project.",
332
+ "help": "Use config_set_default_project(project_id='YOUR-PROJECT') to set default project",
333
+ "check_config": "Use config_get() to view current configuration",
334
+ }
335
+
336
+ # Check if adapter has optimized list_epics method
337
+ if hasattr(adapter, "list_epics"):
338
+ # Build kwargs for adapter-specific parameters with required project scoping
339
+ kwargs: dict[str, Any] = {
340
+ "limit": limit,
341
+ "offset": offset,
342
+ "project": final_project,
343
+ }
344
+
345
+ # Add state filter if supported
346
+ if state is not None:
347
+ kwargs["state"] = state
348
+
349
+ # Add include_completed for Linear adapter
350
+ adapter_type = adapter.adapter_type.lower()
351
+ if adapter_type == "linear" and include_completed:
352
+ kwargs["include_completed"] = include_completed
353
+
354
+ epics = await adapter.list_epics(**kwargs)
355
+ else:
356
+ # Fallback to generic list method with epic filter and project scoping
357
+ filters = {
358
+ "ticket_type": TicketType.EPIC,
359
+ "project": final_project,
360
+ }
361
+ if state is not None:
362
+ filters["state"] = state
363
+ epics = await adapter.list(
364
+ limit=limit, offset=offset, filters=filters
365
+ )
366
+
367
+ return {
368
+ "status": "completed",
369
+ **_build_adapter_metadata(adapter),
370
+ "epics": [epic.model_dump() for epic in epics],
371
+ "count": len(epics),
372
+ "limit": limit,
373
+ "offset": offset,
374
+ "filters_applied": {
375
+ "state": state,
376
+ "include_completed": include_completed,
377
+ },
378
+ }
379
+ except Exception as e:
380
+ return {
381
+ "status": "error",
382
+ "error": f"Failed to list epics: {str(e)}",
383
+ }
384
+
385
+ elif action_lower == "update":
386
+ # Inline implementation of epic_update
387
+ if not entity_id and not epic_id:
388
+ return {
389
+ "status": "error",
390
+ "error": "entity_id or epic_id required for update operation",
391
+ }
392
+ try:
393
+ final_epic_id = entity_id or epic_id or ""
394
+
395
+ # Check if adapter supports epic updates
396
+ if not hasattr(adapter, "update_epic"):
397
+ adapter_name = adapter.adapter_display_name
398
+ return {
399
+ "status": "error",
400
+ "error": f"Epic updates not supported by {adapter_name} adapter",
401
+ "epic_id": final_epic_id,
402
+ "note": "This adapter should implement update_epic() method",
403
+ }
404
+
405
+ # Build updates dictionary
406
+ updates = {}
407
+ if title is not None:
408
+ updates["title"] = title
409
+ if description is not None:
410
+ updates["description"] = description
411
+ if state is not None:
412
+ updates["state"] = state
413
+ if target_date is not None:
414
+ # Parse target date if provided
415
+ try:
416
+ target_datetime = datetime.fromisoformat(target_date)
417
+ updates["target_date"] = target_datetime
418
+ except ValueError:
419
+ return {
420
+ "status": "error",
421
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
422
+ }
423
+
424
+ if not updates:
425
+ return {
426
+ "status": "error",
427
+ "error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
428
+ }
429
+
430
+ # Update via adapter
431
+ updated = await adapter.update_epic(final_epic_id, updates)
432
+
433
+ if updated is None:
434
+ return {
435
+ "status": "error",
436
+ "error": f"Epic {final_epic_id} not found or update failed",
437
+ }
438
+
439
+ return {
440
+ "status": "completed",
441
+ **_build_adapter_metadata(adapter, final_epic_id),
442
+ "epic": updated.model_dump(),
443
+ }
444
+ except AttributeError as e:
445
+ return {
446
+ "status": "error",
447
+ "error": f"Epic update method not available: {str(e)}",
448
+ "epic_id": final_epic_id,
449
+ }
450
+ except Exception as e:
451
+ return {
452
+ "status": "error",
453
+ "error": f"Failed to update epic: {str(e)}",
454
+ "epic_id": final_epic_id,
455
+ }
456
+
457
+ elif action_lower == "delete":
458
+ # Inline implementation of epic_delete
459
+ if not entity_id and not epic_id:
460
+ return {
461
+ "status": "error",
462
+ "error": "entity_id or epic_id required for delete operation",
463
+ }
464
+ try:
465
+ final_epic_id = entity_id or epic_id or ""
466
+
467
+ # Check if adapter supports epic deletion
468
+ if not hasattr(adapter, "delete_epic"):
469
+ adapter_name = adapter.adapter_display_name
470
+ return {
471
+ "status": "error",
472
+ "error": f"Epic deletion not supported by {adapter_name} adapter",
473
+ **_build_adapter_metadata(adapter, final_epic_id),
474
+ "supported_adapters": ["GitHub", "Asana"],
475
+ "note": f"{adapter_name} does not provide API support for deleting epics/projects",
476
+ }
477
+
478
+ # Call adapter's delete_epic method
479
+ success = await adapter.delete_epic(final_epic_id)
480
+
481
+ if not success:
482
+ return {
483
+ "status": "error",
484
+ "error": f"Failed to delete epic {final_epic_id}",
485
+ **_build_adapter_metadata(adapter, final_epic_id),
486
+ }
487
+
488
+ return {
489
+ "status": "completed",
490
+ **_build_adapter_metadata(adapter, final_epic_id),
491
+ "message": f"Epic {final_epic_id} deleted successfully",
492
+ "deleted": True,
493
+ }
494
+ except AttributeError:
495
+ adapter_name = adapter.adapter_display_name
496
+ return {
497
+ "status": "error",
498
+ "error": f"Epic deletion not supported by {adapter_name} adapter",
499
+ **_build_adapter_metadata(adapter, final_epic_id),
500
+ "supported_adapters": ["GitHub", "Asana"],
501
+ }
502
+ except Exception as e:
503
+ return {
504
+ "status": "error",
505
+ "error": f"Failed to delete epic: {str(e)}",
506
+ **_build_adapter_metadata(adapter, final_epic_id),
507
+ }
508
+
509
+ elif action_lower == "get_children":
510
+ # Inline implementation of epic_issues
511
+ if not entity_id and not epic_id:
512
+ return {
513
+ "status": "error",
514
+ "error": "entity_id or epic_id required for get_children operation",
515
+ }
516
+ try:
517
+ final_epic_id = entity_id or epic_id or ""
518
+
519
+ # Read the epic to get child issue IDs
520
+ epic = await adapter.read(final_epic_id)
521
+ if epic is None:
522
+ return {
523
+ "status": "error",
524
+ "error": f"Epic {final_epic_id} not found",
525
+ }
526
+
527
+ # If epic has no child_issues attribute, use empty list
528
+ child_issue_ids = getattr(epic, "child_issues", [])
529
+
530
+ # Fetch each child issue
531
+ issues = []
532
+ for issue_id in child_issue_ids:
533
+ issue = await adapter.read(issue_id)
534
+ if issue:
535
+ issues.append(issue.model_dump())
536
+
537
+ return {
538
+ "status": "completed",
539
+ **_build_adapter_metadata(adapter, final_epic_id),
540
+ "issues": issues,
541
+ "count": len(issues),
542
+ }
543
+ except Exception as e:
544
+ return {
545
+ "status": "error",
546
+ "error": f"Failed to get epic issues: {str(e)}",
547
+ }
548
+
549
+ elif action_lower == "get_tree":
550
+ # Inline implementation of hierarchy_tree
551
+ if not entity_id and not epic_id:
552
+ return {
553
+ "status": "error",
554
+ "error": "entity_id or epic_id required for get_tree operation",
555
+ }
556
+ try:
557
+ final_epic_id = entity_id or epic_id or ""
558
+
559
+ # Read the epic
560
+ epic = await adapter.read(final_epic_id)
561
+ if epic is None:
562
+ return {
563
+ "status": "error",
564
+ "error": f"Epic {final_epic_id} not found",
565
+ }
566
+
567
+ # Build tree structure
568
+ tree = {
569
+ "epic": epic.model_dump(),
570
+ "issues": [],
571
+ }
572
+
573
+ if max_depth < 2:
574
+ return {
575
+ "status": "completed",
576
+ "tree": tree,
577
+ }
578
+
579
+ # Get child issues
580
+ child_issue_ids = getattr(epic, "child_issues", [])
581
+ for issue_id in child_issue_ids:
582
+ issue = await adapter.read(issue_id)
583
+ if issue:
584
+ issue_data = {
585
+ "issue": issue.model_dump(),
586
+ "tasks": [],
587
+ }
588
+
589
+ if max_depth >= 3:
590
+ # Get child tasks
591
+ child_task_ids = getattr(issue, "children", [])
592
+ for task_id in child_task_ids:
593
+ task = await adapter.read(task_id)
594
+ if task:
595
+ issue_data["tasks"].append(task.model_dump())
596
+
597
+ tree["issues"].append(issue_data)
598
+
599
+ return {
600
+ "status": "completed",
601
+ **_build_adapter_metadata(adapter, final_epic_id),
602
+ "tree": tree,
603
+ }
604
+ except Exception as e:
605
+ return {
606
+ "status": "error",
607
+ "error": f"Failed to build hierarchy tree: {str(e)}",
608
+ }
609
+ else:
610
+ valid_actions = [
611
+ "create",
612
+ "get",
613
+ "list",
614
+ "update",
615
+ "delete",
616
+ "get_children",
617
+ "get_tree",
618
+ ]
425
619
  return {
426
620
  "status": "error",
427
- "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
621
+ "error": f"Invalid action '{action}' for entity_type 'epic'",
622
+ "valid_actions": valid_actions,
623
+ "hint": f"Use hierarchy(entity_type='epic', action=<one of {valid_actions}>, ...)",
428
624
  }
429
625
 
430
- if not updates:
431
- return {
432
- "status": "error",
433
- "error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
434
- }
435
-
436
- # Update via adapter
437
- updated = await adapter.update_epic(epic_id, updates) # type: ignore
438
-
439
- if updated is None:
440
- return {
441
- "status": "error",
442
- "error": f"Epic {epic_id} not found or update failed",
443
- }
444
-
445
- return {
446
- "status": "completed",
447
- "epic": updated.model_dump(),
448
- }
449
- except AttributeError as e:
450
- return {
451
- "status": "error",
452
- "error": f"Epic update method not available: {str(e)}",
453
- "epic_id": epic_id,
454
- }
455
- except Exception as e:
456
- return {
457
- "status": "error",
458
- "error": f"Failed to update epic: {str(e)}",
459
- "epic_id": epic_id,
460
- }
461
-
462
-
463
- @mcp.tool()
464
- async def hierarchy_tree(
465
- epic_id: str,
466
- max_depth: int = 3,
467
- ) -> dict[str, Any]:
468
- """Get complete hierarchy tree for an epic.
469
-
470
- Retrieves the full hierarchy tree starting from an epic, including all
471
- child issues and their tasks up to the specified depth.
472
-
473
- Args:
474
- epic_id: Unique identifier of the root epic
475
- max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
626
+ elif entity_type_lower == "issue":
627
+ if action_lower == "create":
628
+ # Inline implementation of issue_create
629
+ try:
630
+ # Validate and convert priority
631
+ try:
632
+ priority_enum = Priority(priority.lower())
633
+ except ValueError:
634
+ return {
635
+ "status": "error",
636
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
637
+ }
638
+
639
+ # Load configuration
640
+ resolver = ConfigResolver(project_path=Path.cwd())
641
+ config = resolver.load_project_config() or TicketerConfig()
642
+
643
+ # Use default_user if no assignee specified
644
+ final_assignee = assignee
645
+ if final_assignee is None and config.default_user:
646
+ final_assignee = config.default_user
647
+
648
+ # Determine final_epic_id based on priority order:
649
+ # Priority 1: Explicit epic_id argument (including explicit None for opt-out)
650
+ # Priority 2: Config default (default_epic or default_project)
651
+ final_epic_id: str | None = None
652
+
653
+ # Handle epic_id with sentinel for explicit None
654
+ effective_epic_id = _UNSET if epic_id is None else epic_id
655
+
656
+ if effective_epic_id is not _UNSET:
657
+ # Priority 1: Explicit value provided (including None for opt-out)
658
+ final_epic_id = effective_epic_id
659
+ elif config.default_project or config.default_epic:
660
+ # Priority 2: Use configured default
661
+ final_epic_id = config.default_project or config.default_epic
662
+
663
+ # Auto-detect labels if enabled
664
+ final_tags = tags
665
+ if auto_detect_labels:
666
+ final_tags = await detect_and_apply_labels(
667
+ adapter, title or "", description or "", tags
668
+ )
669
+
670
+ # Create issue (Task with ISSUE type)
671
+ issue = Task(
672
+ title=title or "",
673
+ description=description or "",
674
+ ticket_type=TicketType.ISSUE,
675
+ parent_epic=final_epic_id,
676
+ assignee=final_assignee,
677
+ priority=priority_enum,
678
+ tags=final_tags or [],
679
+ )
680
+
681
+ # Create via adapter
682
+ created = await adapter.create(issue)
683
+
684
+ return {
685
+ "status": "completed",
686
+ **_build_adapter_metadata(adapter, created.id),
687
+ "issue": created.model_dump(),
688
+ "labels_applied": created.tags or [],
689
+ "auto_detected": auto_detect_labels,
690
+ }
691
+ except Exception as e:
692
+ return {
693
+ "status": "error",
694
+ "error": f"Failed to create issue: {str(e)}",
695
+ }
696
+
697
+ elif action_lower == "get_parent":
698
+ # Inline implementation of issue_get_parent
699
+ if not entity_id and not issue_id:
700
+ return {
701
+ "status": "error",
702
+ "error": "entity_id or issue_id required for get_parent operation",
703
+ }
704
+ try:
705
+ final_issue_id = entity_id or issue_id or ""
706
+
707
+ # Read the issue to check if it has a parent
708
+ issue = await adapter.read(final_issue_id)
709
+ if issue is None:
710
+ return {
711
+ "status": "error",
712
+ "error": f"Issue {final_issue_id} not found",
713
+ }
714
+
715
+ # Check for parent_issue attribute (sub-issues have this set)
716
+ parent_issue_id = getattr(issue, "parent_issue", None)
717
+
718
+ if not parent_issue_id:
719
+ # No parent - this is a top-level issue
720
+ return {
721
+ "status": "completed",
722
+ **_build_adapter_metadata(adapter, final_issue_id),
723
+ "parent": None,
724
+ }
725
+
726
+ # Fetch parent issue details
727
+ parent_issue = await adapter.read(parent_issue_id)
728
+ if parent_issue is None:
729
+ return {
730
+ "status": "error",
731
+ "error": f"Parent issue {parent_issue_id} not found",
732
+ }
733
+
734
+ return {
735
+ "status": "completed",
736
+ **_build_adapter_metadata(adapter, final_issue_id),
737
+ "parent": parent_issue.model_dump(),
738
+ }
739
+ except Exception as e:
740
+ return {
741
+ "status": "error",
742
+ "error": f"Failed to get parent issue: {str(e)}",
743
+ }
744
+
745
+ elif action_lower == "get_children":
746
+ # Inline implementation of issue_tasks
747
+ if not entity_id and not issue_id:
748
+ return {
749
+ "status": "error",
750
+ "error": "entity_id or issue_id required for get_children operation",
751
+ }
752
+ try:
753
+ final_issue_id = entity_id or issue_id or ""
754
+
755
+ # Validate filter parameters
756
+ filters_applied = {}
757
+
758
+ # Validate state if provided
759
+ if state is not None:
760
+ try:
761
+ from ....core.models import TicketState
762
+
763
+ state_enum = TicketState(state.lower())
764
+ filters_applied["state"] = state_enum.value
765
+ except ValueError:
766
+ return {
767
+ "status": "error",
768
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
769
+ }
770
+
771
+ # Validate priority if provided
772
+ if priority is not None:
773
+ try:
774
+ priority_enum = Priority(priority.lower())
775
+ filters_applied["priority"] = priority_enum.value
776
+ except ValueError:
777
+ return {
778
+ "status": "error",
779
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
780
+ }
781
+
782
+ if assignee is not None:
783
+ filters_applied["assignee"] = assignee
784
+
785
+ # Read the issue to get child task IDs
786
+ issue = await adapter.read(final_issue_id)
787
+ if issue is None:
788
+ return {
789
+ "status": "error",
790
+ "error": f"Issue {final_issue_id} not found",
791
+ }
792
+
793
+ # Get child task IDs
794
+ child_task_ids = getattr(issue, "children", [])
476
795
 
477
- Returns:
478
- Complete hierarchy tree structure, or error information
796
+ # Fetch each child task
797
+ tasks = []
798
+ for task_id in child_task_ids:
799
+ task = await adapter.read(task_id)
800
+ if task:
801
+ # Apply filters
802
+ should_include = True
803
+
804
+ # Filter by state
805
+ if state is not None:
806
+ task_state = getattr(task, "state", None)
807
+ # Handle case where state might be stored as string
808
+ if isinstance(task_state, str):
809
+ should_include = should_include and (
810
+ task_state.lower() == state.lower()
811
+ )
812
+ else:
813
+ should_include = should_include and (
814
+ task_state == state_enum
815
+ )
816
+
817
+ # Filter by priority
818
+ if priority is not None:
819
+ task_priority = getattr(task, "priority", None)
820
+ # Handle case where priority might be stored as string
821
+ if isinstance(task_priority, str):
822
+ should_include = should_include and (
823
+ task_priority.lower() == priority.lower()
824
+ )
825
+ else:
826
+ should_include = should_include and (
827
+ task_priority == priority_enum
828
+ )
829
+
830
+ # Filter by assignee
831
+ if assignee is not None:
832
+ task_assignee = getattr(task, "assignee", None)
833
+ # Case-insensitive comparison for emails/usernames
834
+ should_include = should_include and (
835
+ task_assignee is not None
836
+ and assignee.lower() in str(task_assignee).lower()
837
+ )
838
+
839
+ if should_include:
840
+ tasks.append(task.model_dump())
841
+
842
+ return {
843
+ "status": "completed",
844
+ **_build_adapter_metadata(adapter, final_issue_id),
845
+ "tasks": tasks,
846
+ "count": len(tasks),
847
+ "filters_applied": filters_applied,
848
+ }
849
+ except Exception as e:
850
+ return {
851
+ "status": "error",
852
+ "error": f"Failed to get issue tasks: {str(e)}",
853
+ }
854
+ else:
855
+ valid_actions = ["create", "get_parent", "get_children"]
856
+ return {
857
+ "status": "error",
858
+ "error": f"Invalid action '{action}' for entity_type 'issue'",
859
+ "valid_actions": valid_actions,
860
+ "hint": f"Use hierarchy(entity_type='issue', action=<one of {valid_actions}>, ...)",
861
+ }
479
862
 
480
- """
481
- try:
482
- adapter = get_adapter()
863
+ elif entity_type_lower == "task":
864
+ if action_lower == "create":
865
+ # Inline implementation of task_create
866
+ try:
867
+ # Validate and convert priority
868
+ try:
869
+ priority_enum = Priority(priority.lower())
870
+ except ValueError:
871
+ return {
872
+ "status": "error",
873
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
874
+ }
875
+
876
+ # Use default_user if no assignee specified
877
+ final_assignee = assignee
878
+ if final_assignee is None:
879
+ resolver = ConfigResolver(project_path=Path.cwd())
880
+ config = resolver.load_project_config() or TicketerConfig()
881
+ if config.default_user:
882
+ final_assignee = config.default_user
883
+
884
+ # Auto-detect labels if enabled
885
+ final_tags = tags
886
+ if auto_detect_labels:
887
+ final_tags = await detect_and_apply_labels(
888
+ adapter, title or "", description or "", tags
889
+ )
890
+
891
+ # Create task (Task with TASK type)
892
+ task = Task(
893
+ title=title or "",
894
+ description=description or "",
895
+ ticket_type=TicketType.TASK,
896
+ parent_issue=issue_id,
897
+ assignee=final_assignee,
898
+ priority=priority_enum,
899
+ tags=final_tags or [],
900
+ )
901
+
902
+ # Create via adapter
903
+ created = await adapter.create(task)
904
+
905
+ return {
906
+ "status": "completed",
907
+ **_build_adapter_metadata(adapter, created.id),
908
+ "task": created.model_dump(),
909
+ "labels_applied": created.tags or [],
910
+ "auto_detected": auto_detect_labels,
911
+ }
912
+ except Exception as e:
913
+ return {
914
+ "status": "error",
915
+ "error": f"Failed to create task: {str(e)}",
916
+ }
917
+ else:
918
+ valid_actions = ["create"]
919
+ return {
920
+ "status": "error",
921
+ "error": f"Invalid action '{action}' for entity_type 'task'",
922
+ "valid_actions": valid_actions,
923
+ "hint": "Use hierarchy(entity_type='task', action='create', ...)",
924
+ "note": "Tasks support only create operation. Use ticket_read/ticket_update for other operations.",
925
+ }
483
926
 
484
- # Read the epic
485
- epic = await adapter.read(epic_id)
486
- if epic is None:
927
+ else:
928
+ valid_types = ["epic", "issue", "task"]
487
929
  return {
488
930
  "status": "error",
489
- "error": f"Epic {epic_id} not found",
931
+ "error": f"Invalid entity_type: {entity_type}",
932
+ "valid_entity_types": valid_types,
933
+ "hint": f"Use hierarchy(entity_type=<one of {valid_types}>, action=..., ...)",
490
934
  }
491
935
 
492
- # Build tree structure
493
- tree = {
494
- "epic": epic.model_dump(),
495
- "issues": [],
496
- }
497
-
498
- if max_depth < 2:
499
- return {
500
- "status": "completed",
501
- "tree": tree,
502
- }
503
-
504
- # Get child issues
505
- child_issue_ids = getattr(epic, "child_issues", [])
506
- for issue_id in child_issue_ids:
507
- issue = await adapter.read(issue_id)
508
- if issue:
509
- issue_data = {
510
- "issue": issue.model_dump(),
511
- "tasks": [],
512
- }
513
-
514
- if max_depth >= 3:
515
- # Get child tasks
516
- child_task_ids = getattr(issue, "children", [])
517
- for task_id in child_task_ids:
518
- task = await adapter.read(task_id)
519
- if task:
520
- issue_data["tasks"].append(task.model_dump())
521
-
522
- tree["issues"].append(issue_data)
523
-
524
- return {
525
- "status": "completed",
526
- "tree": tree,
527
- }
528
936
  except Exception as e:
529
937
  return {
530
938
  "status": "error",
531
- "error": f"Failed to build hierarchy tree: {str(e)}",
939
+ "error": f"Hierarchy operation failed: {str(e)}",
940
+ "entity_type": entity_type,
941
+ "action": action,
532
942
  }