mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,383 +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
- from typing import Any
15
+ from pathlib import Path
16
+ from typing import Any, Literal
11
17
 
18
+ from ....core.adapter import BaseAdapter
12
19
  from ....core.models import Epic, Priority, Task, TicketType
20
+ from ....core.project_config import ConfigResolver, TicketerConfig
13
21
  from ..server_sdk import get_adapter, mcp
22
+ from .ticket_tools import detect_and_apply_labels
14
23
 
24
+ # Sentinel value to distinguish between "parameter not provided" and "explicitly None"
25
+ _UNSET = object()
15
26
 
16
- @mcp.tool()
17
- async def epic_create(
18
- title: str,
19
- description: str = "",
20
- target_date: str | None = None,
21
- lead_id: str | None = None,
22
- child_issues: list[str] | None = None,
23
- ) -> dict[str, Any]:
24
- """Create a new epic (strategic level container).
25
-
26
- Args:
27
- title: Epic title (required)
28
- description: Detailed description of the epic
29
- target_date: Target completion date in ISO format (YYYY-MM-DD)
30
- lead_id: User ID or email of the epic lead
31
- child_issues: List of existing issue IDs to link to this epic
32
-
33
- Returns:
34
- Created epic details including ID and metadata, or error information
35
27
 
36
- """
37
- try:
38
- adapter = get_adapter()
39
-
40
- # Parse target date if provided
41
- target_datetime = None
42
- if target_date:
43
- try:
44
- target_datetime = datetime.fromisoformat(target_date)
45
- except ValueError:
46
- return {
47
- "status": "error",
48
- "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
49
- }
50
-
51
- # Create epic object
52
- epic = Epic(
53
- title=title,
54
- description=description or "",
55
- due_date=target_datetime,
56
- assignee=lead_id,
57
- child_issues=child_issues or [],
58
- )
59
-
60
- # Create via adapter
61
- created = await adapter.create(epic)
62
-
63
- return {
64
- "status": "completed",
65
- "epic": created.model_dump(),
66
- }
67
- except Exception as e:
68
- return {
69
- "status": "error",
70
- "error": f"Failed to create epic: {str(e)}",
71
- }
72
-
73
-
74
- @mcp.tool()
75
- async def epic_list(
76
- limit: int = 10,
77
- offset: int = 0,
28
+ def _build_adapter_metadata(
29
+ adapter: BaseAdapter,
30
+ ticket_id: str | None = None,
78
31
  ) -> dict[str, Any]:
79
- """List all epics with pagination.
32
+ """Build adapter metadata for MCP responses.
80
33
 
81
34
  Args:
82
- limit: Maximum number of epics to return (default: 10)
83
- 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
84
37
 
85
38
  Returns:
86
- List of epics, or error information
39
+ Dictionary with adapter metadata fields
87
40
 
88
41
  """
89
- try:
90
- adapter = get_adapter()
42
+ metadata = {
43
+ "adapter": adapter.adapter_type,
44
+ "adapter_name": adapter.adapter_display_name,
45
+ }
91
46
 
92
- # List with epic filter
93
- filters = {"ticket_type": TicketType.EPIC}
94
- epics = await adapter.list(limit=limit, offset=offset, filters=filters)
47
+ if ticket_id:
48
+ metadata["ticket_id"] = ticket_id
95
49
 
96
- return {
97
- "status": "completed",
98
- "epics": [epic.model_dump() for epic in epics],
99
- "count": len(epics),
100
- "limit": limit,
101
- "offset": offset,
102
- }
103
- except Exception as e:
104
- return {
105
- "status": "error",
106
- "error": f"Failed to list epics: {str(e)}",
107
- }
50
+ return metadata
108
51
 
109
52
 
110
53
  @mcp.tool()
111
- async def epic_issues(epic_id: str) -> dict[str, Any]:
112
- """Get all issues belonging to an epic.
113
-
114
- Args:
115
- epic_id: Unique identifier of the epic
116
-
117
- Returns:
118
- List of issues in the epic, or error information
119
-
120
- """
121
- try:
122
- adapter = get_adapter()
123
-
124
- # Read the epic to get child issue IDs
125
- epic = await adapter.read(epic_id)
126
- if epic is None:
127
- return {
128
- "status": "error",
129
- "error": f"Epic {epic_id} not found",
130
- }
131
-
132
- # If epic has no child_issues attribute, use empty list
133
- child_issue_ids = getattr(epic, "child_issues", [])
134
-
135
- # Fetch each child issue
136
- issues = []
137
- for issue_id in child_issue_ids:
138
- issue = await adapter.read(issue_id)
139
- if issue:
140
- issues.append(issue.model_dump())
141
-
142
- return {
143
- "status": "completed",
144
- "epic_id": epic_id,
145
- "issues": issues,
146
- "count": len(issues),
147
- }
148
- except Exception as e:
149
- return {
150
- "status": "error",
151
- "error": f"Failed to get epic issues: {str(e)}",
152
- }
153
-
154
-
155
- @mcp.tool()
156
- async def issue_create(
157
- title: str,
158
- 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,
159
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
160
86
  assignee: str | None = None,
161
87
  priority: str = "medium",
88
+ tags: list[str] | None = None,
89
+ auto_detect_labels: bool = True,
162
90
  ) -> dict[str, Any]:
163
- """Create a new issue (standard work item).
164
-
165
- Args:
166
- title: Issue title (required)
167
- description: Detailed description of the issue
168
- epic_id: Parent epic ID to link this issue to
169
- assignee: User ID or email to assign the issue to
170
- priority: Priority level - must be one of: low, medium, high, critical
171
-
172
- Returns:
173
- Created issue details including ID and metadata, or error information
174
-
175
- """
176
- try:
177
- adapter = get_adapter()
178
-
179
- # Validate and convert priority
180
- try:
181
- priority_enum = Priority(priority.lower())
182
- except ValueError:
183
- return {
184
- "status": "error",
185
- "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
186
- }
187
-
188
- # Create issue (Task with ISSUE type)
189
- issue = Task(
190
- title=title,
191
- description=description or "",
192
- ticket_type=TicketType.ISSUE,
193
- parent_epic=epic_id,
194
- assignee=assignee,
195
- priority=priority_enum,
196
- )
197
-
198
- # Create via adapter
199
- created = await adapter.create(issue)
91
+ """Unified hierarchy management tool for epics, issues, and tasks.
200
92
 
201
- return {
202
- "status": "completed",
203
- "issue": created.model_dump(),
204
- }
205
- except Exception as e:
206
- return {
207
- "status": "error",
208
- "error": f"Failed to create issue: {str(e)}",
209
- }
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.
210
96
 
211
-
212
- @mcp.tool()
213
- async def issue_tasks(issue_id: str) -> dict[str, Any]:
214
- """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
215
102
 
216
103
  Args:
217
- 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)
218
125
 
219
126
  Returns:
220
- List of tasks in the issue, or error information
221
-
222
- """
223
- try:
224
- adapter = get_adapter()
225
-
226
- # Read the issue to get child task IDs
227
- issue = await adapter.read(issue_id)
228
- if issue is None:
229
- return {
230
- "status": "error",
231
- "error": f"Issue {issue_id} not found",
232
- }
233
-
234
- # Get child task IDs
235
- child_task_ids = getattr(issue, "children", [])
236
-
237
- # Fetch each child task
238
- tasks = []
239
- for task_id in child_task_ids:
240
- task = await adapter.read(task_id)
241
- if task:
242
- tasks.append(task.model_dump())
243
-
244
- return {
245
- "status": "completed",
246
- "issue_id": issue_id,
247
- "tasks": tasks,
248
- "count": len(tasks),
249
- }
250
- except Exception as e:
251
- return {
252
- "status": "error",
253
- "error": f"Failed to get issue tasks: {str(e)}",
254
- }
255
-
256
-
257
- @mcp.tool()
258
- async def task_create(
259
- title: str,
260
- description: str = "",
261
- issue_id: str | None = None,
262
- assignee: str | None = None,
263
- priority: str = "medium",
264
- ) -> dict[str, Any]:
265
- """Create a new task (sub-work item).
266
-
267
- Args:
268
- title: Task title (required)
269
- description: Detailed description of the task
270
- issue_id: Parent issue ID to link this task to
271
- assignee: User ID or email to assign the task to
272
- priority: Priority level - must be one of: low, medium, high, critical
273
-
274
- Returns:
275
- Created task details including ID and metadata, or error information
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
+ )
276
141
 
277
- """
278
- try:
279
- adapter = get_adapter()
142
+ # Get epic details
143
+ await hierarchy(
144
+ entity_type="epic",
145
+ action="get",
146
+ entity_id="EPIC-123"
147
+ )
280
148
 
281
- # Validate and convert priority
282
- try:
283
- priority_enum = Priority(priority.lower())
284
- except ValueError:
285
- return {
286
- "status": "error",
287
- "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
288
- }
149
+ # List epics in project
150
+ await hierarchy(
151
+ entity_type="epic",
152
+ action="list",
153
+ project_id="PROJECT-1",
154
+ limit=20
155
+ )
289
156
 
290
- # Create task (Task with TASK type)
291
- task = Task(
292
- title=title,
293
- description=description or "",
294
- ticket_type=TicketType.TASK,
295
- parent_issue=issue_id,
296
- assignee=assignee,
297
- priority=priority_enum,
157
+ # Get epic's child issues
158
+ await hierarchy(
159
+ entity_type="epic",
160
+ action="get_children",
161
+ entity_id="EPIC-123"
298
162
  )
299
163
 
300
- # Create via adapter
301
- created = await adapter.create(task)
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"
172
+ )
302
173
 
303
- return {
304
- "status": "completed",
305
- "task": created.model_dump(),
306
- }
307
- except Exception as e:
308
- return {
309
- "status": "error",
310
- "error": f"Failed to create task: {str(e)}",
311
- }
174
+ # Get issue's parent
175
+ await hierarchy(
176
+ entity_type="issue",
177
+ action="get_parent",
178
+ entity_id="ISSUE-456"
179
+ )
312
180
 
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
+ )
313
188
 
314
- @mcp.tool()
315
- async def hierarchy_tree(
316
- epic_id: str,
317
- max_depth: int = 3,
318
- ) -> dict[str, Any]:
319
- """Get complete hierarchy tree for an epic.
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
+ )
320
197
 
321
- Retrieves the full hierarchy tree starting from an epic, including all
322
- child issues and their tasks up to the specified depth.
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
+ )
323
205
 
324
- Args:
325
- epic_id: Unique identifier of the root epic
326
- max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
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
+ )
327
214
 
328
- Returns:
329
- Complete hierarchy tree structure, or error information
215
+ # Delete epic
216
+ await hierarchy(
217
+ entity_type="epic",
218
+ action="delete",
219
+ entity_id="EPIC-123"
220
+ )
330
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
331
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
332
242
  try:
333
243
  adapter = get_adapter()
334
244
 
335
- # Read the epic
336
- epic = await adapter.read(epic_id)
337
- if epic is None:
338
- return {
339
- "status": "error",
340
- "error": f"Epic {epic_id} not found",
341
- }
342
-
343
- # Build tree structure
344
- tree = {
345
- "epic": epic.model_dump(),
346
- "issues": [],
347
- }
348
-
349
- if max_depth < 2:
350
- return {
351
- "status": "completed",
352
- "tree": tree,
353
- }
354
-
355
- # Get child issues
356
- child_issue_ids = getattr(epic, "child_issues", [])
357
- for issue_id in child_issue_ids:
358
- issue = await adapter.read(issue_id)
359
- if issue:
360
- issue_data = {
361
- "issue": issue.model_dump(),
362
- "tasks": [],
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
+ ]
619
+ return {
620
+ "status": "error",
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}>, ...)",
363
624
  }
364
625
 
365
- if max_depth >= 3:
366
- # Get child 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
367
794
  child_task_ids = getattr(issue, "children", [])
795
+
796
+ # Fetch each child task
797
+ tasks = []
368
798
  for task_id in child_task_ids:
369
799
  task = await adapter.read(task_id)
370
800
  if task:
371
- issue_data["tasks"].append(task.model_dump())
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
+ }
372
862
 
373
- tree["issues"].append(issue_data)
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
+ }
926
+
927
+ else:
928
+ valid_types = ["epic", "issue", "task"]
929
+ return {
930
+ "status": "error",
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=..., ...)",
934
+ }
374
935
 
375
- return {
376
- "status": "completed",
377
- "tree": tree,
378
- }
379
936
  except Exception as e:
380
937
  return {
381
938
  "status": "error",
382
- "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,
383
942
  }