mcp-ticketer 0.1.26__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 +707 -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.26.dist-info → mcp_ticketer-0.1.27.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.26.dist-info → mcp_ticketer-0.1.27.dist-info}/RECORD +14 -12
- {mcp_ticketer-0.1.26.dist-info → mcp_ticketer-0.1.27.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.26.dist-info → mcp_ticketer-0.1.27.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.26.dist-info → mcp_ticketer-0.1.27.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.26.dist-info → mcp_ticketer-0.1.27.dist-info}/top_level.txt +0 -0
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -11,6 +11,7 @@ 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
|
|
14
15
|
|
|
15
16
|
# Import adapters module to trigger registration
|
|
16
17
|
import mcp_ticketer.adapters # noqa: F401
|
|
@@ -92,6 +93,36 @@ class MCPTicketServer:
|
|
|
92
93
|
result = await self._handle_create_pr(params)
|
|
93
94
|
elif method == "ticket/link_pr":
|
|
94
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)
|
|
95
126
|
elif method == "tools/list":
|
|
96
127
|
result = await self._handle_tools_list()
|
|
97
128
|
elif method == "tools/call":
|
|
@@ -128,7 +159,30 @@ class MCPTicketServer:
|
|
|
128
159
|
|
|
129
160
|
async def _handle_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
130
161
|
"""Handle ticket creation."""
|
|
131
|
-
#
|
|
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
|
|
132
186
|
queue = Queue()
|
|
133
187
|
task_data = {
|
|
134
188
|
"title": params["title"],
|
|
@@ -146,7 +200,19 @@ class MCPTicketServer:
|
|
|
146
200
|
|
|
147
201
|
# Start worker if needed
|
|
148
202
|
manager = WorkerManager()
|
|
149
|
-
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
|
+
}
|
|
150
216
|
|
|
151
217
|
# Check if async mode is requested (for backward compatibility)
|
|
152
218
|
if params.get("async_mode", False):
|
|
@@ -478,6 +544,452 @@ class MCPTicketServer:
|
|
|
478
544
|
|
|
479
545
|
return response
|
|
480
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
|
+
|
|
481
993
|
async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
482
994
|
"""Handle PR creation for a ticket."""
|
|
483
995
|
ticket_id = params.get("ticket_id")
|
|
@@ -640,6 +1152,172 @@ class MCPTicketServer:
|
|
|
640
1152
|
"""List available MCP tools."""
|
|
641
1153
|
return {
|
|
642
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
|
|
643
1321
|
{
|
|
644
1322
|
"name": "ticket_create_pr",
|
|
645
1323
|
"description": "Create a GitHub PR linked to a ticket",
|
|
@@ -676,6 +1354,7 @@ class MCPTicketServer:
|
|
|
676
1354
|
"required": ["ticket_id"],
|
|
677
1355
|
},
|
|
678
1356
|
},
|
|
1357
|
+
# Standard Ticket Operations
|
|
679
1358
|
{
|
|
680
1359
|
"name": "ticket_link_pr",
|
|
681
1360
|
"description": "Link an existing PR to a ticket",
|
|
@@ -799,7 +1478,31 @@ class MCPTicketServer:
|
|
|
799
1478
|
|
|
800
1479
|
try:
|
|
801
1480
|
# Route to appropriate handler based on tool name
|
|
802
|
-
|
|
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":
|
|
803
1506
|
result = await self._handle_create(arguments)
|
|
804
1507
|
elif tool_name == "ticket_list":
|
|
805
1508
|
result = await self._handle_list(arguments)
|
|
@@ -811,6 +1514,7 @@ class MCPTicketServer:
|
|
|
811
1514
|
result = await self._handle_search(arguments)
|
|
812
1515
|
elif tool_name == "ticket_status":
|
|
813
1516
|
result = await self._handle_queue_status(arguments)
|
|
1517
|
+
# PR integration
|
|
814
1518
|
elif tool_name == "ticket_create_pr":
|
|
815
1519
|
result = await self._handle_create_pr(arguments)
|
|
816
1520
|
elif tool_name == "ticket_link_pr":
|