mcp-ticketer 0.3.1__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 +164 -36
- mcp_ticketer/adapters/github.py +11 -8
- mcp_ticketer/adapters/jira.py +29 -28
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +105 -104
- mcp_ticketer/adapters/linear/client.py +78 -59
- mcp_ticketer/adapters/linear/mappers.py +93 -73
- mcp_ticketer/adapters/linear/queries.py +28 -7
- mcp_ticketer/adapters/linear/types.py +67 -60
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/cli/adapter_diagnostics.py +87 -52
- mcp_ticketer/cli/codex_configure.py +6 -6
- mcp_ticketer/cli/diagnostics.py +180 -88
- mcp_ticketer/cli/linear_commands.py +156 -113
- mcp_ticketer/cli/main.py +153 -82
- mcp_ticketer/cli/simple_health.py +74 -51
- mcp_ticketer/cli/utils.py +15 -10
- mcp_ticketer/core/config.py +23 -19
- mcp_ticketer/core/env_discovery.py +5 -4
- mcp_ticketer/core/env_loader.py +114 -91
- mcp_ticketer/core/exceptions.py +22 -20
- mcp_ticketer/core/models.py +9 -0
- mcp_ticketer/core/project_config.py +1 -1
- 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 +361 -1182
- mcp_ticketer/queue/health_monitor.py +166 -135
- mcp_ticketer/queue/manager.py +70 -19
- mcp_ticketer/queue/queue.py +24 -5
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +203 -145
- mcp_ticketer/queue/worker.py +79 -43
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/METADATA +1 -1
- mcp_ticketer-0.3.3.dist-info/RECORD +62 -0
- mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.1.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
|
|
@@ -8,14 +8,46 @@ from typing import Any, Optional
|
|
|
8
8
|
|
|
9
9
|
from dotenv import load_dotenv
|
|
10
10
|
|
|
11
|
-
from ..core import AdapterRegistry
|
|
12
|
-
from ..core.models import SearchQuery
|
|
13
|
-
from ..queue import Queue, QueueStatus, WorkerManager
|
|
14
|
-
from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
|
|
15
|
-
|
|
16
11
|
# Import adapters module to trigger registration
|
|
17
12
|
import mcp_ticketer.adapters # noqa: F401
|
|
18
13
|
|
|
14
|
+
from ..core import AdapterRegistry
|
|
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
|
|
50
|
+
|
|
19
51
|
# Load environment variables early (prioritize .env.local)
|
|
20
52
|
# Check for .env.local first (takes precedence)
|
|
21
53
|
env_local_file = Path.cwd() / ".env.local"
|
|
@@ -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,768 +190,372 @@ 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 = [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
|
|
186
|
-
queue = Queue()
|
|
187
|
-
task_data = {
|
|
188
|
-
"title": params["title"],
|
|
189
|
-
"description": params.get("description"),
|
|
190
|
-
"priority": params.get("priority", "medium"),
|
|
191
|
-
"tags": params.get("tags", []),
|
|
192
|
-
"assignee": params.get("assignee"),
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
queue_id = queue.add(
|
|
196
|
-
ticket_data=task_data,
|
|
197
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
198
|
-
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,
|
|
199
204
|
)
|
|
200
205
|
|
|
201
|
-
#
|
|
202
|
-
|
|
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
|
-
}
|
|
216
|
-
|
|
217
|
-
# Check if async mode is requested (for backward compatibility)
|
|
218
|
-
if params.get("async_mode", False):
|
|
219
|
-
return {
|
|
220
|
-
"queue_id": queue_id,
|
|
221
|
-
"status": "queued",
|
|
222
|
-
"message": f"Ticket creation queued with ID: {queue_id}",
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# Poll for completion with timeout (default synchronous behavior)
|
|
226
|
-
max_wait_time = params.get("timeout", 30) # seconds, allow override
|
|
227
|
-
poll_interval = 0.5 # seconds
|
|
228
|
-
start_time = asyncio.get_event_loop().time()
|
|
229
|
-
|
|
230
|
-
while True:
|
|
231
|
-
# Check queue status
|
|
232
|
-
item = queue.get_item(queue_id)
|
|
233
|
-
|
|
234
|
-
if not item:
|
|
235
|
-
return {
|
|
236
|
-
"queue_id": queue_id,
|
|
237
|
-
"status": "error",
|
|
238
|
-
"error": f"Queue item {queue_id} not found",
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# If completed, return with ticket ID
|
|
242
|
-
if item.status == QueueStatus.COMPLETED:
|
|
243
|
-
response = {
|
|
244
|
-
"queue_id": queue_id,
|
|
245
|
-
"status": "completed",
|
|
246
|
-
"title": params["title"],
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
# Add ticket ID and other result data if available
|
|
250
|
-
if item.result:
|
|
251
|
-
response["ticket_id"] = item.result.get("id")
|
|
252
|
-
if "state" in item.result:
|
|
253
|
-
response["state"] = item.result["state"]
|
|
254
|
-
# Try to construct URL if we have enough information
|
|
255
|
-
if response.get("ticket_id"):
|
|
256
|
-
# This is adapter-specific, but we can add URL generation later
|
|
257
|
-
response["id"] = response[
|
|
258
|
-
"ticket_id"
|
|
259
|
-
] # Also include as "id" for compatibility
|
|
260
|
-
|
|
261
|
-
response["message"] = (
|
|
262
|
-
f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
|
|
263
|
-
)
|
|
264
|
-
return response
|
|
265
|
-
|
|
266
|
-
# If failed, return error
|
|
267
|
-
if item.status == QueueStatus.FAILED:
|
|
268
|
-
return {
|
|
269
|
-
"queue_id": queue_id,
|
|
270
|
-
"status": "failed",
|
|
271
|
-
"error": item.error_message or "Ticket creation failed",
|
|
272
|
-
"title": params["title"],
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
# Check timeout
|
|
276
|
-
elapsed = asyncio.get_event_loop().time() - start_time
|
|
277
|
-
if elapsed > max_wait_time:
|
|
278
|
-
return {
|
|
279
|
-
"queue_id": queue_id,
|
|
280
|
-
"status": "timeout",
|
|
281
|
-
"message": f"Ticket creation timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
282
|
-
"title": params["title"],
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
# Wait before next poll
|
|
286
|
-
await asyncio.sleep(poll_interval)
|
|
287
|
-
|
|
288
|
-
async def _handle_read(self, params: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
289
|
-
"""Handle ticket read."""
|
|
290
|
-
ticket = await self.adapter.read(params["ticket_id"])
|
|
291
|
-
return ticket.model_dump() if ticket else None
|
|
206
|
+
# Create directly
|
|
207
|
+
created = await self.adapter.create(task)
|
|
292
208
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
queue = Queue()
|
|
297
|
-
updates = params.get("updates", {})
|
|
298
|
-
updates["ticket_id"] = params["ticket_id"]
|
|
299
|
-
|
|
300
|
-
queue_id = queue.add(
|
|
301
|
-
ticket_data=updates,
|
|
302
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
303
|
-
operation="update",
|
|
209
|
+
# Return immediately
|
|
210
|
+
return ResponseBuilder.status_result(
|
|
211
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
304
212
|
)
|
|
305
213
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
# Poll for completion with timeout
|
|
311
|
-
max_wait_time = 30 # seconds
|
|
312
|
-
poll_interval = 0.5 # seconds
|
|
313
|
-
start_time = asyncio.get_event_loop().time()
|
|
314
|
-
|
|
315
|
-
while True:
|
|
316
|
-
# Check queue status
|
|
317
|
-
item = queue.get_item(queue_id)
|
|
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)
|
|
318
218
|
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
"queue_id": queue_id,
|
|
322
|
-
"status": "error",
|
|
323
|
-
"error": f"Queue item {queue_id} not found",
|
|
324
|
-
}
|
|
219
|
+
ticket = await self.adapter.read(request.ticket_id)
|
|
325
220
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"ticket_id": params["ticket_id"],
|
|
332
|
-
}
|
|
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
|
+
)
|
|
333
226
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
response["ticket_id"] = item.result["id"]
|
|
338
|
-
response["success"] = item.result.get("success", True)
|
|
227
|
+
return ResponseBuilder.status_result(
|
|
228
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(ticket)
|
|
229
|
+
)
|
|
339
230
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
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"]
|
|
344
234
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
"ticket_id": params["ticket_id"],
|
|
352
|
-
}
|
|
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"}
|
|
353
241
|
|
|
354
|
-
|
|
355
|
-
elapsed = asyncio.get_event_loop().time() - start_time
|
|
356
|
-
if elapsed > max_wait_time:
|
|
357
|
-
return {
|
|
358
|
-
"queue_id": queue_id,
|
|
359
|
-
"status": "timeout",
|
|
360
|
-
"message": f"Ticket update timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
361
|
-
"ticket_id": params["ticket_id"],
|
|
362
|
-
}
|
|
242
|
+
updated = await self.adapter.update(ticket_id, updates)
|
|
363
243
|
|
|
364
|
-
|
|
365
|
-
|
|
244
|
+
if updated is None:
|
|
245
|
+
return ResponseBuilder.status_result(
|
|
246
|
+
STATUS_ERROR, error=MSG_UPDATE_FAILED.format(ticket_id=ticket_id)
|
|
247
|
+
)
|
|
366
248
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Queue the operation
|
|
370
|
-
queue = Queue()
|
|
371
|
-
queue_id = queue.add(
|
|
372
|
-
ticket_data={"ticket_id": params["ticket_id"]},
|
|
373
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
374
|
-
operation="delete",
|
|
249
|
+
return ResponseBuilder.status_result(
|
|
250
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(updated)
|
|
375
251
|
)
|
|
376
252
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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)
|
|
380
257
|
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
"message": f"Ticket deletion queued with ID: {queue_id}",
|
|
385
|
-
}
|
|
258
|
+
return ResponseBuilder.status_result(
|
|
259
|
+
STATUS_COMPLETED, **ResponseBuilder.deletion_result(ticket_id, success)
|
|
260
|
+
)
|
|
386
261
|
|
|
387
|
-
async def _handle_list(self, params: dict[str, Any]) ->
|
|
388
|
-
"""Handle ticket listing."""
|
|
262
|
+
async def _handle_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
"""Handle ticket listing - SYNCHRONOUS."""
|
|
389
264
|
tickets = await self.adapter.list(
|
|
390
|
-
limit=params.get("limit",
|
|
391
|
-
offset=params.get("offset",
|
|
265
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
266
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
392
267
|
filters=params.get("filters"),
|
|
393
268
|
)
|
|
394
|
-
return [ticket.model_dump() for ticket in tickets]
|
|
395
269
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
query = SearchQuery(**params)
|
|
399
|
-
tickets = await self.adapter.search(query)
|
|
400
|
-
return [ticket.model_dump() for ticket in tickets]
|
|
401
|
-
|
|
402
|
-
async def _handle_transition(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
403
|
-
"""Handle state transition."""
|
|
404
|
-
# Queue the operation
|
|
405
|
-
queue = Queue()
|
|
406
|
-
queue_id = queue.add(
|
|
407
|
-
ticket_data={
|
|
408
|
-
"ticket_id": params["ticket_id"],
|
|
409
|
-
"state": params["target_state"],
|
|
410
|
-
},
|
|
411
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
412
|
-
operation="transition",
|
|
270
|
+
return ResponseBuilder.status_result(
|
|
271
|
+
STATUS_COMPLETED, **ResponseBuilder.tickets_result(tickets)
|
|
413
272
|
)
|
|
414
273
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# Check queue status
|
|
426
|
-
item = queue.get_item(queue_id)
|
|
427
|
-
|
|
428
|
-
if not item:
|
|
429
|
-
return {
|
|
430
|
-
"queue_id": queue_id,
|
|
431
|
-
"status": "error",
|
|
432
|
-
"error": f"Queue item {queue_id} not found",
|
|
433
|
-
}
|
|
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
|
+
)
|
|
434
284
|
|
|
435
|
-
|
|
436
|
-
if item.status == QueueStatus.COMPLETED:
|
|
437
|
-
response = {
|
|
438
|
-
"queue_id": queue_id,
|
|
439
|
-
"status": "completed",
|
|
440
|
-
"ticket_id": params["ticket_id"],
|
|
441
|
-
"state": params["target_state"],
|
|
442
|
-
}
|
|
285
|
+
results = await self.adapter.search(query)
|
|
443
286
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
response["ticket_id"] = item.result["id"]
|
|
448
|
-
response["success"] = item.result.get("success", True)
|
|
287
|
+
return ResponseBuilder.status_result(
|
|
288
|
+
STATUS_COMPLETED, **ResponseBuilder.tickets_result(results)
|
|
289
|
+
)
|
|
449
290
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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"])
|
|
454
295
|
|
|
455
|
-
|
|
456
|
-
if item.status == QueueStatus.FAILED:
|
|
457
|
-
return {
|
|
458
|
-
"queue_id": queue_id,
|
|
459
|
-
"status": "failed",
|
|
460
|
-
"error": item.error_message or "State transition failed",
|
|
461
|
-
"ticket_id": params["ticket_id"],
|
|
462
|
-
}
|
|
296
|
+
updated = await self.adapter.transition_state(ticket_id, target_state)
|
|
463
297
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"queue_id": queue_id,
|
|
469
|
-
"status": "timeout",
|
|
470
|
-
"message": f"State transition timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
471
|
-
"ticket_id": params["ticket_id"],
|
|
472
|
-
}
|
|
298
|
+
if updated is None:
|
|
299
|
+
return ResponseBuilder.status_result(
|
|
300
|
+
STATUS_ERROR, error=MSG_TRANSITION_FAILED.format(ticket_id=ticket_id)
|
|
301
|
+
)
|
|
473
302
|
|
|
474
|
-
|
|
475
|
-
|
|
303
|
+
return ResponseBuilder.status_result(
|
|
304
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(updated)
|
|
305
|
+
)
|
|
476
306
|
|
|
477
307
|
async def _handle_comment(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
478
|
-
"""Handle comment operations."""
|
|
308
|
+
"""Handle comment operations - SYNCHRONOUS."""
|
|
479
309
|
operation = params.get("operation", "add")
|
|
480
310
|
|
|
481
311
|
if operation == "add":
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"ticket_id": params["ticket_id"],
|
|
487
|
-
"content": params["content"],
|
|
488
|
-
"author": params.get("author"),
|
|
489
|
-
},
|
|
490
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
491
|
-
operation="comment",
|
|
312
|
+
comment = Comment(
|
|
313
|
+
ticket_id=params["ticket_id"],
|
|
314
|
+
content=params["content"],
|
|
315
|
+
author=params.get("author"),
|
|
492
316
|
)
|
|
493
317
|
|
|
494
|
-
|
|
495
|
-
manager = WorkerManager()
|
|
496
|
-
manager.start_if_needed()
|
|
318
|
+
created = await self.adapter.add_comment(comment)
|
|
497
319
|
|
|
498
|
-
return
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"message": f"Comment addition queued with ID: {queue_id}",
|
|
502
|
-
}
|
|
320
|
+
return ResponseBuilder.status_result(
|
|
321
|
+
STATUS_COMPLETED, **ResponseBuilder.comment_result(created)
|
|
322
|
+
)
|
|
503
323
|
|
|
504
324
|
elif operation == "list":
|
|
505
|
-
# Comments list is read-only, execute directly
|
|
506
325
|
comments = await self.adapter.get_comments(
|
|
507
326
|
params["ticket_id"],
|
|
508
|
-
limit=params.get("limit",
|
|
509
|
-
offset=params.get("offset",
|
|
327
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
328
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
510
329
|
)
|
|
511
|
-
return [comment.model_dump() for comment in comments]
|
|
512
|
-
|
|
513
|
-
else:
|
|
514
|
-
raise ValueError(f"Unknown comment operation: {operation}")
|
|
515
|
-
|
|
516
|
-
async def _handle_queue_status(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
517
|
-
"""Check status of queued operation."""
|
|
518
|
-
queue_id = params.get("queue_id")
|
|
519
|
-
if not queue_id:
|
|
520
|
-
raise ValueError("queue_id is required")
|
|
521
|
-
|
|
522
|
-
queue = Queue()
|
|
523
|
-
item = queue.get_item(queue_id)
|
|
524
|
-
|
|
525
|
-
if not item:
|
|
526
|
-
return {"error": f"Queue item not found: {queue_id}"}
|
|
527
|
-
|
|
528
|
-
response = {
|
|
529
|
-
"queue_id": item.id,
|
|
530
|
-
"status": item.status.value,
|
|
531
|
-
"operation": item.operation,
|
|
532
|
-
"created_at": item.created_at.isoformat(),
|
|
533
|
-
"retry_count": item.retry_count,
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if item.processed_at:
|
|
537
|
-
response["processed_at"] = item.processed_at.isoformat()
|
|
538
330
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if item.result:
|
|
543
|
-
response["result"] = item.result
|
|
544
|
-
|
|
545
|
-
return response
|
|
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())
|
|
331
|
+
return ResponseBuilder.status_result(
|
|
332
|
+
STATUS_COMPLETED, **ResponseBuilder.comments_result(comments)
|
|
333
|
+
)
|
|
559
334
|
|
|
560
|
-
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError(MSG_UNKNOWN_OPERATION.format(operation=operation))
|
|
561
337
|
|
|
562
338
|
# Hierarchy Management Handlers
|
|
563
339
|
|
|
564
340
|
async def _handle_epic_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
565
|
-
"""Handle epic creation."""
|
|
566
|
-
#
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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",
|
|
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,
|
|
600
352
|
)
|
|
601
353
|
|
|
602
|
-
#
|
|
603
|
-
|
|
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
|
-
}
|
|
354
|
+
# Create directly
|
|
355
|
+
created = await self.adapter.create(epic)
|
|
616
356
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
"epic_data": epic_data
|
|
622
|
-
}
|
|
357
|
+
# Return immediately
|
|
358
|
+
return ResponseBuilder.status_result(
|
|
359
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
360
|
+
)
|
|
623
361
|
|
|
624
|
-
async def _handle_epic_list(self, params: dict[str, Any]) ->
|
|
625
|
-
"""Handle epic listing."""
|
|
362
|
+
async def _handle_epic_list(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
363
|
+
"""Handle epic listing - SYNCHRONOUS."""
|
|
626
364
|
epics = await self.adapter.list_epics(
|
|
627
|
-
limit=params.get("limit",
|
|
628
|
-
offset=params.get("offset",
|
|
629
|
-
**{k: v for k, v in params.items() if k not in ["limit", "offset"]}
|
|
365
|
+
limit=params.get("limit", DEFAULT_LIMIT),
|
|
366
|
+
offset=params.get("offset", DEFAULT_OFFSET),
|
|
367
|
+
**{k: v for k, v in params.items() if k not in ["limit", "offset"]},
|
|
630
368
|
)
|
|
631
|
-
return [epic.model_dump() for epic in epics]
|
|
632
369
|
|
|
633
|
-
|
|
634
|
-
|
|
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."""
|
|
635
376
|
epic_id = params["epic_id"]
|
|
636
377
|
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
378
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
}
|
|
379
|
+
return ResponseBuilder.status_result(
|
|
380
|
+
STATUS_COMPLETED, **ResponseBuilder.issues_result(issues)
|
|
381
|
+
)
|
|
660
382
|
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
}
|
|
383
|
+
async def _handle_issue_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
384
|
+
"""Handle issue creation - SYNCHRONOUS with validation.
|
|
672
385
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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,
|
|
677
400
|
)
|
|
678
401
|
|
|
679
|
-
#
|
|
680
|
-
|
|
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
|
-
}
|
|
402
|
+
# Create directly
|
|
403
|
+
created = await self.adapter.create(task)
|
|
693
404
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
"issue_data": issue_data
|
|
699
|
-
}
|
|
405
|
+
# Return immediately
|
|
406
|
+
return ResponseBuilder.status_result(
|
|
407
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
408
|
+
)
|
|
700
409
|
|
|
701
|
-
async def _handle_issue_tasks(self, params: dict[str, Any]) ->
|
|
702
|
-
"""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."""
|
|
703
412
|
issue_id = params["issue_id"]
|
|
704
413
|
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
414
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
|
|
752
|
-
operation="create_task",
|
|
415
|
+
return ResponseBuilder.status_result(
|
|
416
|
+
STATUS_COMPLETED, **ResponseBuilder.tasks_result(tasks)
|
|
753
417
|
)
|
|
754
418
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
)
|
|
758
434
|
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
}
|
|
435
|
+
# Create directly
|
|
436
|
+
created = await self.adapter.create(task)
|
|
769
437
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
"task_data": task_data
|
|
775
|
-
}
|
|
438
|
+
# Return immediately
|
|
439
|
+
return ResponseBuilder.status_result(
|
|
440
|
+
STATUS_COMPLETED, **ResponseBuilder.ticket_result(created)
|
|
441
|
+
)
|
|
776
442
|
|
|
777
443
|
async def _handle_hierarchy_tree(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
778
|
-
"""Handle hierarchy tree visualization."""
|
|
444
|
+
"""Handle hierarchy tree visualization - SYNCHRONOUS."""
|
|
779
445
|
epic_id = params.get("epic_id")
|
|
780
|
-
max_depth = params.get("max_depth",
|
|
446
|
+
max_depth = params.get("max_depth", DEFAULT_MAX_DEPTH)
|
|
781
447
|
|
|
782
448
|
if epic_id:
|
|
783
449
|
# Get specific epic tree
|
|
784
450
|
epic = await self.adapter.get_epic(epic_id)
|
|
785
451
|
if not epic:
|
|
786
|
-
return
|
|
452
|
+
return ResponseBuilder.status_result(
|
|
453
|
+
STATUS_ERROR, error=MSG_EPIC_NOT_FOUND.format(epic_id=epic_id)
|
|
454
|
+
)
|
|
787
455
|
|
|
788
456
|
# Build tree structure
|
|
789
|
-
tree = {
|
|
790
|
-
"epic": epic.model_dump(),
|
|
791
|
-
"issues": []
|
|
792
|
-
}
|
|
457
|
+
tree = {"epic": epic.model_dump(), "issues": []}
|
|
793
458
|
|
|
794
|
-
# Get issues in epic
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
"issue": issue.model_dump(),
|
|
799
|
-
"tasks": []
|
|
800
|
-
}
|
|
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": []}
|
|
801
464
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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]
|
|
806
469
|
|
|
807
|
-
|
|
470
|
+
tree["issues"].append(issue_node)
|
|
808
471
|
|
|
809
|
-
return tree
|
|
472
|
+
return ResponseBuilder.status_result(STATUS_COMPLETED, **tree)
|
|
810
473
|
else:
|
|
811
474
|
# Get all epics with their hierarchies
|
|
812
|
-
epics = await self.adapter.list_epics(
|
|
475
|
+
epics = await self.adapter.list_epics(
|
|
476
|
+
limit=params.get("limit", DEFAULT_LIMIT)
|
|
477
|
+
)
|
|
813
478
|
trees = []
|
|
814
479
|
|
|
815
480
|
for epic in epics:
|
|
816
|
-
tree = await self._handle_hierarchy_tree(
|
|
481
|
+
tree = await self._handle_hierarchy_tree(
|
|
482
|
+
{"epic_id": epic.id, "max_depth": max_depth}
|
|
483
|
+
)
|
|
817
484
|
trees.append(tree)
|
|
818
485
|
|
|
819
|
-
return
|
|
486
|
+
return ResponseBuilder.status_result(STATUS_COMPLETED, trees=trees)
|
|
820
487
|
|
|
821
488
|
async def _handle_bulk_create(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
822
|
-
"""Handle bulk ticket creation."""
|
|
489
|
+
"""Handle bulk ticket creation - SYNCHRONOUS."""
|
|
823
490
|
tickets = params.get("tickets", [])
|
|
824
491
|
if not tickets:
|
|
825
|
-
return
|
|
826
|
-
|
|
827
|
-
|
|
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 = []
|
|
492
|
+
return ResponseBuilder.status_result(
|
|
493
|
+
STATUS_ERROR, error=MSG_NO_TICKETS_PROVIDED
|
|
494
|
+
)
|
|
845
495
|
|
|
496
|
+
results = []
|
|
846
497
|
for i, ticket_data in enumerate(tickets):
|
|
847
498
|
if not ticket_data.get("title"):
|
|
848
|
-
return
|
|
849
|
-
|
|
850
|
-
|
|
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)
|
|
499
|
+
return ResponseBuilder.status_result(
|
|
500
|
+
STATUS_ERROR, error=MSG_MISSING_TITLE.format(index=i)
|
|
501
|
+
)
|
|
859
502
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
+
)
|
|
863
523
|
|
|
864
|
-
return
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
"message": f"Bulk creation of {len(tickets)} tickets queued",
|
|
868
|
-
"count": len(tickets)
|
|
869
|
-
}
|
|
524
|
+
return ResponseBuilder.status_result(
|
|
525
|
+
STATUS_COMPLETED, **ResponseBuilder.bulk_result(results)
|
|
526
|
+
)
|
|
870
527
|
|
|
871
528
|
async def _handle_bulk_update(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
872
|
-
"""Handle bulk ticket updates."""
|
|
529
|
+
"""Handle bulk ticket updates - SYNCHRONOUS."""
|
|
873
530
|
updates = params.get("updates", [])
|
|
874
531
|
if not updates:
|
|
875
|
-
return
|
|
876
|
-
|
|
877
|
-
|
|
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 = []
|
|
532
|
+
return ResponseBuilder.status_result(
|
|
533
|
+
STATUS_ERROR, error=MSG_NO_UPDATES_PROVIDED
|
|
534
|
+
)
|
|
895
535
|
|
|
536
|
+
results = []
|
|
896
537
|
for i, update_data in enumerate(updates):
|
|
897
538
|
if not update_data.get("ticket_id"):
|
|
898
|
-
return
|
|
899
|
-
|
|
900
|
-
|
|
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)
|
|
539
|
+
return ResponseBuilder.status_result(
|
|
540
|
+
STATUS_ERROR, error=MSG_MISSING_TICKET_ID.format(index=i)
|
|
541
|
+
)
|
|
909
542
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
+
)
|
|
913
552
|
|
|
914
|
-
return
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
"message": f"Bulk update of {len(updates)} tickets queued",
|
|
918
|
-
"count": len(updates)
|
|
919
|
-
}
|
|
553
|
+
return ResponseBuilder.status_result(
|
|
554
|
+
STATUS_COMPLETED, **ResponseBuilder.bulk_result(results)
|
|
555
|
+
)
|
|
920
556
|
|
|
921
557
|
async def _handle_search_hierarchy(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
922
|
-
"""Handle hierarchy-aware search."""
|
|
558
|
+
"""Handle hierarchy-aware search - SYNCHRONOUS."""
|
|
923
559
|
query = params.get("query", "")
|
|
924
560
|
include_children = params.get("include_children", True)
|
|
925
561
|
include_parents = params.get("include_parents", True)
|
|
@@ -927,9 +563,9 @@ class MCPTicketServer:
|
|
|
927
563
|
# Perform basic search
|
|
928
564
|
search_query = SearchQuery(
|
|
929
565
|
query=query,
|
|
930
|
-
state=params.get("state"),
|
|
931
|
-
priority=params.get("priority"),
|
|
932
|
-
limit=params.get("limit", 50)
|
|
566
|
+
state=TicketState(params["state"]) if params.get("state") else None,
|
|
567
|
+
priority=Priority(params["priority"]) if params.get("priority") else None,
|
|
568
|
+
limit=params.get("limit", 50),
|
|
933
569
|
)
|
|
934
570
|
|
|
935
571
|
tickets = await self.adapter.search(search_query)
|
|
@@ -937,19 +573,16 @@ class MCPTicketServer:
|
|
|
937
573
|
# Enhance with hierarchy information
|
|
938
574
|
enhanced_results = []
|
|
939
575
|
for ticket in tickets:
|
|
940
|
-
result = {
|
|
941
|
-
"ticket": ticket.model_dump(),
|
|
942
|
-
"hierarchy": {}
|
|
943
|
-
}
|
|
576
|
+
result = {"ticket": ticket.model_dump(), "hierarchy": {}}
|
|
944
577
|
|
|
945
578
|
# Add parent information
|
|
946
579
|
if include_parents:
|
|
947
|
-
if hasattr(ticket,
|
|
580
|
+
if hasattr(ticket, "parent_epic") and ticket.parent_epic:
|
|
948
581
|
parent_epic = await self.adapter.get_epic(ticket.parent_epic)
|
|
949
582
|
if parent_epic:
|
|
950
583
|
result["hierarchy"]["epic"] = parent_epic.model_dump()
|
|
951
584
|
|
|
952
|
-
if hasattr(ticket,
|
|
585
|
+
if hasattr(ticket, "parent_issue") and ticket.parent_issue:
|
|
953
586
|
parent_issue = await self.adapter.read(ticket.parent_issue)
|
|
954
587
|
if parent_issue:
|
|
955
588
|
result["hierarchy"]["parent_issue"] = parent_issue.model_dump()
|
|
@@ -958,7 +591,9 @@ class MCPTicketServer:
|
|
|
958
591
|
if include_children:
|
|
959
592
|
if ticket.ticket_type == "epic":
|
|
960
593
|
issues = await self.adapter.list_issues_by_epic(ticket.id)
|
|
961
|
-
result["hierarchy"]["issues"] = [
|
|
594
|
+
result["hierarchy"]["issues"] = [
|
|
595
|
+
issue.model_dump() for issue in issues
|
|
596
|
+
]
|
|
962
597
|
elif ticket.ticket_type == "issue":
|
|
963
598
|
tasks = await self.adapter.list_tasks_by_issue(ticket.id)
|
|
964
599
|
result["hierarchy"]["tasks"] = [task.model_dump() for task in tasks]
|
|
@@ -966,9 +601,10 @@ class MCPTicketServer:
|
|
|
966
601
|
enhanced_results.append(result)
|
|
967
602
|
|
|
968
603
|
return {
|
|
604
|
+
"status": "completed",
|
|
969
605
|
"results": enhanced_results,
|
|
970
606
|
"count": len(enhanced_results),
|
|
971
|
-
"query": query
|
|
607
|
+
"query": query,
|
|
972
608
|
}
|
|
973
609
|
|
|
974
610
|
async def _handle_attach(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -981,14 +617,17 @@ class MCPTicketServer:
|
|
|
981
617
|
"ticket_id": params.get("ticket_id"),
|
|
982
618
|
"details": {
|
|
983
619
|
"reason": "File attachments require adapter-specific implementation",
|
|
984
|
-
"alternatives": [
|
|
985
|
-
|
|
620
|
+
"alternatives": [
|
|
621
|
+
"Add file URLs in comments",
|
|
622
|
+
"Use external file storage",
|
|
623
|
+
],
|
|
624
|
+
},
|
|
986
625
|
}
|
|
987
626
|
|
|
988
|
-
async def _handle_list_attachments(self, params: dict[str, Any]) ->
|
|
627
|
+
async def _handle_list_attachments(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
989
628
|
"""Handle listing ticket attachments."""
|
|
990
629
|
# Note: This is a placeholder for attachment functionality
|
|
991
|
-
return []
|
|
630
|
+
return {"status": "completed", "attachments": []}
|
|
992
631
|
|
|
993
632
|
async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
994
633
|
"""Handle PR creation for a ticket."""
|
|
@@ -1143,8 +782,8 @@ class MCPTicketServer:
|
|
|
1143
782
|
|
|
1144
783
|
"""
|
|
1145
784
|
return {
|
|
1146
|
-
"protocolVersion":
|
|
1147
|
-
"serverInfo": {"name":
|
|
785
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
786
|
+
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
1148
787
|
"capabilities": {"tools": {"listChanged": False}},
|
|
1149
788
|
}
|
|
1150
789
|
|
|
@@ -1160,219 +799,47 @@ class MCPTicketServer:
|
|
|
1160
799
|
"type": "object",
|
|
1161
800
|
"properties": {
|
|
1162
801
|
"title": {"type": "string", "description": "Epic title"},
|
|
1163
|
-
"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
|
|
1321
|
-
{
|
|
1322
|
-
"name": "ticket_create_pr",
|
|
1323
|
-
"description": "Create a GitHub PR linked to a ticket",
|
|
1324
|
-
"inputSchema": {
|
|
1325
|
-
"type": "object",
|
|
1326
|
-
"properties": {
|
|
1327
|
-
"ticket_id": {
|
|
1328
|
-
"type": "string",
|
|
1329
|
-
"description": "Ticket ID to link the PR to",
|
|
1330
|
-
},
|
|
1331
|
-
"base_branch": {
|
|
1332
|
-
"type": "string",
|
|
1333
|
-
"description": "Target branch for the PR",
|
|
1334
|
-
"default": "main",
|
|
1335
|
-
},
|
|
1336
|
-
"head_branch": {
|
|
802
|
+
"description": {
|
|
1337
803
|
"type": "string",
|
|
1338
|
-
"description": "
|
|
804
|
+
"description": "Epic description",
|
|
1339
805
|
},
|
|
1340
|
-
"
|
|
806
|
+
"target_date": {
|
|
1341
807
|
"type": "string",
|
|
1342
|
-
"description": "
|
|
808
|
+
"description": "Target completion date (ISO format)",
|
|
1343
809
|
},
|
|
1344
|
-
"
|
|
810
|
+
"lead_id": {
|
|
1345
811
|
"type": "string",
|
|
1346
|
-
"description": "
|
|
812
|
+
"description": "Epic lead/owner ID",
|
|
1347
813
|
},
|
|
1348
|
-
"
|
|
1349
|
-
"type": "
|
|
1350
|
-
"
|
|
1351
|
-
"
|
|
814
|
+
"child_issues": {
|
|
815
|
+
"type": "array",
|
|
816
|
+
"items": {"type": "string"},
|
|
817
|
+
"description": "Initial child issue IDs",
|
|
1352
818
|
},
|
|
1353
819
|
},
|
|
1354
|
-
"required": ["
|
|
820
|
+
"required": ["title"],
|
|
1355
821
|
},
|
|
1356
822
|
},
|
|
1357
|
-
# Standard Ticket Operations
|
|
1358
823
|
{
|
|
1359
|
-
"name": "
|
|
1360
|
-
"description": "
|
|
824
|
+
"name": "epic_list",
|
|
825
|
+
"description": "List all epics",
|
|
1361
826
|
"inputSchema": {
|
|
1362
827
|
"type": "object",
|
|
1363
828
|
"properties": {
|
|
1364
|
-
"
|
|
1365
|
-
"type": "
|
|
1366
|
-
"
|
|
829
|
+
"limit": {
|
|
830
|
+
"type": "integer",
|
|
831
|
+
"default": 10,
|
|
832
|
+
"description": "Maximum number of epics to return",
|
|
1367
833
|
},
|
|
1368
|
-
"
|
|
1369
|
-
"type": "
|
|
1370
|
-
"
|
|
834
|
+
"offset": {
|
|
835
|
+
"type": "integer",
|
|
836
|
+
"default": 0,
|
|
837
|
+
"description": "Number of epics to skip",
|
|
1371
838
|
},
|
|
1372
839
|
},
|
|
1373
|
-
"required": ["ticket_id", "pr_url"],
|
|
1374
840
|
},
|
|
1375
841
|
},
|
|
842
|
+
# ... (rest of the tools list)
|
|
1376
843
|
{
|
|
1377
844
|
"name": "ticket_create",
|
|
1378
845
|
"description": "Create a new ticket",
|
|
@@ -1394,95 +861,6 @@ class MCPTicketServer:
|
|
|
1394
861
|
"required": ["title"],
|
|
1395
862
|
},
|
|
1396
863
|
},
|
|
1397
|
-
{
|
|
1398
|
-
"name": "ticket_list",
|
|
1399
|
-
"description": "List tickets",
|
|
1400
|
-
"inputSchema": {
|
|
1401
|
-
"type": "object",
|
|
1402
|
-
"properties": {
|
|
1403
|
-
"limit": {"type": "integer", "default": 10},
|
|
1404
|
-
"state": {"type": "string"},
|
|
1405
|
-
"priority": {"type": "string"},
|
|
1406
|
-
},
|
|
1407
|
-
},
|
|
1408
|
-
},
|
|
1409
|
-
{
|
|
1410
|
-
"name": "ticket_update",
|
|
1411
|
-
"description": "Update a ticket",
|
|
1412
|
-
"inputSchema": {
|
|
1413
|
-
"type": "object",
|
|
1414
|
-
"properties": {
|
|
1415
|
-
"ticket_id": {"type": "string", "description": "Ticket ID"},
|
|
1416
|
-
"updates": {
|
|
1417
|
-
"type": "object",
|
|
1418
|
-
"description": "Fields to update",
|
|
1419
|
-
},
|
|
1420
|
-
},
|
|
1421
|
-
"required": ["ticket_id", "updates"],
|
|
1422
|
-
},
|
|
1423
|
-
},
|
|
1424
|
-
{
|
|
1425
|
-
"name": "ticket_transition",
|
|
1426
|
-
"description": "Change ticket state",
|
|
1427
|
-
"inputSchema": {
|
|
1428
|
-
"type": "object",
|
|
1429
|
-
"properties": {
|
|
1430
|
-
"ticket_id": {"type": "string"},
|
|
1431
|
-
"target_state": {"type": "string"},
|
|
1432
|
-
},
|
|
1433
|
-
"required": ["ticket_id", "target_state"],
|
|
1434
|
-
},
|
|
1435
|
-
},
|
|
1436
|
-
{
|
|
1437
|
-
"name": "ticket_search",
|
|
1438
|
-
"description": "Search tickets",
|
|
1439
|
-
"inputSchema": {
|
|
1440
|
-
"type": "object",
|
|
1441
|
-
"properties": {
|
|
1442
|
-
"query": {"type": "string"},
|
|
1443
|
-
"state": {"type": "string"},
|
|
1444
|
-
"priority": {"type": "string"},
|
|
1445
|
-
"limit": {"type": "integer", "default": 10},
|
|
1446
|
-
},
|
|
1447
|
-
},
|
|
1448
|
-
},
|
|
1449
|
-
{
|
|
1450
|
-
"name": "ticket_status",
|
|
1451
|
-
"description": "Check status of queued ticket operation",
|
|
1452
|
-
"inputSchema": {
|
|
1453
|
-
"type": "object",
|
|
1454
|
-
"properties": {
|
|
1455
|
-
"queue_id": {
|
|
1456
|
-
"type": "string",
|
|
1457
|
-
"description": "Queue ID returned from create/update/delete operations",
|
|
1458
|
-
},
|
|
1459
|
-
},
|
|
1460
|
-
"required": ["queue_id"],
|
|
1461
|
-
},
|
|
1462
|
-
},
|
|
1463
|
-
# System diagnostics tools
|
|
1464
|
-
{
|
|
1465
|
-
"name": "system_health",
|
|
1466
|
-
"description": "Quick system health check - shows configuration, queue worker, and failure rates",
|
|
1467
|
-
"inputSchema": {
|
|
1468
|
-
"type": "object",
|
|
1469
|
-
"properties": {},
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
{
|
|
1473
|
-
"name": "system_diagnose",
|
|
1474
|
-
"description": "Comprehensive system diagnostics - detailed analysis of all components",
|
|
1475
|
-
"inputSchema": {
|
|
1476
|
-
"type": "object",
|
|
1477
|
-
"properties": {
|
|
1478
|
-
"include_logs": {
|
|
1479
|
-
"type": "boolean",
|
|
1480
|
-
"default": False,
|
|
1481
|
-
"description": "Include recent log analysis in diagnosis",
|
|
1482
|
-
},
|
|
1483
|
-
},
|
|
1484
|
-
},
|
|
1485
|
-
},
|
|
1486
864
|
]
|
|
1487
865
|
}
|
|
1488
866
|
|
|
@@ -1535,13 +913,6 @@ class MCPTicketServer:
|
|
|
1535
913
|
result = await self._handle_transition(arguments)
|
|
1536
914
|
elif tool_name == "ticket_search":
|
|
1537
915
|
result = await self._handle_search(arguments)
|
|
1538
|
-
elif tool_name == "ticket_status":
|
|
1539
|
-
result = await self._handle_queue_status(arguments)
|
|
1540
|
-
# System diagnostics
|
|
1541
|
-
elif tool_name == "system_health":
|
|
1542
|
-
result = await self._handle_system_health(arguments)
|
|
1543
|
-
elif tool_name == "system_diagnose":
|
|
1544
|
-
result = await self._handle_system_diagnose(arguments)
|
|
1545
916
|
# PR integration
|
|
1546
917
|
elif tool_name == "ticket_create_pr":
|
|
1547
918
|
result = await self._handle_create_pr(arguments)
|
|
@@ -1615,8 +986,8 @@ class MCPTicketServer:
|
|
|
1615
986
|
sys.stdout.flush()
|
|
1616
987
|
|
|
1617
988
|
except json.JSONDecodeError as e:
|
|
1618
|
-
error_response =
|
|
1619
|
-
None,
|
|
989
|
+
error_response = ResponseBuilder.error(
|
|
990
|
+
None, ERROR_PARSE, f"Parse error: {str(e)}"
|
|
1620
991
|
)
|
|
1621
992
|
sys.stdout.write(json.dumps(error_response) + "\n")
|
|
1622
993
|
sys.stdout.flush()
|
|
@@ -1652,14 +1023,13 @@ async def main():
|
|
|
1652
1023
|
# Load configuration
|
|
1653
1024
|
import json
|
|
1654
1025
|
import logging
|
|
1655
|
-
import os
|
|
1656
1026
|
from pathlib import Path
|
|
1657
1027
|
|
|
1658
1028
|
logger = logging.getLogger(__name__)
|
|
1659
1029
|
|
|
1660
1030
|
# Initialize defaults
|
|
1661
1031
|
adapter_type = "aitrackdown"
|
|
1662
|
-
adapter_config = {"base_path":
|
|
1032
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1663
1033
|
|
|
1664
1034
|
# Priority 1: Check .env files (highest priority for MCP)
|
|
1665
1035
|
env_config = _load_env_configuration()
|
|
@@ -1703,12 +1073,12 @@ async def main():
|
|
|
1703
1073
|
except (OSError, json.JSONDecodeError) as e:
|
|
1704
1074
|
logger.warning(f"Could not load project config: {e}, using defaults")
|
|
1705
1075
|
adapter_type = "aitrackdown"
|
|
1706
|
-
adapter_config = {"base_path":
|
|
1076
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1707
1077
|
else:
|
|
1708
1078
|
# Priority 3: Default to aitrackdown
|
|
1709
1079
|
logger.info("No configuration found, defaulting to aitrackdown adapter")
|
|
1710
1080
|
adapter_type = "aitrackdown"
|
|
1711
|
-
adapter_config = {"base_path":
|
|
1081
|
+
adapter_config = {"base_path": DEFAULT_BASE_PATH}
|
|
1712
1082
|
|
|
1713
1083
|
# Log final configuration for debugging
|
|
1714
1084
|
logger.info(f"Starting MCP server with adapter: {adapter_type}")
|
|
@@ -1726,6 +1096,7 @@ def _load_env_configuration() -> Optional[dict[str, Any]]:
|
|
|
1726
1096
|
|
|
1727
1097
|
Returns:
|
|
1728
1098
|
Dictionary with 'adapter_type' and 'adapter_config' keys, or None if no config found
|
|
1099
|
+
|
|
1729
1100
|
"""
|
|
1730
1101
|
from pathlib import Path
|
|
1731
1102
|
|
|
@@ -1738,11 +1109,11 @@ def _load_env_configuration() -> Optional[dict[str, Any]]:
|
|
|
1738
1109
|
if env_path.exists():
|
|
1739
1110
|
try:
|
|
1740
1111
|
# Parse .env file manually to avoid external dependencies
|
|
1741
|
-
with open(env_path
|
|
1112
|
+
with open(env_path) as f:
|
|
1742
1113
|
for line in f:
|
|
1743
1114
|
line = line.strip()
|
|
1744
|
-
if line and not line.startswith(
|
|
1745
|
-
key, value = line.split(
|
|
1115
|
+
if line and not line.startswith("#") and "=" in line:
|
|
1116
|
+
key, value = line.split("=", 1)
|
|
1746
1117
|
key = key.strip()
|
|
1747
1118
|
value = value.strip().strip('"').strip("'")
|
|
1748
1119
|
if value: # Only add non-empty values
|
|
@@ -1772,13 +1143,12 @@ def _load_env_configuration() -> Optional[dict[str, Any]]:
|
|
|
1772
1143
|
if not adapter_config:
|
|
1773
1144
|
return None
|
|
1774
1145
|
|
|
1775
|
-
return {
|
|
1776
|
-
"adapter_type": adapter_type,
|
|
1777
|
-
"adapter_config": adapter_config
|
|
1778
|
-
}
|
|
1146
|
+
return {"adapter_type": adapter_type, "adapter_config": adapter_config}
|
|
1779
1147
|
|
|
1780
1148
|
|
|
1781
|
-
def _build_adapter_config_from_env_vars(
|
|
1149
|
+
def _build_adapter_config_from_env_vars(
|
|
1150
|
+
adapter_type: str, env_vars: dict[str, str]
|
|
1151
|
+
) -> dict[str, Any]:
|
|
1782
1152
|
"""Build adapter configuration from parsed environment variables.
|
|
1783
1153
|
|
|
1784
1154
|
Args:
|
|
@@ -1787,6 +1157,7 @@ def _build_adapter_config_from_env_vars(adapter_type: str, env_vars: dict[str, s
|
|
|
1787
1157
|
|
|
1788
1158
|
Returns:
|
|
1789
1159
|
Dictionary of adapter configuration
|
|
1160
|
+
|
|
1790
1161
|
"""
|
|
1791
1162
|
config = {}
|
|
1792
1163
|
|
|
@@ -1823,7 +1194,7 @@ def _build_adapter_config_from_env_vars(adapter_type: str, env_vars: dict[str, s
|
|
|
1823
1194
|
|
|
1824
1195
|
elif adapter_type == "aitrackdown":
|
|
1825
1196
|
# AITrackdown adapter configuration
|
|
1826
|
-
base_path = env_vars.get("MCP_TICKETER_BASE_PATH",
|
|
1197
|
+
base_path = env_vars.get("MCP_TICKETER_BASE_PATH", DEFAULT_BASE_PATH)
|
|
1827
1198
|
config["base_path"] = base_path
|
|
1828
1199
|
config["auto_create_dirs"] = True
|
|
1829
1200
|
|
|
@@ -1834,197 +1205,5 @@ def _build_adapter_config_from_env_vars(adapter_type: str, env_vars: dict[str, s
|
|
|
1834
1205
|
return config
|
|
1835
1206
|
|
|
1836
1207
|
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
# Add diagnostic handler methods to MCPTicketServer class
|
|
1841
|
-
async def _handle_system_health(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
1842
|
-
"""Handle system health check."""
|
|
1843
|
-
from ..cli.diagnostics import SystemDiagnostics
|
|
1844
|
-
|
|
1845
|
-
try:
|
|
1846
|
-
diagnostics = SystemDiagnostics()
|
|
1847
|
-
|
|
1848
|
-
# Quick health checks
|
|
1849
|
-
health_status = {
|
|
1850
|
-
"overall_status": "healthy",
|
|
1851
|
-
"components": {},
|
|
1852
|
-
"issues": [],
|
|
1853
|
-
"warnings": [],
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
# Check configuration
|
|
1857
|
-
try:
|
|
1858
|
-
from ..core.config import get_config
|
|
1859
|
-
config = get_config()
|
|
1860
|
-
adapters = config.get_enabled_adapters()
|
|
1861
|
-
if adapters:
|
|
1862
|
-
health_status["components"]["configuration"] = {
|
|
1863
|
-
"status": "healthy",
|
|
1864
|
-
"adapters_count": len(adapters),
|
|
1865
|
-
}
|
|
1866
|
-
else:
|
|
1867
|
-
health_status["components"]["configuration"] = {
|
|
1868
|
-
"status": "failed",
|
|
1869
|
-
"error": "No adapters configured",
|
|
1870
|
-
}
|
|
1871
|
-
health_status["issues"].append("No adapters configured")
|
|
1872
|
-
health_status["overall_status"] = "critical"
|
|
1873
|
-
except Exception as e:
|
|
1874
|
-
health_status["components"]["configuration"] = {
|
|
1875
|
-
"status": "failed",
|
|
1876
|
-
"error": str(e),
|
|
1877
|
-
}
|
|
1878
|
-
health_status["issues"].append(f"Configuration error: {str(e)}")
|
|
1879
|
-
health_status["overall_status"] = "critical"
|
|
1880
|
-
|
|
1881
|
-
# Check queue system
|
|
1882
|
-
try:
|
|
1883
|
-
from ..queue.manager import WorkerManager
|
|
1884
|
-
worker_manager = WorkerManager()
|
|
1885
|
-
worker_status = worker_manager.get_status()
|
|
1886
|
-
stats = worker_manager.queue.get_stats()
|
|
1887
|
-
|
|
1888
|
-
total = stats.get("total", 0)
|
|
1889
|
-
failed = stats.get("failed", 0)
|
|
1890
|
-
failure_rate = (failed / total * 100) if total > 0 else 0
|
|
1891
|
-
|
|
1892
|
-
queue_health = {
|
|
1893
|
-
"status": "healthy",
|
|
1894
|
-
"worker_running": worker_status.get("running", False),
|
|
1895
|
-
"worker_pid": worker_status.get("pid"),
|
|
1896
|
-
"failure_rate": failure_rate,
|
|
1897
|
-
"total_processed": total,
|
|
1898
|
-
"failed_items": failed,
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
if not worker_status.get("running", False):
|
|
1902
|
-
queue_health["status"] = "failed"
|
|
1903
|
-
health_status["issues"].append("Queue worker not running")
|
|
1904
|
-
health_status["overall_status"] = "critical"
|
|
1905
|
-
elif failure_rate > 50:
|
|
1906
|
-
queue_health["status"] = "degraded"
|
|
1907
|
-
health_status["issues"].append(f"High queue failure rate: {failure_rate:.1f}%")
|
|
1908
|
-
health_status["overall_status"] = "critical"
|
|
1909
|
-
elif failure_rate > 20:
|
|
1910
|
-
queue_health["status"] = "warning"
|
|
1911
|
-
health_status["warnings"].append(f"Elevated queue failure rate: {failure_rate:.1f}%")
|
|
1912
|
-
if health_status["overall_status"] == "healthy":
|
|
1913
|
-
health_status["overall_status"] = "warning"
|
|
1914
|
-
|
|
1915
|
-
health_status["components"]["queue_system"] = queue_health
|
|
1916
|
-
|
|
1917
|
-
except Exception as e:
|
|
1918
|
-
health_status["components"]["queue_system"] = {
|
|
1919
|
-
"status": "failed",
|
|
1920
|
-
"error": str(e),
|
|
1921
|
-
}
|
|
1922
|
-
health_status["issues"].append(f"Queue system error: {str(e)}")
|
|
1923
|
-
health_status["overall_status"] = "critical"
|
|
1924
|
-
|
|
1925
|
-
return {
|
|
1926
|
-
"content": [
|
|
1927
|
-
{
|
|
1928
|
-
"type": "text",
|
|
1929
|
-
"text": f"System Health Status: {health_status['overall_status'].upper()}\n\n" +
|
|
1930
|
-
f"Configuration: {health_status['components'].get('configuration', {}).get('status', 'unknown')}\n" +
|
|
1931
|
-
f"Queue System: {health_status['components'].get('queue_system', {}).get('status', 'unknown')}\n\n" +
|
|
1932
|
-
f"Issues: {len(health_status['issues'])}\n" +
|
|
1933
|
-
f"Warnings: {len(health_status['warnings'])}\n\n" +
|
|
1934
|
-
(f"Critical Issues:\n" + "\n".join(f"• {issue}" for issue in health_status['issues']) + "\n\n" if health_status['issues'] else "") +
|
|
1935
|
-
(f"Warnings:\n" + "\n".join(f"• {warning}" for warning in health_status['warnings']) + "\n\n" if health_status['warnings'] else "") +
|
|
1936
|
-
"For detailed diagnosis, use system_diagnose tool.",
|
|
1937
|
-
}
|
|
1938
|
-
],
|
|
1939
|
-
"isError": health_status["overall_status"] == "critical",
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
except Exception as e:
|
|
1943
|
-
return {
|
|
1944
|
-
"content": [
|
|
1945
|
-
{
|
|
1946
|
-
"type": "text",
|
|
1947
|
-
"text": f"Health check failed: {str(e)}",
|
|
1948
|
-
}
|
|
1949
|
-
],
|
|
1950
|
-
"isError": True,
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
async def _handle_system_diagnose(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
1955
|
-
"""Handle comprehensive system diagnosis."""
|
|
1956
|
-
from ..cli.diagnostics import SystemDiagnostics
|
|
1957
|
-
|
|
1958
|
-
try:
|
|
1959
|
-
diagnostics = SystemDiagnostics()
|
|
1960
|
-
report = await diagnostics.run_full_diagnosis()
|
|
1961
|
-
|
|
1962
|
-
# Format report for MCP response
|
|
1963
|
-
summary = f"""System Diagnosis Report
|
|
1964
|
-
Generated: {report['timestamp']}
|
|
1965
|
-
Version: {report['version']}
|
|
1966
|
-
|
|
1967
|
-
OVERALL STATUS: {
|
|
1968
|
-
'CRITICAL' if diagnostics.issues else
|
|
1969
|
-
'WARNING' if diagnostics.warnings else
|
|
1970
|
-
'HEALTHY'
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
COMPONENT STATUS:
|
|
1974
|
-
• Configuration: {len(report['configuration']['issues'])} issues
|
|
1975
|
-
• Adapters: {report['adapters']['failed_adapters']}/{report['adapters']['total_adapters']} failed
|
|
1976
|
-
• Queue System: {report['queue_system']['health_score']}/100 health score
|
|
1977
|
-
|
|
1978
|
-
STATISTICS:
|
|
1979
|
-
• Successes: {len(diagnostics.successes)}
|
|
1980
|
-
• Warnings: {len(diagnostics.warnings)}
|
|
1981
|
-
• Critical Issues: {len(diagnostics.issues)}
|
|
1982
|
-
|
|
1983
|
-
"""
|
|
1984
|
-
|
|
1985
|
-
if diagnostics.issues:
|
|
1986
|
-
summary += "CRITICAL ISSUES:\n"
|
|
1987
|
-
for issue in diagnostics.issues:
|
|
1988
|
-
summary += f"• {issue}\n"
|
|
1989
|
-
summary += "\n"
|
|
1990
|
-
|
|
1991
|
-
if diagnostics.warnings:
|
|
1992
|
-
summary += "WARNINGS:\n"
|
|
1993
|
-
for warning in diagnostics.warnings:
|
|
1994
|
-
summary += f"• {warning}\n"
|
|
1995
|
-
summary += "\n"
|
|
1996
|
-
|
|
1997
|
-
if report['recommendations']:
|
|
1998
|
-
summary += "RECOMMENDATIONS:\n"
|
|
1999
|
-
for rec in report['recommendations']:
|
|
2000
|
-
summary += f"{rec}\n"
|
|
2001
|
-
|
|
2002
|
-
return {
|
|
2003
|
-
"content": [
|
|
2004
|
-
{
|
|
2005
|
-
"type": "text",
|
|
2006
|
-
"text": summary,
|
|
2007
|
-
}
|
|
2008
|
-
],
|
|
2009
|
-
"isError": bool(diagnostics.issues),
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
except Exception as e:
|
|
2013
|
-
return {
|
|
2014
|
-
"content": [
|
|
2015
|
-
{
|
|
2016
|
-
"type": "text",
|
|
2017
|
-
"text": f"System diagnosis failed: {str(e)}",
|
|
2018
|
-
}
|
|
2019
|
-
],
|
|
2020
|
-
"isError": True,
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
# Monkey patch the methods onto the class
|
|
2025
|
-
MCPTicketServer._handle_system_health = _handle_system_health
|
|
2026
|
-
MCPTicketServer._handle_system_diagnose = _handle_system_diagnose
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
1208
|
if __name__ == "__main__":
|
|
2030
1209
|
asyncio.run(main())
|