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
@@ -4,15 +4,14 @@ This adapter enables synchronization across multiple ticketing systems
4
4
  (Linear, JIRA, GitHub, AITrackdown) with configurable sync strategies.
5
5
  """
6
6
 
7
- import asyncio
7
+ import builtins
8
+ import json
8
9
  import logging
9
- from typing import List, Optional, Dict, Any
10
- from datetime import datetime
11
10
  from pathlib import Path
12
- import json
11
+ from typing import Any, Optional
13
12
 
14
13
  from ..core.adapter import BaseAdapter
15
- from ..core.models import Task, Epic, Comment, SearchQuery, TicketState, Priority
14
+ from ..core.models import Comment, Epic, SearchQuery, Task, TicketState
16
15
  from ..core.registry import AdapterRegistry
17
16
 
18
17
  logger = logging.getLogger(__name__)
@@ -29,7 +28,7 @@ class HybridAdapter(BaseAdapter):
29
28
  Maintains mapping between ticket IDs across different systems.
30
29
  """
31
30
 
32
- def __init__(self, config: Dict[str, Any]):
31
+ def __init__(self, config: dict[str, Any]):
33
32
  """Initialize hybrid adapter.
34
33
 
35
34
  Args:
@@ -38,10 +37,11 @@ class HybridAdapter(BaseAdapter):
38
37
  - primary_adapter: Name of primary adapter
39
38
  - sync_strategy: Sync strategy (primary_source, bidirectional, mirror)
40
39
  - mapping_file: Path to ID mapping file (optional)
40
+
41
41
  """
42
42
  super().__init__(config)
43
43
 
44
- self.adapters: Dict[str, BaseAdapter] = {}
44
+ self.adapters: dict[str, BaseAdapter] = {}
45
45
  self.primary_adapter_name = config.get("primary_adapter")
46
46
  self.sync_strategy = config.get("sync_strategy", "primary_source")
47
47
 
@@ -50,7 +50,9 @@ class HybridAdapter(BaseAdapter):
50
50
  for name, adapter_config in adapter_configs.items():
51
51
  try:
52
52
  adapter_type = adapter_config.get("adapter")
53
- self.adapters[name] = AdapterRegistry.get_adapter(adapter_type, adapter_config)
53
+ self.adapters[name] = AdapterRegistry.get_adapter(
54
+ adapter_type, adapter_config
55
+ )
54
56
  logger.info(f"Initialized adapter: {name} ({adapter_type})")
55
57
  except Exception as e:
56
58
  logger.error(f"Failed to initialize adapter {name}: {e}")
@@ -59,18 +61,22 @@ class HybridAdapter(BaseAdapter):
59
61
  raise ValueError("No adapters successfully initialized")
60
62
 
61
63
  if self.primary_adapter_name not in self.adapters:
62
- raise ValueError(f"Primary adapter {self.primary_adapter_name} not found in adapters")
64
+ raise ValueError(
65
+ f"Primary adapter {self.primary_adapter_name} not found in adapters"
66
+ )
63
67
 
64
68
  # Load or initialize ID mapping
65
- self.mapping_file = Path(config.get("mapping_file", ".mcp-ticketer/hybrid_mapping.json"))
69
+ self.mapping_file = Path(
70
+ config.get("mapping_file", ".mcp-ticketer/hybrid_mapping.json")
71
+ )
66
72
  self.id_mapping = self._load_mapping()
67
73
 
68
- def _get_state_mapping(self) -> Dict[TicketState, str]:
74
+ def _get_state_mapping(self) -> dict[TicketState, str]:
69
75
  """Get state mapping from primary adapter."""
70
76
  primary = self.adapters[self.primary_adapter_name]
71
77
  return primary._get_state_mapping()
72
78
 
73
- def _load_mapping(self) -> Dict[str, Dict[str, str]]:
79
+ def _load_mapping(self) -> dict[str, dict[str, str]]:
74
80
  """Load ID mapping from file.
75
81
 
76
82
  Mapping format:
@@ -84,10 +90,11 @@ class HybridAdapter(BaseAdapter):
84
90
 
85
91
  Returns:
86
92
  Dictionary mapping universal ticket IDs to adapter-specific IDs
93
+
87
94
  """
88
95
  if self.mapping_file.exists():
89
96
  try:
90
- with open(self.mapping_file, 'r') as f:
97
+ with open(self.mapping_file) as f:
91
98
  return json.load(f)
92
99
  except Exception as e:
93
100
  logger.error(f"Failed to load mapping file: {e}")
@@ -98,18 +105,21 @@ class HybridAdapter(BaseAdapter):
98
105
  """Save ID mapping to file."""
99
106
  try:
100
107
  self.mapping_file.parent.mkdir(parents=True, exist_ok=True)
101
- with open(self.mapping_file, 'w') as f:
108
+ with open(self.mapping_file, "w") as f:
102
109
  json.dump(self.id_mapping, f, indent=2)
103
110
  except Exception as e:
104
111
  logger.error(f"Failed to save mapping file: {e}")
105
112
 
106
- def _store_ticket_mapping(self, universal_id: str, adapter_name: str, adapter_ticket_id: str) -> None:
113
+ def _store_ticket_mapping(
114
+ self, universal_id: str, adapter_name: str, adapter_ticket_id: str
115
+ ) -> None:
107
116
  """Store mapping between universal ID and adapter-specific ID.
108
117
 
109
118
  Args:
110
119
  universal_id: Universal ticket identifier
111
120
  adapter_name: Name of adapter
112
121
  adapter_ticket_id: Adapter-specific ticket ID
122
+
113
123
  """
114
124
  if universal_id not in self.id_mapping:
115
125
  self.id_mapping[universal_id] = {}
@@ -117,7 +127,9 @@ class HybridAdapter(BaseAdapter):
117
127
  self.id_mapping[universal_id][adapter_name] = adapter_ticket_id
118
128
  self._save_mapping()
119
129
 
120
- def _get_adapter_ticket_id(self, universal_id: str, adapter_name: str) -> Optional[str]:
130
+ def _get_adapter_ticket_id(
131
+ self, universal_id: str, adapter_name: str
132
+ ) -> Optional[str]:
121
133
  """Get adapter-specific ticket ID from universal ID.
122
134
 
123
135
  Args:
@@ -126,6 +138,7 @@ class HybridAdapter(BaseAdapter):
126
138
 
127
139
  Returns:
128
140
  Adapter-specific ticket ID or None
141
+
129
142
  """
130
143
  return self.id_mapping.get(universal_id, {}).get(adapter_name)
131
144
 
@@ -134,8 +147,10 @@ class HybridAdapter(BaseAdapter):
134
147
 
135
148
  Returns:
136
149
  UUID-like universal ticket identifier
150
+
137
151
  """
138
152
  import uuid
153
+
139
154
  return f"hybrid-{uuid.uuid4().hex[:12]}"
140
155
 
141
156
  async def create(self, ticket: Task | Epic) -> Task | Epic:
@@ -146,6 +161,7 @@ class HybridAdapter(BaseAdapter):
146
161
 
147
162
  Returns:
148
163
  Created ticket with universal ID
164
+
149
165
  """
150
166
  universal_id = self._generate_universal_id()
151
167
  results = []
@@ -154,11 +170,17 @@ class HybridAdapter(BaseAdapter):
154
170
  primary = self.adapters[self.primary_adapter_name]
155
171
  try:
156
172
  primary_ticket = await primary.create(ticket)
157
- self._store_ticket_mapping(universal_id, self.primary_adapter_name, primary_ticket.id)
173
+ self._store_ticket_mapping(
174
+ universal_id, self.primary_adapter_name, primary_ticket.id
175
+ )
158
176
  results.append((self.primary_adapter_name, primary_ticket))
159
- logger.info(f"Created ticket in primary adapter {self.primary_adapter_name}: {primary_ticket.id}")
177
+ logger.info(
178
+ f"Created ticket in primary adapter {self.primary_adapter_name}: {primary_ticket.id}"
179
+ )
160
180
  except Exception as e:
161
- logger.error(f"Failed to create ticket in primary adapter {self.primary_adapter_name}: {e}")
181
+ logger.error(
182
+ f"Failed to create ticket in primary adapter {self.primary_adapter_name}: {e}"
183
+ )
162
184
  raise
163
185
 
164
186
  # Create in secondary adapters
@@ -185,12 +207,15 @@ class HybridAdapter(BaseAdapter):
185
207
 
186
208
  return primary_ticket
187
209
 
188
- def _add_cross_references(self, ticket: Task | Epic, results: List[tuple[str, Task | Epic]]) -> None:
210
+ def _add_cross_references(
211
+ self, ticket: Task | Epic, results: list[tuple[str, Task | Epic]]
212
+ ) -> None:
189
213
  """Add cross-references to ticket description.
190
214
 
191
215
  Args:
192
216
  ticket: Ticket to update
193
217
  results: List of (adapter_name, ticket) tuples
218
+
194
219
  """
195
220
  cross_refs = "\n\n---\n**Cross-Platform References:**\n"
196
221
  for adapter_name, adapter_ticket in results:
@@ -209,13 +234,18 @@ class HybridAdapter(BaseAdapter):
209
234
 
210
235
  Returns:
211
236
  Ticket if found, None otherwise
237
+
212
238
  """
213
239
  # Check if this is a universal ID
214
240
  if ticket_id.startswith("hybrid-"):
215
241
  # Get primary adapter ticket ID
216
- primary_id = self._get_adapter_ticket_id(ticket_id, self.primary_adapter_name)
242
+ primary_id = self._get_adapter_ticket_id(
243
+ ticket_id, self.primary_adapter_name
244
+ )
217
245
  if not primary_id:
218
- logger.warning(f"No primary ticket ID found for universal ID: {ticket_id}")
246
+ logger.warning(
247
+ f"No primary ticket ID found for universal ID: {ticket_id}"
248
+ )
219
249
  return None
220
250
  ticket_id = primary_id
221
251
 
@@ -223,7 +253,9 @@ class HybridAdapter(BaseAdapter):
223
253
  primary = self.adapters[self.primary_adapter_name]
224
254
  return await primary.read(ticket_id)
225
255
 
226
- async def update(self, ticket_id: str, updates: Dict[str, Any]) -> Optional[Task | Epic]:
256
+ async def update(
257
+ self, ticket_id: str, updates: dict[str, Any]
258
+ ) -> Optional[Task | Epic]:
227
259
  """Update ticket across all adapters.
228
260
 
229
261
  Args:
@@ -232,6 +264,7 @@ class HybridAdapter(BaseAdapter):
232
264
 
233
265
  Returns:
234
266
  Updated ticket from primary adapter
267
+
235
268
  """
236
269
  universal_id = ticket_id
237
270
  if not ticket_id.startswith("hybrid-"):
@@ -254,7 +287,9 @@ class HybridAdapter(BaseAdapter):
254
287
  try:
255
288
  updated_ticket = await adapter.update(adapter_ticket_id, updates)
256
289
  results.append((adapter_name, updated_ticket))
257
- logger.info(f"Updated ticket in adapter {adapter_name}: {adapter_ticket_id}")
290
+ logger.info(
291
+ f"Updated ticket in adapter {adapter_name}: {adapter_ticket_id}"
292
+ )
258
293
  except Exception as e:
259
294
  logger.error(f"Failed to update ticket in adapter {adapter_name}: {e}")
260
295
 
@@ -273,6 +308,7 @@ class HybridAdapter(BaseAdapter):
273
308
 
274
309
  Returns:
275
310
  Universal ID if found, None otherwise
311
+
276
312
  """
277
313
  for universal_id, mapping in self.id_mapping.items():
278
314
  if adapter_ticket_id in mapping.values():
@@ -287,6 +323,7 @@ class HybridAdapter(BaseAdapter):
287
323
 
288
324
  Returns:
289
325
  True if deleted from at least one adapter
326
+
290
327
  """
291
328
  universal_id = ticket_id
292
329
  if not ticket_id.startswith("hybrid-"):
@@ -306,9 +343,13 @@ class HybridAdapter(BaseAdapter):
306
343
  try:
307
344
  if await adapter.delete(adapter_ticket_id):
308
345
  success_count += 1
309
- logger.info(f"Deleted ticket from adapter {adapter_name}: {adapter_ticket_id}")
346
+ logger.info(
347
+ f"Deleted ticket from adapter {adapter_name}: {adapter_ticket_id}"
348
+ )
310
349
  except Exception as e:
311
- logger.error(f"Failed to delete ticket from adapter {adapter_name}: {e}")
350
+ logger.error(
351
+ f"Failed to delete ticket from adapter {adapter_name}: {e}"
352
+ )
312
353
 
313
354
  # Remove from mapping
314
355
  if universal_id in self.id_mapping:
@@ -318,11 +359,8 @@ class HybridAdapter(BaseAdapter):
318
359
  return success_count > 0
319
360
 
320
361
  async def list(
321
- self,
322
- limit: int = 10,
323
- offset: int = 0,
324
- filters: Optional[Dict[str, Any]] = None
325
- ) -> List[Task | Epic]:
362
+ self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
363
+ ) -> list[Task | Epic]:
326
364
  """List tickets from primary adapter.
327
365
 
328
366
  Args:
@@ -332,11 +370,12 @@ class HybridAdapter(BaseAdapter):
332
370
 
333
371
  Returns:
334
372
  List of tickets from primary adapter
373
+
335
374
  """
336
375
  primary = self.adapters[self.primary_adapter_name]
337
376
  return await primary.list(limit, offset, filters)
338
377
 
339
- async def search(self, query: SearchQuery) -> List[Task | Epic]:
378
+ async def search(self, query: SearchQuery) -> builtins.list[Task | Epic]:
340
379
  """Search tickets in primary adapter.
341
380
 
342
381
  Args:
@@ -344,14 +383,13 @@ class HybridAdapter(BaseAdapter):
344
383
 
345
384
  Returns:
346
385
  List of tickets matching search criteria
386
+
347
387
  """
348
388
  primary = self.adapters[self.primary_adapter_name]
349
389
  return await primary.search(query)
350
390
 
351
391
  async def transition_state(
352
- self,
353
- ticket_id: str,
354
- target_state: TicketState
392
+ self, ticket_id: str, target_state: TicketState
355
393
  ) -> Optional[Task | Epic]:
356
394
  """Transition ticket state across all adapters.
357
395
 
@@ -361,6 +399,7 @@ class HybridAdapter(BaseAdapter):
361
399
 
362
400
  Returns:
363
401
  Updated ticket from primary adapter
402
+
364
403
  """
365
404
  universal_id = ticket_id
366
405
  if not ticket_id.startswith("hybrid-"):
@@ -378,11 +417,17 @@ class HybridAdapter(BaseAdapter):
378
417
  continue
379
418
 
380
419
  try:
381
- updated_ticket = await adapter.transition_state(adapter_ticket_id, target_state)
420
+ updated_ticket = await adapter.transition_state(
421
+ adapter_ticket_id, target_state
422
+ )
382
423
  results.append((adapter_name, updated_ticket))
383
- logger.info(f"Transitioned ticket in adapter {adapter_name}: {adapter_ticket_id}")
424
+ logger.info(
425
+ f"Transitioned ticket in adapter {adapter_name}: {adapter_ticket_id}"
426
+ )
384
427
  except Exception as e:
385
- logger.error(f"Failed to transition ticket in adapter {adapter_name}: {e}")
428
+ logger.error(
429
+ f"Failed to transition ticket in adapter {adapter_name}: {e}"
430
+ )
386
431
 
387
432
  # Return result from primary adapter
388
433
  for adapter_name, ticket in results:
@@ -399,6 +444,7 @@ class HybridAdapter(BaseAdapter):
399
444
 
400
445
  Returns:
401
446
  Created comment from primary adapter
447
+
402
448
  """
403
449
  universal_id = comment.ticket_id
404
450
  if not comment.ticket_id.startswith("hybrid-"):
@@ -420,11 +466,13 @@ class HybridAdapter(BaseAdapter):
420
466
  adapter_comment = Comment(
421
467
  ticket_id=adapter_ticket_id,
422
468
  content=comment.content,
423
- author=comment.author
469
+ author=comment.author,
424
470
  )
425
471
  created_comment = await adapter.add_comment(adapter_comment)
426
472
  results.append((adapter_name, created_comment))
427
- logger.info(f"Added comment to adapter {adapter_name}: {adapter_ticket_id}")
473
+ logger.info(
474
+ f"Added comment to adapter {adapter_name}: {adapter_ticket_id}"
475
+ )
428
476
  except Exception as e:
429
477
  logger.error(f"Failed to add comment to adapter {adapter_name}: {e}")
430
478
 
@@ -440,11 +488,8 @@ class HybridAdapter(BaseAdapter):
440
488
  raise RuntimeError("Failed to add comment to any adapter")
441
489
 
442
490
  async def get_comments(
443
- self,
444
- ticket_id: str,
445
- limit: int = 10,
446
- offset: int = 0
447
- ) -> List[Comment]:
491
+ self, ticket_id: str, limit: int = 10, offset: int = 0
492
+ ) -> builtins.list[Comment]:
448
493
  """Get comments from primary adapter.
449
494
 
450
495
  Args:
@@ -454,10 +499,13 @@ class HybridAdapter(BaseAdapter):
454
499
 
455
500
  Returns:
456
501
  List of comments from primary adapter
502
+
457
503
  """
458
504
  if ticket_id.startswith("hybrid-"):
459
505
  # Get primary adapter ticket ID
460
- primary_id = self._get_adapter_ticket_id(ticket_id, self.primary_adapter_name)
506
+ primary_id = self._get_adapter_ticket_id(
507
+ ticket_id, self.primary_adapter_name
508
+ )
461
509
  if not primary_id:
462
510
  return []
463
511
  ticket_id = primary_id
@@ -473,17 +521,18 @@ class HybridAdapter(BaseAdapter):
473
521
  except Exception as e:
474
522
  logger.error(f"Error closing adapter: {e}")
475
523
 
476
- async def sync_status(self) -> Dict[str, Any]:
524
+ async def sync_status(self) -> dict[str, Any]:
477
525
  """Get synchronization status across all adapters.
478
526
 
479
527
  Returns:
480
528
  Dictionary with sync status information
529
+
481
530
  """
482
531
  status = {
483
532
  "primary_adapter": self.primary_adapter_name,
484
533
  "sync_strategy": self.sync_strategy,
485
534
  "total_mapped_tickets": len(self.id_mapping),
486
- "adapters": {}
535
+ "adapters": {},
487
536
  }
488
537
 
489
538
  for adapter_name, adapter in self.adapters.items():
@@ -494,12 +543,9 @@ class HybridAdapter(BaseAdapter):
494
543
 
495
544
  status["adapters"][adapter_name] = {
496
545
  "ticket_count": ticket_count,
497
- "status": "connected"
546
+ "status": "connected",
498
547
  }
499
548
  except Exception as e:
500
- status["adapters"][adapter_name] = {
501
- "status": "error",
502
- "error": str(e)
503
- }
549
+ status["adapters"][adapter_name] = {"status": "error", "error": str(e)}
504
550
 
505
551
  return status