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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +507 -6
- mcp_ticketer/adapters/asana/adapter.py +229 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +2730 -139
- mcp_ticketer/adapters/linear/client.py +175 -3
- mcp_ticketer/adapters/linear/mappers.py +203 -8
- mcp_ticketer/adapters/linear/queries.py +280 -3
- mcp_ticketer/adapters/linear/types.py +120 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +1288 -105
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +267 -3175
- mcp_ticketer/cli/mcp_configure.py +821 -119
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +795 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +705 -103
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +56 -6
- mcp_ticketer/core/adapter.py +533 -2
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +480 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +625 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +33 -11
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/queue.py +68 -0
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1574
- mcp_ticketer/adapters/jira.py +0 -1258
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
"""
|
|
32
|
+
"""Build adapter metadata for MCP responses.
|
|
83
33
|
|
|
84
34
|
Args:
|
|
85
|
-
|
|
86
|
-
|
|
35
|
+
adapter: The adapter that handled the operation
|
|
36
|
+
ticket_id: Optional ticket ID to include in metadata
|
|
87
37
|
|
|
88
38
|
Returns:
|
|
89
|
-
|
|
39
|
+
Dictionary with adapter metadata fields
|
|
90
40
|
|
|
91
41
|
"""
|
|
92
|
-
|
|
93
|
-
adapter
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
"""
|
|
91
|
+
"""Unified hierarchy management tool for epics, issues, and tasks.
|
|
169
92
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
321
|
-
|
|
142
|
+
# Get epic details
|
|
143
|
+
await hierarchy(
|
|
144
|
+
entity_type="epic",
|
|
145
|
+
action="get",
|
|
146
|
+
entity_id="EPIC-123"
|
|
147
|
+
)
|
|
322
148
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
#
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
"
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
if epic is None:
|
|
927
|
+
else:
|
|
928
|
+
valid_types = ["epic", "issue", "task"]
|
|
487
929
|
return {
|
|
488
930
|
"status": "error",
|
|
489
|
-
"error": f"
|
|
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"
|
|
939
|
+
"error": f"Hierarchy operation failed: {str(e)}",
|
|
940
|
+
"entity_type": entity_type,
|
|
941
|
+
"action": action,
|
|
532
942
|
}
|