mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +66 -49
  5. mcp_ticketer/adapters/github.py +192 -125
  6. mcp_ticketer/adapters/hybrid.py +99 -53
  7. mcp_ticketer/adapters/jira.py +161 -151
  8. mcp_ticketer/adapters/linear.py +396 -246
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +15 -16
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +283 -298
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +11 -13
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +121 -66
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +46 -39
  21. mcp_ticketer/core/config.py +128 -92
  22. mcp_ticketer/core/env_discovery.py +69 -37
  23. mcp_ticketer/core/http_client.py +57 -40
  24. mcp_ticketer/core/mappers.py +98 -54
  25. mcp_ticketer/core/models.py +38 -24
  26. mcp_ticketer/core/project_config.py +145 -80
  27. mcp_ticketer/core/registry.py +16 -16
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +199 -145
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +30 -26
  33. mcp_ticketer/queue/queue.py +147 -85
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +55 -40
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
@@ -3,13 +3,13 @@
3
3
  import asyncio
4
4
  import json
5
5
  import sys
6
- from typing import Any, Dict, List, Optional
7
6
  from pathlib import Path
7
+ from typing import Any, Optional
8
+
8
9
  from dotenv import load_dotenv
9
10
 
10
- from ..core import Task, TicketState, Priority, AdapterRegistry
11
- from ..core.models import SearchQuery, Comment
12
- from ..adapters import AITrackdownAdapter
11
+ from ..core import AdapterRegistry
12
+ from ..core.models import SearchQuery
13
13
  from ..queue import Queue, QueueStatus, WorkerManager
14
14
 
15
15
  # Load environment variables early (prioritize .env.local)
@@ -33,20 +33,22 @@ else:
33
33
  class MCPTicketServer:
34
34
  """MCP server for ticket operations over stdio."""
35
35
 
36
- def __init__(self, adapter_type: str = "aitrackdown", config: Optional[Dict[str, Any]] = None):
36
+ def __init__(
37
+ self, adapter_type: str = "aitrackdown", config: Optional[dict[str, Any]] = None
38
+ ):
37
39
  """Initialize MCP server.
38
40
 
39
41
  Args:
40
42
  adapter_type: Type of adapter to use
41
43
  config: Adapter configuration
44
+
42
45
  """
43
46
  self.adapter = AdapterRegistry.get_adapter(
44
- adapter_type,
45
- config or {"base_path": ".aitrackdown"}
47
+ adapter_type, config or {"base_path": ".aitrackdown"}
46
48
  )
47
49
  self.running = False
48
50
 
49
- async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
51
+ async def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
50
52
  """Handle JSON-RPC request.
51
53
 
52
54
  Args:
@@ -54,6 +56,7 @@ class MCPTicketServer:
54
56
 
55
57
  Returns:
56
58
  JSON-RPC response
59
+
57
60
  """
58
61
  method = request.get("method")
59
62
  params = request.get("params", {})
@@ -92,30 +95,17 @@ class MCPTicketServer:
92
95
  result = await self._handle_tools_call(params)
93
96
  else:
94
97
  return self._error_response(
95
- request_id,
96
- -32601,
97
- f"Method not found: {method}"
98
+ request_id, -32601, f"Method not found: {method}"
98
99
  )
99
100
 
100
- return {
101
- "jsonrpc": "2.0",
102
- "result": result,
103
- "id": request_id
104
- }
101
+ return {"jsonrpc": "2.0", "result": result, "id": request_id}
105
102
 
106
103
  except Exception as e:
107
- return self._error_response(
108
- request_id,
109
- -32603,
110
- f"Internal error: {str(e)}"
111
- )
104
+ return self._error_response(request_id, -32603, f"Internal error: {str(e)}")
112
105
 
113
106
  def _error_response(
114
- self,
115
- request_id: Any,
116
- code: int,
117
- message: str
118
- ) -> Dict[str, Any]:
107
+ self, request_id: Any, code: int, message: str
108
+ ) -> dict[str, Any]:
119
109
  """Create error response.
120
110
 
121
111
  Args:
@@ -125,17 +115,15 @@ class MCPTicketServer:
125
115
 
126
116
  Returns:
127
117
  Error response
118
+
128
119
  """
129
120
  return {
130
121
  "jsonrpc": "2.0",
131
- "error": {
132
- "code": code,
133
- "message": message
134
- },
135
- "id": request_id
122
+ "error": {"code": code, "message": message},
123
+ "id": request_id,
136
124
  }
137
125
 
138
- async def _handle_create(self, params: Dict[str, Any]) -> Dict[str, Any]:
126
+ async def _handle_create(self, params: dict[str, Any]) -> dict[str, Any]:
139
127
  """Handle ticket creation."""
140
128
  # Queue the operation instead of direct execution
141
129
  queue = Queue()
@@ -150,7 +138,7 @@ class MCPTicketServer:
150
138
  queue_id = queue.add(
151
139
  ticket_data=task_data,
152
140
  adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
153
- operation="create"
141
+ operation="create",
154
142
  )
155
143
 
156
144
  # Start worker if needed
@@ -162,7 +150,7 @@ class MCPTicketServer:
162
150
  return {
163
151
  "queue_id": queue_id,
164
152
  "status": "queued",
165
- "message": f"Ticket creation queued with ID: {queue_id}"
153
+ "message": f"Ticket creation queued with ID: {queue_id}",
166
154
  }
167
155
 
168
156
  # Poll for completion with timeout (default synchronous behavior)
@@ -178,7 +166,7 @@ class MCPTicketServer:
178
166
  return {
179
167
  "queue_id": queue_id,
180
168
  "status": "error",
181
- "error": f"Queue item {queue_id} not found"
169
+ "error": f"Queue item {queue_id} not found",
182
170
  }
183
171
 
184
172
  # If completed, return with ticket ID
@@ -186,7 +174,7 @@ class MCPTicketServer:
186
174
  response = {
187
175
  "queue_id": queue_id,
188
176
  "status": "completed",
189
- "title": params["title"]
177
+ "title": params["title"],
190
178
  }
191
179
 
192
180
  # Add ticket ID and other result data if available
@@ -197,9 +185,13 @@ class MCPTicketServer:
197
185
  # Try to construct URL if we have enough information
198
186
  if response.get("ticket_id"):
199
187
  # This is adapter-specific, but we can add URL generation later
200
- response["id"] = response["ticket_id"] # Also include as "id" for compatibility
188
+ response["id"] = response[
189
+ "ticket_id"
190
+ ] # Also include as "id" for compatibility
201
191
 
202
- response["message"] = f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
192
+ response["message"] = (
193
+ f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
194
+ )
203
195
  return response
204
196
 
205
197
  # If failed, return error
@@ -208,7 +200,7 @@ class MCPTicketServer:
208
200
  "queue_id": queue_id,
209
201
  "status": "failed",
210
202
  "error": item.error_message or "Ticket creation failed",
211
- "title": params["title"]
203
+ "title": params["title"],
212
204
  }
213
205
 
214
206
  # Check timeout
@@ -218,18 +210,18 @@ class MCPTicketServer:
218
210
  "queue_id": queue_id,
219
211
  "status": "timeout",
220
212
  "message": f"Ticket creation timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
221
- "title": params["title"]
213
+ "title": params["title"],
222
214
  }
223
215
 
224
216
  # Wait before next poll
225
217
  await asyncio.sleep(poll_interval)
226
218
 
227
- async def _handle_read(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
219
+ async def _handle_read(self, params: dict[str, Any]) -> Optional[dict[str, Any]]:
228
220
  """Handle ticket read."""
229
221
  ticket = await self.adapter.read(params["ticket_id"])
230
222
  return ticket.model_dump() if ticket else None
231
223
 
232
- async def _handle_update(self, params: Dict[str, Any]) -> Dict[str, Any]:
224
+ async def _handle_update(self, params: dict[str, Any]) -> dict[str, Any]:
233
225
  """Handle ticket update."""
234
226
  # Queue the operation
235
227
  queue = Queue()
@@ -239,7 +231,7 @@ class MCPTicketServer:
239
231
  queue_id = queue.add(
240
232
  ticket_data=updates,
241
233
  adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
242
- operation="update"
234
+ operation="update",
243
235
  )
244
236
 
245
237
  # Start worker if needed
@@ -259,7 +251,7 @@ class MCPTicketServer:
259
251
  return {
260
252
  "queue_id": queue_id,
261
253
  "status": "error",
262
- "error": f"Queue item {queue_id} not found"
254
+ "error": f"Queue item {queue_id} not found",
263
255
  }
264
256
 
265
257
  # If completed, return with ticket ID
@@ -267,7 +259,7 @@ class MCPTicketServer:
267
259
  response = {
268
260
  "queue_id": queue_id,
269
261
  "status": "completed",
270
- "ticket_id": params["ticket_id"]
262
+ "ticket_id": params["ticket_id"],
271
263
  }
272
264
 
273
265
  # Add result data if available
@@ -276,7 +268,9 @@ class MCPTicketServer:
276
268
  response["ticket_id"] = item.result["id"]
277
269
  response["success"] = item.result.get("success", True)
278
270
 
279
- response["message"] = f"Ticket updated successfully: {response['ticket_id']}"
271
+ response["message"] = (
272
+ f"Ticket updated successfully: {response['ticket_id']}"
273
+ )
280
274
  return response
281
275
 
282
276
  # If failed, return error
@@ -285,7 +279,7 @@ class MCPTicketServer:
285
279
  "queue_id": queue_id,
286
280
  "status": "failed",
287
281
  "error": item.error_message or "Ticket update failed",
288
- "ticket_id": params["ticket_id"]
282
+ "ticket_id": params["ticket_id"],
289
283
  }
290
284
 
291
285
  # Check timeout
@@ -295,20 +289,20 @@ class MCPTicketServer:
295
289
  "queue_id": queue_id,
296
290
  "status": "timeout",
297
291
  "message": f"Ticket update timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
298
- "ticket_id": params["ticket_id"]
292
+ "ticket_id": params["ticket_id"],
299
293
  }
300
294
 
301
295
  # Wait before next poll
302
296
  await asyncio.sleep(poll_interval)
303
297
 
304
- async def _handle_delete(self, params: Dict[str, Any]) -> Dict[str, Any]:
298
+ async def _handle_delete(self, params: dict[str, Any]) -> dict[str, Any]:
305
299
  """Handle ticket deletion."""
306
300
  # Queue the operation
307
301
  queue = Queue()
308
302
  queue_id = queue.add(
309
303
  ticket_data={"ticket_id": params["ticket_id"]},
310
304
  adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
311
- operation="delete"
305
+ operation="delete",
312
306
  )
313
307
 
314
308
  # Start worker if needed
@@ -318,35 +312,35 @@ class MCPTicketServer:
318
312
  return {
319
313
  "queue_id": queue_id,
320
314
  "status": "queued",
321
- "message": f"Ticket deletion queued with ID: {queue_id}"
315
+ "message": f"Ticket deletion queued with ID: {queue_id}",
322
316
  }
323
317
 
324
- async def _handle_list(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
318
+ async def _handle_list(self, params: dict[str, Any]) -> list[dict[str, Any]]:
325
319
  """Handle ticket listing."""
326
320
  tickets = await self.adapter.list(
327
321
  limit=params.get("limit", 10),
328
322
  offset=params.get("offset", 0),
329
- filters=params.get("filters")
323
+ filters=params.get("filters"),
330
324
  )
331
325
  return [ticket.model_dump() for ticket in tickets]
332
326
 
333
- async def _handle_search(self, params: Dict[str, Any]) -> List[Dict[str, Any]]:
327
+ async def _handle_search(self, params: dict[str, Any]) -> list[dict[str, Any]]:
334
328
  """Handle ticket search."""
335
329
  query = SearchQuery(**params)
336
330
  tickets = await self.adapter.search(query)
337
331
  return [ticket.model_dump() for ticket in tickets]
338
332
 
339
- async def _handle_transition(self, params: Dict[str, Any]) -> Dict[str, Any]:
333
+ async def _handle_transition(self, params: dict[str, Any]) -> dict[str, Any]:
340
334
  """Handle state transition."""
341
335
  # Queue the operation
342
336
  queue = Queue()
343
337
  queue_id = queue.add(
344
338
  ticket_data={
345
339
  "ticket_id": params["ticket_id"],
346
- "state": params["target_state"]
340
+ "state": params["target_state"],
347
341
  },
348
342
  adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
349
- operation="transition"
343
+ operation="transition",
350
344
  )
351
345
 
352
346
  # Start worker if needed
@@ -366,7 +360,7 @@ class MCPTicketServer:
366
360
  return {
367
361
  "queue_id": queue_id,
368
362
  "status": "error",
369
- "error": f"Queue item {queue_id} not found"
363
+ "error": f"Queue item {queue_id} not found",
370
364
  }
371
365
 
372
366
  # If completed, return with ticket ID
@@ -375,7 +369,7 @@ class MCPTicketServer:
375
369
  "queue_id": queue_id,
376
370
  "status": "completed",
377
371
  "ticket_id": params["ticket_id"],
378
- "state": params["target_state"]
372
+ "state": params["target_state"],
379
373
  }
380
374
 
381
375
  # Add result data if available
@@ -384,7 +378,9 @@ class MCPTicketServer:
384
378
  response["ticket_id"] = item.result["id"]
385
379
  response["success"] = item.result.get("success", True)
386
380
 
387
- response["message"] = f"State transition completed successfully: {response['ticket_id']} → {params['target_state']}"
381
+ response["message"] = (
382
+ f"State transition completed successfully: {response['ticket_id']} → {params['target_state']}"
383
+ )
388
384
  return response
389
385
 
390
386
  # If failed, return error
@@ -393,7 +389,7 @@ class MCPTicketServer:
393
389
  "queue_id": queue_id,
394
390
  "status": "failed",
395
391
  "error": item.error_message or "State transition failed",
396
- "ticket_id": params["ticket_id"]
392
+ "ticket_id": params["ticket_id"],
397
393
  }
398
394
 
399
395
  # Check timeout
@@ -403,13 +399,13 @@ class MCPTicketServer:
403
399
  "queue_id": queue_id,
404
400
  "status": "timeout",
405
401
  "message": f"State transition timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
406
- "ticket_id": params["ticket_id"]
402
+ "ticket_id": params["ticket_id"],
407
403
  }
408
404
 
409
405
  # Wait before next poll
410
406
  await asyncio.sleep(poll_interval)
411
407
 
412
- async def _handle_comment(self, params: Dict[str, Any]) -> Dict[str, Any]:
408
+ async def _handle_comment(self, params: dict[str, Any]) -> dict[str, Any]:
413
409
  """Handle comment operations."""
414
410
  operation = params.get("operation", "add")
415
411
 
@@ -420,10 +416,10 @@ class MCPTicketServer:
420
416
  ticket_data={
421
417
  "ticket_id": params["ticket_id"],
422
418
  "content": params["content"],
423
- "author": params.get("author")
419
+ "author": params.get("author"),
424
420
  },
425
421
  adapter=self.adapter.__class__.__name__.lower().replace("adapter", ""),
426
- operation="comment"
422
+ operation="comment",
427
423
  )
428
424
 
429
425
  # Start worker if needed
@@ -433,7 +429,7 @@ class MCPTicketServer:
433
429
  return {
434
430
  "queue_id": queue_id,
435
431
  "status": "queued",
436
- "message": f"Comment addition queued with ID: {queue_id}"
432
+ "message": f"Comment addition queued with ID: {queue_id}",
437
433
  }
438
434
 
439
435
  elif operation == "list":
@@ -441,14 +437,14 @@ class MCPTicketServer:
441
437
  comments = await self.adapter.get_comments(
442
438
  params["ticket_id"],
443
439
  limit=params.get("limit", 10),
444
- offset=params.get("offset", 0)
440
+ offset=params.get("offset", 0),
445
441
  )
446
442
  return [comment.model_dump() for comment in comments]
447
443
 
448
444
  else:
449
445
  raise ValueError(f"Unknown comment operation: {operation}")
450
446
 
451
- async def _handle_queue_status(self, params: Dict[str, Any]) -> Dict[str, Any]:
447
+ async def _handle_queue_status(self, params: dict[str, Any]) -> dict[str, Any]:
452
448
  """Check status of queued operation."""
453
449
  queue_id = params.get("queue_id")
454
450
  if not queue_id:
@@ -458,16 +454,14 @@ class MCPTicketServer:
458
454
  item = queue.get_item(queue_id)
459
455
 
460
456
  if not item:
461
- return {
462
- "error": f"Queue item not found: {queue_id}"
463
- }
457
+ return {"error": f"Queue item not found: {queue_id}"}
464
458
 
465
459
  response = {
466
460
  "queue_id": item.id,
467
461
  "status": item.status.value,
468
462
  "operation": item.operation,
469
463
  "created_at": item.created_at.isoformat(),
470
- "retry_count": item.retry_count
464
+ "retry_count": item.retry_count,
471
465
  }
472
466
 
473
467
  if item.processed_at:
@@ -481,7 +475,7 @@ class MCPTicketServer:
481
475
 
482
476
  return response
483
477
 
484
- async def _handle_create_pr(self, params: Dict[str, Any]) -> Dict[str, Any]:
478
+ async def _handle_create_pr(self, params: dict[str, Any]) -> dict[str, Any]:
485
479
  """Handle PR creation for a ticket."""
486
480
  ticket_id = params.get("ticket_id")
487
481
  if not ticket_id:
@@ -493,6 +487,7 @@ class MCPTicketServer:
493
487
  if "github" in adapter_name:
494
488
  # GitHub adapter supports direct PR creation
495
489
  from ..adapters.github import GitHubAdapter
490
+
496
491
  if isinstance(self.adapter, GitHubAdapter):
497
492
  try:
498
493
  result = await self.adapter.create_pull_request(
@@ -520,6 +515,7 @@ class MCPTicketServer:
520
515
  elif "linear" in adapter_name:
521
516
  # Linear adapter needs GitHub config for PR creation
522
517
  from ..adapters.linear import LinearAdapter
518
+
523
519
  if isinstance(self.adapter, LinearAdapter):
524
520
  # For Linear, we prepare the branch and metadata but can't create the actual PR
525
521
  # without GitHub integration configured
@@ -567,7 +563,7 @@ class MCPTicketServer:
567
563
  "ticket_id": ticket_id,
568
564
  }
569
565
 
570
- async def _handle_link_pr(self, params: Dict[str, Any]) -> Dict[str, Any]:
566
+ async def _handle_link_pr(self, params: dict[str, Any]) -> dict[str, Any]:
571
567
  """Handle linking an existing PR to a ticket."""
572
568
  ticket_id = params.get("ticket_id")
573
569
  pr_url = params.get("pr_url")
@@ -581,6 +577,7 @@ class MCPTicketServer:
581
577
 
582
578
  if "github" in adapter_name:
583
579
  from ..adapters.github import GitHubAdapter
580
+
584
581
  if isinstance(self.adapter, GitHubAdapter):
585
582
  try:
586
583
  result = await self.adapter.link_existing_pull_request(
@@ -597,6 +594,7 @@ class MCPTicketServer:
597
594
  }
598
595
  elif "linear" in adapter_name:
599
596
  from ..adapters.linear import LinearAdapter
597
+
600
598
  if isinstance(self.adapter, LinearAdapter):
601
599
  try:
602
600
  result = await self.adapter.link_to_pull_request(
@@ -619,7 +617,7 @@ class MCPTicketServer:
619
617
  "pr_url": pr_url,
620
618
  }
621
619
 
622
- async def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
620
+ async def _handle_initialize(self, params: dict[str, Any]) -> dict[str, Any]:
623
621
  """Handle initialize request from MCP client.
624
622
 
625
623
  Args:
@@ -627,21 +625,15 @@ class MCPTicketServer:
627
625
 
628
626
  Returns:
629
627
  Server capabilities
628
+
630
629
  """
631
630
  return {
632
631
  "protocolVersion": "2024-11-05",
633
- "serverInfo": {
634
- "name": "mcp-ticketer",
635
- "version": "0.1.8"
636
- },
637
- "capabilities": {
638
- "tools": {
639
- "listChanged": False
640
- }
641
- }
632
+ "serverInfo": {"name": "mcp-ticketer", "version": "0.1.8"},
633
+ "capabilities": {"tools": {"listChanged": False}},
642
634
  }
643
635
 
644
- async def _handle_tools_list(self) -> Dict[str, Any]:
636
+ async def _handle_tools_list(self) -> dict[str, Any]:
645
637
  """List available MCP tools."""
646
638
  return {
647
639
  "tools": [
@@ -651,15 +643,35 @@ class MCPTicketServer:
651
643
  "inputSchema": {
652
644
  "type": "object",
653
645
  "properties": {
654
- "ticket_id": {"type": "string", "description": "Ticket ID to link the PR to"},
655
- "base_branch": {"type": "string", "description": "Target branch for the PR", "default": "main"},
656
- "head_branch": {"type": "string", "description": "Source branch name (auto-generated if not provided)"},
657
- "title": {"type": "string", "description": "PR title (uses ticket title if not provided)"},
658
- "body": {"type": "string", "description": "PR description (auto-generated with issue link if not provided)"},
659
- "draft": {"type": "boolean", "description": "Create as draft PR", "default": False},
646
+ "ticket_id": {
647
+ "type": "string",
648
+ "description": "Ticket ID to link the PR to",
649
+ },
650
+ "base_branch": {
651
+ "type": "string",
652
+ "description": "Target branch for the PR",
653
+ "default": "main",
654
+ },
655
+ "head_branch": {
656
+ "type": "string",
657
+ "description": "Source branch name (auto-generated if not provided)",
658
+ },
659
+ "title": {
660
+ "type": "string",
661
+ "description": "PR title (uses ticket title if not provided)",
662
+ },
663
+ "body": {
664
+ "type": "string",
665
+ "description": "PR description (auto-generated with issue link if not provided)",
666
+ },
667
+ "draft": {
668
+ "type": "boolean",
669
+ "description": "Create as draft PR",
670
+ "default": False,
671
+ },
660
672
  },
661
- "required": ["ticket_id"]
662
- }
673
+ "required": ["ticket_id"],
674
+ },
663
675
  },
664
676
  {
665
677
  "name": "ticket_link_pr",
@@ -667,11 +679,17 @@ class MCPTicketServer:
667
679
  "inputSchema": {
668
680
  "type": "object",
669
681
  "properties": {
670
- "ticket_id": {"type": "string", "description": "Ticket ID to link the PR to"},
671
- "pr_url": {"type": "string", "description": "GitHub PR URL to link"},
682
+ "ticket_id": {
683
+ "type": "string",
684
+ "description": "Ticket ID to link the PR to",
685
+ },
686
+ "pr_url": {
687
+ "type": "string",
688
+ "description": "GitHub PR URL to link",
689
+ },
672
690
  },
673
- "required": ["ticket_id", "pr_url"]
674
- }
691
+ "required": ["ticket_id", "pr_url"],
692
+ },
675
693
  },
676
694
  {
677
695
  "name": "ticket_create",
@@ -680,13 +698,19 @@ class MCPTicketServer:
680
698
  "type": "object",
681
699
  "properties": {
682
700
  "title": {"type": "string", "description": "Ticket title"},
683
- "description": {"type": "string", "description": "Description"},
684
- "priority": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
701
+ "description": {
702
+ "type": "string",
703
+ "description": "Description",
704
+ },
705
+ "priority": {
706
+ "type": "string",
707
+ "enum": ["low", "medium", "high", "critical"],
708
+ },
685
709
  "tags": {"type": "array", "items": {"type": "string"}},
686
710
  "assignee": {"type": "string"},
687
711
  },
688
- "required": ["title"]
689
- }
712
+ "required": ["title"],
713
+ },
690
714
  },
691
715
  {
692
716
  "name": "ticket_list",
@@ -697,8 +721,8 @@ class MCPTicketServer:
697
721
  "limit": {"type": "integer", "default": 10},
698
722
  "state": {"type": "string"},
699
723
  "priority": {"type": "string"},
700
- }
701
- }
724
+ },
725
+ },
702
726
  },
703
727
  {
704
728
  "name": "ticket_update",
@@ -707,10 +731,13 @@ class MCPTicketServer:
707
731
  "type": "object",
708
732
  "properties": {
709
733
  "ticket_id": {"type": "string", "description": "Ticket ID"},
710
- "updates": {"type": "object", "description": "Fields to update"},
734
+ "updates": {
735
+ "type": "object",
736
+ "description": "Fields to update",
737
+ },
711
738
  },
712
- "required": ["ticket_id", "updates"]
713
- }
739
+ "required": ["ticket_id", "updates"],
740
+ },
714
741
  },
715
742
  {
716
743
  "name": "ticket_transition",
@@ -721,8 +748,8 @@ class MCPTicketServer:
721
748
  "ticket_id": {"type": "string"},
722
749
  "target_state": {"type": "string"},
723
750
  },
724
- "required": ["ticket_id", "target_state"]
725
- }
751
+ "required": ["ticket_id", "target_state"],
752
+ },
726
753
  },
727
754
  {
728
755
  "name": "ticket_search",
@@ -734,8 +761,8 @@ class MCPTicketServer:
734
761
  "state": {"type": "string"},
735
762
  "priority": {"type": "string"},
736
763
  "limit": {"type": "integer", "default": 10},
737
- }
738
- }
764
+ },
765
+ },
739
766
  },
740
767
  {
741
768
  "name": "ticket_status",
@@ -743,15 +770,18 @@ class MCPTicketServer:
743
770
  "inputSchema": {
744
771
  "type": "object",
745
772
  "properties": {
746
- "queue_id": {"type": "string", "description": "Queue ID returned from create/update/delete operations"},
773
+ "queue_id": {
774
+ "type": "string",
775
+ "description": "Queue ID returned from create/update/delete operations",
776
+ },
747
777
  },
748
- "required": ["queue_id"]
749
- }
778
+ "required": ["queue_id"],
779
+ },
750
780
  },
751
781
  ]
752
782
  }
753
783
 
754
- async def _handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
784
+ async def _handle_tools_call(self, params: dict[str, Any]) -> dict[str, Any]:
755
785
  """Handle tool invocation from MCP client.
756
786
 
757
787
  Args:
@@ -759,6 +789,7 @@ class MCPTicketServer:
759
789
 
760
790
  Returns:
761
791
  MCP formatted response with content array
792
+
762
793
  """
763
794
  tool_name = params.get("name")
764
795
  arguments = params.get("arguments", {})
@@ -783,13 +814,8 @@ class MCPTicketServer:
783
814
  result = await self._handle_link_pr(arguments)
784
815
  else:
785
816
  return {
786
- "content": [
787
- {
788
- "type": "text",
789
- "text": f"Unknown tool: {tool_name}"
790
- }
791
- ],
792
- "isError": True
817
+ "content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}],
818
+ "isError": True,
793
819
  }
794
820
 
795
821
  # Format successful response in MCP content format
@@ -804,13 +830,8 @@ class MCPTicketServer:
804
830
  result_text = str(result)
805
831
 
806
832
  return {
807
- "content": [
808
- {
809
- "type": "text",
810
- "text": result_text
811
- }
812
- ],
813
- "isError": False
833
+ "content": [{"type": "text", "text": result_text}],
834
+ "isError": False,
814
835
  }
815
836
 
816
837
  except Exception as e:
@@ -819,10 +840,10 @@ class MCPTicketServer:
819
840
  "content": [
820
841
  {
821
842
  "type": "text",
822
- "text": f"Error calling tool {tool_name}: {str(e)}"
843
+ "text": f"Error calling tool {tool_name}: {str(e)}",
823
844
  }
824
845
  ],
825
- "isError": True
846
+ "isError": True,
826
847
  }
827
848
 
828
849
  async def run(self) -> None:
@@ -832,7 +853,9 @@ class MCPTicketServer:
832
853
  try:
833
854
  reader = asyncio.StreamReader()
834
855
  protocol = asyncio.StreamReaderProtocol(reader)
835
- await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
856
+ await asyncio.get_event_loop().connect_read_pipe(
857
+ lambda: protocol, sys.stdin
858
+ )
836
859
  except Exception as e:
837
860
  sys.stderr.write(f"Failed to connect to stdin: {str(e)}\n")
838
861
  return
@@ -858,9 +881,7 @@ class MCPTicketServer:
858
881
 
859
882
  except json.JSONDecodeError as e:
860
883
  error_response = self._error_response(
861
- None,
862
- -32700,
863
- f"Parse error: {str(e)}"
884
+ None, -32700, f"Parse error: {str(e)}"
864
885
  )
865
886
  sys.stdout.write(json.dumps(error_response) + "\n")
866
887
  sys.stdout.flush()
@@ -888,23 +909,56 @@ async def main():
888
909
 
889
910
  This function is maintained in case it's being called directly,
890
911
  but the preferred way is now through the CLI: `mcp-ticketer mcp`
912
+
913
+ SECURITY: This method ONLY reads from the current project directory
914
+ to prevent configuration leakage across projects. It will NEVER read
915
+ from user home directory or system-wide locations.
891
916
  """
892
917
  # Load configuration
893
918
  import json
919
+ import logging
894
920
  from pathlib import Path
895
921
 
896
- config_file = Path.home() / ".mcp-ticketer" / "config.json"
922
+ logger = logging.getLogger(__name__)
923
+
924
+ # ONLY read from project-local config, never from user home
925
+ config_file = Path.cwd() / ".mcp-ticketer" / "config.json"
897
926
  if config_file.exists():
898
- with open(config_file, "r") as f:
899
- config = json.load(f)
900
- adapter_type = config.get("default_adapter", "aitrackdown")
901
- # Get adapter-specific config
902
- adapters_config = config.get("adapters", {})
903
- adapter_config = adapters_config.get(adapter_type, {})
904
- # Fallback to legacy config format
905
- if not adapter_config and "config" in config:
906
- adapter_config = config["config"]
927
+ # Validate config is within project
928
+ try:
929
+ if not config_file.resolve().is_relative_to(Path.cwd().resolve()):
930
+ logger.error(
931
+ f"Security violation: Config file {config_file} "
932
+ "is not within project directory"
933
+ )
934
+ raise ValueError(
935
+ f"Security violation: Config file {config_file} "
936
+ "is not within project directory"
937
+ )
938
+ except (ValueError, RuntimeError):
939
+ # is_relative_to may raise ValueError in some cases
940
+ pass
941
+
942
+ try:
943
+ with open(config_file) as f:
944
+ config = json.load(f)
945
+ adapter_type = config.get("default_adapter", "aitrackdown")
946
+ # Get adapter-specific config
947
+ adapters_config = config.get("adapters", {})
948
+ adapter_config = adapters_config.get(adapter_type, {})
949
+ # Fallback to legacy config format
950
+ if not adapter_config and "config" in config:
951
+ adapter_config = config["config"]
952
+ logger.info(
953
+ f"Loaded MCP configuration from project-local: {config_file}"
954
+ )
955
+ except (OSError, json.JSONDecodeError) as e:
956
+ logger.warning(f"Could not load project config: {e}, using defaults")
957
+ adapter_type = "aitrackdown"
958
+ adapter_config = {"base_path": ".aitrackdown"}
907
959
  else:
960
+ # Default to aitrackdown with local base path
961
+ logger.info("No project-local config found, defaulting to aitrackdown adapter")
908
962
  adapter_type = "aitrackdown"
909
963
  adapter_config = {"base_path": ".aitrackdown"}
910
964
 
@@ -914,4 +968,4 @@ async def main():
914
968
 
915
969
 
916
970
  if __name__ == "__main__":
917
- asyncio.run(main())
971
+ asyncio.run(main())