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.

Files changed (41) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/adapters/aitrackdown.py +164 -36
  3. mcp_ticketer/adapters/github.py +11 -8
  4. mcp_ticketer/adapters/jira.py +29 -28
  5. mcp_ticketer/adapters/linear/__init__.py +1 -1
  6. mcp_ticketer/adapters/linear/adapter.py +105 -104
  7. mcp_ticketer/adapters/linear/client.py +78 -59
  8. mcp_ticketer/adapters/linear/mappers.py +93 -73
  9. mcp_ticketer/adapters/linear/queries.py +28 -7
  10. mcp_ticketer/adapters/linear/types.py +67 -60
  11. mcp_ticketer/adapters/linear.py +2 -2
  12. mcp_ticketer/cli/adapter_diagnostics.py +87 -52
  13. mcp_ticketer/cli/codex_configure.py +6 -6
  14. mcp_ticketer/cli/diagnostics.py +180 -88
  15. mcp_ticketer/cli/linear_commands.py +156 -113
  16. mcp_ticketer/cli/main.py +153 -82
  17. mcp_ticketer/cli/simple_health.py +74 -51
  18. mcp_ticketer/cli/utils.py +15 -10
  19. mcp_ticketer/core/config.py +23 -19
  20. mcp_ticketer/core/env_discovery.py +5 -4
  21. mcp_ticketer/core/env_loader.py +114 -91
  22. mcp_ticketer/core/exceptions.py +22 -20
  23. mcp_ticketer/core/models.py +9 -0
  24. mcp_ticketer/core/project_config.py +1 -1
  25. mcp_ticketer/mcp/constants.py +58 -0
  26. mcp_ticketer/mcp/dto.py +195 -0
  27. mcp_ticketer/mcp/response_builder.py +206 -0
  28. mcp_ticketer/mcp/server.py +361 -1182
  29. mcp_ticketer/queue/health_monitor.py +166 -135
  30. mcp_ticketer/queue/manager.py +70 -19
  31. mcp_ticketer/queue/queue.py +24 -5
  32. mcp_ticketer/queue/run_worker.py +1 -1
  33. mcp_ticketer/queue/ticket_registry.py +203 -145
  34. mcp_ticketer/queue/worker.py +79 -43
  35. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/METADATA +1 -1
  36. mcp_ticketer-0.3.3.dist-info/RECORD +62 -0
  37. mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
  38. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/WHEEL +0 -0
  39. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/entry_points.txt +0 -0
  40. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/licenses/LICENSE +0 -0
  41. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/top_level.txt +0 -0
@@ -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.adapter = AdapterRegistry.get_adapter(
51
- adapter_type, config or {"base_path": ".aitrackdown"}
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 self._error_response(
132
- request_id, -32601, f"Method not found: {method}"
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": "2.0", "result": result, "id": request_id}
165
+ return {"jsonrpc": JSONRPC_VERSION, "result": result, "id": request_id}
136
166
 
137
167
  except Exception as e:
138
- return self._error_response(request_id, -32603, f"Internal error: {str(e)}")
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 ticket creation."""
162
- # Check queue health before proceeding
163
- health_monitor = QueueHealthMonitor()
164
- health = health_monitor.check_health()
165
-
166
- # If queue is in critical state, try auto-repair
167
- if health["status"] == HealthStatus.CRITICAL:
168
- repair_result = health_monitor.auto_repair()
169
- # Re-check health after repair
170
- health = health_monitor.check_health()
171
-
172
- # If still critical, return error immediately
173
- if health["status"] == HealthStatus.CRITICAL:
174
- critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
175
- return {
176
- "status": "error",
177
- "error": "Queue system is in critical state",
178
- "details": {
179
- "health_status": health["status"],
180
- "critical_issues": critical_alerts,
181
- "repair_attempted": repair_result["actions_taken"]
182
- }
183
- }
184
-
185
- # Queue the operation
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
- # Start worker if needed
202
- manager = WorkerManager()
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
- async def _handle_update(self, params: dict[str, Any]) -> dict[str, Any]:
294
- """Handle ticket update."""
295
- # Queue the operation
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
- # Start worker if needed
307
- manager = WorkerManager()
308
- manager.start_if_needed()
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
- if not item:
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
- # If completed, return with ticket ID
327
- if item.status == QueueStatus.COMPLETED:
328
- response = {
329
- "queue_id": queue_id,
330
- "status": "completed",
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
- # Add result data if available
335
- if item.result:
336
- if item.result.get("id"):
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
- response["message"] = (
341
- f"Ticket updated successfully: {response['ticket_id']}"
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
- # If failed, return error
346
- if item.status == QueueStatus.FAILED:
347
- return {
348
- "queue_id": queue_id,
349
- "status": "failed",
350
- "error": item.error_message or "Ticket update failed",
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
- # Check timeout
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
- # Wait before next poll
365
- await asyncio.sleep(poll_interval)
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
- async def _handle_delete(self, params: dict[str, Any]) -> dict[str, Any]:
368
- """Handle ticket deletion."""
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
- # Start worker if needed
378
- manager = WorkerManager()
379
- manager.start_if_needed()
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
- "queue_id": queue_id,
383
- "status": "queued",
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]) -> list[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", 10),
391
- offset=params.get("offset", 0),
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
- async def _handle_search(self, params: dict[str, Any]) -> list[dict[str, Any]]:
397
- """Handle ticket search."""
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
- # Start worker if needed
416
- manager = WorkerManager()
417
- manager.start_if_needed()
418
-
419
- # Poll for completion with timeout
420
- max_wait_time = 30 # seconds
421
- poll_interval = 0.5 # seconds
422
- start_time = asyncio.get_event_loop().time()
423
-
424
- while True:
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
- # If completed, return with ticket ID
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
- # Add result data if available
445
- if item.result:
446
- if item.result.get("id"):
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
- response["message"] = (
451
- f"State transition completed successfully: {response['ticket_id']} → {params['target_state']}"
452
- )
453
- return response
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
- # If failed, return error
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
- # Check timeout
465
- elapsed = asyncio.get_event_loop().time() - start_time
466
- if elapsed > max_wait_time:
467
- return {
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
- # Wait before next poll
475
- await asyncio.sleep(poll_interval)
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
- # Queue the comment addition
483
- queue = Queue()
484
- queue_id = queue.add(
485
- ticket_data={
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
- # Start worker if needed
495
- manager = WorkerManager()
496
- manager.start_if_needed()
318
+ created = await self.adapter.add_comment(comment)
497
319
 
498
- return {
499
- "queue_id": queue_id,
500
- "status": "queued",
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", 10),
509
- offset=params.get("offset", 0),
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
- if item.error_message:
540
- response["error"] = item.error_message
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
- return health
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
- # Check queue health before proceeding
567
- health_monitor = QueueHealthMonitor()
568
- health = health_monitor.check_health()
569
-
570
- if health["status"] == HealthStatus.CRITICAL:
571
- repair_result = health_monitor.auto_repair()
572
- health = health_monitor.check_health()
573
-
574
- if health["status"] == HealthStatus.CRITICAL:
575
- critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
576
- return {
577
- "status": "error",
578
- "error": "Queue system is in critical state",
579
- "details": {
580
- "health_status": health["status"],
581
- "critical_issues": critical_alerts,
582
- "repair_attempted": repair_result["actions_taken"]
583
- }
584
- }
585
-
586
- # Queue the epic creation
587
- queue = Queue()
588
- epic_data = {
589
- "title": params["title"],
590
- "description": params.get("description"),
591
- "child_issues": params.get("child_issues", []),
592
- "target_date": params.get("target_date"),
593
- "lead_id": params.get("lead_id"),
594
- }
595
-
596
- queue_id = queue.add(
597
- ticket_data=epic_data,
598
- adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
599
- operation="create_epic",
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
- # Start worker if needed
603
- manager = WorkerManager()
604
- worker_started = manager.start_if_needed()
605
-
606
- if not worker_started and queue.get_pending_count() > 0:
607
- return {
608
- "status": "error",
609
- "error": "Failed to start worker process",
610
- "queue_id": queue_id,
611
- "details": {
612
- "pending_count": queue.get_pending_count(),
613
- "action": "Worker process could not be started to process queued operations"
614
- }
615
- }
354
+ # Create directly
355
+ created = await self.adapter.create(epic)
616
356
 
617
- return {
618
- "queue_id": queue_id,
619
- "status": "queued",
620
- "message": f"Epic creation queued with ID: {queue_id}",
621
- "epic_data": epic_data
622
- }
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]) -> list[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", 10),
628
- offset=params.get("offset", 0),
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
- async def _handle_epic_issues(self, params: dict[str, Any]) -> list[dict[str, Any]]:
634
- """Handle listing issues in an epic."""
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
- if health["status"] == HealthStatus.CRITICAL:
650
- critical_alerts = [alert for alert in health["alerts"] if alert["level"] == "critical"]
651
- return {
652
- "status": "error",
653
- "error": "Queue system is in critical state",
654
- "details": {
655
- "health_status": health["status"],
656
- "critical_issues": critical_alerts,
657
- "repair_attempted": repair_result["actions_taken"]
658
- }
659
- }
379
+ return ResponseBuilder.status_result(
380
+ STATUS_COMPLETED, **ResponseBuilder.issues_result(issues)
381
+ )
660
382
 
661
- # Queue the issue creation
662
- queue = Queue()
663
- issue_data = {
664
- "title": params["title"],
665
- "description": params.get("description"),
666
- "epic_id": params.get("epic_id"),
667
- "priority": params.get("priority", "medium"),
668
- "assignee": params.get("assignee"),
669
- "tags": params.get("tags", []),
670
- "estimated_hours": params.get("estimated_hours"),
671
- }
383
+ async def _handle_issue_create(self, params: dict[str, Any]) -> dict[str, Any]:
384
+ """Handle issue creation - SYNCHRONOUS with validation.
672
385
 
673
- queue_id = queue.add(
674
- ticket_data=issue_data,
675
- adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
676
- operation="create_issue",
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
- # Start worker if needed
680
- manager = WorkerManager()
681
- worker_started = manager.start_if_needed()
682
-
683
- if not worker_started and queue.get_pending_count() > 0:
684
- return {
685
- "status": "error",
686
- "error": "Failed to start worker process",
687
- "queue_id": queue_id,
688
- "details": {
689
- "pending_count": queue.get_pending_count(),
690
- "action": "Worker process could not be started to process queued operations"
691
- }
692
- }
402
+ # Create directly
403
+ created = await self.adapter.create(task)
693
404
 
694
- return {
695
- "queue_id": queue_id,
696
- "status": "queued",
697
- "message": f"Issue creation queued with ID: {queue_id}",
698
- "issue_data": issue_data
699
- }
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]) -> list[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
- queue_id = queue.add(
750
- ticket_data=task_data,
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
- # Start worker if needed
756
- manager = WorkerManager()
757
- worker_started = manager.start_if_needed()
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
- if not worker_started and queue.get_pending_count() > 0:
760
- return {
761
- "status": "error",
762
- "error": "Failed to start worker process",
763
- "queue_id": queue_id,
764
- "details": {
765
- "pending_count": queue.get_pending_count(),
766
- "action": "Worker process could not be started to process queued operations"
767
- }
768
- }
435
+ # Create directly
436
+ created = await self.adapter.create(task)
769
437
 
770
- return {
771
- "queue_id": queue_id,
772
- "status": "queued",
773
- "message": f"Task creation queued with ID: {queue_id}",
774
- "task_data": task_data
775
- }
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", 3)
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 {"error": f"Epic {epic_id} not found"}
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
- issues = await self.adapter.list_issues_by_epic(epic_id)
796
- for issue in issues:
797
- issue_node = {
798
- "issue": issue.model_dump(),
799
- "tasks": []
800
- }
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
- # Get tasks in issue if depth allows
803
- if max_depth > 2:
804
- tasks = await self.adapter.list_tasks_by_issue(issue.id)
805
- issue_node["tasks"] = [task.model_dump() for task in tasks]
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
- tree["issues"].append(issue_node)
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(limit=params.get("limit", 10))
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({"epic_id": epic.id, "max_depth": max_depth})
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 {"trees": trees}
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 {"error": "No tickets provided for bulk creation"}
826
-
827
- # Check queue health
828
- health_monitor = QueueHealthMonitor()
829
- health = health_monitor.check_health()
830
-
831
- if health["status"] == HealthStatus.CRITICAL:
832
- repair_result = health_monitor.auto_repair()
833
- health = health_monitor.check_health()
834
-
835
- if health["status"] == HealthStatus.CRITICAL:
836
- return {
837
- "status": "error",
838
- "error": "Queue system is in critical state - cannot process bulk operations",
839
- "details": {"health_status": health["status"]}
840
- }
841
-
842
- # Queue all tickets
843
- queue = Queue()
844
- queue_ids = []
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
- "status": "error",
850
- "error": f"Ticket {i} missing required 'title' field"
851
- }
852
-
853
- queue_id = queue.add(
854
- ticket_data=ticket_data,
855
- adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
856
- operation=ticket_data.get("operation", "create"),
857
- )
858
- queue_ids.append(queue_id)
499
+ return ResponseBuilder.status_result(
500
+ STATUS_ERROR, error=MSG_MISSING_TITLE.format(index=i)
501
+ )
859
502
 
860
- # Start worker if needed
861
- manager = WorkerManager()
862
- manager.start_if_needed()
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
- "queue_ids": queue_ids,
866
- "status": "queued",
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 {"error": "No updates provided for bulk operation"}
876
-
877
- # Check queue health
878
- health_monitor = QueueHealthMonitor()
879
- health = health_monitor.check_health()
880
-
881
- if health["status"] == HealthStatus.CRITICAL:
882
- repair_result = health_monitor.auto_repair()
883
- health = health_monitor.check_health()
884
-
885
- if health["status"] == HealthStatus.CRITICAL:
886
- return {
887
- "status": "error",
888
- "error": "Queue system is in critical state - cannot process bulk operations",
889
- "details": {"health_status": health["status"]}
890
- }
891
-
892
- # Queue all updates
893
- queue = Queue()
894
- queue_ids = []
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
- "status": "error",
900
- "error": f"Update {i} missing required 'ticket_id' field"
901
- }
902
-
903
- queue_id = queue.add(
904
- ticket_data=update_data,
905
- adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
906
- operation="update",
907
- )
908
- queue_ids.append(queue_id)
539
+ return ResponseBuilder.status_result(
540
+ STATUS_ERROR, error=MSG_MISSING_TICKET_ID.format(index=i)
541
+ )
909
542
 
910
- # Start worker if needed
911
- manager = WorkerManager()
912
- manager.start_if_needed()
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
- "queue_ids": queue_ids,
916
- "status": "queued",
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, 'parent_epic') and ticket.parent_epic:
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, 'parent_issue') and ticket.parent_issue:
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"] = [issue.model_dump() for issue in 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": ["Add file URLs in comments", "Use external file storage"]
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]) -> list[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": "2024-11-05",
1147
- "serverInfo": {"name": "mcp-ticketer", "version": "0.1.8"},
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": {"type": "string", "description": "Epic description"},
1164
- "target_date": {"type": "string", "description": "Target completion date (ISO format)"},
1165
- "lead_id": {"type": "string", "description": "Epic lead/owner ID"},
1166
- "child_issues": {"type": "array", "items": {"type": "string"}, "description": "Initial child issue IDs"}
1167
- },
1168
- "required": ["title"]
1169
- }
1170
- },
1171
- {
1172
- "name": "epic_list",
1173
- "description": "List all epics",
1174
- "inputSchema": {
1175
- "type": "object",
1176
- "properties": {
1177
- "limit": {"type": "integer", "default": 10, "description": "Maximum number of epics to return"},
1178
- "offset": {"type": "integer", "default": 0, "description": "Number of epics to skip"}
1179
- }
1180
- }
1181
- },
1182
- {
1183
- "name": "epic_issues",
1184
- "description": "List all issues in an epic",
1185
- "inputSchema": {
1186
- "type": "object",
1187
- "properties": {
1188
- "epic_id": {"type": "string", "description": "Epic ID to get issues for"}
1189
- },
1190
- "required": ["epic_id"]
1191
- }
1192
- },
1193
- {
1194
- "name": "issue_create",
1195
- "description": "Create a new issue (work item)",
1196
- "inputSchema": {
1197
- "type": "object",
1198
- "properties": {
1199
- "title": {"type": "string", "description": "Issue title"},
1200
- "description": {"type": "string", "description": "Issue description"},
1201
- "epic_id": {"type": "string", "description": "Parent epic ID"},
1202
- "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"], "default": "medium"},
1203
- "assignee": {"type": "string", "description": "Assignee username"},
1204
- "tags": {"type": "array", "items": {"type": "string"}, "description": "Issue tags"},
1205
- "estimated_hours": {"type": "number", "description": "Estimated hours to complete"}
1206
- },
1207
- "required": ["title"]
1208
- }
1209
- },
1210
- {
1211
- "name": "issue_tasks",
1212
- "description": "List all tasks in an issue",
1213
- "inputSchema": {
1214
- "type": "object",
1215
- "properties": {
1216
- "issue_id": {"type": "string", "description": "Issue ID to get tasks for"}
1217
- },
1218
- "required": ["issue_id"]
1219
- }
1220
- },
1221
- {
1222
- "name": "task_create",
1223
- "description": "Create a new task (sub-item under an issue)",
1224
- "inputSchema": {
1225
- "type": "object",
1226
- "properties": {
1227
- "title": {"type": "string", "description": "Task title"},
1228
- "parent_id": {"type": "string", "description": "Parent issue ID (required)"},
1229
- "description": {"type": "string", "description": "Task description"},
1230
- "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"], "default": "medium"},
1231
- "assignee": {"type": "string", "description": "Assignee username"},
1232
- "tags": {"type": "array", "items": {"type": "string"}, "description": "Task tags"},
1233
- "estimated_hours": {"type": "number", "description": "Estimated hours to complete"}
1234
- },
1235
- "required": ["title", "parent_id"]
1236
- }
1237
- },
1238
- {
1239
- "name": "hierarchy_tree",
1240
- "description": "Get hierarchy tree view of epic/issues/tasks",
1241
- "inputSchema": {
1242
- "type": "object",
1243
- "properties": {
1244
- "epic_id": {"type": "string", "description": "Specific epic ID (optional - if not provided, returns all epics)"},
1245
- "max_depth": {"type": "integer", "default": 3, "description": "Maximum depth to traverse (1=epics only, 2=epics+issues, 3=full tree)"},
1246
- "limit": {"type": "integer", "default": 10, "description": "Maximum number of epics to return (when epic_id not specified)"}
1247
- }
1248
- }
1249
- },
1250
- # Bulk Operations
1251
- {
1252
- "name": "ticket_bulk_create",
1253
- "description": "Create multiple tickets in one operation",
1254
- "inputSchema": {
1255
- "type": "object",
1256
- "properties": {
1257
- "tickets": {
1258
- "type": "array",
1259
- "items": {
1260
- "type": "object",
1261
- "properties": {
1262
- "title": {"type": "string"},
1263
- "description": {"type": "string"},
1264
- "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
1265
- "operation": {"type": "string", "enum": ["create", "create_epic", "create_issue", "create_task"], "default": "create"},
1266
- "epic_id": {"type": "string", "description": "For issues"},
1267
- "parent_id": {"type": "string", "description": "For tasks"}
1268
- },
1269
- "required": ["title"]
1270
- },
1271
- "description": "Array of tickets to create"
1272
- }
1273
- },
1274
- "required": ["tickets"]
1275
- }
1276
- },
1277
- {
1278
- "name": "ticket_bulk_update",
1279
- "description": "Update multiple tickets in one operation",
1280
- "inputSchema": {
1281
- "type": "object",
1282
- "properties": {
1283
- "updates": {
1284
- "type": "array",
1285
- "items": {
1286
- "type": "object",
1287
- "properties": {
1288
- "ticket_id": {"type": "string"},
1289
- "title": {"type": "string"},
1290
- "description": {"type": "string"},
1291
- "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
1292
- "state": {"type": "string"},
1293
- "assignee": {"type": "string"}
1294
- },
1295
- "required": ["ticket_id"]
1296
- },
1297
- "description": "Array of ticket updates"
1298
- }
1299
- },
1300
- "required": ["updates"]
1301
- }
1302
- },
1303
- # Advanced Search
1304
- {
1305
- "name": "ticket_search_hierarchy",
1306
- "description": "Search tickets with hierarchy context",
1307
- "inputSchema": {
1308
- "type": "object",
1309
- "properties": {
1310
- "query": {"type": "string", "description": "Search query"},
1311
- "state": {"type": "string", "description": "Filter by state"},
1312
- "priority": {"type": "string", "description": "Filter by priority"},
1313
- "limit": {"type": "integer", "default": 50, "description": "Maximum results"},
1314
- "include_children": {"type": "boolean", "default": True, "description": "Include child items in results"},
1315
- "include_parents": {"type": "boolean", "default": True, "description": "Include parent context in results"}
1316
- },
1317
- "required": ["query"]
1318
- }
1319
- },
1320
- # PR Integration
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": "Source branch name (auto-generated if not provided)",
804
+ "description": "Epic description",
1339
805
  },
1340
- "title": {
806
+ "target_date": {
1341
807
  "type": "string",
1342
- "description": "PR title (uses ticket title if not provided)",
808
+ "description": "Target completion date (ISO format)",
1343
809
  },
1344
- "body": {
810
+ "lead_id": {
1345
811
  "type": "string",
1346
- "description": "PR description (auto-generated with issue link if not provided)",
812
+ "description": "Epic lead/owner ID",
1347
813
  },
1348
- "draft": {
1349
- "type": "boolean",
1350
- "description": "Create as draft PR",
1351
- "default": False,
814
+ "child_issues": {
815
+ "type": "array",
816
+ "items": {"type": "string"},
817
+ "description": "Initial child issue IDs",
1352
818
  },
1353
819
  },
1354
- "required": ["ticket_id"],
820
+ "required": ["title"],
1355
821
  },
1356
822
  },
1357
- # Standard Ticket Operations
1358
823
  {
1359
- "name": "ticket_link_pr",
1360
- "description": "Link an existing PR to a ticket",
824
+ "name": "epic_list",
825
+ "description": "List all epics",
1361
826
  "inputSchema": {
1362
827
  "type": "object",
1363
828
  "properties": {
1364
- "ticket_id": {
1365
- "type": "string",
1366
- "description": "Ticket ID to link the PR to",
829
+ "limit": {
830
+ "type": "integer",
831
+ "default": 10,
832
+ "description": "Maximum number of epics to return",
1367
833
  },
1368
- "pr_url": {
1369
- "type": "string",
1370
- "description": "GitHub PR URL to link",
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 = self._error_response(
1619
- None, -32700, f"Parse error: {str(e)}"
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": ".aitrackdown"}
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": ".aitrackdown"}
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": ".aitrackdown"}
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, 'r') as f:
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('#') and '=' in line:
1745
- key, value = line.split('=', 1)
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(adapter_type: str, env_vars: dict[str, str]) -> dict[str, Any]:
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", ".aitrackdown")
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())