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