mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) 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 +796 -46
  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 +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,921 @@
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.adapter import BaseAdapter
14
+ from ....core.models import Epic, Priority, Task, TicketType
15
+ from ....core.project_config import ConfigResolver, TicketerConfig
16
+ from ..server_sdk import get_adapter, mcp
17
+ from .ticket_tools import detect_and_apply_labels
18
+
19
+
20
+ def _build_adapter_metadata(
21
+ adapter: BaseAdapter,
22
+ ticket_id: str | None = None,
23
+ ) -> dict[str, Any]:
24
+ """Build adapter metadata for MCP responses.
25
+
26
+ Args:
27
+ adapter: The adapter that handled the operation
28
+ ticket_id: Optional ticket ID to include in metadata
29
+
30
+ Returns:
31
+ Dictionary with adapter metadata fields
32
+
33
+ """
34
+ metadata = {
35
+ "adapter": adapter.adapter_type,
36
+ "adapter_name": adapter.adapter_display_name,
37
+ }
38
+
39
+ if ticket_id:
40
+ metadata["ticket_id"] = ticket_id
41
+
42
+ return metadata
43
+
44
+
45
+ @mcp.tool()
46
+ async def epic_create(
47
+ title: str,
48
+ description: str = "",
49
+ target_date: str | None = None,
50
+ lead_id: str | None = None,
51
+ child_issues: list[str] | None = None,
52
+ ) -> dict[str, Any]:
53
+ """Create a new epic (strategic level container).
54
+
55
+ Adapter Support: All adapters support epic creation
56
+ - Linear: Creates project with timeline (via create())
57
+ - JIRA: Creates epic in configured project (dedicated create_epic())
58
+ - GitHub: Creates milestone (via create_milestone())
59
+ - Asana: Creates project (dedicated create_epic())
60
+ - AiTrackDown: Creates epic in local storage (dedicated create_epic())
61
+
62
+ Args:
63
+ title: Epic title (required)
64
+ description: Detailed description of the epic
65
+ target_date: Target completion date in ISO format (YYYY-MM-DD)
66
+ lead_id: User ID or email of the epic lead
67
+ child_issues: List of existing issue IDs to link to this epic
68
+
69
+ Returns:
70
+ Created epic details including ID and metadata, or error information
71
+
72
+ """
73
+ try:
74
+ adapter = get_adapter()
75
+
76
+ # Parse target date if provided
77
+ target_datetime = None
78
+ if target_date:
79
+ try:
80
+ target_datetime = datetime.fromisoformat(target_date)
81
+ except ValueError:
82
+ return {
83
+ "status": "error",
84
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
85
+ }
86
+
87
+ # Create epic object
88
+ epic = Epic(
89
+ title=title,
90
+ description=description or "",
91
+ due_date=target_datetime,
92
+ assignee=lead_id,
93
+ child_issues=child_issues or [],
94
+ )
95
+
96
+ # Create via adapter
97
+ created = await adapter.create(epic)
98
+
99
+ return {
100
+ "status": "completed",
101
+ **_build_adapter_metadata(adapter, created.id),
102
+ "epic": created.model_dump(),
103
+ }
104
+ except Exception as e:
105
+ return {
106
+ "status": "error",
107
+ "error": f"Failed to create epic: {str(e)}",
108
+ }
109
+
110
+
111
+ @mcp.tool()
112
+ async def epic_get(epic_id: str) -> dict[str, Any]:
113
+ """Read an epic by its ID.
114
+
115
+ This tool retrieves detailed information about a specific epic/project/milestone.
116
+
117
+ Adapter Support: All adapters support reading epics
118
+ - Linear: Reads project details (via read())
119
+ - JIRA: Reads epic with dedicated get_epic() method
120
+ - GitHub: Reads milestone (via read())
121
+ - Asana: Reads project with dedicated get_epic() method
122
+ - AiTrackDown: Reads epic with dedicated get_epic() method
123
+
124
+ Args:
125
+ epic_id: Unique identifier of the epic to retrieve
126
+
127
+ Returns:
128
+ Epic details if found, or error information
129
+
130
+ """
131
+ try:
132
+ adapter = get_adapter()
133
+
134
+ # Use adapter's get_epic method if available (optimized for some adapters)
135
+ if hasattr(adapter, "get_epic"):
136
+ epic = await adapter.get_epic(epic_id)
137
+ else:
138
+ # Fallback to generic read method
139
+ epic = await adapter.read(epic_id)
140
+
141
+ if epic is None:
142
+ return {
143
+ "status": "error",
144
+ "error": f"Epic {epic_id} not found",
145
+ **_build_adapter_metadata(adapter, epic_id),
146
+ }
147
+
148
+ return {
149
+ "status": "completed",
150
+ **_build_adapter_metadata(adapter, epic_id),
151
+ "epic": epic.model_dump(),
152
+ }
153
+ except Exception as e:
154
+ return {
155
+ "status": "error",
156
+ "error": f"Failed to get epic: {str(e)}",
157
+ }
158
+
159
+
160
+ @mcp.tool()
161
+ async def epic_list(
162
+ limit: int = 10,
163
+ offset: int = 0,
164
+ state: str | None = None,
165
+ include_completed: bool = False,
166
+ ) -> dict[str, Any]:
167
+ """List all epics with pagination and optional filtering.
168
+
169
+ Adapter Support: All adapters support listing epics
170
+ - Linear: Optimized list_epics() with state filter and include_completed
171
+ - JIRA: Optimized list_epics() with state filtering (mapped to JIRA status)
172
+ - GitHub: Generic list() method (state filter not supported)
173
+ - Asana: Optimized list_epics() method (state filter not supported)
174
+ - AiTrackDown: Optimized list_epics() with basic pagination
175
+
176
+ Adapter-Specific Parameters:
177
+ - state: Supported by Linear (e.g., "planned", "started", "completed") and JIRA (e.g., "To Do", "In Progress", "Done")
178
+ - include_completed: Linear-specific parameter to include/exclude completed projects
179
+
180
+ Args:
181
+ limit: Maximum number of epics to return (default: 10)
182
+ offset: Number of epics to skip for pagination (default: 0)
183
+ state: Optional state filter - adapter-specific behavior
184
+ include_completed: Include completed epics (Linear-specific, default: False)
185
+
186
+ Returns:
187
+ List of epics with adapter information, or error information
188
+
189
+ """
190
+ try:
191
+ adapter = get_adapter()
192
+
193
+ # Check if adapter has optimized list_epics method
194
+ if hasattr(adapter, "list_epics"):
195
+ # Build kwargs for adapter-specific parameters
196
+ kwargs: dict[str, Any] = {"limit": limit, "offset": offset}
197
+
198
+ # Add state filter if supported
199
+ if state is not None:
200
+ kwargs["state"] = state
201
+
202
+ # Add include_completed for Linear adapter
203
+ adapter_type = adapter.adapter_type.lower()
204
+ if adapter_type == "linear" and include_completed:
205
+ kwargs["include_completed"] = include_completed
206
+
207
+ epics = await adapter.list_epics(**kwargs)
208
+ else:
209
+ # Fallback to generic list method with epic filter
210
+ filters = {"ticket_type": TicketType.EPIC}
211
+ if state is not None:
212
+ filters["state"] = state
213
+ epics = await adapter.list(limit=limit, offset=offset, filters=filters)
214
+
215
+ return {
216
+ "status": "completed",
217
+ **_build_adapter_metadata(adapter),
218
+ "epics": [epic.model_dump() for epic in epics],
219
+ "count": len(epics),
220
+ "limit": limit,
221
+ "offset": offset,
222
+ "filters_applied": {
223
+ "state": state,
224
+ "include_completed": include_completed,
225
+ },
226
+ }
227
+ except Exception as e:
228
+ return {
229
+ "status": "error",
230
+ "error": f"Failed to list epics: {str(e)}",
231
+ }
232
+
233
+
234
+ @mcp.tool()
235
+ async def epic_issues(epic_id: str) -> dict[str, Any]:
236
+ """Get all issues belonging to an epic.
237
+
238
+ Args:
239
+ epic_id: Unique identifier of the epic
240
+
241
+ Returns:
242
+ List of issues in the epic, or error information
243
+
244
+ """
245
+ try:
246
+ adapter = get_adapter()
247
+
248
+ # Read the epic to get child issue IDs
249
+ epic = await adapter.read(epic_id)
250
+ if epic is None:
251
+ return {
252
+ "status": "error",
253
+ "error": f"Epic {epic_id} not found",
254
+ }
255
+
256
+ # If epic has no child_issues attribute, use empty list
257
+ child_issue_ids = getattr(epic, "child_issues", [])
258
+
259
+ # Fetch each child issue
260
+ issues = []
261
+ for issue_id in child_issue_ids:
262
+ issue = await adapter.read(issue_id)
263
+ if issue:
264
+ issues.append(issue.model_dump())
265
+
266
+ return {
267
+ "status": "completed",
268
+ **_build_adapter_metadata(adapter, epic_id),
269
+ "issues": issues,
270
+ "count": len(issues),
271
+ }
272
+ except Exception as e:
273
+ return {
274
+ "status": "error",
275
+ "error": f"Failed to get epic issues: {str(e)}",
276
+ }
277
+
278
+
279
+ @mcp.tool()
280
+ async def issue_create(
281
+ title: str,
282
+ description: str = "",
283
+ epic_id: str | None = None,
284
+ assignee: str | None = None,
285
+ priority: str = "medium",
286
+ tags: list[str] | None = None,
287
+ auto_detect_labels: bool = True,
288
+ ) -> dict[str, Any]:
289
+ """Create a new issue (standard work item) with automatic label detection.
290
+
291
+ This tool automatically scans available labels/tags and intelligently
292
+ applies relevant ones based on the issue title and description.
293
+
294
+ Args:
295
+ title: Issue title (required)
296
+ description: Detailed description of the issue
297
+ epic_id: Parent epic ID to link this issue to
298
+ assignee: User ID or email to assign the issue to
299
+ priority: Priority level - must be one of: low, medium, high, critical
300
+ tags: List of tags to categorize the issue (auto-detection adds to these)
301
+ auto_detect_labels: Automatically detect and apply relevant labels (default: True)
302
+
303
+ Returns:
304
+ Created issue details including ID and metadata, or error information
305
+
306
+ """
307
+ try:
308
+ adapter = get_adapter()
309
+
310
+ # Validate and convert priority
311
+ try:
312
+ priority_enum = Priority(priority.lower())
313
+ except ValueError:
314
+ return {
315
+ "status": "error",
316
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
317
+ }
318
+
319
+ # Use default_user if no assignee specified
320
+ final_assignee = assignee
321
+ if final_assignee is None:
322
+ resolver = ConfigResolver(project_path=Path.cwd())
323
+ config = resolver.load_project_config() or TicketerConfig()
324
+ if config.default_user:
325
+ final_assignee = config.default_user
326
+
327
+ # Use default_project if no epic_id specified
328
+ final_epic_id = epic_id
329
+ if final_epic_id is None:
330
+ resolver = ConfigResolver(project_path=Path.cwd())
331
+ config = resolver.load_project_config() or TicketerConfig()
332
+ # Try default_project first, fall back to default_epic
333
+ if config.default_project:
334
+ final_epic_id = config.default_project
335
+ elif config.default_epic:
336
+ final_epic_id = config.default_epic
337
+
338
+ # Auto-detect labels if enabled
339
+ final_tags = tags
340
+ if auto_detect_labels:
341
+ final_tags = await detect_and_apply_labels(
342
+ adapter, title, description or "", tags
343
+ )
344
+
345
+ # Create issue (Task with ISSUE type)
346
+ issue = Task(
347
+ title=title,
348
+ description=description or "",
349
+ ticket_type=TicketType.ISSUE,
350
+ parent_epic=final_epic_id,
351
+ assignee=final_assignee,
352
+ priority=priority_enum,
353
+ tags=final_tags or [],
354
+ )
355
+
356
+ # Create via adapter
357
+ created = await adapter.create(issue)
358
+
359
+ return {
360
+ "status": "completed",
361
+ **_build_adapter_metadata(adapter, created.id),
362
+ "issue": created.model_dump(),
363
+ "labels_applied": created.tags or [],
364
+ "auto_detected": auto_detect_labels,
365
+ }
366
+ except Exception as e:
367
+ return {
368
+ "status": "error",
369
+ "error": f"Failed to create issue: {str(e)}",
370
+ }
371
+
372
+
373
+ @mcp.tool()
374
+ async def issue_get_parent(issue_id: str) -> dict[str, Any]:
375
+ """Get the parent issue of a sub-issue.
376
+
377
+ This tool retrieves the parent issue details for a given sub-issue ID.
378
+ Returns None if the issue has no parent (i.e., it's a top-level issue).
379
+
380
+ Args:
381
+ issue_id: Unique identifier of the sub-issue (e.g., "ENG-842", UUID)
382
+
383
+ Returns:
384
+ Dictionary containing:
385
+ - status: "completed" or "error"
386
+ - parent: Parent issue details (dict) if exists, None if no parent
387
+ - adapter: Adapter type that handled the operation
388
+ - adapter_name: Human-readable adapter name
389
+ - error: Error message (if failed)
390
+
391
+ Example response (has parent):
392
+ {
393
+ "status": "completed",
394
+ "parent": {
395
+ "id": "abc-123",
396
+ "identifier": "ENG-840",
397
+ "title": "Implement hierarchy features",
398
+ "state": "in_progress",
399
+ ...
400
+ },
401
+ "adapter": "linear",
402
+ "adapter_name": "Linear"
403
+ }
404
+
405
+ Example response (no parent):
406
+ {
407
+ "status": "completed",
408
+ "parent": None,
409
+ "adapter": "linear",
410
+ "adapter_name": "Linear"
411
+ }
412
+
413
+ """
414
+ try:
415
+ adapter = get_adapter()
416
+
417
+ # Read the issue to check if it has a parent
418
+ issue = await adapter.read(issue_id)
419
+ if issue is None:
420
+ return {
421
+ "status": "error",
422
+ "error": f"Issue {issue_id} not found",
423
+ }
424
+
425
+ # Check for parent_issue attribute (sub-issues have this set)
426
+ parent_issue_id = getattr(issue, "parent_issue", None)
427
+
428
+ if not parent_issue_id:
429
+ # No parent - this is a top-level issue
430
+ return {
431
+ "status": "completed",
432
+ **_build_adapter_metadata(adapter, issue_id),
433
+ "parent": None,
434
+ }
435
+
436
+ # Fetch parent issue details
437
+ parent_issue = await adapter.read(parent_issue_id)
438
+ if parent_issue is None:
439
+ return {
440
+ "status": "error",
441
+ "error": f"Parent issue {parent_issue_id} not found",
442
+ }
443
+
444
+ return {
445
+ "status": "completed",
446
+ **_build_adapter_metadata(adapter, issue_id),
447
+ "parent": parent_issue.model_dump(),
448
+ }
449
+ except Exception as e:
450
+ return {
451
+ "status": "error",
452
+ "error": f"Failed to get parent issue: {str(e)}",
453
+ }
454
+
455
+
456
+ @mcp.tool()
457
+ async def issue_tasks(
458
+ issue_id: str,
459
+ state: str | None = None,
460
+ assignee: str | None = None,
461
+ priority: str | None = None,
462
+ ) -> dict[str, Any]:
463
+ """Get all tasks (sub-items) belonging to an issue with optional filtering.
464
+
465
+ This tool retrieves child tasks/sub-issues for a given issue ID, with support
466
+ for filtering by state, assignee, and priority. All filter parameters are optional.
467
+
468
+ Args:
469
+ issue_id: Unique identifier of the issue
470
+ state: Optional state filter - must be one of: open, in_progress, ready,
471
+ tested, done, closed, waiting, blocked
472
+ assignee: Optional user ID or email to filter by assignee
473
+ priority: Optional priority filter - must be one of: low, medium, high, critical
474
+
475
+ Returns:
476
+ Dictionary containing:
477
+ - status: "completed" or "error"
478
+ - tasks: List of task objects matching filters
479
+ - count: Number of tasks returned
480
+ - filters_applied: Dict showing which filters were used
481
+ - adapter: Adapter type that handled the operation
482
+ - error: Error message (if failed)
483
+
484
+ Example:
485
+ # Get all tasks for issue
486
+ result = issue_tasks("ENG-840")
487
+
488
+ # Get only in-progress tasks assigned to user
489
+ result = issue_tasks("ENG-840", state="in_progress", assignee="user@example.com")
490
+
491
+ # Get high priority tasks
492
+ result = issue_tasks("ENG-840", priority="high")
493
+
494
+ """
495
+ try:
496
+ adapter = get_adapter()
497
+
498
+ # Validate filter parameters
499
+ filters_applied = {}
500
+
501
+ # Validate state if provided
502
+ if state is not None:
503
+ try:
504
+ from ....core.models import TicketState
505
+
506
+ state_enum = TicketState(state.lower())
507
+ filters_applied["state"] = state_enum.value
508
+ except ValueError:
509
+ return {
510
+ "status": "error",
511
+ "error": f"Invalid state '{state}'. Must be one of: open, in_progress, ready, tested, done, closed, waiting, blocked",
512
+ }
513
+
514
+ # Validate priority if provided
515
+ if priority is not None:
516
+ try:
517
+ from ....core.models import Priority
518
+
519
+ priority_enum = Priority(priority.lower())
520
+ filters_applied["priority"] = priority_enum.value
521
+ except ValueError:
522
+ return {
523
+ "status": "error",
524
+ "error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
525
+ }
526
+
527
+ if assignee is not None:
528
+ filters_applied["assignee"] = assignee
529
+
530
+ # Read the issue to get child task IDs
531
+ issue = await adapter.read(issue_id)
532
+ if issue is None:
533
+ return {
534
+ "status": "error",
535
+ "error": f"Issue {issue_id} not found",
536
+ }
537
+
538
+ # Get child task IDs
539
+ child_task_ids = getattr(issue, "children", [])
540
+
541
+ # Fetch each child task
542
+ tasks = []
543
+ for task_id in child_task_ids:
544
+ task = await adapter.read(task_id)
545
+ if task:
546
+ # Apply filters
547
+ should_include = True
548
+
549
+ # Filter by state
550
+ if state is not None:
551
+ task_state = getattr(task, "state", None)
552
+ # Handle case where state might be stored as string
553
+ if isinstance(task_state, str):
554
+ should_include = should_include and (
555
+ task_state.lower() == state.lower()
556
+ )
557
+ else:
558
+ should_include = should_include and (task_state == state_enum)
559
+
560
+ # Filter by priority
561
+ if priority is not None:
562
+ task_priority = getattr(task, "priority", None)
563
+ # Handle case where priority might be stored as string
564
+ if isinstance(task_priority, str):
565
+ should_include = should_include and (
566
+ task_priority.lower() == priority.lower()
567
+ )
568
+ else:
569
+ should_include = should_include and (
570
+ task_priority == priority_enum
571
+ )
572
+
573
+ # Filter by assignee
574
+ if assignee is not None:
575
+ task_assignee = getattr(task, "assignee", None)
576
+ # Case-insensitive comparison for emails/usernames
577
+ should_include = should_include and (
578
+ task_assignee is not None
579
+ and assignee.lower() in str(task_assignee).lower()
580
+ )
581
+
582
+ if should_include:
583
+ tasks.append(task.model_dump())
584
+
585
+ return {
586
+ "status": "completed",
587
+ **_build_adapter_metadata(adapter, issue_id),
588
+ "tasks": tasks,
589
+ "count": len(tasks),
590
+ "filters_applied": filters_applied,
591
+ }
592
+ except Exception as e:
593
+ return {
594
+ "status": "error",
595
+ "error": f"Failed to get issue tasks: {str(e)}",
596
+ }
597
+
598
+
599
+ @mcp.tool()
600
+ async def task_create(
601
+ title: str,
602
+ description: str = "",
603
+ issue_id: str | None = None,
604
+ assignee: str | None = None,
605
+ priority: str = "medium",
606
+ tags: list[str] | None = None,
607
+ auto_detect_labels: bool = True,
608
+ ) -> dict[str, Any]:
609
+ """Create a new task (sub-work item) with automatic label detection.
610
+
611
+ This tool automatically scans available labels/tags and intelligently
612
+ applies relevant ones based on the task title and description.
613
+
614
+ Args:
615
+ title: Task title (required)
616
+ description: Detailed description of the task
617
+ issue_id: Parent issue ID to link this task to
618
+ assignee: User ID or email to assign the task to
619
+ priority: Priority level - must be one of: low, medium, high, critical
620
+ tags: List of tags to categorize the task (auto-detection adds to these)
621
+ auto_detect_labels: Automatically detect and apply relevant labels (default: True)
622
+
623
+ Returns:
624
+ Created task details including ID and metadata, or error information
625
+
626
+ """
627
+ try:
628
+ adapter = get_adapter()
629
+
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
+ # Use default_user if no assignee specified
640
+ final_assignee = assignee
641
+ if final_assignee is None:
642
+ resolver = ConfigResolver(project_path=Path.cwd())
643
+ config = resolver.load_project_config() or TicketerConfig()
644
+ if config.default_user:
645
+ final_assignee = config.default_user
646
+
647
+ # Auto-detect labels if enabled
648
+ final_tags = tags
649
+ if auto_detect_labels:
650
+ final_tags = await detect_and_apply_labels(
651
+ adapter, title, description or "", tags
652
+ )
653
+
654
+ # Create task (Task with TASK type)
655
+ task = Task(
656
+ title=title,
657
+ description=description or "",
658
+ ticket_type=TicketType.TASK,
659
+ parent_issue=issue_id,
660
+ assignee=final_assignee,
661
+ priority=priority_enum,
662
+ tags=final_tags or [],
663
+ )
664
+
665
+ # Create via adapter
666
+ created = await adapter.create(task)
667
+
668
+ return {
669
+ "status": "completed",
670
+ **_build_adapter_metadata(adapter, created.id),
671
+ "task": created.model_dump(),
672
+ "labels_applied": created.tags or [],
673
+ "auto_detected": auto_detect_labels,
674
+ }
675
+ except Exception as e:
676
+ return {
677
+ "status": "error",
678
+ "error": f"Failed to create task: {str(e)}",
679
+ }
680
+
681
+
682
+ @mcp.tool()
683
+ async def epic_update(
684
+ epic_id: str,
685
+ title: str | None = None,
686
+ description: str | None = None,
687
+ state: str | None = None,
688
+ target_date: str | None = None,
689
+ ) -> dict[str, Any]:
690
+ """Update an existing epic's metadata and description.
691
+
692
+ Adapter Support: All adapters support epic updates with dedicated update_epic() method
693
+ - Linear: ✓ Updates project fields (title, description, state, target_date, etc.)
694
+ - JIRA: ✓ Updates epic fields (title, description, status, due date)
695
+ - GitHub: ✓ Updates milestone (title, description, state, due_on)
696
+ - Asana: ✓ Updates project metadata (name, notes, due_on, color, etc.)
697
+ - AiTrackDown: ✓ Updates epic in local storage
698
+
699
+ Supported Update Fields:
700
+ - title: Epic/project/milestone title (all adapters)
701
+ - description: Detailed description (all adapters)
702
+ - state: Epic state - adapter-specific values (Linear, JIRA, GitHub)
703
+ - target_date: Due date in ISO format YYYY-MM-DD (all adapters)
704
+
705
+ Args:
706
+ epic_id: Epic identifier (required)
707
+ title: New title for the epic
708
+ description: New description for the epic
709
+ state: New state (open, in_progress, done, closed)
710
+ target_date: Target completion date in ISO format (YYYY-MM-DD)
711
+
712
+ Returns:
713
+ Updated epic details, or error information
714
+
715
+ """
716
+ try:
717
+ adapter = get_adapter()
718
+
719
+ # Check if adapter supports epic updates
720
+ if not hasattr(adapter, "update_epic"):
721
+ adapter_name = adapter.adapter_display_name
722
+ return {
723
+ "status": "error",
724
+ "error": f"Epic updates not supported by {adapter_name} adapter",
725
+ "epic_id": epic_id,
726
+ "note": "This adapter should implement update_epic() method",
727
+ }
728
+
729
+ # Build updates dictionary
730
+ updates = {}
731
+ if title is not None:
732
+ updates["title"] = title
733
+ if description is not None:
734
+ updates["description"] = description
735
+ if state is not None:
736
+ updates["state"] = state
737
+ if target_date is not None:
738
+ # Parse target date if provided
739
+ try:
740
+ target_datetime = datetime.fromisoformat(target_date)
741
+ updates["target_date"] = target_datetime
742
+ except ValueError:
743
+ return {
744
+ "status": "error",
745
+ "error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
746
+ }
747
+
748
+ if not updates:
749
+ return {
750
+ "status": "error",
751
+ "error": "No updates provided. At least one field (title, description, state, target_date) must be specified.",
752
+ }
753
+
754
+ # Update via adapter
755
+ updated = await adapter.update_epic(epic_id, updates)
756
+
757
+ if updated is None:
758
+ return {
759
+ "status": "error",
760
+ "error": f"Epic {epic_id} not found or update failed",
761
+ }
762
+
763
+ return {
764
+ "status": "completed",
765
+ **_build_adapter_metadata(adapter, epic_id),
766
+ "epic": updated.model_dump(),
767
+ }
768
+ except AttributeError as e:
769
+ return {
770
+ "status": "error",
771
+ "error": f"Epic update method not available: {str(e)}",
772
+ "epic_id": epic_id,
773
+ }
774
+ except Exception as e:
775
+ return {
776
+ "status": "error",
777
+ "error": f"Failed to update epic: {str(e)}",
778
+ "epic_id": epic_id,
779
+ }
780
+
781
+
782
+ @mcp.tool()
783
+ async def epic_delete(epic_id: str) -> dict[str, Any]:
784
+ """Delete an epic/project/milestone by ID.
785
+
786
+ Adapter Support:
787
+ - GitHub: ✓ Deletes milestone (delete_epic() - permanent deletion)
788
+ - Asana: ✓ Archives project (delete_epic() - can be restored from archive)
789
+ - Linear: ✗ Linear API doesn't support project deletion
790
+ - JIRA: ✗ JIRA API doesn't support epic deletion
791
+ - AiTrackDown: ✗ Epic deletion not implemented yet
792
+
793
+ Important Notes:
794
+ - GitHub: Deletion is permanent and cannot be undone
795
+ - Asana: Project is archived, not deleted, and can be restored
796
+ - For unsupported adapters, the tool returns an error with details
797
+
798
+ Args:
799
+ epic_id: Unique identifier of the epic to delete
800
+
801
+ Returns:
802
+ Success/failure status with adapter information
803
+
804
+ """
805
+ try:
806
+ adapter = get_adapter()
807
+
808
+ # Check if adapter supports epic deletion
809
+ if not hasattr(adapter, "delete_epic"):
810
+ adapter_name = adapter.adapter_display_name
811
+ return {
812
+ "status": "error",
813
+ "error": f"Epic deletion not supported by {adapter_name} adapter",
814
+ **_build_adapter_metadata(adapter, epic_id),
815
+ "supported_adapters": ["GitHub", "Asana"],
816
+ "note": f"{adapter_name} does not provide API support for deleting epics/projects",
817
+ }
818
+
819
+ # Call adapter's delete_epic method
820
+ success = await adapter.delete_epic(epic_id)
821
+
822
+ if not success:
823
+ return {
824
+ "status": "error",
825
+ "error": f"Failed to delete epic {epic_id}",
826
+ **_build_adapter_metadata(adapter, epic_id),
827
+ }
828
+
829
+ return {
830
+ "status": "completed",
831
+ **_build_adapter_metadata(adapter, epic_id),
832
+ "message": f"Epic {epic_id} deleted successfully",
833
+ "deleted": True,
834
+ }
835
+ except AttributeError:
836
+ adapter_name = adapter.adapter_display_name
837
+ return {
838
+ "status": "error",
839
+ "error": f"Epic deletion not supported by {adapter_name} adapter",
840
+ **_build_adapter_metadata(adapter, epic_id),
841
+ "supported_adapters": ["GitHub", "Asana"],
842
+ }
843
+ except Exception as e:
844
+ return {
845
+ "status": "error",
846
+ "error": f"Failed to delete epic: {str(e)}",
847
+ **_build_adapter_metadata(adapter, epic_id),
848
+ }
849
+
850
+
851
+ @mcp.tool()
852
+ async def hierarchy_tree(
853
+ epic_id: str,
854
+ max_depth: int = 3,
855
+ ) -> dict[str, Any]:
856
+ """Get complete hierarchy tree for an epic.
857
+
858
+ Retrieves the full hierarchy tree starting from an epic, including all
859
+ child issues and their tasks up to the specified depth.
860
+
861
+ Args:
862
+ epic_id: Unique identifier of the root epic
863
+ max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
864
+
865
+ Returns:
866
+ Complete hierarchy tree structure, or error information
867
+
868
+ """
869
+ try:
870
+ adapter = get_adapter()
871
+
872
+ # Read the epic
873
+ epic = await adapter.read(epic_id)
874
+ if epic is None:
875
+ return {
876
+ "status": "error",
877
+ "error": f"Epic {epic_id} not found",
878
+ }
879
+
880
+ # Build tree structure
881
+ tree = {
882
+ "epic": epic.model_dump(),
883
+ "issues": [],
884
+ }
885
+
886
+ if max_depth < 2:
887
+ return {
888
+ "status": "completed",
889
+ "tree": tree,
890
+ }
891
+
892
+ # Get child issues
893
+ child_issue_ids = getattr(epic, "child_issues", [])
894
+ for issue_id in child_issue_ids:
895
+ issue = await adapter.read(issue_id)
896
+ if issue:
897
+ issue_data = {
898
+ "issue": issue.model_dump(),
899
+ "tasks": [],
900
+ }
901
+
902
+ if max_depth >= 3:
903
+ # Get child tasks
904
+ child_task_ids = getattr(issue, "children", [])
905
+ for task_id in child_task_ids:
906
+ task = await adapter.read(task_id)
907
+ if task:
908
+ issue_data["tasks"].append(task.model_dump())
909
+
910
+ tree["issues"].append(issue_data)
911
+
912
+ return {
913
+ "status": "completed",
914
+ **_build_adapter_metadata(adapter, epic_id),
915
+ "tree": tree,
916
+ }
917
+ except Exception as e:
918
+ return {
919
+ "status": "error",
920
+ "error": f"Failed to build hierarchy tree: {str(e)}",
921
+ }