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.

Files changed (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {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
+ }