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.
- mcp_ticketer/__init__.py +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- 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 +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.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.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Base adapter abstract class for ticket systems."""
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
5
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
|
|
6
8
|
|
|
7
9
|
# Generic type for tickets
|
|
8
10
|
T = TypeVar("T", Epic, Task)
|
|
@@ -11,21 +13,23 @@ T = TypeVar("T", Epic, Task)
|
|
|
11
13
|
class BaseAdapter(ABC, Generic[T]):
|
|
12
14
|
"""Abstract base class for all ticket system adapters."""
|
|
13
15
|
|
|
14
|
-
def __init__(self, config:
|
|
16
|
+
def __init__(self, config: dict[str, Any]):
|
|
15
17
|
"""Initialize adapter with configuration.
|
|
16
18
|
|
|
17
19
|
Args:
|
|
18
20
|
config: Adapter-specific configuration dictionary
|
|
21
|
+
|
|
19
22
|
"""
|
|
20
23
|
self.config = config
|
|
21
24
|
self._state_mapping = self._get_state_mapping()
|
|
22
25
|
|
|
23
26
|
@abstractmethod
|
|
24
|
-
def _get_state_mapping(self) ->
|
|
27
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
25
28
|
"""Get mapping from universal states to system-specific states.
|
|
26
29
|
|
|
27
30
|
Returns:
|
|
28
31
|
Dictionary mapping TicketState to system-specific state strings
|
|
32
|
+
|
|
29
33
|
"""
|
|
30
34
|
pass
|
|
31
35
|
|
|
@@ -35,6 +39,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
35
39
|
|
|
36
40
|
Returns:
|
|
37
41
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
42
|
+
|
|
38
43
|
"""
|
|
39
44
|
pass
|
|
40
45
|
|
|
@@ -47,6 +52,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
47
52
|
|
|
48
53
|
Returns:
|
|
49
54
|
Created ticket with ID populated
|
|
55
|
+
|
|
50
56
|
"""
|
|
51
57
|
pass
|
|
52
58
|
|
|
@@ -59,11 +65,12 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
59
65
|
|
|
60
66
|
Returns:
|
|
61
67
|
Ticket if found, None otherwise
|
|
68
|
+
|
|
62
69
|
"""
|
|
63
70
|
pass
|
|
64
71
|
|
|
65
72
|
@abstractmethod
|
|
66
|
-
async def update(self, ticket_id: str, updates:
|
|
73
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[T]:
|
|
67
74
|
"""Update a ticket.
|
|
68
75
|
|
|
69
76
|
Args:
|
|
@@ -72,6 +79,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
72
79
|
|
|
73
80
|
Returns:
|
|
74
81
|
Updated ticket if successful, None otherwise
|
|
82
|
+
|
|
75
83
|
"""
|
|
76
84
|
pass
|
|
77
85
|
|
|
@@ -84,16 +92,14 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
84
92
|
|
|
85
93
|
Returns:
|
|
86
94
|
True if deleted, False otherwise
|
|
95
|
+
|
|
87
96
|
"""
|
|
88
97
|
pass
|
|
89
98
|
|
|
90
99
|
@abstractmethod
|
|
91
100
|
async def list(
|
|
92
|
-
self,
|
|
93
|
-
|
|
94
|
-
offset: int = 0,
|
|
95
|
-
filters: Optional[Dict[str, Any]] = None
|
|
96
|
-
) -> List[T]:
|
|
101
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
102
|
+
) -> list[T]:
|
|
97
103
|
"""List tickets with pagination and filters.
|
|
98
104
|
|
|
99
105
|
Args:
|
|
@@ -103,11 +109,12 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
103
109
|
|
|
104
110
|
Returns:
|
|
105
111
|
List of tickets matching criteria
|
|
112
|
+
|
|
106
113
|
"""
|
|
107
114
|
pass
|
|
108
115
|
|
|
109
116
|
@abstractmethod
|
|
110
|
-
async def search(self, query: SearchQuery) ->
|
|
117
|
+
async def search(self, query: SearchQuery) -> builtins.list[T]:
|
|
111
118
|
"""Search tickets using advanced query.
|
|
112
119
|
|
|
113
120
|
Args:
|
|
@@ -115,14 +122,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
115
122
|
|
|
116
123
|
Returns:
|
|
117
124
|
List of tickets matching search criteria
|
|
125
|
+
|
|
118
126
|
"""
|
|
119
127
|
pass
|
|
120
128
|
|
|
121
129
|
@abstractmethod
|
|
122
130
|
async def transition_state(
|
|
123
|
-
self,
|
|
124
|
-
ticket_id: str,
|
|
125
|
-
target_state: TicketState
|
|
131
|
+
self, ticket_id: str, target_state: TicketState
|
|
126
132
|
) -> Optional[T]:
|
|
127
133
|
"""Transition ticket to a new state.
|
|
128
134
|
|
|
@@ -132,6 +138,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
132
138
|
|
|
133
139
|
Returns:
|
|
134
140
|
Updated ticket if transition successful, None otherwise
|
|
141
|
+
|
|
135
142
|
"""
|
|
136
143
|
pass
|
|
137
144
|
|
|
@@ -144,16 +151,14 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
144
151
|
|
|
145
152
|
Returns:
|
|
146
153
|
Created comment with ID populated
|
|
154
|
+
|
|
147
155
|
"""
|
|
148
156
|
pass
|
|
149
157
|
|
|
150
158
|
@abstractmethod
|
|
151
159
|
async def get_comments(
|
|
152
|
-
self,
|
|
153
|
-
|
|
154
|
-
limit: int = 10,
|
|
155
|
-
offset: int = 0
|
|
156
|
-
) -> List[Comment]:
|
|
160
|
+
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
161
|
+
) -> builtins.list[Comment]:
|
|
157
162
|
"""Get comments for a ticket.
|
|
158
163
|
|
|
159
164
|
Args:
|
|
@@ -163,6 +168,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
163
168
|
|
|
164
169
|
Returns:
|
|
165
170
|
List of comments for the ticket
|
|
171
|
+
|
|
166
172
|
"""
|
|
167
173
|
pass
|
|
168
174
|
|
|
@@ -174,6 +180,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
174
180
|
|
|
175
181
|
Returns:
|
|
176
182
|
System-specific state string
|
|
183
|
+
|
|
177
184
|
"""
|
|
178
185
|
return self._state_mapping.get(state, state.value)
|
|
179
186
|
|
|
@@ -185,14 +192,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
185
192
|
|
|
186
193
|
Returns:
|
|
187
194
|
Universal ticket state
|
|
195
|
+
|
|
188
196
|
"""
|
|
189
197
|
reverse_mapping = {v: k for k, v in self._state_mapping.items()}
|
|
190
198
|
return reverse_mapping.get(system_state, TicketState.OPEN)
|
|
191
199
|
|
|
192
200
|
async def validate_transition(
|
|
193
|
-
self,
|
|
194
|
-
ticket_id: str,
|
|
195
|
-
target_state: TicketState
|
|
201
|
+
self, ticket_id: str, target_state: TicketState
|
|
196
202
|
) -> bool:
|
|
197
203
|
"""Validate if state transition is allowed.
|
|
198
204
|
|
|
@@ -202,6 +208,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
202
208
|
|
|
203
209
|
Returns:
|
|
204
210
|
True if transition is valid
|
|
211
|
+
|
|
205
212
|
"""
|
|
206
213
|
ticket = await self.read(ticket_id)
|
|
207
214
|
if not ticket:
|
|
@@ -218,10 +225,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
218
225
|
# Epic/Issue/Task Hierarchy Methods
|
|
219
226
|
|
|
220
227
|
async def create_epic(
|
|
221
|
-
self,
|
|
222
|
-
title: str,
|
|
223
|
-
description: Optional[str] = None,
|
|
224
|
-
**kwargs
|
|
228
|
+
self, title: str, description: Optional[str] = None, **kwargs
|
|
225
229
|
) -> Optional[Epic]:
|
|
226
230
|
"""Create epic (top-level grouping).
|
|
227
231
|
|
|
@@ -232,12 +236,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
232
236
|
|
|
233
237
|
Returns:
|
|
234
238
|
Created epic or None if failed
|
|
239
|
+
|
|
235
240
|
"""
|
|
236
241
|
epic = Epic(
|
|
237
242
|
title=title,
|
|
238
243
|
description=description,
|
|
239
244
|
ticket_type=TicketType.EPIC,
|
|
240
|
-
**{k: v for k, v in kwargs.items() if k in Epic.__fields__}
|
|
245
|
+
**{k: v for k, v in kwargs.items() if k in Epic.__fields__},
|
|
241
246
|
)
|
|
242
247
|
result = await self.create(epic)
|
|
243
248
|
if isinstance(result, Epic):
|
|
@@ -252,6 +257,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
252
257
|
|
|
253
258
|
Returns:
|
|
254
259
|
Epic if found, None otherwise
|
|
260
|
+
|
|
255
261
|
"""
|
|
256
262
|
# Default implementation - subclasses should override for platform-specific logic
|
|
257
263
|
result = await self.read(epic_id)
|
|
@@ -259,7 +265,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
259
265
|
return result
|
|
260
266
|
return None
|
|
261
267
|
|
|
262
|
-
async def list_epics(self, **kwargs) ->
|
|
268
|
+
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
263
269
|
"""List all epics.
|
|
264
270
|
|
|
265
271
|
Args:
|
|
@@ -267,6 +273,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
267
273
|
|
|
268
274
|
Returns:
|
|
269
275
|
List of epics
|
|
276
|
+
|
|
270
277
|
"""
|
|
271
278
|
# Default implementation - subclasses should override
|
|
272
279
|
filters = kwargs.copy()
|
|
@@ -279,7 +286,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
279
286
|
title: str,
|
|
280
287
|
description: Optional[str] = None,
|
|
281
288
|
epic_id: Optional[str] = None,
|
|
282
|
-
**kwargs
|
|
289
|
+
**kwargs,
|
|
283
290
|
) -> Optional[Task]:
|
|
284
291
|
"""Create issue, optionally linked to epic.
|
|
285
292
|
|
|
@@ -291,17 +298,18 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
291
298
|
|
|
292
299
|
Returns:
|
|
293
300
|
Created issue or None if failed
|
|
301
|
+
|
|
294
302
|
"""
|
|
295
303
|
task = Task(
|
|
296
304
|
title=title,
|
|
297
305
|
description=description,
|
|
298
306
|
ticket_type=TicketType.ISSUE,
|
|
299
307
|
parent_epic=epic_id,
|
|
300
|
-
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
308
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__},
|
|
301
309
|
)
|
|
302
310
|
return await self.create(task)
|
|
303
311
|
|
|
304
|
-
async def list_issues_by_epic(self, epic_id: str) ->
|
|
312
|
+
async def list_issues_by_epic(self, epic_id: str) -> builtins.list[Task]:
|
|
305
313
|
"""List all issues in epic.
|
|
306
314
|
|
|
307
315
|
Args:
|
|
@@ -309,6 +317,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
309
317
|
|
|
310
318
|
Returns:
|
|
311
319
|
List of issues belonging to epic
|
|
320
|
+
|
|
312
321
|
"""
|
|
313
322
|
# Default implementation - subclasses should override for efficiency
|
|
314
323
|
filters = {"parent_epic": epic_id, "ticket_type": TicketType.ISSUE}
|
|
@@ -316,11 +325,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
316
325
|
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
317
326
|
|
|
318
327
|
async def create_task(
|
|
319
|
-
self,
|
|
320
|
-
title: str,
|
|
321
|
-
parent_id: str,
|
|
322
|
-
description: Optional[str] = None,
|
|
323
|
-
**kwargs
|
|
328
|
+
self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
|
|
324
329
|
) -> Optional[Task]:
|
|
325
330
|
"""Create task as sub-ticket of parent issue.
|
|
326
331
|
|
|
@@ -335,6 +340,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
335
340
|
|
|
336
341
|
Raises:
|
|
337
342
|
ValueError: If parent_id is not provided
|
|
343
|
+
|
|
338
344
|
"""
|
|
339
345
|
if not parent_id:
|
|
340
346
|
raise ValueError("Tasks must have a parent_id (issue)")
|
|
@@ -344,7 +350,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
344
350
|
description=description,
|
|
345
351
|
ticket_type=TicketType.TASK,
|
|
346
352
|
parent_issue=parent_id,
|
|
347
|
-
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
353
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__},
|
|
348
354
|
)
|
|
349
355
|
|
|
350
356
|
# Validate hierarchy before creating
|
|
@@ -354,7 +360,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
354
360
|
|
|
355
361
|
return await self.create(task)
|
|
356
362
|
|
|
357
|
-
async def list_tasks_by_issue(self, issue_id: str) ->
|
|
363
|
+
async def list_tasks_by_issue(self, issue_id: str) -> builtins.list[Task]:
|
|
358
364
|
"""List all tasks under an issue.
|
|
359
365
|
|
|
360
366
|
Args:
|
|
@@ -362,6 +368,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
362
368
|
|
|
363
369
|
Returns:
|
|
364
370
|
List of tasks belonging to issue
|
|
371
|
+
|
|
365
372
|
"""
|
|
366
373
|
# Default implementation - subclasses should override for efficiency
|
|
367
374
|
filters = {"parent_issue": issue_id, "ticket_type": TicketType.TASK}
|
|
@@ -370,4 +377,4 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
370
377
|
|
|
371
378
|
async def close(self) -> None:
|
|
372
379
|
"""Close adapter and cleanup resources."""
|
|
373
|
-
pass
|
|
380
|
+
pass
|