mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +263 -14
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +326 -109
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +271 -25
- mcp_ticketer/adapters/linear/adapter.py +693 -39
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/adapters/linear/queries.py +9 -7
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +104 -15
- mcp_ticketer/cli/codex_configure.py +188 -32
- mcp_ticketer/cli/configure.py +37 -48
- mcp_ticketer/cli/diagnostics.py +20 -18
- mcp_ticketer/cli/discover.py +292 -26
- mcp_ticketer/cli/gemini_configure.py +107 -26
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +105 -22
- mcp_ticketer/cli/main.py +1830 -435
- mcp_ticketer/cli/mcp_configure.py +296 -89
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +773 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +67 -62
- mcp_ticketer/core/__init__.py +14 -1
- mcp_ticketer/core/adapter.py +84 -15
- mcp_ticketer/core/config.py +44 -39
- mcp_ticketer/core/env_discovery.py +42 -12
- mcp_ticketer/core/env_loader.py +15 -14
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +11 -11
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +57 -35
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
- mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
- mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
- mcp_ticketer/mcp/server/server_sdk.py +93 -0
- mcp_ticketer/mcp/server/tools/__init__.py +47 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +5 -4
- mcp_ticketer/queue/manager.py +15 -51
- mcp_ticketer/queue/queue.py +19 -19
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +14 -14
- mcp_ticketer/queue/worker.py +16 -14
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
- /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""Hierarchy management tools for Epic/Issue/Task structure.
|
|
2
|
+
|
|
3
|
+
This module implements tools for managing the three-level ticket hierarchy:
|
|
4
|
+
- Epic: Strategic level containers
|
|
5
|
+
- Issue: Standard work items
|
|
6
|
+
- Task: Sub-work items
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ....core.models import Epic, Priority, Task, TicketType
|
|
14
|
+
from ....core.project_config import ConfigResolver, TicketerConfig
|
|
15
|
+
from ..server_sdk import get_adapter, mcp
|
|
16
|
+
from .ticket_tools import detect_and_apply_labels
|
|
17
|
+
|
|
18
|
+
|
|
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
|
+
|
|
66
|
+
return {
|
|
67
|
+
"status": "completed",
|
|
68
|
+
"epic": created.model_dump(),
|
|
69
|
+
}
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return {
|
|
72
|
+
"status": "error",
|
|
73
|
+
"error": f"Failed to create epic: {str(e)}",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@mcp.tool()
|
|
78
|
+
async def epic_list(
|
|
79
|
+
limit: int = 10,
|
|
80
|
+
offset: int = 0,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""List all epics with pagination.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
limit: Maximum number of epics to return (default: 10)
|
|
86
|
+
offset: Number of epics to skip for pagination (default: 0)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of epics, or error information
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
adapter = get_adapter()
|
|
94
|
+
|
|
95
|
+
# List with epic filter
|
|
96
|
+
filters = {"ticket_type": TicketType.EPIC}
|
|
97
|
+
epics = await adapter.list(limit=limit, offset=offset, filters=filters)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"status": "completed",
|
|
101
|
+
"epics": [epic.model_dump() for epic in epics],
|
|
102
|
+
"count": len(epics),
|
|
103
|
+
"limit": limit,
|
|
104
|
+
"offset": offset,
|
|
105
|
+
}
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return {
|
|
108
|
+
"status": "error",
|
|
109
|
+
"error": f"Failed to list epics: {str(e)}",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mcp.tool()
|
|
114
|
+
async def epic_issues(epic_id: str) -> dict[str, Any]:
|
|
115
|
+
"""Get all issues belonging to an epic.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
epic_id: Unique identifier of the epic
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of issues in the epic, or error information
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
adapter = get_adapter()
|
|
126
|
+
|
|
127
|
+
# Read the epic to get child issue IDs
|
|
128
|
+
epic = await adapter.read(epic_id)
|
|
129
|
+
if epic is None:
|
|
130
|
+
return {
|
|
131
|
+
"status": "error",
|
|
132
|
+
"error": f"Epic {epic_id} not found",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# If epic has no child_issues attribute, use empty list
|
|
136
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
137
|
+
|
|
138
|
+
# Fetch each child issue
|
|
139
|
+
issues = []
|
|
140
|
+
for issue_id in child_issue_ids:
|
|
141
|
+
issue = await adapter.read(issue_id)
|
|
142
|
+
if issue:
|
|
143
|
+
issues.append(issue.model_dump())
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"status": "completed",
|
|
147
|
+
"epic_id": epic_id,
|
|
148
|
+
"issues": issues,
|
|
149
|
+
"count": len(issues),
|
|
150
|
+
}
|
|
151
|
+
except Exception as e:
|
|
152
|
+
return {
|
|
153
|
+
"status": "error",
|
|
154
|
+
"error": f"Failed to get epic issues: {str(e)}",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
async def issue_create(
|
|
160
|
+
title: str,
|
|
161
|
+
description: str = "",
|
|
162
|
+
epic_id: str | None = None,
|
|
163
|
+
assignee: str | None = None,
|
|
164
|
+
priority: str = "medium",
|
|
165
|
+
tags: list[str] | None = None,
|
|
166
|
+
auto_detect_labels: bool = True,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Create a new issue (standard work item) with automatic label detection.
|
|
169
|
+
|
|
170
|
+
This tool automatically scans available labels/tags and intelligently
|
|
171
|
+
applies relevant ones based on the issue title and description.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
title: Issue title (required)
|
|
175
|
+
description: Detailed description of the issue
|
|
176
|
+
epic_id: Parent epic ID to link this issue to
|
|
177
|
+
assignee: User ID or email to assign the issue to
|
|
178
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
179
|
+
tags: List of tags to categorize the issue (auto-detection adds to these)
|
|
180
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Created issue details including ID and metadata, or error information
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
adapter = get_adapter()
|
|
188
|
+
|
|
189
|
+
# Validate and convert priority
|
|
190
|
+
try:
|
|
191
|
+
priority_enum = Priority(priority.lower())
|
|
192
|
+
except ValueError:
|
|
193
|
+
return {
|
|
194
|
+
"status": "error",
|
|
195
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Use default_user if no assignee specified
|
|
199
|
+
final_assignee = assignee
|
|
200
|
+
if final_assignee is None:
|
|
201
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
202
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
203
|
+
if config.default_user:
|
|
204
|
+
final_assignee = config.default_user
|
|
205
|
+
|
|
206
|
+
# Use default_project if no epic_id specified
|
|
207
|
+
final_epic_id = epic_id
|
|
208
|
+
if final_epic_id is None:
|
|
209
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
210
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
211
|
+
# Try default_project first, fall back to default_epic
|
|
212
|
+
if config.default_project:
|
|
213
|
+
final_epic_id = config.default_project
|
|
214
|
+
elif config.default_epic:
|
|
215
|
+
final_epic_id = config.default_epic
|
|
216
|
+
|
|
217
|
+
# Auto-detect labels if enabled
|
|
218
|
+
final_tags = tags
|
|
219
|
+
if auto_detect_labels:
|
|
220
|
+
final_tags = await detect_and_apply_labels(
|
|
221
|
+
adapter, title, description or "", tags
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Create issue (Task with ISSUE type)
|
|
225
|
+
issue = Task(
|
|
226
|
+
title=title,
|
|
227
|
+
description=description or "",
|
|
228
|
+
ticket_type=TicketType.ISSUE,
|
|
229
|
+
parent_epic=final_epic_id,
|
|
230
|
+
assignee=final_assignee,
|
|
231
|
+
priority=priority_enum,
|
|
232
|
+
tags=final_tags or [],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Create via adapter
|
|
236
|
+
created = await adapter.create(issue)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"status": "completed",
|
|
240
|
+
"issue": created.model_dump(),
|
|
241
|
+
"labels_applied": created.tags or [],
|
|
242
|
+
"auto_detected": auto_detect_labels,
|
|
243
|
+
}
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return {
|
|
246
|
+
"status": "error",
|
|
247
|
+
"error": f"Failed to create issue: {str(e)}",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@mcp.tool()
|
|
252
|
+
async def issue_tasks(issue_id: str) -> dict[str, Any]:
|
|
253
|
+
"""Get all tasks (sub-items) belonging to an issue.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
issue_id: Unique identifier of the issue
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of tasks in the issue, or error information
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
adapter = get_adapter()
|
|
264
|
+
|
|
265
|
+
# Read the issue to get child task IDs
|
|
266
|
+
issue = await adapter.read(issue_id)
|
|
267
|
+
if issue is None:
|
|
268
|
+
return {
|
|
269
|
+
"status": "error",
|
|
270
|
+
"error": f"Issue {issue_id} not found",
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Get child task IDs
|
|
274
|
+
child_task_ids = getattr(issue, "children", [])
|
|
275
|
+
|
|
276
|
+
# Fetch each child task
|
|
277
|
+
tasks = []
|
|
278
|
+
for task_id in child_task_ids:
|
|
279
|
+
task = await adapter.read(task_id)
|
|
280
|
+
if task:
|
|
281
|
+
tasks.append(task.model_dump())
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"status": "completed",
|
|
285
|
+
"issue_id": issue_id,
|
|
286
|
+
"tasks": tasks,
|
|
287
|
+
"count": len(tasks),
|
|
288
|
+
}
|
|
289
|
+
except Exception as e:
|
|
290
|
+
return {
|
|
291
|
+
"status": "error",
|
|
292
|
+
"error": f"Failed to get issue tasks: {str(e)}",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@mcp.tool()
|
|
297
|
+
async def task_create(
|
|
298
|
+
title: str,
|
|
299
|
+
description: str = "",
|
|
300
|
+
issue_id: str | None = None,
|
|
301
|
+
assignee: str | None = None,
|
|
302
|
+
priority: str = "medium",
|
|
303
|
+
tags: list[str] | None = None,
|
|
304
|
+
auto_detect_labels: bool = True,
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
"""Create a new task (sub-work item) with automatic label detection.
|
|
307
|
+
|
|
308
|
+
This tool automatically scans available labels/tags and intelligently
|
|
309
|
+
applies relevant ones based on the task title and description.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
title: Task title (required)
|
|
313
|
+
description: Detailed description of the task
|
|
314
|
+
issue_id: Parent issue ID to link this task to
|
|
315
|
+
assignee: User ID or email to assign the task to
|
|
316
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
317
|
+
tags: List of tags to categorize the task (auto-detection adds to these)
|
|
318
|
+
auto_detect_labels: Automatically detect and apply relevant labels (default: True)
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Created task details including ID and metadata, or error information
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
adapter = get_adapter()
|
|
326
|
+
|
|
327
|
+
# Validate and convert priority
|
|
328
|
+
try:
|
|
329
|
+
priority_enum = Priority(priority.lower())
|
|
330
|
+
except ValueError:
|
|
331
|
+
return {
|
|
332
|
+
"status": "error",
|
|
333
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Use default_user if no assignee specified
|
|
337
|
+
final_assignee = assignee
|
|
338
|
+
if final_assignee is None:
|
|
339
|
+
resolver = ConfigResolver(project_path=Path.cwd())
|
|
340
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
341
|
+
if config.default_user:
|
|
342
|
+
final_assignee = config.default_user
|
|
343
|
+
|
|
344
|
+
# Auto-detect labels if enabled
|
|
345
|
+
final_tags = tags
|
|
346
|
+
if auto_detect_labels:
|
|
347
|
+
final_tags = await detect_and_apply_labels(
|
|
348
|
+
adapter, title, description or "", tags
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Create task (Task with TASK type)
|
|
352
|
+
task = Task(
|
|
353
|
+
title=title,
|
|
354
|
+
description=description or "",
|
|
355
|
+
ticket_type=TicketType.TASK,
|
|
356
|
+
parent_issue=issue_id,
|
|
357
|
+
assignee=final_assignee,
|
|
358
|
+
priority=priority_enum,
|
|
359
|
+
tags=final_tags or [],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Create via adapter
|
|
363
|
+
created = await adapter.create(task)
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
"status": "completed",
|
|
367
|
+
"task": created.model_dump(),
|
|
368
|
+
"labels_applied": created.tags or [],
|
|
369
|
+
"auto_detected": auto_detect_labels,
|
|
370
|
+
}
|
|
371
|
+
except Exception as e:
|
|
372
|
+
return {
|
|
373
|
+
"status": "error",
|
|
374
|
+
"error": f"Failed to create task: {str(e)}",
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@mcp.tool()
|
|
379
|
+
async def epic_update(
|
|
380
|
+
epic_id: str,
|
|
381
|
+
title: str | None = None,
|
|
382
|
+
description: str | None = None,
|
|
383
|
+
state: str | None = None,
|
|
384
|
+
target_date: str | None = None,
|
|
385
|
+
) -> dict[str, Any]:
|
|
386
|
+
"""Update an existing epic's metadata and description.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
epic_id: Epic identifier (required)
|
|
390
|
+
title: New title for the epic
|
|
391
|
+
description: New description for the epic
|
|
392
|
+
state: New state (open, in_progress, done, closed)
|
|
393
|
+
target_date: Target completion date in ISO format (YYYY-MM-DD)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Updated epic details, or error information
|
|
397
|
+
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
adapter = get_adapter()
|
|
401
|
+
|
|
402
|
+
# Check if adapter supports epic updates
|
|
403
|
+
if not hasattr(adapter, "update_epic"):
|
|
404
|
+
return {
|
|
405
|
+
"status": "error",
|
|
406
|
+
"error": f"Epic updates not supported by {type(adapter).__name__} adapter",
|
|
407
|
+
"epic_id": epic_id,
|
|
408
|
+
"note": "Use ticket_update instead for basic field updates",
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Build updates dictionary
|
|
412
|
+
updates = {}
|
|
413
|
+
if title is not None:
|
|
414
|
+
updates["title"] = title
|
|
415
|
+
if description is not None:
|
|
416
|
+
updates["description"] = description
|
|
417
|
+
if state is not None:
|
|
418
|
+
updates["state"] = state
|
|
419
|
+
if target_date is not None:
|
|
420
|
+
# Parse target date if provided
|
|
421
|
+
try:
|
|
422
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
423
|
+
updates["target_date"] = target_datetime
|
|
424
|
+
except ValueError:
|
|
425
|
+
return {
|
|
426
|
+
"status": "error",
|
|
427
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if not updates:
|
|
431
|
+
return {
|
|
432
|
+
"status": "error",
|
|
433
|
+
"error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Update via adapter
|
|
437
|
+
updated = await adapter.update_epic(epic_id, updates) # type: ignore
|
|
438
|
+
|
|
439
|
+
if updated is None:
|
|
440
|
+
return {
|
|
441
|
+
"status": "error",
|
|
442
|
+
"error": f"Epic {epic_id} not found or update failed",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"status": "completed",
|
|
447
|
+
"epic": updated.model_dump(),
|
|
448
|
+
}
|
|
449
|
+
except AttributeError as e:
|
|
450
|
+
return {
|
|
451
|
+
"status": "error",
|
|
452
|
+
"error": f"Epic update method not available: {str(e)}",
|
|
453
|
+
"epic_id": epic_id,
|
|
454
|
+
}
|
|
455
|
+
except Exception as e:
|
|
456
|
+
return {
|
|
457
|
+
"status": "error",
|
|
458
|
+
"error": f"Failed to update epic: {str(e)}",
|
|
459
|
+
"epic_id": epic_id,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@mcp.tool()
|
|
464
|
+
async def hierarchy_tree(
|
|
465
|
+
epic_id: str,
|
|
466
|
+
max_depth: int = 3,
|
|
467
|
+
) -> dict[str, Any]:
|
|
468
|
+
"""Get complete hierarchy tree for an epic.
|
|
469
|
+
|
|
470
|
+
Retrieves the full hierarchy tree starting from an epic, including all
|
|
471
|
+
child issues and their tasks up to the specified depth.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
epic_id: Unique identifier of the root epic
|
|
475
|
+
max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Complete hierarchy tree structure, or error information
|
|
479
|
+
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
adapter = get_adapter()
|
|
483
|
+
|
|
484
|
+
# Read the epic
|
|
485
|
+
epic = await adapter.read(epic_id)
|
|
486
|
+
if epic is None:
|
|
487
|
+
return {
|
|
488
|
+
"status": "error",
|
|
489
|
+
"error": f"Epic {epic_id} not found",
|
|
490
|
+
}
|
|
491
|
+
|
|
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
|
+
except Exception as e:
|
|
529
|
+
return {
|
|
530
|
+
"status": "error",
|
|
531
|
+
"error": f"Failed to build hierarchy tree: {str(e)}",
|
|
532
|
+
}
|