mcp-ticketer 0.1.24__py3-none-any.whl → 0.1.27__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 +1 -1
- mcp_ticketer/adapters/hybrid.py +8 -8
- mcp_ticketer/cli/main.py +146 -4
- mcp_ticketer/mcp/server.py +710 -3
- mcp_ticketer/queue/health_monitor.py +322 -0
- mcp_ticketer/queue/queue.py +147 -66
- mcp_ticketer/queue/ticket_registry.py +416 -0
- mcp_ticketer/queue/worker.py +102 -8
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/RECORD +14 -12
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.24.dist-info → mcp_ticketer-0.1.27.dist-info}/top_level.txt +0 -0
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -11,6 +11,10 @@ from dotenv import load_dotenv
|
|
|
11
11
|
from ..core import AdapterRegistry
|
|
12
12
|
from ..core.models import SearchQuery
|
|
13
13
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
14
|
+
from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
|
|
15
|
+
|
|
16
|
+
# Import adapters module to trigger registration
|
|
17
|
+
import mcp_ticketer.adapters # noqa: F401
|
|
14
18
|
|
|
15
19
|
# Load environment variables early (prioritize .env.local)
|
|
16
20
|
# Check for .env.local first (takes precedence)
|
|
@@ -89,6 +93,36 @@ class MCPTicketServer:
|
|
|
89
93
|
result = await self._handle_create_pr(params)
|
|
90
94
|
elif method == "ticket/link_pr":
|
|
91
95
|
result = await self._handle_link_pr(params)
|
|
96
|
+
elif method == "queue/health":
|
|
97
|
+
result = await self._handle_queue_health(params)
|
|
98
|
+
# Hierarchy management tools
|
|
99
|
+
elif method == "epic/create":
|
|
100
|
+
result = await self._handle_epic_create(params)
|
|
101
|
+
elif method == "epic/list":
|
|
102
|
+
result = await self._handle_epic_list(params)
|
|
103
|
+
elif method == "epic/issues":
|
|
104
|
+
result = await self._handle_epic_issues(params)
|
|
105
|
+
elif method == "issue/create":
|
|
106
|
+
result = await self._handle_issue_create(params)
|
|
107
|
+
elif method == "issue/tasks":
|
|
108
|
+
result = await self._handle_issue_tasks(params)
|
|
109
|
+
elif method == "task/create":
|
|
110
|
+
result = await self._handle_task_create(params)
|
|
111
|
+
elif method == "hierarchy/tree":
|
|
112
|
+
result = await self._handle_hierarchy_tree(params)
|
|
113
|
+
# Bulk operations
|
|
114
|
+
elif method == "ticket/bulk_create":
|
|
115
|
+
result = await self._handle_bulk_create(params)
|
|
116
|
+
elif method == "ticket/bulk_update":
|
|
117
|
+
result = await self._handle_bulk_update(params)
|
|
118
|
+
# Advanced search
|
|
119
|
+
elif method == "ticket/search_hierarchy":
|
|
120
|
+
result = await self._handle_search_hierarchy(params)
|
|
121
|
+
# Attachment handling
|
|
122
|
+
elif method == "ticket/attach":
|
|
123
|
+
result = await self._handle_attach(params)
|
|
124
|
+
elif method == "ticket/attachments":
|
|
125
|
+
result = await self._handle_list_attachments(params)
|
|
92
126
|
elif method == "tools/list":
|
|
93
127
|
result = await self._handle_tools_list()
|
|
94
128
|
elif method == "tools/call":
|
|
@@ -125,7 +159,30 @@ class MCPTicketServer:
|
|
|
125
159
|
|
|
126
160
|
async def _handle_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
127
161
|
"""Handle ticket creation."""
|
|
128
|
-
#
|
|
162
|
+
# Check queue health before proceeding
|
|
163
|
+
health_monitor = QueueHealthMonitor()
|
|
164
|
+
health = health_monitor.check_health()
|
|
165
|
+
|
|
166
|
+
# If queue is in critical state, try auto-repair
|
|
167
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
168
|
+
repair_result = health_monitor.auto_repair()
|
|
169
|
+
# Re-check health after repair
|
|
170
|
+
health = health_monitor.check_health()
|
|
171
|
+
|
|
172
|
+
# If still critical, return error immediately
|
|
173
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
174
|
+
critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
|
|
175
|
+
return {
|
|
176
|
+
"status": "error",
|
|
177
|
+
"error": "Queue system is in critical state",
|
|
178
|
+
"details": {
|
|
179
|
+
"health_status": health["status"],
|
|
180
|
+
"critical_issues": critical_alerts,
|
|
181
|
+
"repair_attempted": repair_result["actions_taken"]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Queue the operation
|
|
129
186
|
queue = Queue()
|
|
130
187
|
task_data = {
|
|
131
188
|
"title": params["title"],
|
|
@@ -143,7 +200,19 @@ class MCPTicketServer:
|
|
|
143
200
|
|
|
144
201
|
# Start worker if needed
|
|
145
202
|
manager = WorkerManager()
|
|
146
|
-
manager.start_if_needed()
|
|
203
|
+
worker_started = manager.start_if_needed()
|
|
204
|
+
|
|
205
|
+
# If worker failed to start and we have pending items, that's critical
|
|
206
|
+
if not worker_started and queue.get_pending_count() > 0:
|
|
207
|
+
return {
|
|
208
|
+
"status": "error",
|
|
209
|
+
"error": "Failed to start worker process",
|
|
210
|
+
"queue_id": queue_id,
|
|
211
|
+
"details": {
|
|
212
|
+
"pending_count": queue.get_pending_count(),
|
|
213
|
+
"action": "Worker process could not be started to process queued operations"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
147
216
|
|
|
148
217
|
# Check if async mode is requested (for backward compatibility)
|
|
149
218
|
if params.get("async_mode", False):
|
|
@@ -475,6 +544,452 @@ class MCPTicketServer:
|
|
|
475
544
|
|
|
476
545
|
return response
|
|
477
546
|
|
|
547
|
+
async def _handle_queue_health(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
548
|
+
"""Handle queue health check."""
|
|
549
|
+
health_monitor = QueueHealthMonitor()
|
|
550
|
+
health = health_monitor.check_health()
|
|
551
|
+
|
|
552
|
+
# Add auto-repair option
|
|
553
|
+
auto_repair = params.get("auto_repair", False)
|
|
554
|
+
if auto_repair and health["status"] in [HealthStatus.CRITICAL, HealthStatus.WARNING]:
|
|
555
|
+
repair_result = health_monitor.auto_repair()
|
|
556
|
+
health["auto_repair"] = repair_result
|
|
557
|
+
# Re-check health after repair
|
|
558
|
+
health.update(health_monitor.check_health())
|
|
559
|
+
|
|
560
|
+
return health
|
|
561
|
+
|
|
562
|
+
# Hierarchy Management Handlers
|
|
563
|
+
|
|
564
|
+
async def _handle_epic_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
565
|
+
"""Handle epic creation."""
|
|
566
|
+
# Check queue health before proceeding
|
|
567
|
+
health_monitor = QueueHealthMonitor()
|
|
568
|
+
health = health_monitor.check_health()
|
|
569
|
+
|
|
570
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
571
|
+
repair_result = health_monitor.auto_repair()
|
|
572
|
+
health = health_monitor.check_health()
|
|
573
|
+
|
|
574
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
575
|
+
critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
|
|
576
|
+
return {
|
|
577
|
+
"status": "error",
|
|
578
|
+
"error": "Queue system is in critical state",
|
|
579
|
+
"details": {
|
|
580
|
+
"health_status": health["status"],
|
|
581
|
+
"critical_issues": critical_alerts,
|
|
582
|
+
"repair_attempted": repair_result["actions_taken"]
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# Queue the epic creation
|
|
587
|
+
queue = Queue()
|
|
588
|
+
epic_data = {
|
|
589
|
+
"title": params["title"],
|
|
590
|
+
"description": params.get("description"),
|
|
591
|
+
"child_issues": params.get("child_issues", []),
|
|
592
|
+
"target_date": params.get("target_date"),
|
|
593
|
+
"lead_id": params.get("lead_id"),
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
queue_id = queue.add(
|
|
597
|
+
ticket_data=epic_data,
|
|
598
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
599
|
+
operation="create_epic",
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Start worker if needed
|
|
603
|
+
manager = WorkerManager()
|
|
604
|
+
worker_started = manager.start_if_needed()
|
|
605
|
+
|
|
606
|
+
if not worker_started and queue.get_pending_count() > 0:
|
|
607
|
+
return {
|
|
608
|
+
"status": "error",
|
|
609
|
+
"error": "Failed to start worker process",
|
|
610
|
+
"queue_id": queue_id,
|
|
611
|
+
"details": {
|
|
612
|
+
"pending_count": queue.get_pending_count(),
|
|
613
|
+
"action": "Worker process could not be started to process queued operations"
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
"queue_id": queue_id,
|
|
619
|
+
"status": "queued",
|
|
620
|
+
"message": f"Epic creation queued with ID: {queue_id}",
|
|
621
|
+
"epic_data": epic_data
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async def _handle_epic_list(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
625
|
+
"""Handle epic listing."""
|
|
626
|
+
epics = await self.adapter.list_epics(
|
|
627
|
+
limit=params.get("limit", 10),
|
|
628
|
+
offset=params.get("offset", 0),
|
|
629
|
+
**{k: v for k, v in params.items() if k not in ["limit", "offset"]}
|
|
630
|
+
)
|
|
631
|
+
return [epic.model_dump() for epic in epics]
|
|
632
|
+
|
|
633
|
+
async def _handle_epic_issues(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
634
|
+
"""Handle listing issues in an epic."""
|
|
635
|
+
epic_id = params["epic_id"]
|
|
636
|
+
issues = await self.adapter.list_issues_by_epic(epic_id)
|
|
637
|
+
return [issue.model_dump() for issue in issues]
|
|
638
|
+
|
|
639
|
+
async def _handle_issue_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
640
|
+
"""Handle issue creation."""
|
|
641
|
+
# Check queue health
|
|
642
|
+
health_monitor = QueueHealthMonitor()
|
|
643
|
+
health = health_monitor.check_health()
|
|
644
|
+
|
|
645
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
646
|
+
repair_result = health_monitor.auto_repair()
|
|
647
|
+
health = health_monitor.check_health()
|
|
648
|
+
|
|
649
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
650
|
+
critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
|
|
651
|
+
return {
|
|
652
|
+
"status": "error",
|
|
653
|
+
"error": "Queue system is in critical state",
|
|
654
|
+
"details": {
|
|
655
|
+
"health_status": health["status"],
|
|
656
|
+
"critical_issues": critical_alerts,
|
|
657
|
+
"repair_attempted": repair_result["actions_taken"]
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
# Queue the issue creation
|
|
662
|
+
queue = Queue()
|
|
663
|
+
issue_data = {
|
|
664
|
+
"title": params["title"],
|
|
665
|
+
"description": params.get("description"),
|
|
666
|
+
"epic_id": params.get("epic_id"),
|
|
667
|
+
"priority": params.get("priority", "medium"),
|
|
668
|
+
"assignee": params.get("assignee"),
|
|
669
|
+
"tags": params.get("tags", []),
|
|
670
|
+
"estimated_hours": params.get("estimated_hours"),
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
queue_id = queue.add(
|
|
674
|
+
ticket_data=issue_data,
|
|
675
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
676
|
+
operation="create_issue",
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Start worker if needed
|
|
680
|
+
manager = WorkerManager()
|
|
681
|
+
worker_started = manager.start_if_needed()
|
|
682
|
+
|
|
683
|
+
if not worker_started and queue.get_pending_count() > 0:
|
|
684
|
+
return {
|
|
685
|
+
"status": "error",
|
|
686
|
+
"error": "Failed to start worker process",
|
|
687
|
+
"queue_id": queue_id,
|
|
688
|
+
"details": {
|
|
689
|
+
"pending_count": queue.get_pending_count(),
|
|
690
|
+
"action": "Worker process could not be started to process queued operations"
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
"queue_id": queue_id,
|
|
696
|
+
"status": "queued",
|
|
697
|
+
"message": f"Issue creation queued with ID: {queue_id}",
|
|
698
|
+
"issue_data": issue_data
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async def _handle_issue_tasks(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
702
|
+
"""Handle listing tasks in an issue."""
|
|
703
|
+
issue_id = params["issue_id"]
|
|
704
|
+
tasks = await self.adapter.list_tasks_by_issue(issue_id)
|
|
705
|
+
return [task.model_dump() for task in tasks]
|
|
706
|
+
|
|
707
|
+
async def _handle_task_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
708
|
+
"""Handle task creation."""
|
|
709
|
+
# Check queue health
|
|
710
|
+
health_monitor = QueueHealthMonitor()
|
|
711
|
+
health = health_monitor.check_health()
|
|
712
|
+
|
|
713
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
714
|
+
repair_result = health_monitor.auto_repair()
|
|
715
|
+
health = health_monitor.check_health()
|
|
716
|
+
|
|
717
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
718
|
+
critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
|
|
719
|
+
return {
|
|
720
|
+
"status": "error",
|
|
721
|
+
"error": "Queue system is in critical state",
|
|
722
|
+
"details": {
|
|
723
|
+
"health_status": health["status"],
|
|
724
|
+
"critical_issues": critical_alerts,
|
|
725
|
+
"repair_attempted": repair_result["actions_taken"]
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
# Validate required parent_id
|
|
730
|
+
if not params.get("parent_id"):
|
|
731
|
+
return {
|
|
732
|
+
"status": "error",
|
|
733
|
+
"error": "Tasks must have a parent_id (issue identifier)",
|
|
734
|
+
"details": {"required_field": "parent_id"}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
# Queue the task creation
|
|
738
|
+
queue = Queue()
|
|
739
|
+
task_data = {
|
|
740
|
+
"title": params["title"],
|
|
741
|
+
"parent_id": params["parent_id"],
|
|
742
|
+
"description": params.get("description"),
|
|
743
|
+
"priority": params.get("priority", "medium"),
|
|
744
|
+
"assignee": params.get("assignee"),
|
|
745
|
+
"tags": params.get("tags", []),
|
|
746
|
+
"estimated_hours": params.get("estimated_hours"),
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
queue_id = queue.add(
|
|
750
|
+
ticket_data=task_data,
|
|
751
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
752
|
+
operation="create_task",
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# Start worker if needed
|
|
756
|
+
manager = WorkerManager()
|
|
757
|
+
worker_started = manager.start_if_needed()
|
|
758
|
+
|
|
759
|
+
if not worker_started and queue.get_pending_count() > 0:
|
|
760
|
+
return {
|
|
761
|
+
"status": "error",
|
|
762
|
+
"error": "Failed to start worker process",
|
|
763
|
+
"queue_id": queue_id,
|
|
764
|
+
"details": {
|
|
765
|
+
"pending_count": queue.get_pending_count(),
|
|
766
|
+
"action": "Worker process could not be started to process queued operations"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
"queue_id": queue_id,
|
|
772
|
+
"status": "queued",
|
|
773
|
+
"message": f"Task creation queued with ID: {queue_id}",
|
|
774
|
+
"task_data": task_data
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async def _handle_hierarchy_tree(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
778
|
+
"""Handle hierarchy tree visualization."""
|
|
779
|
+
epic_id = params.get("epic_id")
|
|
780
|
+
max_depth = params.get("max_depth", 3)
|
|
781
|
+
|
|
782
|
+
if epic_id:
|
|
783
|
+
# Get specific epic tree
|
|
784
|
+
epic = await self.adapter.get_epic(epic_id)
|
|
785
|
+
if not epic:
|
|
786
|
+
return {"error": f"Epic {epic_id} not found"}
|
|
787
|
+
|
|
788
|
+
# Build tree structure
|
|
789
|
+
tree = {
|
|
790
|
+
"epic": epic.model_dump(),
|
|
791
|
+
"issues": []
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
# Get issues in epic
|
|
795
|
+
issues = await self.adapter.list_issues_by_epic(epic_id)
|
|
796
|
+
for issue in issues:
|
|
797
|
+
issue_node = {
|
|
798
|
+
"issue": issue.model_dump(),
|
|
799
|
+
"tasks": []
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
# Get tasks in issue if depth allows
|
|
803
|
+
if max_depth > 2:
|
|
804
|
+
tasks = await self.adapter.list_tasks_by_issue(issue.id)
|
|
805
|
+
issue_node["tasks"] = [task.model_dump() for task in tasks]
|
|
806
|
+
|
|
807
|
+
tree["issues"].append(issue_node)
|
|
808
|
+
|
|
809
|
+
return tree
|
|
810
|
+
else:
|
|
811
|
+
# Get all epics with their hierarchies
|
|
812
|
+
epics = await self.adapter.list_epics(limit=params.get("limit", 10))
|
|
813
|
+
trees = []
|
|
814
|
+
|
|
815
|
+
for epic in epics:
|
|
816
|
+
tree = await self._handle_hierarchy_tree({"epic_id": epic.id, "max_depth": max_depth})
|
|
817
|
+
trees.append(tree)
|
|
818
|
+
|
|
819
|
+
return {"trees": trees}
|
|
820
|
+
|
|
821
|
+
async def _handle_bulk_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
822
|
+
"""Handle bulk ticket creation."""
|
|
823
|
+
tickets = params.get("tickets", [])
|
|
824
|
+
if not tickets:
|
|
825
|
+
return {"error": "No tickets provided for bulk creation"}
|
|
826
|
+
|
|
827
|
+
# Check queue health
|
|
828
|
+
health_monitor = QueueHealthMonitor()
|
|
829
|
+
health = health_monitor.check_health()
|
|
830
|
+
|
|
831
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
832
|
+
repair_result = health_monitor.auto_repair()
|
|
833
|
+
health = health_monitor.check_health()
|
|
834
|
+
|
|
835
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
836
|
+
return {
|
|
837
|
+
"status": "error",
|
|
838
|
+
"error": "Queue system is in critical state - cannot process bulk operations",
|
|
839
|
+
"details": {"health_status": health["status"]}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
# Queue all tickets
|
|
843
|
+
queue = Queue()
|
|
844
|
+
queue_ids = []
|
|
845
|
+
|
|
846
|
+
for i, ticket_data in enumerate(tickets):
|
|
847
|
+
if not ticket_data.get("title"):
|
|
848
|
+
return {
|
|
849
|
+
"status": "error",
|
|
850
|
+
"error": f"Ticket {i} missing required 'title' field"
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
queue_id = queue.add(
|
|
854
|
+
ticket_data=ticket_data,
|
|
855
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
856
|
+
operation=ticket_data.get("operation", "create"),
|
|
857
|
+
)
|
|
858
|
+
queue_ids.append(queue_id)
|
|
859
|
+
|
|
860
|
+
# Start worker if needed
|
|
861
|
+
manager = WorkerManager()
|
|
862
|
+
manager.start_if_needed()
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
"queue_ids": queue_ids,
|
|
866
|
+
"status": "queued",
|
|
867
|
+
"message": f"Bulk creation of {len(tickets)} tickets queued",
|
|
868
|
+
"count": len(tickets)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async def _handle_bulk_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
872
|
+
"""Handle bulk ticket updates."""
|
|
873
|
+
updates = params.get("updates", [])
|
|
874
|
+
if not updates:
|
|
875
|
+
return {"error": "No updates provided for bulk operation"}
|
|
876
|
+
|
|
877
|
+
# Check queue health
|
|
878
|
+
health_monitor = QueueHealthMonitor()
|
|
879
|
+
health = health_monitor.check_health()
|
|
880
|
+
|
|
881
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
882
|
+
repair_result = health_monitor.auto_repair()
|
|
883
|
+
health = health_monitor.check_health()
|
|
884
|
+
|
|
885
|
+
if health["status"] == HealthStatus.CRITICAL:
|
|
886
|
+
return {
|
|
887
|
+
"status": "error",
|
|
888
|
+
"error": "Queue system is in critical state - cannot process bulk operations",
|
|
889
|
+
"details": {"health_status": health["status"]}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
# Queue all updates
|
|
893
|
+
queue = Queue()
|
|
894
|
+
queue_ids = []
|
|
895
|
+
|
|
896
|
+
for i, update_data in enumerate(updates):
|
|
897
|
+
if not update_data.get("ticket_id"):
|
|
898
|
+
return {
|
|
899
|
+
"status": "error",
|
|
900
|
+
"error": f"Update {i} missing required 'ticket_id' field"
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
queue_id = queue.add(
|
|
904
|
+
ticket_data=update_data,
|
|
905
|
+
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
906
|
+
operation="update",
|
|
907
|
+
)
|
|
908
|
+
queue_ids.append(queue_id)
|
|
909
|
+
|
|
910
|
+
# Start worker if needed
|
|
911
|
+
manager = WorkerManager()
|
|
912
|
+
manager.start_if_needed()
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
"queue_ids": queue_ids,
|
|
916
|
+
"status": "queued",
|
|
917
|
+
"message": f"Bulk update of {len(updates)} tickets queued",
|
|
918
|
+
"count": len(updates)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async def _handle_search_hierarchy(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
922
|
+
"""Handle hierarchy-aware search."""
|
|
923
|
+
query = params.get("query", "")
|
|
924
|
+
include_children = params.get("include_children", True)
|
|
925
|
+
include_parents = params.get("include_parents", True)
|
|
926
|
+
|
|
927
|
+
# Perform basic search
|
|
928
|
+
search_query = SearchQuery(
|
|
929
|
+
query=query,
|
|
930
|
+
state=params.get("state"),
|
|
931
|
+
priority=params.get("priority"),
|
|
932
|
+
limit=params.get("limit", 50)
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
tickets = await self.adapter.search(search_query)
|
|
936
|
+
|
|
937
|
+
# Enhance with hierarchy information
|
|
938
|
+
enhanced_results = []
|
|
939
|
+
for ticket in tickets:
|
|
940
|
+
result = {
|
|
941
|
+
"ticket": ticket.model_dump(),
|
|
942
|
+
"hierarchy": {}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
# Add parent information
|
|
946
|
+
if include_parents:
|
|
947
|
+
if hasattr(ticket, 'parent_epic') and ticket.parent_epic:
|
|
948
|
+
parent_epic = await self.adapter.get_epic(ticket.parent_epic)
|
|
949
|
+
if parent_epic:
|
|
950
|
+
result["hierarchy"]["epic"] = parent_epic.model_dump()
|
|
951
|
+
|
|
952
|
+
if hasattr(ticket, 'parent_issue') and ticket.parent_issue:
|
|
953
|
+
parent_issue = await self.adapter.read(ticket.parent_issue)
|
|
954
|
+
if parent_issue:
|
|
955
|
+
result["hierarchy"]["parent_issue"] = parent_issue.model_dump()
|
|
956
|
+
|
|
957
|
+
# Add children information
|
|
958
|
+
if include_children:
|
|
959
|
+
if ticket.ticket_type == "epic":
|
|
960
|
+
issues = await self.adapter.list_issues_by_epic(ticket.id)
|
|
961
|
+
result["hierarchy"]["issues"] = [issue.model_dump() for issue in issues]
|
|
962
|
+
elif ticket.ticket_type == "issue":
|
|
963
|
+
tasks = await self.adapter.list_tasks_by_issue(ticket.id)
|
|
964
|
+
result["hierarchy"]["tasks"] = [task.model_dump() for task in tasks]
|
|
965
|
+
|
|
966
|
+
enhanced_results.append(result)
|
|
967
|
+
|
|
968
|
+
return {
|
|
969
|
+
"results": enhanced_results,
|
|
970
|
+
"count": len(enhanced_results),
|
|
971
|
+
"query": query
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async def _handle_attach(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
975
|
+
"""Handle file attachment to ticket."""
|
|
976
|
+
# Note: This is a placeholder for attachment functionality
|
|
977
|
+
# Most adapters don't support file attachments directly
|
|
978
|
+
return {
|
|
979
|
+
"status": "not_implemented",
|
|
980
|
+
"error": "Attachment functionality not yet implemented",
|
|
981
|
+
"ticket_id": params.get("ticket_id"),
|
|
982
|
+
"details": {
|
|
983
|
+
"reason": "File attachments require adapter-specific implementation",
|
|
984
|
+
"alternatives": ["Add file URLs in comments", "Use external file storage"]
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async def _handle_list_attachments(self, params: dict[str, Any]) -> list[dict[str, Any]]:
|
|
989
|
+
"""Handle listing ticket attachments."""
|
|
990
|
+
# Note: This is a placeholder for attachment functionality
|
|
991
|
+
return []
|
|
992
|
+
|
|
478
993
|
async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
479
994
|
"""Handle PR creation for a ticket."""
|
|
480
995
|
ticket_id = params.get("ticket_id")
|
|
@@ -637,6 +1152,172 @@ class MCPTicketServer:
|
|
|
637
1152
|
"""List available MCP tools."""
|
|
638
1153
|
return {
|
|
639
1154
|
"tools": [
|
|
1155
|
+
# Hierarchy Management Tools
|
|
1156
|
+
{
|
|
1157
|
+
"name": "epic_create",
|
|
1158
|
+
"description": "Create a new epic (top-level project/milestone)",
|
|
1159
|
+
"inputSchema": {
|
|
1160
|
+
"type": "object",
|
|
1161
|
+
"properties": {
|
|
1162
|
+
"title": {"type": "string", "description": "Epic title"},
|
|
1163
|
+
"description": {"type": "string", "description": "Epic description"},
|
|
1164
|
+
"target_date": {"type": "string", "description": "Target completion date (ISO format)"},
|
|
1165
|
+
"lead_id": {"type": "string", "description": "Epic lead/owner ID"},
|
|
1166
|
+
"child_issues": {"type": "array", "items": {"type": "string"}, "description": "Initial child issue IDs"}
|
|
1167
|
+
},
|
|
1168
|
+
"required": ["title"]
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
"name": "epic_list",
|
|
1173
|
+
"description": "List all epics",
|
|
1174
|
+
"inputSchema": {
|
|
1175
|
+
"type": "object",
|
|
1176
|
+
"properties": {
|
|
1177
|
+
"limit": {"type": "integer", "default": 10, "description": "Maximum number of epics to return"},
|
|
1178
|
+
"offset": {"type": "integer", "default": 0, "description": "Number of epics to skip"}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"name": "epic_issues",
|
|
1184
|
+
"description": "List all issues in an epic",
|
|
1185
|
+
"inputSchema": {
|
|
1186
|
+
"type": "object",
|
|
1187
|
+
"properties": {
|
|
1188
|
+
"epic_id": {"type": "string", "description": "Epic ID to get issues for"}
|
|
1189
|
+
},
|
|
1190
|
+
"required": ["epic_id"]
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"name": "issue_create",
|
|
1195
|
+
"description": "Create a new issue (work item)",
|
|
1196
|
+
"inputSchema": {
|
|
1197
|
+
"type": "object",
|
|
1198
|
+
"properties": {
|
|
1199
|
+
"title": {"type": "string", "description": "Issue title"},
|
|
1200
|
+
"description": {"type": "string", "description": "Issue description"},
|
|
1201
|
+
"epic_id": {"type": "string", "description": "Parent epic ID"},
|
|
1202
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"], "default": "medium"},
|
|
1203
|
+
"assignee": {"type": "string", "description": "Assignee username"},
|
|
1204
|
+
"tags": {"type": "array", "items": {"type": "string"}, "description": "Issue tags"},
|
|
1205
|
+
"estimated_hours": {"type": "number", "description": "Estimated hours to complete"}
|
|
1206
|
+
},
|
|
1207
|
+
"required": ["title"]
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
"name": "issue_tasks",
|
|
1212
|
+
"description": "List all tasks in an issue",
|
|
1213
|
+
"inputSchema": {
|
|
1214
|
+
"type": "object",
|
|
1215
|
+
"properties": {
|
|
1216
|
+
"issue_id": {"type": "string", "description": "Issue ID to get tasks for"}
|
|
1217
|
+
},
|
|
1218
|
+
"required": ["issue_id"]
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
"name": "task_create",
|
|
1223
|
+
"description": "Create a new task (sub-item under an issue)",
|
|
1224
|
+
"inputSchema": {
|
|
1225
|
+
"type": "object",
|
|
1226
|
+
"properties": {
|
|
1227
|
+
"title": {"type": "string", "description": "Task title"},
|
|
1228
|
+
"parent_id": {"type": "string", "description": "Parent issue ID (required)"},
|
|
1229
|
+
"description": {"type": "string", "description": "Task description"},
|
|
1230
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"], "default": "medium"},
|
|
1231
|
+
"assignee": {"type": "string", "description": "Assignee username"},
|
|
1232
|
+
"tags": {"type": "array", "items": {"type": "string"}, "description": "Task tags"},
|
|
1233
|
+
"estimated_hours": {"type": "number", "description": "Estimated hours to complete"}
|
|
1234
|
+
},
|
|
1235
|
+
"required": ["title", "parent_id"]
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"name": "hierarchy_tree",
|
|
1240
|
+
"description": "Get hierarchy tree view of epic/issues/tasks",
|
|
1241
|
+
"inputSchema": {
|
|
1242
|
+
"type": "object",
|
|
1243
|
+
"properties": {
|
|
1244
|
+
"epic_id": {"type": "string", "description": "Specific epic ID (optional - if not provided, returns all epics)"},
|
|
1245
|
+
"max_depth": {"type": "integer", "default": 3, "description": "Maximum depth to traverse (1=epics only, 2=epics+issues, 3=full tree)"},
|
|
1246
|
+
"limit": {"type": "integer", "default": 10, "description": "Maximum number of epics to return (when epic_id not specified)"}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
# Bulk Operations
|
|
1251
|
+
{
|
|
1252
|
+
"name": "ticket_bulk_create",
|
|
1253
|
+
"description": "Create multiple tickets in one operation",
|
|
1254
|
+
"inputSchema": {
|
|
1255
|
+
"type": "object",
|
|
1256
|
+
"properties": {
|
|
1257
|
+
"tickets": {
|
|
1258
|
+
"type": "array",
|
|
1259
|
+
"items": {
|
|
1260
|
+
"type": "object",
|
|
1261
|
+
"properties": {
|
|
1262
|
+
"title": {"type": "string"},
|
|
1263
|
+
"description": {"type": "string"},
|
|
1264
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
|
|
1265
|
+
"operation": {"type": "string", "enum": ["create", "create_epic", "create_issue", "create_task"], "default": "create"},
|
|
1266
|
+
"epic_id": {"type": "string", "description": "For issues"},
|
|
1267
|
+
"parent_id": {"type": "string", "description": "For tasks"}
|
|
1268
|
+
},
|
|
1269
|
+
"required": ["title"]
|
|
1270
|
+
},
|
|
1271
|
+
"description": "Array of tickets to create"
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
"required": ["tickets"]
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
"name": "ticket_bulk_update",
|
|
1279
|
+
"description": "Update multiple tickets in one operation",
|
|
1280
|
+
"inputSchema": {
|
|
1281
|
+
"type": "object",
|
|
1282
|
+
"properties": {
|
|
1283
|
+
"updates": {
|
|
1284
|
+
"type": "array",
|
|
1285
|
+
"items": {
|
|
1286
|
+
"type": "object",
|
|
1287
|
+
"properties": {
|
|
1288
|
+
"ticket_id": {"type": "string"},
|
|
1289
|
+
"title": {"type": "string"},
|
|
1290
|
+
"description": {"type": "string"},
|
|
1291
|
+
"priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
|
|
1292
|
+
"state": {"type": "string"},
|
|
1293
|
+
"assignee": {"type": "string"}
|
|
1294
|
+
},
|
|
1295
|
+
"required": ["ticket_id"]
|
|
1296
|
+
},
|
|
1297
|
+
"description": "Array of ticket updates"
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
"required": ["updates"]
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
# Advanced Search
|
|
1304
|
+
{
|
|
1305
|
+
"name": "ticket_search_hierarchy",
|
|
1306
|
+
"description": "Search tickets with hierarchy context",
|
|
1307
|
+
"inputSchema": {
|
|
1308
|
+
"type": "object",
|
|
1309
|
+
"properties": {
|
|
1310
|
+
"query": {"type": "string", "description": "Search query"},
|
|
1311
|
+
"state": {"type": "string", "description": "Filter by state"},
|
|
1312
|
+
"priority": {"type": "string", "description": "Filter by priority"},
|
|
1313
|
+
"limit": {"type": "integer", "default": 50, "description": "Maximum results"},
|
|
1314
|
+
"include_children": {"type": "boolean", "default": True, "description": "Include child items in results"},
|
|
1315
|
+
"include_parents": {"type": "boolean", "default": True, "description": "Include parent context in results"}
|
|
1316
|
+
},
|
|
1317
|
+
"required": ["query"]
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
# PR Integration
|
|
640
1321
|
{
|
|
641
1322
|
"name": "ticket_create_pr",
|
|
642
1323
|
"description": "Create a GitHub PR linked to a ticket",
|
|
@@ -673,6 +1354,7 @@ class MCPTicketServer:
|
|
|
673
1354
|
"required": ["ticket_id"],
|
|
674
1355
|
},
|
|
675
1356
|
},
|
|
1357
|
+
# Standard Ticket Operations
|
|
676
1358
|
{
|
|
677
1359
|
"name": "ticket_link_pr",
|
|
678
1360
|
"description": "Link an existing PR to a ticket",
|
|
@@ -796,7 +1478,31 @@ class MCPTicketServer:
|
|
|
796
1478
|
|
|
797
1479
|
try:
|
|
798
1480
|
# Route to appropriate handler based on tool name
|
|
799
|
-
|
|
1481
|
+
# Hierarchy management tools
|
|
1482
|
+
if tool_name == "epic_create":
|
|
1483
|
+
result = await self._handle_epic_create(arguments)
|
|
1484
|
+
elif tool_name == "epic_list":
|
|
1485
|
+
result = await self._handle_epic_list(arguments)
|
|
1486
|
+
elif tool_name == "epic_issues":
|
|
1487
|
+
result = await self._handle_epic_issues(arguments)
|
|
1488
|
+
elif tool_name == "issue_create":
|
|
1489
|
+
result = await self._handle_issue_create(arguments)
|
|
1490
|
+
elif tool_name == "issue_tasks":
|
|
1491
|
+
result = await self._handle_issue_tasks(arguments)
|
|
1492
|
+
elif tool_name == "task_create":
|
|
1493
|
+
result = await self._handle_task_create(arguments)
|
|
1494
|
+
elif tool_name == "hierarchy_tree":
|
|
1495
|
+
result = await self._handle_hierarchy_tree(arguments)
|
|
1496
|
+
# Bulk operations
|
|
1497
|
+
elif tool_name == "ticket_bulk_create":
|
|
1498
|
+
result = await self._handle_bulk_create(arguments)
|
|
1499
|
+
elif tool_name == "ticket_bulk_update":
|
|
1500
|
+
result = await self._handle_bulk_update(arguments)
|
|
1501
|
+
# Advanced search
|
|
1502
|
+
elif tool_name == "ticket_search_hierarchy":
|
|
1503
|
+
result = await self._handle_search_hierarchy(arguments)
|
|
1504
|
+
# Standard ticket operations
|
|
1505
|
+
elif tool_name == "ticket_create":
|
|
800
1506
|
result = await self._handle_create(arguments)
|
|
801
1507
|
elif tool_name == "ticket_list":
|
|
802
1508
|
result = await self._handle_list(arguments)
|
|
@@ -808,6 +1514,7 @@ class MCPTicketServer:
|
|
|
808
1514
|
result = await self._handle_search(arguments)
|
|
809
1515
|
elif tool_name == "ticket_status":
|
|
810
1516
|
result = await self._handle_queue_status(arguments)
|
|
1517
|
+
# PR integration
|
|
811
1518
|
elif tool_name == "ticket_create_pr":
|
|
812
1519
|
result = await self._handle_create_pr(arguments)
|
|
813
1520
|
elif tool_name == "ticket_link_pr":
|