mcp-ticketer 0.1.21__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.
- mcp_ticketer/__init__.py +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +54 -38
- mcp_ticketer/adapters/github.py +175 -109
- mcp_ticketer/adapters/hybrid.py +90 -45
- mcp_ticketer/adapters/jira.py +139 -130
- mcp_ticketer/adapters/linear.py +374 -225
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +14 -15
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +250 -293
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +10 -12
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +115 -60
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +36 -30
- mcp_ticketer/core/config.py +113 -77
- mcp_ticketer/core/env_discovery.py +51 -19
- mcp_ticketer/core/http_client.py +46 -29
- mcp_ticketer/core/mappers.py +79 -35
- mcp_ticketer/core/models.py +29 -15
- mcp_ticketer/core/project_config.py +131 -66
- mcp_ticketer/core/registry.py +12 -12
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +183 -129
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +29 -25
- mcp_ticketer/queue/queue.py +144 -82
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +48 -33
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.22.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/hybrid.py
CHANGED
|
@@ -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
|
|
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
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
13
11
|
|
|
14
12
|
from ..core.adapter import BaseAdapter
|
|
15
|
-
from ..core.models import
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
345
|
+
logger.info(
|
|
346
|
+
f"Deleted ticket from adapter {adapter_name}: {adapter_ticket_id}"
|
|
347
|
+
)
|
|
310
348
|
except Exception as e:
|
|
311
|
-
logger.error(
|
|
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(
|
|
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(
|
|
423
|
+
logger.info(
|
|
424
|
+
f"Transitioned ticket in adapter {adapter_name}: {adapter_ticket_id}"
|
|
425
|
+
)
|
|
384
426
|
except Exception as e:
|
|
385
|
-
logger.error(
|
|
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(
|
|
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(
|
|
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
|