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