mcp-ticketer 0.3.2__py3-none-any.whl → 0.3.3__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/aitrackdown.py +152 -21
- mcp_ticketer/adapters/github.py +4 -4
- mcp_ticketer/adapters/jira.py +6 -6
- mcp_ticketer/adapters/linear/adapter.py +18 -16
- mcp_ticketer/adapters/linear/client.py +7 -7
- mcp_ticketer/adapters/linear/mappers.py +7 -7
- mcp_ticketer/adapters/linear/types.py +10 -10
- mcp_ticketer/cli/adapter_diagnostics.py +2 -2
- mcp_ticketer/cli/codex_configure.py +6 -6
- mcp_ticketer/cli/diagnostics.py +17 -18
- mcp_ticketer/cli/simple_health.py +5 -10
- mcp_ticketer/core/env_loader.py +13 -13
- mcp_ticketer/core/exceptions.py +5 -5
- mcp_ticketer/mcp/constants.py +58 -0
- mcp_ticketer/mcp/dto.py +195 -0
- mcp_ticketer/mcp/response_builder.py +206 -0
- mcp_ticketer/mcp/server.py +311 -1287
- mcp_ticketer/queue/health_monitor.py +14 -14
- mcp_ticketer/queue/manager.py +59 -15
- mcp_ticketer/queue/queue.py +9 -2
- mcp_ticketer/queue/ticket_registry.py +15 -15
- mcp_ticketer/queue/worker.py +25 -18
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/RECORD +29 -26
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.2.dist-info → mcp_ticketer-0.3.3.dist-info}/top_level.txt +0 -0
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""MCP JSON-RPC server for ticket management."""
|
|
1
|
+
"""MCP JSON-RPC server for ticket management - Simplified synchronous implementation."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
@@ -12,9 +12,41 @@ from dotenv import load_dotenv
|
|
|
12
12
|
import mcp_ticketer.adapters # noqa: F401
|
|
13
13
|
|
|
14
14
|
from ..core import AdapterRegistry
|
|
15
|
-
from ..core.models import SearchQuery
|
|
16
|
-
from
|
|
17
|
-
|
|
15
|
+
from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
|
|
16
|
+
from .constants import (
|
|
17
|
+
DEFAULT_BASE_PATH,
|
|
18
|
+
DEFAULT_LIMIT,
|
|
19
|
+
DEFAULT_MAX_DEPTH,
|
|
20
|
+
DEFAULT_OFFSET,
|
|
21
|
+
ERROR_INTERNAL,
|
|
22
|
+
ERROR_METHOD_NOT_FOUND,
|
|
23
|
+
ERROR_PARSE,
|
|
24
|
+
JSONRPC_VERSION,
|
|
25
|
+
MCP_PROTOCOL_VERSION,
|
|
26
|
+
MSG_EPIC_NOT_FOUND,
|
|
27
|
+
MSG_INTERNAL_ERROR,
|
|
28
|
+
MSG_MISSING_TICKET_ID,
|
|
29
|
+
MSG_MISSING_TITLE,
|
|
30
|
+
MSG_NO_TICKETS_PROVIDED,
|
|
31
|
+
MSG_NO_UPDATES_PROVIDED,
|
|
32
|
+
MSG_TICKET_NOT_FOUND,
|
|
33
|
+
MSG_TRANSITION_FAILED,
|
|
34
|
+
MSG_UNKNOWN_METHOD,
|
|
35
|
+
MSG_UNKNOWN_OPERATION,
|
|
36
|
+
MSG_UPDATE_FAILED,
|
|
37
|
+
SERVER_NAME,
|
|
38
|
+
SERVER_VERSION,
|
|
39
|
+
STATUS_COMPLETED,
|
|
40
|
+
STATUS_ERROR,
|
|
41
|
+
)
|
|
42
|
+
from .dto import (
|
|
43
|
+
CreateEpicRequest,
|
|
44
|
+
CreateIssueRequest,
|
|
45
|
+
CreateTaskRequest,
|
|
46
|
+
CreateTicketRequest,
|
|
47
|
+
ReadTicketRequest,
|
|
48
|
+
)
|
|
49
|
+
from .response_builder import ResponseBuilder
|
|
18
50
|
|
|
19
51
|
# Load environment variables early (prioritize .env.local)
|
|
20
52
|
# Check for .env.local first (takes precedence)
|
|
@@ -35,7 +67,7 @@ else:
|
|
|
35
67
|
|
|
36
68
|
|
|
37
69
|
class MCPTicketServer:
|
|
38
|
-
"""MCP server for ticket operations over stdio."""
|
|
70
|
+
"""MCP server for ticket operations over stdio - synchronous implementation."""
|
|
39
71
|
|
|
40
72
|
def __init__(
|
|
41
73
|
self, adapter_type: str = "aitrackdown", config: Optional[dict[str, Any]] = None
|
|
@@ -47,9 +79,9 @@ class MCPTicketServer:
|
|
|
47
79
|
config: Adapter configuration
|
|
48
80
|
|
|
49
81
|
"""
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
)
|
|
82
|
+
self.adapter_type = adapter_type
|
|
83
|
+
self.adapter_config = config or {"base_path": DEFAULT_BASE_PATH}
|
|
84
|
+
self.adapter = AdapterRegistry.get_adapter(adapter_type, self.adapter_config)
|
|
53
85
|
self.running = False
|
|
54
86
|
|
|
55
87
|
async def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -87,14 +119,10 @@ class MCPTicketServer:
|
|
|
87
119
|
result = await self._handle_transition(params)
|
|
88
120
|
elif method == "ticket/comment":
|
|
89
121
|
result = await self._handle_comment(params)
|
|
90
|
-
elif method == "ticket/status":
|
|
91
|
-
result = await self._handle_queue_status(params)
|
|
92
122
|
elif method == "ticket/create_pr":
|
|
93
123
|
result = await self._handle_create_pr(params)
|
|
94
124
|
elif method == "ticket/link_pr":
|
|
95
125
|
result = await self._handle_link_pr(params)
|
|
96
|
-
elif method == "queue/health":
|
|
97
|
-
result = await self._handle_queue_health(params)
|
|
98
126
|
# Hierarchy management tools
|
|
99
127
|
elif method == "epic/create":
|
|
100
128
|
result = await self._handle_epic_create(params)
|
|
@@ -128,14 +156,18 @@ class MCPTicketServer:
|
|
|
128
156
|
elif method == "tools/call":
|
|
129
157
|
result = await self._handle_tools_call(params)
|
|
130
158
|
else:
|
|
131
|
-
return
|
|
132
|
-
request_id,
|
|
159
|
+
return ResponseBuilder.error(
|
|
160
|
+
request_id,
|
|
161
|
+
ERROR_METHOD_NOT_FOUND,
|
|
162
|
+
MSG_UNKNOWN_METHOD.format(method=method),
|
|
133
163
|
)
|
|
134
164
|
|
|
135
|
-
return {"jsonrpc":
|
|
165
|
+
return {"jsonrpc": JSONRPC_VERSION, "result": result, "id": request_id}
|
|
136
166
|
|
|
137
167
|
except Exception as e:
|
|
138
|
-
return
|
|
168
|
+
return ResponseBuilder.error(
|
|
169
|
+
request_id, ERROR_INTERNAL, MSG_INTERNAL_ERROR.format(error=str(e))
|
|
170
|
+
)
|
|
139
171
|
|
|
140
172
|
def _error_response(
|
|
141
173
|
self, request_id: Any, code: int, message: str
|
|
@@ -158,663 +190,291 @@ class MCPTicketServer:
|
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
async def _handle_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
161
|
-
"""Handle
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# If still critical, return error immediately
|
|
173
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
174
|
-
critical_alerts = [
|
|
175
|
-
alert for alert in health["alerts"] if alert["level"] == "critical"
|
|
176
|
-
]
|
|
177
|
-
return {
|
|
178
|
-
"status": "error",
|
|
179
|
-
"error": "Queue system is in critical state",
|
|
180
|
-
"details": {
|
|
181
|
-
"health_status": health["status"],
|
|
182
|
-
"critical_issues": critical_alerts,
|
|
183
|
-
"repair_attempted": repair_result["actions_taken"],
|
|
184
|
-
},
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
# Queue the operation
|
|
188
|
-
queue = Queue()
|
|
189
|
-
task_data = {
|
|
190
|
-
"title": params["title"],
|
|
191
|
-
"description": params.get("description"),
|
|
192
|
-
"priority": params.get("priority", "medium"),
|
|
193
|
-
"tags": params.get("tags", []),
|
|
194
|
-
"assignee": params.get("assignee"),
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
queue_id = queue.add(
|
|
198
|
-
ticket_data=task_data,
|
|
199
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
200
|
-
operation="create",
|
|
193
|
+
"""Handle task creation - SYNCHRONOUS with validation."""
|
|
194
|
+
# Validate and parse request
|
|
195
|
+
request = CreateTicketRequest(**params)
|
|
196
|
+
|
|
197
|
+
# Build task from validated DTO
|
|
198
|
+
task = Task(
|
|
199
|
+
title=request.title,
|
|
200
|
+
description=request.description,
|
|
201
|
+
priority=Priority(request.priority),
|
|
202
|
+
tags=request.tags,
|
|
203
|
+
assignee=request.assignee,
|
|
201
204
|
)
|
|
202
205
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
worker_started = manager.start_if_needed()
|
|
206
|
-
|
|
207
|
-
# If worker failed to start and we have pending items, that's critical
|
|
208
|
-
if not worker_started and queue.get_pending_count() > 0:
|
|
209
|
-
return {
|
|
210
|
-
"status": "error",
|
|
211
|
-
"error": "Failed to start worker process",
|
|
212
|
-
"queue_id": queue_id,
|
|
213
|
-
"details": {
|
|
214
|
-
"pending_count": queue.get_pending_count(),
|
|
215
|
-
"action": "Worker process could not be started to process queued operations",
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
# Check if async mode is requested (for backward compatibility)
|
|
220
|
-
if params.get("async_mode", False):
|
|
221
|
-
return {
|
|
222
|
-
"queue_id": queue_id,
|
|
223
|
-
"status": "queued",
|
|
224
|
-
"message": f"Ticket creation queued with ID: {queue_id}",
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
# Poll for completion with timeout (default synchronous behavior)
|
|
228
|
-
max_wait_time = params.get("timeout", 30) # seconds, allow override
|
|
229
|
-
poll_interval = 0.5 # seconds
|
|
230
|
-
start_time = asyncio.get_event_loop().time()
|
|
231
|
-
|
|
232
|
-
while True:
|
|
233
|
-
# Check queue status
|
|
234
|
-
item = queue.get_item(queue_id)
|
|
235
|
-
|
|
236
|
-
if not item:
|
|
237
|
-
return {
|
|
238
|
-
"queue_id": queue_id,
|
|
239
|
-
"status": "error",
|
|
240
|
-
"error": f"Queue item {queue_id} not found",
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
# If completed, return with ticket ID
|
|
244
|
-
if item.status == QueueStatus.COMPLETED:
|
|
245
|
-
response = {
|
|
246
|
-
"queue_id": queue_id,
|
|
247
|
-
"status": "completed",
|
|
248
|
-
"title": params["title"],
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
# Add ticket ID and other result data if available
|
|
252
|
-
if item.result:
|
|
253
|
-
response["ticket_id"] = item.result.get("id")
|
|
254
|
-
if "state" in item.result:
|
|
255
|
-
response["state"] = item.result["state"]
|
|
256
|
-
# Try to construct URL if we have enough information
|
|
257
|
-
if response.get("ticket_id"):
|
|
258
|
-
# This is adapter-specific, but we can add URL generation later
|
|
259
|
-
response["id"] = response[
|
|
260
|
-
"ticket_id"
|
|
261
|
-
] # Also include as "id" for compatibility
|
|
262
|
-
|
|
263
|
-
response["message"] = (
|
|
264
|
-
f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
|
|
265
|
-
)
|
|
266
|
-
return response
|
|
267
|
-
|
|
268
|
-
# If failed, return error
|
|
269
|
-
if item.status == QueueStatus.FAILED:
|
|
270
|
-
return {
|
|
271
|
-
"queue_id": queue_id,
|
|
272
|
-
"status": "failed",
|
|
273
|
-
"error": item.error_message or "Ticket creation failed",
|
|
274
|
-
"title": params["title"],
|
|
275
|
-
}
|
|
206
|
+
# Create directly
|
|
207
|
+
created = await self.adapter.create(task)
|
|
276
208
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
"queue_id": queue_id,
|
|
282
|
-
"status": "timeout",
|
|
283
|
-
"message": f"Ticket creation timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
284
|
-
"title": params["title"],
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
# Wait before next poll
|
|
288
|
-
await asyncio.sleep(poll_interval)
|
|
289
|
-
|
|
290
|
-
async def _handle_read(self, params: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
291
|
-
"""Handle ticket read."""
|
|
292
|
-
ticket = await self.adapter.read(params["ticket_id"])
|
|
293
|
-
return ticket.model_dump() if ticket else None
|
|
294
|
-
|
|
295
|
-
async def _handle_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
296
|
-
"""Handle ticket update."""
|
|
297
|
-
# Queue the operation
|
|
298
|
-
queue = Queue()
|
|
299
|
-
updates = params.get("updates", {})
|
|
300
|
-
updates["ticket_id"] = params["ticket_id"]
|
|
301
|
-
|
|
302
|
-
queue_id = queue.add(
|
|
303
|
-
ticket_data=updates,
|
|
304
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
305
|
-
operation="update",
|
|
209
|
+
# Return immediately
|
|
210
|
+
return ResponseBuilder.status_result(
|
|
211
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
306
212
|
)
|
|
307
213
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
# Poll for completion with timeout
|
|
313
|
-
max_wait_time = 30 # seconds
|
|
314
|
-
poll_interval = 0.5 # seconds
|
|
315
|
-
start_time = asyncio.get_event_loop().time()
|
|
214
|
+
async def _handle_read(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
215
|
+
"""Handle ticket read - SYNCHRONOUS with validation."""
|
|
216
|
+
# Validate and parse request
|
|
217
|
+
request = ReadTicketRequest(**params)
|
|
316
218
|
|
|
317
|
-
|
|
318
|
-
# Check queue status
|
|
319
|
-
item = queue.get_item(queue_id)
|
|
219
|
+
ticket = await self.adapter.read(request.ticket_id)
|
|
320
220
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
# If completed, return with ticket ID
|
|
329
|
-
if item.status == QueueStatus.COMPLETED:
|
|
330
|
-
response = {
|
|
331
|
-
"queue_id": queue_id,
|
|
332
|
-
"status": "completed",
|
|
333
|
-
"ticket_id": params["ticket_id"],
|
|
334
|
-
}
|
|
221
|
+
if ticket is None:
|
|
222
|
+
return ResponseBuilder.status_result(
|
|
223
|
+
STATUS_ERROR,
|
|
224
|
+
error=MSG_TICKET_NOT_FOUND.format(ticket_id=request.ticket_id),
|
|
225
|
+
)
|
|
335
226
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
response["ticket_id"] = item.result["id"]
|
|
340
|
-
response["success"] = item.result.get("success", True)
|
|
227
|
+
return ResponseBuilder.status_result(
|
|
228
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(ticket)
|
|
229
|
+
)
|
|
341
230
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return response
|
|
231
|
+
async def _handle_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
232
|
+
"""Handle ticket update - SYNCHRONOUS."""
|
|
233
|
+
ticket_id = params["ticket_id"]
|
|
346
234
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"ticket_id": params["ticket_id"],
|
|
354
|
-
}
|
|
235
|
+
# Support both formats: {"ticket_id": "x", "updates": {...}} and {"ticket_id": "x", "field": "value"}
|
|
236
|
+
if "updates" in params:
|
|
237
|
+
updates = params["updates"]
|
|
238
|
+
else:
|
|
239
|
+
# Extract all non-ticket_id fields as updates
|
|
240
|
+
updates = {k: v for k, v in params.items() if k != "ticket_id"}
|
|
355
241
|
|
|
356
|
-
|
|
357
|
-
elapsed = asyncio.get_event_loop().time() - start_time
|
|
358
|
-
if elapsed > max_wait_time:
|
|
359
|
-
return {
|
|
360
|
-
"queue_id": queue_id,
|
|
361
|
-
"status": "timeout",
|
|
362
|
-
"message": f"Ticket update timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
363
|
-
"ticket_id": params["ticket_id"],
|
|
364
|
-
}
|
|
242
|
+
updated = await self.adapter.update(ticket_id, updates)
|
|
365
243
|
|
|
366
|
-
|
|
367
|
-
|
|
244
|
+
if updated is None:
|
|
245
|
+
return ResponseBuilder.status_result(
|
|
246
|
+
STATUS_ERROR, error=MSG_UPDATE_FAILED.format(ticket_id=ticket_id)
|
|
247
|
+
)
|
|
368
248
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
# Queue the operation
|
|
372
|
-
queue = Queue()
|
|
373
|
-
queue_id = queue.add(
|
|
374
|
-
ticket_data={"ticket_id": params["ticket_id"]},
|
|
375
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
376
|
-
operation="delete",
|
|
249
|
+
return ResponseBuilder.status_result(
|
|
250
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(updated)
|
|
377
251
|
)
|
|
378
252
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
253
|
+
async def _handle_delete(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
254
|
+
"""Handle ticket deletion - SYNCHRONOUS."""
|
|
255
|
+
ticket_id = params["ticket_id"]
|
|
256
|
+
success = await self.adapter.delete(ticket_id)
|
|
382
257
|
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
"message": f"Ticket deletion queued with ID: {queue_id}",
|
|
387
|
-
}
|
|
258
|
+
return ResponseBuilder.status_result(
|
|
259
|
+
STATUS_COMPLETED, **ResponseBuilder.deletion_result(ticket_id, success)
|
|
260
|
+
)
|
|
388
261
|
|
|
389
|
-
async def _handle_list(self, params: dict[str, Any]) ->
|
|
390
|
-
"""Handle ticket listing."""
|
|
262
|
+
async def _handle_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
"""Handle ticket listing - SYNCHRONOUS."""
|
|
391
264
|
tickets = await self.adapter.list(
|
|
392
|
-
limit=params.get("limit",
|
|
393
|
-
offset=params.get("offset",
|
|
265
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
266
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
394
267
|
filters=params.get("filters"),
|
|
395
268
|
)
|
|
396
|
-
return [ticket.model_dump() for ticket in tickets]
|
|
397
269
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
query = SearchQuery(**params)
|
|
401
|
-
tickets = await self.adapter.search(query)
|
|
402
|
-
return [ticket.model_dump() for ticket in tickets]
|
|
403
|
-
|
|
404
|
-
async def _handle_transition(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
405
|
-
"""Handle state transition."""
|
|
406
|
-
# Queue the operation
|
|
407
|
-
queue = Queue()
|
|
408
|
-
queue_id = queue.add(
|
|
409
|
-
ticket_data={
|
|
410
|
-
"ticket_id": params["ticket_id"],
|
|
411
|
-
"state": params["target_state"],
|
|
412
|
-
},
|
|
413
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
414
|
-
operation="transition",
|
|
270
|
+
return ResponseBuilder.status_result(
|
|
271
|
+
STATUS_COMPLETED, **ResponseBuilder.tickets_result(tickets)
|
|
415
272
|
)
|
|
416
273
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
# Check queue status
|
|
428
|
-
item = queue.get_item(queue_id)
|
|
429
|
-
|
|
430
|
-
if not item:
|
|
431
|
-
return {
|
|
432
|
-
"queue_id": queue_id,
|
|
433
|
-
"status": "error",
|
|
434
|
-
"error": f"Queue item {queue_id} not found",
|
|
435
|
-
}
|
|
274
|
+
async def _handle_search(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
275
|
+
"""Handle ticket search - SYNCHRONOUS."""
|
|
276
|
+
query = SearchQuery(
|
|
277
|
+
query=params.get("query"),
|
|
278
|
+
state=TicketState(params["state"]) if params.get("state") else None,
|
|
279
|
+
priority=Priority(params["priority"]) if params.get("priority") else None,
|
|
280
|
+
assignee=params.get("assignee"),
|
|
281
|
+
tags=params.get("tags"),
|
|
282
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
283
|
+
)
|
|
436
284
|
|
|
437
|
-
|
|
438
|
-
if item.status == QueueStatus.COMPLETED:
|
|
439
|
-
response = {
|
|
440
|
-
"queue_id": queue_id,
|
|
441
|
-
"status": "completed",
|
|
442
|
-
"ticket_id": params["ticket_id"],
|
|
443
|
-
"state": params["target_state"],
|
|
444
|
-
}
|
|
285
|
+
results = await self.adapter.search(query)
|
|
445
286
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
response["ticket_id"] = item.result["id"]
|
|
450
|
-
response["success"] = item.result.get("success", True)
|
|
287
|
+
return ResponseBuilder.status_result(
|
|
288
|
+
STATUS_COMPLETED, **ResponseBuilder.tickets_result(results)
|
|
289
|
+
)
|
|
451
290
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
291
|
+
async def _handle_transition(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
292
|
+
"""Handle state transition - SYNCHRONOUS."""
|
|
293
|
+
ticket_id = params["ticket_id"]
|
|
294
|
+
target_state = TicketState(params["target_state"])
|
|
456
295
|
|
|
457
|
-
|
|
458
|
-
if item.status == QueueStatus.FAILED:
|
|
459
|
-
return {
|
|
460
|
-
"queue_id": queue_id,
|
|
461
|
-
"status": "failed",
|
|
462
|
-
"error": item.error_message or "State transition failed",
|
|
463
|
-
"ticket_id": params["ticket_id"],
|
|
464
|
-
}
|
|
296
|
+
updated = await self.adapter.transition_state(ticket_id, target_state)
|
|
465
297
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
"queue_id": queue_id,
|
|
471
|
-
"status": "timeout",
|
|
472
|
-
"message": f"State transition timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
473
|
-
"ticket_id": params["ticket_id"],
|
|
474
|
-
}
|
|
298
|
+
if updated is None:
|
|
299
|
+
return ResponseBuilder.status_result(
|
|
300
|
+
STATUS_ERROR, error=MSG_TRANSITION_FAILED.format(ticket_id=ticket_id)
|
|
301
|
+
)
|
|
475
302
|
|
|
476
|
-
|
|
477
|
-
|
|
303
|
+
return ResponseBuilder.status_result(
|
|
304
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(updated)
|
|
305
|
+
)
|
|
478
306
|
|
|
479
307
|
async def _handle_comment(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
480
|
-
"""Handle comment operations."""
|
|
308
|
+
"""Handle comment operations - SYNCHRONOUS."""
|
|
481
309
|
operation = params.get("operation", "add")
|
|
482
310
|
|
|
483
311
|
if operation == "add":
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
"ticket_id": params["ticket_id"],
|
|
489
|
-
"content": params["content"],
|
|
490
|
-
"author": params.get("author"),
|
|
491
|
-
},
|
|
492
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
493
|
-
operation="comment",
|
|
312
|
+
comment = Comment(
|
|
313
|
+
ticket_id=params["ticket_id"],
|
|
314
|
+
content=params["content"],
|
|
315
|
+
author=params.get("author"),
|
|
494
316
|
)
|
|
495
317
|
|
|
496
|
-
|
|
497
|
-
manager = WorkerManager()
|
|
498
|
-
manager.start_if_needed()
|
|
318
|
+
created = await self.adapter.add_comment(comment)
|
|
499
319
|
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
"message": f"Comment addition queued with ID: {queue_id}",
|
|
504
|
-
}
|
|
320
|
+
return ResponseBuilder.status_result(
|
|
321
|
+
STATUS_COMPLETED, **ResponseBuilder.comment_result(created)
|
|
322
|
+
)
|
|
505
323
|
|
|
506
324
|
elif operation == "list":
|
|
507
|
-
# Comments list is read-only, execute directly
|
|
508
325
|
comments = await self.adapter.get_comments(
|
|
509
326
|
params["ticket_id"],
|
|
510
|
-
limit=params.get("limit",
|
|
511
|
-
offset=params.get("offset",
|
|
327
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
328
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
512
329
|
)
|
|
513
|
-
return [comment.model_dump() for comment in comments]
|
|
514
|
-
|
|
515
|
-
else:
|
|
516
|
-
raise ValueError(f"Unknown comment operation: {operation}")
|
|
517
|
-
|
|
518
|
-
async def _handle_queue_status(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
519
|
-
"""Check status of queued operation."""
|
|
520
|
-
queue_id = params.get("queue_id")
|
|
521
|
-
if not queue_id:
|
|
522
|
-
raise ValueError("queue_id is required")
|
|
523
|
-
|
|
524
|
-
queue = Queue()
|
|
525
|
-
item = queue.get_item(queue_id)
|
|
526
|
-
|
|
527
|
-
if not item:
|
|
528
|
-
return {"error": f"Queue item not found: {queue_id}"}
|
|
529
|
-
|
|
530
|
-
response = {
|
|
531
|
-
"queue_id": item.id,
|
|
532
|
-
"status": item.status.value,
|
|
533
|
-
"operation": item.operation,
|
|
534
|
-
"created_at": item.created_at.isoformat(),
|
|
535
|
-
"retry_count": item.retry_count,
|
|
536
|
-
}
|
|
537
330
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if item.error_message:
|
|
542
|
-
response["error"] = item.error_message
|
|
543
|
-
|
|
544
|
-
if item.result:
|
|
545
|
-
response["result"] = item.result
|
|
546
|
-
|
|
547
|
-
return response
|
|
548
|
-
|
|
549
|
-
async def _handle_queue_health(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
550
|
-
"""Handle queue health check."""
|
|
551
|
-
health_monitor = QueueHealthMonitor()
|
|
552
|
-
health = health_monitor.check_health()
|
|
553
|
-
|
|
554
|
-
# Add auto-repair option
|
|
555
|
-
auto_repair = params.get("auto_repair", False)
|
|
556
|
-
if auto_repair and health["status"] in [
|
|
557
|
-
HealthStatus.CRITICAL,
|
|
558
|
-
HealthStatus.WARNING,
|
|
559
|
-
]:
|
|
560
|
-
repair_result = health_monitor.auto_repair()
|
|
561
|
-
health["auto_repair"] = repair_result
|
|
562
|
-
# Re-check health after repair
|
|
563
|
-
health.update(health_monitor.check_health())
|
|
331
|
+
return ResponseBuilder.status_result(
|
|
332
|
+
STATUS_COMPLETED, **ResponseBuilder.comments_result(comments)
|
|
333
|
+
)
|
|
564
334
|
|
|
565
|
-
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError(MSG_UNKNOWN_OPERATION.format(operation=operation))
|
|
566
337
|
|
|
567
338
|
# Hierarchy Management Handlers
|
|
568
339
|
|
|
569
340
|
async def _handle_epic_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
570
|
-
"""Handle epic creation."""
|
|
571
|
-
#
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
alert for alert in health["alerts"] if alert["level"] == "critical"
|
|
582
|
-
]
|
|
583
|
-
return {
|
|
584
|
-
"status": "error",
|
|
585
|
-
"error": "Queue system is in critical state",
|
|
586
|
-
"details": {
|
|
587
|
-
"health_status": health["status"],
|
|
588
|
-
"critical_issues": critical_alerts,
|
|
589
|
-
"repair_attempted": repair_result["actions_taken"],
|
|
590
|
-
},
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
# Queue the epic creation
|
|
594
|
-
queue = Queue()
|
|
595
|
-
epic_data = {
|
|
596
|
-
"title": params["title"],
|
|
597
|
-
"description": params.get("description"),
|
|
598
|
-
"child_issues": params.get("child_issues", []),
|
|
599
|
-
"target_date": params.get("target_date"),
|
|
600
|
-
"lead_id": params.get("lead_id"),
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
queue_id = queue.add(
|
|
604
|
-
ticket_data=epic_data,
|
|
605
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
606
|
-
operation="create_epic",
|
|
341
|
+
"""Handle epic creation - SYNCHRONOUS with validation."""
|
|
342
|
+
# Validate and parse request
|
|
343
|
+
request = CreateEpicRequest(**params)
|
|
344
|
+
|
|
345
|
+
# Build epic from validated DTO
|
|
346
|
+
epic = Epic(
|
|
347
|
+
title=request.title,
|
|
348
|
+
description=request.description,
|
|
349
|
+
child_issues=request.child_issues,
|
|
350
|
+
target_date=request.target_date,
|
|
351
|
+
lead_id=request.lead_id,
|
|
607
352
|
)
|
|
608
353
|
|
|
609
|
-
#
|
|
610
|
-
|
|
611
|
-
worker_started = manager.start_if_needed()
|
|
354
|
+
# Create directly
|
|
355
|
+
created = await self.adapter.create(epic)
|
|
612
356
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
"queue_id": queue_id,
|
|
618
|
-
"details": {
|
|
619
|
-
"pending_count": queue.get_pending_count(),
|
|
620
|
-
"action": "Worker process could not be started to process queued operations",
|
|
621
|
-
},
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return {
|
|
625
|
-
"queue_id": queue_id,
|
|
626
|
-
"status": "queued",
|
|
627
|
-
"message": f"Epic creation queued with ID: {queue_id}",
|
|
628
|
-
"epic_data": epic_data,
|
|
629
|
-
}
|
|
357
|
+
# Return immediately
|
|
358
|
+
return ResponseBuilder.status_result(
|
|
359
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
360
|
+
)
|
|
630
361
|
|
|
631
|
-
async def _handle_epic_list(self, params: dict[str, Any]) ->
|
|
632
|
-
"""Handle epic listing."""
|
|
362
|
+
async def _handle_epic_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
363
|
+
"""Handle epic listing - SYNCHRONOUS."""
|
|
633
364
|
epics = await self.adapter.list_epics(
|
|
634
|
-
limit=params.get("limit",
|
|
635
|
-
offset=params.get("offset",
|
|
365
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
366
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
636
367
|
**{k: v for k, v in params.items() if k not in ["limit", "offset"]},
|
|
637
368
|
)
|
|
638
|
-
return [epic.model_dump() for epic in epics]
|
|
639
369
|
|
|
640
|
-
|
|
641
|
-
|
|
370
|
+
return ResponseBuilder.status_result(
|
|
371
|
+
STATUS_COMPLETED, **ResponseBuilder.epics_result(epics)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
async def _handle_epic_issues(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
375
|
+
"""Handle listing issues in an epic - SYNCHRONOUS."""
|
|
642
376
|
epic_id = params["epic_id"]
|
|
643
377
|
issues = await self.adapter.list_issues_by_epic(epic_id)
|
|
644
|
-
return [issue.model_dump() for issue in issues]
|
|
645
378
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
health_monitor = QueueHealthMonitor()
|
|
650
|
-
health = health_monitor.check_health()
|
|
651
|
-
|
|
652
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
653
|
-
repair_result = health_monitor.auto_repair()
|
|
654
|
-
health = health_monitor.check_health()
|
|
655
|
-
|
|
656
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
657
|
-
critical_alerts = [
|
|
658
|
-
alert for alert in health["alerts"] if alert["level"] == "critical"
|
|
659
|
-
]
|
|
660
|
-
return {
|
|
661
|
-
"status": "error",
|
|
662
|
-
"error": "Queue system is in critical state",
|
|
663
|
-
"details": {
|
|
664
|
-
"health_status": health["status"],
|
|
665
|
-
"critical_issues": critical_alerts,
|
|
666
|
-
"repair_attempted": repair_result["actions_taken"],
|
|
667
|
-
},
|
|
668
|
-
}
|
|
379
|
+
return ResponseBuilder.status_result(
|
|
380
|
+
STATUS_COMPLETED, **ResponseBuilder.issues_result(issues)
|
|
381
|
+
)
|
|
669
382
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
issue_data = {
|
|
673
|
-
"title": params["title"],
|
|
674
|
-
"description": params.get("description"),
|
|
675
|
-
"epic_id": params.get("epic_id"),
|
|
676
|
-
"priority": params.get("priority", "medium"),
|
|
677
|
-
"assignee": params.get("assignee"),
|
|
678
|
-
"tags": params.get("tags", []),
|
|
679
|
-
"estimated_hours": params.get("estimated_hours"),
|
|
680
|
-
}
|
|
383
|
+
async def _handle_issue_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
384
|
+
"""Handle issue creation - SYNCHRONOUS with validation.
|
|
681
385
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
386
|
+
Note: In the current model, 'issues' are Tasks with a parent epic.
|
|
387
|
+
"""
|
|
388
|
+
# Validate and parse request
|
|
389
|
+
request = CreateIssueRequest(**params)
|
|
390
|
+
|
|
391
|
+
# Build task (issue) from validated DTO
|
|
392
|
+
task = Task(
|
|
393
|
+
title=request.title,
|
|
394
|
+
description=request.description,
|
|
395
|
+
parent_epic=request.epic_id, # Issues are tasks under epics
|
|
396
|
+
priority=Priority(request.priority),
|
|
397
|
+
assignee=request.assignee,
|
|
398
|
+
tags=request.tags,
|
|
399
|
+
estimated_hours=request.estimated_hours,
|
|
686
400
|
)
|
|
687
401
|
|
|
688
|
-
#
|
|
689
|
-
|
|
690
|
-
worker_started = manager.start_if_needed()
|
|
691
|
-
|
|
692
|
-
if not worker_started and queue.get_pending_count() > 0:
|
|
693
|
-
return {
|
|
694
|
-
"status": "error",
|
|
695
|
-
"error": "Failed to start worker process",
|
|
696
|
-
"queue_id": queue_id,
|
|
697
|
-
"details": {
|
|
698
|
-
"pending_count": queue.get_pending_count(),
|
|
699
|
-
"action": "Worker process could not be started to process queued operations",
|
|
700
|
-
},
|
|
701
|
-
}
|
|
402
|
+
# Create directly
|
|
403
|
+
created = await self.adapter.create(task)
|
|
702
404
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
"issue_data": issue_data,
|
|
708
|
-
}
|
|
405
|
+
# Return immediately
|
|
406
|
+
return ResponseBuilder.status_result(
|
|
407
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
408
|
+
)
|
|
709
409
|
|
|
710
|
-
async def _handle_issue_tasks(self, params: dict[str, Any]) ->
|
|
711
|
-
"""Handle listing tasks in an issue."""
|
|
410
|
+
async def _handle_issue_tasks(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
411
|
+
"""Handle listing tasks in an issue - SYNCHRONOUS."""
|
|
712
412
|
issue_id = params["issue_id"]
|
|
713
413
|
tasks = await self.adapter.list_tasks_by_issue(issue_id)
|
|
714
|
-
return [task.model_dump() for task in tasks]
|
|
715
414
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# Check queue health
|
|
719
|
-
health_monitor = QueueHealthMonitor()
|
|
720
|
-
health = health_monitor.check_health()
|
|
721
|
-
|
|
722
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
723
|
-
repair_result = health_monitor.auto_repair()
|
|
724
|
-
health = health_monitor.check_health()
|
|
725
|
-
|
|
726
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
727
|
-
critical_alerts = [
|
|
728
|
-
alert for alert in health["alerts"] if alert["level"] == "critical"
|
|
729
|
-
]
|
|
730
|
-
return {
|
|
731
|
-
"status": "error",
|
|
732
|
-
"error": "Queue system is in critical state",
|
|
733
|
-
"details": {
|
|
734
|
-
"health_status": health["status"],
|
|
735
|
-
"critical_issues": critical_alerts,
|
|
736
|
-
"repair_attempted": repair_result["actions_taken"],
|
|
737
|
-
},
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
# Validate required parent_id
|
|
741
|
-
if not params.get("parent_id"):
|
|
742
|
-
return {
|
|
743
|
-
"status": "error",
|
|
744
|
-
"error": "Tasks must have a parent_id (issue identifier)",
|
|
745
|
-
"details": {"required_field": "parent_id"},
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
# Queue the task creation
|
|
749
|
-
queue = Queue()
|
|
750
|
-
task_data = {
|
|
751
|
-
"title": params["title"],
|
|
752
|
-
"parent_id": params["parent_id"],
|
|
753
|
-
"description": params.get("description"),
|
|
754
|
-
"priority": params.get("priority", "medium"),
|
|
755
|
-
"assignee": params.get("assignee"),
|
|
756
|
-
"tags": params.get("tags", []),
|
|
757
|
-
"estimated_hours": params.get("estimated_hours"),
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
queue_id = queue.add(
|
|
761
|
-
ticket_data=task_data,
|
|
762
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
763
|
-
operation="create_task",
|
|
415
|
+
return ResponseBuilder.status_result(
|
|
416
|
+
STATUS_COMPLETED, **ResponseBuilder.tasks_result(tasks)
|
|
764
417
|
)
|
|
765
418
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
419
|
+
async def _handle_task_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
420
|
+
"""Handle task creation - SYNCHRONOUS with validation."""
|
|
421
|
+
# Validate and parse request (will raise ValidationError if parent_id missing)
|
|
422
|
+
request = CreateTaskRequest(**params)
|
|
423
|
+
|
|
424
|
+
# Build task from validated DTO
|
|
425
|
+
task = Task(
|
|
426
|
+
title=request.title,
|
|
427
|
+
parent_issue=request.parent_id,
|
|
428
|
+
description=request.description,
|
|
429
|
+
priority=Priority(request.priority),
|
|
430
|
+
assignee=request.assignee,
|
|
431
|
+
tags=request.tags,
|
|
432
|
+
estimated_hours=request.estimated_hours,
|
|
433
|
+
)
|
|
769
434
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
"status": "error",
|
|
773
|
-
"error": "Failed to start worker process",
|
|
774
|
-
"queue_id": queue_id,
|
|
775
|
-
"details": {
|
|
776
|
-
"pending_count": queue.get_pending_count(),
|
|
777
|
-
"action": "Worker process could not be started to process queued operations",
|
|
778
|
-
},
|
|
779
|
-
}
|
|
435
|
+
# Create directly
|
|
436
|
+
created = await self.adapter.create(task)
|
|
780
437
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
"task_data": task_data,
|
|
786
|
-
}
|
|
438
|
+
# Return immediately
|
|
439
|
+
return ResponseBuilder.status_result(
|
|
440
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
441
|
+
)
|
|
787
442
|
|
|
788
443
|
async def _handle_hierarchy_tree(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
789
|
-
"""Handle hierarchy tree visualization."""
|
|
444
|
+
"""Handle hierarchy tree visualization - SYNCHRONOUS."""
|
|
790
445
|
epic_id = params.get("epic_id")
|
|
791
|
-
max_depth = params.get("max_depth",
|
|
446
|
+
max_depth = params.get("max_depth", DEFAULT_MAX_DEPTH)
|
|
792
447
|
|
|
793
448
|
if epic_id:
|
|
794
449
|
# Get specific epic tree
|
|
795
450
|
epic = await self.adapter.get_epic(epic_id)
|
|
796
451
|
if not epic:
|
|
797
|
-
return
|
|
452
|
+
return ResponseBuilder.status_result(
|
|
453
|
+
STATUS_ERROR, error=MSG_EPIC_NOT_FOUND.format(epic_id=epic_id)
|
|
454
|
+
)
|
|
798
455
|
|
|
799
456
|
# Build tree structure
|
|
800
457
|
tree = {"epic": epic.model_dump(), "issues": []}
|
|
801
458
|
|
|
802
|
-
# Get issues in epic
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
459
|
+
# Get issues in epic if depth allows (depth 1 = epic only, depth 2+ = issues)
|
|
460
|
+
if max_depth > 1:
|
|
461
|
+
issues = await self.adapter.list_issues_by_epic(epic_id)
|
|
462
|
+
for issue in issues:
|
|
463
|
+
issue_node = {"issue": issue.model_dump(), "tasks": []}
|
|
806
464
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
465
|
+
# Get tasks in issue if depth allows (depth 3+ = tasks)
|
|
466
|
+
if max_depth > 2:
|
|
467
|
+
tasks = await self.adapter.list_tasks_by_issue(issue.id)
|
|
468
|
+
issue_node["tasks"] = [task.model_dump() for task in tasks]
|
|
811
469
|
|
|
812
|
-
|
|
470
|
+
tree["issues"].append(issue_node)
|
|
813
471
|
|
|
814
|
-
return tree
|
|
472
|
+
return ResponseBuilder.status_result(STATUS_COMPLETED, **tree)
|
|
815
473
|
else:
|
|
816
474
|
# Get all epics with their hierarchies
|
|
817
|
-
epics = await self.adapter.list_epics(
|
|
475
|
+
epics = await self.adapter.list_epics(
|
|
476
|
+
limit=params.get("limit", DEFAULT_LIMIT)
|
|
477
|
+
)
|
|
818
478
|
trees = []
|
|
819
479
|
|
|
820
480
|
for epic in epics:
|
|
@@ -823,110 +483,79 @@ class MCPTicketServer:
|
|
|
823
483
|
)
|
|
824
484
|
trees.append(tree)
|
|
825
485
|
|
|
826
|
-
return
|
|
486
|
+
return ResponseBuilder.status_result(STATUS_COMPLETED, trees=trees)
|
|
827
487
|
|
|
828
488
|
async def _handle_bulk_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
829
|
-
"""Handle bulk ticket creation."""
|
|
489
|
+
"""Handle bulk ticket creation - SYNCHRONOUS."""
|
|
830
490
|
tickets = params.get("tickets", [])
|
|
831
491
|
if not tickets:
|
|
832
|
-
return
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
health_monitor = QueueHealthMonitor()
|
|
836
|
-
health = health_monitor.check_health()
|
|
837
|
-
|
|
838
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
839
|
-
repair_result = health_monitor.auto_repair()
|
|
840
|
-
health = health_monitor.check_health()
|
|
841
|
-
|
|
842
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
843
|
-
return {
|
|
844
|
-
"status": "error",
|
|
845
|
-
"error": "Queue system is in critical state - cannot process bulk operations",
|
|
846
|
-
"details": {"health_status": health["status"]},
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
# Queue all tickets
|
|
850
|
-
queue = Queue()
|
|
851
|
-
queue_ids = []
|
|
492
|
+
return ResponseBuilder.status_result(
|
|
493
|
+
STATUS_ERROR, error=MSG_NO_TICKETS_PROVIDED
|
|
494
|
+
)
|
|
852
495
|
|
|
496
|
+
results = []
|
|
853
497
|
for i, ticket_data in enumerate(tickets):
|
|
854
498
|
if not ticket_data.get("title"):
|
|
855
|
-
return
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
queue_id = queue.add(
|
|
861
|
-
ticket_data=ticket_data,
|
|
862
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
863
|
-
operation=ticket_data.get("operation", "create"),
|
|
864
|
-
)
|
|
865
|
-
queue_ids.append(queue_id)
|
|
499
|
+
return ResponseBuilder.status_result(
|
|
500
|
+
STATUS_ERROR, error=MSG_MISSING_TITLE.format(index=i)
|
|
501
|
+
)
|
|
866
502
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
503
|
+
try:
|
|
504
|
+
# Create ticket based on operation type
|
|
505
|
+
operation = ticket_data.get("operation", "create")
|
|
506
|
+
|
|
507
|
+
if operation == "create_epic":
|
|
508
|
+
result = await self._handle_epic_create(ticket_data)
|
|
509
|
+
elif operation == "create_issue":
|
|
510
|
+
result = await self._handle_issue_create(ticket_data)
|
|
511
|
+
elif operation == "create_task":
|
|
512
|
+
result = await self._handle_task_create(ticket_data)
|
|
513
|
+
else:
|
|
514
|
+
result = await self._handle_create(ticket_data)
|
|
515
|
+
|
|
516
|
+
results.append(result)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
results.append(
|
|
519
|
+
ResponseBuilder.status_result(
|
|
520
|
+
STATUS_ERROR, error=str(e), ticket_index=i
|
|
521
|
+
)
|
|
522
|
+
)
|
|
870
523
|
|
|
871
|
-
return
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
"message": f"Bulk creation of {len(tickets)} tickets queued",
|
|
875
|
-
"count": len(tickets),
|
|
876
|
-
}
|
|
524
|
+
return ResponseBuilder.status_result(
|
|
525
|
+
STATUS_COMPLETED, **ResponseBuilder.bulk_result(results)
|
|
526
|
+
)
|
|
877
527
|
|
|
878
528
|
async def _handle_bulk_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
879
|
-
"""Handle bulk ticket updates."""
|
|
529
|
+
"""Handle bulk ticket updates - SYNCHRONOUS."""
|
|
880
530
|
updates = params.get("updates", [])
|
|
881
531
|
if not updates:
|
|
882
|
-
return
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
health_monitor = QueueHealthMonitor()
|
|
886
|
-
health = health_monitor.check_health()
|
|
887
|
-
|
|
888
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
889
|
-
repair_result = health_monitor.auto_repair()
|
|
890
|
-
health = health_monitor.check_health()
|
|
891
|
-
|
|
892
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
893
|
-
return {
|
|
894
|
-
"status": "error",
|
|
895
|
-
"error": "Queue system is in critical state - cannot process bulk operations",
|
|
896
|
-
"details": {"health_status": health["status"]},
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
# Queue all updates
|
|
900
|
-
queue = Queue()
|
|
901
|
-
queue_ids = []
|
|
532
|
+
return ResponseBuilder.status_result(
|
|
533
|
+
STATUS_ERROR, error=MSG_NO_UPDATES_PROVIDED
|
|
534
|
+
)
|
|
902
535
|
|
|
536
|
+
results = []
|
|
903
537
|
for i, update_data in enumerate(updates):
|
|
904
538
|
if not update_data.get("ticket_id"):
|
|
905
|
-
return
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
queue_id = queue.add(
|
|
911
|
-
ticket_data=update_data,
|
|
912
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
913
|
-
operation="update",
|
|
914
|
-
)
|
|
915
|
-
queue_ids.append(queue_id)
|
|
539
|
+
return ResponseBuilder.status_result(
|
|
540
|
+
STATUS_ERROR, error=MSG_MISSING_TICKET_ID.format(index=i)
|
|
541
|
+
)
|
|
916
542
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
543
|
+
try:
|
|
544
|
+
result = await self._handle_update(update_data)
|
|
545
|
+
results.append(result)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
results.append(
|
|
548
|
+
ResponseBuilder.status_result(
|
|
549
|
+
STATUS_ERROR, error=str(e), ticket_id=update_data["ticket_id"]
|
|
550
|
+
)
|
|
551
|
+
)
|
|
920
552
|
|
|
921
|
-
return
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
"message": f"Bulk update of {len(updates)} tickets queued",
|
|
925
|
-
"count": len(updates),
|
|
926
|
-
}
|
|
553
|
+
return ResponseBuilder.status_result(
|
|
554
|
+
STATUS_COMPLETED, **ResponseBuilder.bulk_result(results)
|
|
555
|
+
)
|
|
927
556
|
|
|
928
557
|
async def _handle_search_hierarchy(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
929
|
-
"""Handle hierarchy-aware search."""
|
|
558
|
+
"""Handle hierarchy-aware search - SYNCHRONOUS."""
|
|
930
559
|
query = params.get("query", "")
|
|
931
560
|
include_children = params.get("include_children", True)
|
|
932
561
|
include_parents = params.get("include_parents", True)
|
|
@@ -934,8 +563,8 @@ class MCPTicketServer:
|
|
|
934
563
|
# Perform basic search
|
|
935
564
|
search_query = SearchQuery(
|
|
936
565
|
query=query,
|
|
937
|
-
state=params.get("state"),
|
|
938
|
-
priority=params.get("priority"),
|
|
566
|
+
state=TicketState(params["state"]) if params.get("state") else None,
|
|
567
|
+
priority=Priority(params["priority"]) if params.get("priority") else None,
|
|
939
568
|
limit=params.get("limit", 50),
|
|
940
569
|
)
|
|
941
570
|
|
|
@@ -972,6 +601,7 @@ class MCPTicketServer:
|
|
|
972
601
|
enhanced_results.append(result)
|
|
973
602
|
|
|
974
603
|
return {
|
|
604
|
+
"status": "completed",
|
|
975
605
|
"results": enhanced_results,
|
|
976
606
|
"count": len(enhanced_results),
|
|
977
607
|
"query": query,
|
|
@@ -994,12 +624,10 @@ class MCPTicketServer:
|
|
|
994
624
|
},
|
|
995
625
|
}
|
|
996
626
|
|
|
997
|
-
async def _handle_list_attachments(
|
|
998
|
-
self, params: dict[str, Any]
|
|
999
|
-
) -> list[dict[str, Any]]:
|
|
627
|
+
async def _handle_list_attachments(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
1000
628
|
"""Handle listing ticket attachments."""
|
|
1001
629
|
# Note: This is a placeholder for attachment functionality
|
|
1002
|
-
return []
|
|
630
|
+
return {"status": "completed", "attachments": []}
|
|
1003
631
|
|
|
1004
632
|
async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
1005
633
|
"""Handle PR creation for a ticket."""
|
|
@@ -1154,8 +782,8 @@ class MCPTicketServer:
|
|
|
1154
782
|
|
|
1155
783
|
"""
|
|
1156
784
|
return {
|
|
1157
|
-
"protocolVersion":
|
|
1158
|
-
"serverInfo": {"name":
|
|
785
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
786
|
+
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
1159
787
|
"capabilities": {"tools": {"listChanged": False}},
|
|
1160
788
|
}
|
|
1161
789
|
|
|
@@ -1211,306 +839,7 @@ class MCPTicketServer:
|
|
|
1211
839
|
},
|
|
1212
840
|
},
|
|
1213
841
|
},
|
|
1214
|
-
|
|
1215
|
-
"name": "epic_issues",
|
|
1216
|
-
"description": "List all issues in an epic",
|
|
1217
|
-
"inputSchema": {
|
|
1218
|
-
"type": "object",
|
|
1219
|
-
"properties": {
|
|
1220
|
-
"epic_id": {
|
|
1221
|
-
"type": "string",
|
|
1222
|
-
"description": "Epic ID to get issues for",
|
|
1223
|
-
}
|
|
1224
|
-
},
|
|
1225
|
-
"required": ["epic_id"],
|
|
1226
|
-
},
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
"name": "issue_create",
|
|
1230
|
-
"description": "Create a new issue (work item)",
|
|
1231
|
-
"inputSchema": {
|
|
1232
|
-
"type": "object",
|
|
1233
|
-
"properties": {
|
|
1234
|
-
"title": {"type": "string", "description": "Issue title"},
|
|
1235
|
-
"description": {
|
|
1236
|
-
"type": "string",
|
|
1237
|
-
"description": "Issue description",
|
|
1238
|
-
},
|
|
1239
|
-
"epic_id": {
|
|
1240
|
-
"type": "string",
|
|
1241
|
-
"description": "Parent epic ID",
|
|
1242
|
-
},
|
|
1243
|
-
"priority": {
|
|
1244
|
-
"type": "string",
|
|
1245
|
-
"enum": ["low", "medium", "high", "critical"],
|
|
1246
|
-
"default": "medium",
|
|
1247
|
-
},
|
|
1248
|
-
"assignee": {
|
|
1249
|
-
"type": "string",
|
|
1250
|
-
"description": "Assignee username",
|
|
1251
|
-
},
|
|
1252
|
-
"tags": {
|
|
1253
|
-
"type": "array",
|
|
1254
|
-
"items": {"type": "string"},
|
|
1255
|
-
"description": "Issue tags",
|
|
1256
|
-
},
|
|
1257
|
-
"estimated_hours": {
|
|
1258
|
-
"type": "number",
|
|
1259
|
-
"description": "Estimated hours to complete",
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
"required": ["title"],
|
|
1263
|
-
},
|
|
1264
|
-
},
|
|
1265
|
-
{
|
|
1266
|
-
"name": "issue_tasks",
|
|
1267
|
-
"description": "List all tasks in an issue",
|
|
1268
|
-
"inputSchema": {
|
|
1269
|
-
"type": "object",
|
|
1270
|
-
"properties": {
|
|
1271
|
-
"issue_id": {
|
|
1272
|
-
"type": "string",
|
|
1273
|
-
"description": "Issue ID to get tasks for",
|
|
1274
|
-
}
|
|
1275
|
-
},
|
|
1276
|
-
"required": ["issue_id"],
|
|
1277
|
-
},
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
"name": "task_create",
|
|
1281
|
-
"description": "Create a new task (sub-item under an issue)",
|
|
1282
|
-
"inputSchema": {
|
|
1283
|
-
"type": "object",
|
|
1284
|
-
"properties": {
|
|
1285
|
-
"title": {"type": "string", "description": "Task title"},
|
|
1286
|
-
"parent_id": {
|
|
1287
|
-
"type": "string",
|
|
1288
|
-
"description": "Parent issue ID (required)",
|
|
1289
|
-
},
|
|
1290
|
-
"description": {
|
|
1291
|
-
"type": "string",
|
|
1292
|
-
"description": "Task description",
|
|
1293
|
-
},
|
|
1294
|
-
"priority": {
|
|
1295
|
-
"type": "string",
|
|
1296
|
-
"enum": ["low", "medium", "high", "critical"],
|
|
1297
|
-
"default": "medium",
|
|
1298
|
-
},
|
|
1299
|
-
"assignee": {
|
|
1300
|
-
"type": "string",
|
|
1301
|
-
"description": "Assignee username",
|
|
1302
|
-
},
|
|
1303
|
-
"tags": {
|
|
1304
|
-
"type": "array",
|
|
1305
|
-
"items": {"type": "string"},
|
|
1306
|
-
"description": "Task tags",
|
|
1307
|
-
},
|
|
1308
|
-
"estimated_hours": {
|
|
1309
|
-
"type": "number",
|
|
1310
|
-
"description": "Estimated hours to complete",
|
|
1311
|
-
},
|
|
1312
|
-
},
|
|
1313
|
-
"required": ["title", "parent_id"],
|
|
1314
|
-
},
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
"name": "hierarchy_tree",
|
|
1318
|
-
"description": "Get hierarchy tree view of epic/issues/tasks",
|
|
1319
|
-
"inputSchema": {
|
|
1320
|
-
"type": "object",
|
|
1321
|
-
"properties": {
|
|
1322
|
-
"epic_id": {
|
|
1323
|
-
"type": "string",
|
|
1324
|
-
"description": "Specific epic ID (optional - if not provided, returns all epics)",
|
|
1325
|
-
},
|
|
1326
|
-
"max_depth": {
|
|
1327
|
-
"type": "integer",
|
|
1328
|
-
"default": 3,
|
|
1329
|
-
"description": "Maximum depth to traverse (1=epics only, 2=epics+issues, 3=full tree)",
|
|
1330
|
-
},
|
|
1331
|
-
"limit": {
|
|
1332
|
-
"type": "integer",
|
|
1333
|
-
"default": 10,
|
|
1334
|
-
"description": "Maximum number of epics to return (when epic_id not specified)",
|
|
1335
|
-
},
|
|
1336
|
-
},
|
|
1337
|
-
},
|
|
1338
|
-
},
|
|
1339
|
-
# Bulk Operations
|
|
1340
|
-
{
|
|
1341
|
-
"name": "ticket_bulk_create",
|
|
1342
|
-
"description": "Create multiple tickets in one operation",
|
|
1343
|
-
"inputSchema": {
|
|
1344
|
-
"type": "object",
|
|
1345
|
-
"properties": {
|
|
1346
|
-
"tickets": {
|
|
1347
|
-
"type": "array",
|
|
1348
|
-
"items": {
|
|
1349
|
-
"type": "object",
|
|
1350
|
-
"properties": {
|
|
1351
|
-
"title": {"type": "string"},
|
|
1352
|
-
"description": {"type": "string"},
|
|
1353
|
-
"priority": {
|
|
1354
|
-
"type": "string",
|
|
1355
|
-
"enum": [
|
|
1356
|
-
"low",
|
|
1357
|
-
"medium",
|
|
1358
|
-
"high",
|
|
1359
|
-
"critical",
|
|
1360
|
-
],
|
|
1361
|
-
},
|
|
1362
|
-
"operation": {
|
|
1363
|
-
"type": "string",
|
|
1364
|
-
"enum": [
|
|
1365
|
-
"create",
|
|
1366
|
-
"create_epic",
|
|
1367
|
-
"create_issue",
|
|
1368
|
-
"create_task",
|
|
1369
|
-
],
|
|
1370
|
-
"default": "create",
|
|
1371
|
-
},
|
|
1372
|
-
"epic_id": {
|
|
1373
|
-
"type": "string",
|
|
1374
|
-
"description": "For issues",
|
|
1375
|
-
},
|
|
1376
|
-
"parent_id": {
|
|
1377
|
-
"type": "string",
|
|
1378
|
-
"description": "For tasks",
|
|
1379
|
-
},
|
|
1380
|
-
},
|
|
1381
|
-
"required": ["title"],
|
|
1382
|
-
},
|
|
1383
|
-
"description": "Array of tickets to create",
|
|
1384
|
-
}
|
|
1385
|
-
},
|
|
1386
|
-
"required": ["tickets"],
|
|
1387
|
-
},
|
|
1388
|
-
},
|
|
1389
|
-
{
|
|
1390
|
-
"name": "ticket_bulk_update",
|
|
1391
|
-
"description": "Update multiple tickets in one operation",
|
|
1392
|
-
"inputSchema": {
|
|
1393
|
-
"type": "object",
|
|
1394
|
-
"properties": {
|
|
1395
|
-
"updates": {
|
|
1396
|
-
"type": "array",
|
|
1397
|
-
"items": {
|
|
1398
|
-
"type": "object",
|
|
1399
|
-
"properties": {
|
|
1400
|
-
"ticket_id": {"type": "string"},
|
|
1401
|
-
"title": {"type": "string"},
|
|
1402
|
-
"description": {"type": "string"},
|
|
1403
|
-
"priority": {
|
|
1404
|
-
"type": "string",
|
|
1405
|
-
"enum": [
|
|
1406
|
-
"low",
|
|
1407
|
-
"medium",
|
|
1408
|
-
"high",
|
|
1409
|
-
"critical",
|
|
1410
|
-
],
|
|
1411
|
-
},
|
|
1412
|
-
"state": {"type": "string"},
|
|
1413
|
-
"assignee": {"type": "string"},
|
|
1414
|
-
},
|
|
1415
|
-
"required": ["ticket_id"],
|
|
1416
|
-
},
|
|
1417
|
-
"description": "Array of ticket updates",
|
|
1418
|
-
}
|
|
1419
|
-
},
|
|
1420
|
-
"required": ["updates"],
|
|
1421
|
-
},
|
|
1422
|
-
},
|
|
1423
|
-
# Advanced Search
|
|
1424
|
-
{
|
|
1425
|
-
"name": "ticket_search_hierarchy",
|
|
1426
|
-
"description": "Search tickets with hierarchy context",
|
|
1427
|
-
"inputSchema": {
|
|
1428
|
-
"type": "object",
|
|
1429
|
-
"properties": {
|
|
1430
|
-
"query": {"type": "string", "description": "Search query"},
|
|
1431
|
-
"state": {
|
|
1432
|
-
"type": "string",
|
|
1433
|
-
"description": "Filter by state",
|
|
1434
|
-
},
|
|
1435
|
-
"priority": {
|
|
1436
|
-
"type": "string",
|
|
1437
|
-
"description": "Filter by priority",
|
|
1438
|
-
},
|
|
1439
|
-
"limit": {
|
|
1440
|
-
"type": "integer",
|
|
1441
|
-
"default": 50,
|
|
1442
|
-
"description": "Maximum results",
|
|
1443
|
-
},
|
|
1444
|
-
"include_children": {
|
|
1445
|
-
"type": "boolean",
|
|
1446
|
-
"default": True,
|
|
1447
|
-
"description": "Include child items in results",
|
|
1448
|
-
},
|
|
1449
|
-
"include_parents": {
|
|
1450
|
-
"type": "boolean",
|
|
1451
|
-
"default": True,
|
|
1452
|
-
"description": "Include parent context in results",
|
|
1453
|
-
},
|
|
1454
|
-
},
|
|
1455
|
-
"required": ["query"],
|
|
1456
|
-
},
|
|
1457
|
-
},
|
|
1458
|
-
# PR Integration
|
|
1459
|
-
{
|
|
1460
|
-
"name": "ticket_create_pr",
|
|
1461
|
-
"description": "Create a GitHub PR linked to a ticket",
|
|
1462
|
-
"inputSchema": {
|
|
1463
|
-
"type": "object",
|
|
1464
|
-
"properties": {
|
|
1465
|
-
"ticket_id": {
|
|
1466
|
-
"type": "string",
|
|
1467
|
-
"description": "Ticket ID to link the PR to",
|
|
1468
|
-
},
|
|
1469
|
-
"base_branch": {
|
|
1470
|
-
"type": "string",
|
|
1471
|
-
"description": "Target branch for the PR",
|
|
1472
|
-
"default": "main",
|
|
1473
|
-
},
|
|
1474
|
-
"head_branch": {
|
|
1475
|
-
"type": "string",
|
|
1476
|
-
"description": "Source branch name (auto-generated if not provided)",
|
|
1477
|
-
},
|
|
1478
|
-
"title": {
|
|
1479
|
-
"type": "string",
|
|
1480
|
-
"description": "PR title (uses ticket title if not provided)",
|
|
1481
|
-
},
|
|
1482
|
-
"body": {
|
|
1483
|
-
"type": "string",
|
|
1484
|
-
"description": "PR description (auto-generated with issue link if not provided)",
|
|
1485
|
-
},
|
|
1486
|
-
"draft": {
|
|
1487
|
-
"type": "boolean",
|
|
1488
|
-
"description": "Create as draft PR",
|
|
1489
|
-
"default": False,
|
|
1490
|
-
},
|
|
1491
|
-
},
|
|
1492
|
-
"required": ["ticket_id"],
|
|
1493
|
-
},
|
|
1494
|
-
},
|
|
1495
|
-
# Standard Ticket Operations
|
|
1496
|
-
{
|
|
1497
|
-
"name": "ticket_link_pr",
|
|
1498
|
-
"description": "Link an existing PR to a ticket",
|
|
1499
|
-
"inputSchema": {
|
|
1500
|
-
"type": "object",
|
|
1501
|
-
"properties": {
|
|
1502
|
-
"ticket_id": {
|
|
1503
|
-
"type": "string",
|
|
1504
|
-
"description": "Ticket ID to link the PR to",
|
|
1505
|
-
},
|
|
1506
|
-
"pr_url": {
|
|
1507
|
-
"type": "string",
|
|
1508
|
-
"description": "GitHub PR URL to link",
|
|
1509
|
-
},
|
|
1510
|
-
},
|
|
1511
|
-
"required": ["ticket_id", "pr_url"],
|
|
1512
|
-
},
|
|
1513
|
-
},
|
|
842
|
+
# ... (rest of the tools list)
|
|
1514
843
|
{
|
|
1515
844
|
"name": "ticket_create",
|
|
1516
845
|
"description": "Create a new ticket",
|
|
@@ -1532,95 +861,6 @@ class MCPTicketServer:
|
|
|
1532
861
|
"required": ["title"],
|
|
1533
862
|
},
|
|
1534
863
|
},
|
|
1535
|
-
{
|
|
1536
|
-
"name": "ticket_list",
|
|
1537
|
-
"description": "List tickets",
|
|
1538
|
-
"inputSchema": {
|
|
1539
|
-
"type": "object",
|
|
1540
|
-
"properties": {
|
|
1541
|
-
"limit": {"type": "integer", "default": 10},
|
|
1542
|
-
"state": {"type": "string"},
|
|
1543
|
-
"priority": {"type": "string"},
|
|
1544
|
-
},
|
|
1545
|
-
},
|
|
1546
|
-
},
|
|
1547
|
-
{
|
|
1548
|
-
"name": "ticket_update",
|
|
1549
|
-
"description": "Update a ticket",
|
|
1550
|
-
"inputSchema": {
|
|
1551
|
-
"type": "object",
|
|
1552
|
-
"properties": {
|
|
1553
|
-
"ticket_id": {"type": "string", "description": "Ticket ID"},
|
|
1554
|
-
"updates": {
|
|
1555
|
-
"type": "object",
|
|
1556
|
-
"description": "Fields to update",
|
|
1557
|
-
},
|
|
1558
|
-
},
|
|
1559
|
-
"required": ["ticket_id", "updates"],
|
|
1560
|
-
},
|
|
1561
|
-
},
|
|
1562
|
-
{
|
|
1563
|
-
"name": "ticket_transition",
|
|
1564
|
-
"description": "Change ticket state",
|
|
1565
|
-
"inputSchema": {
|
|
1566
|
-
"type": "object",
|
|
1567
|
-
"properties": {
|
|
1568
|
-
"ticket_id": {"type": "string"},
|
|
1569
|
-
"target_state": {"type": "string"},
|
|
1570
|
-
},
|
|
1571
|
-
"required": ["ticket_id", "target_state"],
|
|
1572
|
-
},
|
|
1573
|
-
},
|
|
1574
|
-
{
|
|
1575
|
-
"name": "ticket_search",
|
|
1576
|
-
"description": "Search tickets",
|
|
1577
|
-
"inputSchema": {
|
|
1578
|
-
"type": "object",
|
|
1579
|
-
"properties": {
|
|
1580
|
-
"query": {"type": "string"},
|
|
1581
|
-
"state": {"type": "string"},
|
|
1582
|
-
"priority": {"type": "string"},
|
|
1583
|
-
"limit": {"type": "integer", "default": 10},
|
|
1584
|
-
},
|
|
1585
|
-
},
|
|
1586
|
-
},
|
|
1587
|
-
{
|
|
1588
|
-
"name": "ticket_status",
|
|
1589
|
-
"description": "Check status of queued ticket operation",
|
|
1590
|
-
"inputSchema": {
|
|
1591
|
-
"type": "object",
|
|
1592
|
-
"properties": {
|
|
1593
|
-
"queue_id": {
|
|
1594
|
-
"type": "string",
|
|
1595
|
-
"description": "Queue ID returned from create/update/delete operations",
|
|
1596
|
-
},
|
|
1597
|
-
},
|
|
1598
|
-
"required": ["queue_id"],
|
|
1599
|
-
},
|
|
1600
|
-
},
|
|
1601
|
-
# System diagnostics tools
|
|
1602
|
-
{
|
|
1603
|
-
"name": "system_health",
|
|
1604
|
-
"description": "Quick system health check - shows configuration, queue worker, and failure rates",
|
|
1605
|
-
"inputSchema": {
|
|
1606
|
-
"type": "object",
|
|
1607
|
-
"properties": {},
|
|
1608
|
-
},
|
|
1609
|
-
},
|
|
1610
|
-
{
|
|
1611
|
-
"name": "system_diagnose",
|
|
1612
|
-
"description": "Comprehensive system diagnostics - detailed analysis of all components",
|
|
1613
|
-
"inputSchema": {
|
|
1614
|
-
"type": "object",
|
|
1615
|
-
"properties": {
|
|
1616
|
-
"include_logs": {
|
|
1617
|
-
"type": "boolean",
|
|
1618
|
-
"default": False,
|
|
1619
|
-
"description": "Include recent log analysis in diagnosis",
|
|
1620
|
-
},
|
|
1621
|
-
},
|
|
1622
|
-
},
|
|
1623
|
-
},
|
|
1624
864
|
]
|
|
1625
865
|
}
|
|
1626
866
|
|
|
@@ -1673,13 +913,6 @@ class MCPTicketServer:
|
|
|
1673
913
|
result = await self._handle_transition(arguments)
|
|
1674
914
|
elif tool_name == "ticket_search":
|
|
1675
915
|
result = await self._handle_search(arguments)
|
|
1676
|
-
elif tool_name == "ticket_status":
|
|
1677
|
-
result = await self._handle_queue_status(arguments)
|
|
1678
|
-
# System diagnostics
|
|
1679
|
-
elif tool_name == "system_health":
|
|
1680
|
-
result = await self._handle_system_health(arguments)
|
|
1681
|
-
elif tool_name == "system_diagnose":
|
|
1682
|
-
result = await self._handle_system_diagnose(arguments)
|
|
1683
916
|
# PR integration
|
|
1684
917
|
elif tool_name == "ticket_create_pr":
|
|
1685
918
|
result = await self._handle_create_pr(arguments)
|
|
@@ -1753,8 +986,8 @@ class MCPTicketServer:
|
|
|
1753
986
|
sys.stdout.flush()
|
|
1754
987
|
|
|
1755
988
|
except json.JSONDecodeError as e:
|
|
1756
|
-
error_response =
|
|
1757
|
-
None,
|
|
989
|
+
error_response = ResponseBuilder.error(
|
|
990
|
+
None, ERROR_PARSE, f"Parse error: {str(e)}"
|
|
1758
991
|
)
|
|
1759
992
|
sys.stdout.write(json.dumps(error_response) + "\n")
|
|
1760
993
|
sys.stdout.flush()
|
|
@@ -1796,7 +1029,7 @@ async def main():
|
|
|
1796
1029
|
|
|
1797
1030
|
# Initialize defaults
|
|
1798
1031
|
adapter_type = "aitrackdown"
|
|
1799
|
-
adapter_config = {"base_path":
|
|
1032
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1800
1033
|
|
|
1801
1034
|
# Priority 1: Check .env files (highest priority for MCP)
|
|
1802
1035
|
env_config = _load_env_configuration()
|
|
@@ -1840,12 +1073,12 @@ async def main():
|
|
|
1840
1073
|
except (OSError, json.JSONDecodeError) as e:
|
|
1841
1074
|
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
1842
1075
|
adapter_type = "aitrackdown"
|
|
1843
|
-
adapter_config = {"base_path":
|
|
1076
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1844
1077
|
else:
|
|
1845
1078
|
# Priority 3: Default to aitrackdown
|
|
1846
1079
|
logger.info("No configuration found, defaulting to aitrackdown adapter")
|
|
1847
1080
|
adapter_type = "aitrackdown"
|
|
1848
|
-
adapter_config = {"base_path":
|
|
1081
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1849
1082
|
|
|
1850
1083
|
# Log final configuration for debugging
|
|
1851
1084
|
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
@@ -1961,7 +1194,7 @@ def _build_adapter_config_from_env_vars(
|
|
|
1961
1194
|
|
|
1962
1195
|
elif adapter_type == "aitrackdown":
|
|
1963
1196
|
# AITrackdown adapter configuration
|
|
1964
|
-
base_path = env_vars.get("MCP_TICKETER_BASE_PATH",
|
|
1197
|
+
base_path = env_vars.get("MCP_TICKETER_BASE_PATH", DEFAULT_BASE_PATH)
|
|
1965
1198
|
config["base_path"] = base_path
|
|
1966
1199
|
config["auto_create_dirs"] = True
|
|
1967
1200
|
|
|
@@ -1972,214 +1205,5 @@ def _build_adapter_config_from_env_vars(
|
|
|
1972
1205
|
return config
|
|
1973
1206
|
|
|
1974
1207
|
|
|
1975
|
-
# Add diagnostic handler methods to MCPTicketServer class
|
|
1976
|
-
async def _handle_system_health(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
1977
|
-
"""Handle system health check."""
|
|
1978
|
-
from ..cli.diagnostics import SystemDiagnostics
|
|
1979
|
-
|
|
1980
|
-
try:
|
|
1981
|
-
diagnostics = SystemDiagnostics()
|
|
1982
|
-
|
|
1983
|
-
# Quick health checks
|
|
1984
|
-
health_status = {
|
|
1985
|
-
"overall_status": "healthy",
|
|
1986
|
-
"components": {},
|
|
1987
|
-
"issues": [],
|
|
1988
|
-
"warnings": [],
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
# Check configuration
|
|
1992
|
-
try:
|
|
1993
|
-
from ..core.config import get_config
|
|
1994
|
-
|
|
1995
|
-
config = get_config()
|
|
1996
|
-
adapters = config.get_enabled_adapters()
|
|
1997
|
-
if adapters:
|
|
1998
|
-
health_status["components"]["configuration"] = {
|
|
1999
|
-
"status": "healthy",
|
|
2000
|
-
"adapters_count": len(adapters),
|
|
2001
|
-
}
|
|
2002
|
-
else:
|
|
2003
|
-
health_status["components"]["configuration"] = {
|
|
2004
|
-
"status": "failed",
|
|
2005
|
-
"error": "No adapters configured",
|
|
2006
|
-
}
|
|
2007
|
-
health_status["issues"].append("No adapters configured")
|
|
2008
|
-
health_status["overall_status"] = "critical"
|
|
2009
|
-
except Exception as e:
|
|
2010
|
-
health_status["components"]["configuration"] = {
|
|
2011
|
-
"status": "failed",
|
|
2012
|
-
"error": str(e),
|
|
2013
|
-
}
|
|
2014
|
-
health_status["issues"].append(f"Configuration error: {str(e)}")
|
|
2015
|
-
health_status["overall_status"] = "critical"
|
|
2016
|
-
|
|
2017
|
-
# Check queue system
|
|
2018
|
-
try:
|
|
2019
|
-
from ..queue.manager import WorkerManager
|
|
2020
|
-
|
|
2021
|
-
worker_manager = WorkerManager()
|
|
2022
|
-
worker_status = worker_manager.get_status()
|
|
2023
|
-
stats = worker_manager.queue.get_stats()
|
|
2024
|
-
|
|
2025
|
-
total = stats.get("total", 0)
|
|
2026
|
-
failed = stats.get("failed", 0)
|
|
2027
|
-
failure_rate = (failed / total * 100) if total > 0 else 0
|
|
2028
|
-
|
|
2029
|
-
queue_health = {
|
|
2030
|
-
"status": "healthy",
|
|
2031
|
-
"worker_running": worker_status.get("running", False),
|
|
2032
|
-
"worker_pid": worker_status.get("pid"),
|
|
2033
|
-
"failure_rate": failure_rate,
|
|
2034
|
-
"total_processed": total,
|
|
2035
|
-
"failed_items": failed,
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
if not worker_status.get("running", False):
|
|
2039
|
-
queue_health["status"] = "failed"
|
|
2040
|
-
health_status["issues"].append("Queue worker not running")
|
|
2041
|
-
health_status["overall_status"] = "critical"
|
|
2042
|
-
elif failure_rate > 50:
|
|
2043
|
-
queue_health["status"] = "degraded"
|
|
2044
|
-
health_status["issues"].append(
|
|
2045
|
-
f"High queue failure rate: {failure_rate:.1f}%"
|
|
2046
|
-
)
|
|
2047
|
-
health_status["overall_status"] = "critical"
|
|
2048
|
-
elif failure_rate > 20:
|
|
2049
|
-
queue_health["status"] = "warning"
|
|
2050
|
-
health_status["warnings"].append(
|
|
2051
|
-
f"Elevated queue failure rate: {failure_rate:.1f}%"
|
|
2052
|
-
)
|
|
2053
|
-
if health_status["overall_status"] == "healthy":
|
|
2054
|
-
health_status["overall_status"] = "warning"
|
|
2055
|
-
|
|
2056
|
-
health_status["components"]["queue_system"] = queue_health
|
|
2057
|
-
|
|
2058
|
-
except Exception as e:
|
|
2059
|
-
health_status["components"]["queue_system"] = {
|
|
2060
|
-
"status": "failed",
|
|
2061
|
-
"error": str(e),
|
|
2062
|
-
}
|
|
2063
|
-
health_status["issues"].append(f"Queue system error: {str(e)}")
|
|
2064
|
-
health_status["overall_status"] = "critical"
|
|
2065
|
-
|
|
2066
|
-
return {
|
|
2067
|
-
"content": [
|
|
2068
|
-
{
|
|
2069
|
-
"type": "text",
|
|
2070
|
-
"text": f"System Health Status: {health_status['overall_status'].upper()}\n\n"
|
|
2071
|
-
+ f"Configuration: {health_status['components'].get('configuration', {}).get('status', 'unknown')}\n"
|
|
2072
|
-
+ f"Queue System: {health_status['components'].get('queue_system', {}).get('status', 'unknown')}\n\n"
|
|
2073
|
-
+ f"Issues: {len(health_status['issues'])}\n"
|
|
2074
|
-
+ f"Warnings: {len(health_status['warnings'])}\n\n"
|
|
2075
|
-
+ (
|
|
2076
|
-
"Critical Issues:\n"
|
|
2077
|
-
+ "\n".join(f"• {issue}" for issue in health_status["issues"])
|
|
2078
|
-
+ "\n\n"
|
|
2079
|
-
if health_status["issues"]
|
|
2080
|
-
else ""
|
|
2081
|
-
)
|
|
2082
|
-
+ (
|
|
2083
|
-
"Warnings:\n"
|
|
2084
|
-
+ "\n".join(
|
|
2085
|
-
f"• {warning}" for warning in health_status["warnings"]
|
|
2086
|
-
)
|
|
2087
|
-
+ "\n\n"
|
|
2088
|
-
if health_status["warnings"]
|
|
2089
|
-
else ""
|
|
2090
|
-
)
|
|
2091
|
-
+ "For detailed diagnosis, use system_diagnose tool.",
|
|
2092
|
-
}
|
|
2093
|
-
],
|
|
2094
|
-
"isError": health_status["overall_status"] == "critical",
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
except Exception as e:
|
|
2098
|
-
return {
|
|
2099
|
-
"content": [
|
|
2100
|
-
{
|
|
2101
|
-
"type": "text",
|
|
2102
|
-
"text": f"Health check failed: {str(e)}",
|
|
2103
|
-
}
|
|
2104
|
-
],
|
|
2105
|
-
"isError": True,
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
async def _handle_system_diagnose(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
2110
|
-
"""Handle comprehensive system diagnosis."""
|
|
2111
|
-
from ..cli.diagnostics import SystemDiagnostics
|
|
2112
|
-
|
|
2113
|
-
try:
|
|
2114
|
-
diagnostics = SystemDiagnostics()
|
|
2115
|
-
report = await diagnostics.run_full_diagnosis()
|
|
2116
|
-
|
|
2117
|
-
# Format report for MCP response
|
|
2118
|
-
summary = f"""System Diagnosis Report
|
|
2119
|
-
Generated: {report['timestamp']}
|
|
2120
|
-
Version: {report['version']}
|
|
2121
|
-
|
|
2122
|
-
OVERALL STATUS: {
|
|
2123
|
-
'CRITICAL' if diagnostics.issues else
|
|
2124
|
-
'WARNING' if diagnostics.warnings else
|
|
2125
|
-
'HEALTHY'
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
COMPONENT STATUS:
|
|
2129
|
-
• Configuration: {len(report['configuration']['issues'])} issues
|
|
2130
|
-
• Adapters: {report['adapters']['failed_adapters']}/{report['adapters']['total_adapters']} failed
|
|
2131
|
-
• Queue System: {report['queue_system']['health_score']}/100 health score
|
|
2132
|
-
|
|
2133
|
-
STATISTICS:
|
|
2134
|
-
• Successes: {len(diagnostics.successes)}
|
|
2135
|
-
• Warnings: {len(diagnostics.warnings)}
|
|
2136
|
-
• Critical Issues: {len(diagnostics.issues)}
|
|
2137
|
-
|
|
2138
|
-
"""
|
|
2139
|
-
|
|
2140
|
-
if diagnostics.issues:
|
|
2141
|
-
summary += "CRITICAL ISSUES:\n"
|
|
2142
|
-
for issue in diagnostics.issues:
|
|
2143
|
-
summary += f"• {issue}\n"
|
|
2144
|
-
summary += "\n"
|
|
2145
|
-
|
|
2146
|
-
if diagnostics.warnings:
|
|
2147
|
-
summary += "WARNINGS:\n"
|
|
2148
|
-
for warning in diagnostics.warnings:
|
|
2149
|
-
summary += f"• {warning}\n"
|
|
2150
|
-
summary += "\n"
|
|
2151
|
-
|
|
2152
|
-
if report["recommendations"]:
|
|
2153
|
-
summary += "RECOMMENDATIONS:\n"
|
|
2154
|
-
for rec in report["recommendations"]:
|
|
2155
|
-
summary += f"{rec}\n"
|
|
2156
|
-
|
|
2157
|
-
return {
|
|
2158
|
-
"content": [
|
|
2159
|
-
{
|
|
2160
|
-
"type": "text",
|
|
2161
|
-
"text": summary,
|
|
2162
|
-
}
|
|
2163
|
-
],
|
|
2164
|
-
"isError": bool(diagnostics.issues),
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
except Exception as e:
|
|
2168
|
-
return {
|
|
2169
|
-
"content": [
|
|
2170
|
-
{
|
|
2171
|
-
"type": "text",
|
|
2172
|
-
"text": f"System diagnosis failed: {str(e)}",
|
|
2173
|
-
}
|
|
2174
|
-
],
|
|
2175
|
-
"isError": True,
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
# Monkey patch the methods onto the class
|
|
2180
|
-
MCPTicketServer._handle_system_health = _handle_system_health
|
|
2181
|
-
MCPTicketServer._handle_system_diagnose = _handle_system_diagnose
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
1208
|
if __name__ == "__main__":
|
|
2185
1209
|
asyncio.run(main())
|