mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
8
8
|
|
|
9
9
|
from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
|
|
10
|
+
from .state_matcher import get_state_matcher
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from .models import Attachment
|
|
@@ -22,17 +23,52 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
22
23
|
"""Initialize adapter with configuration.
|
|
23
24
|
|
|
24
25
|
Args:
|
|
26
|
+
----
|
|
25
27
|
config: Adapter-specific configuration dictionary
|
|
26
28
|
|
|
27
29
|
"""
|
|
28
30
|
self.config = config
|
|
29
31
|
self._state_mapping = self._get_state_mapping()
|
|
30
32
|
|
|
33
|
+
@property
|
|
34
|
+
def adapter_type(self) -> str:
|
|
35
|
+
"""Return lowercase adapter type identifier.
|
|
36
|
+
|
|
37
|
+
This identifier is used in MCP responses to show which adapter
|
|
38
|
+
handled the operation (e.g., "linear", "github", "jira", "asana").
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
-------
|
|
42
|
+
Lowercase adapter type (e.g., "linear", "github")
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
# Extract adapter type from class name
|
|
46
|
+
# LinearAdapter -> linear, GitHubAdapter -> github
|
|
47
|
+
class_name = self.__class__.__name__
|
|
48
|
+
if class_name.endswith("Adapter"):
|
|
49
|
+
adapter_name = class_name[: -len("Adapter")]
|
|
50
|
+
else:
|
|
51
|
+
adapter_name = class_name
|
|
52
|
+
|
|
53
|
+
return adapter_name.lower()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def adapter_display_name(self) -> str:
|
|
57
|
+
"""Return human-readable adapter name.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
-------
|
|
61
|
+
Title-cased adapter name (e.g., "Linear", "Github", "Jira")
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
return self.adapter_type.title()
|
|
65
|
+
|
|
31
66
|
@abstractmethod
|
|
32
67
|
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
33
68
|
"""Get mapping from universal states to system-specific states.
|
|
34
69
|
|
|
35
70
|
Returns:
|
|
71
|
+
-------
|
|
36
72
|
Dictionary mapping TicketState to system-specific state strings
|
|
37
73
|
|
|
38
74
|
"""
|
|
@@ -43,6 +79,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
43
79
|
"""Validate that required credentials are present.
|
|
44
80
|
|
|
45
81
|
Returns:
|
|
82
|
+
-------
|
|
46
83
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
47
84
|
|
|
48
85
|
"""
|
|
@@ -53,9 +90,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
53
90
|
"""Create a new ticket.
|
|
54
91
|
|
|
55
92
|
Args:
|
|
93
|
+
----
|
|
56
94
|
ticket: Ticket to create (Epic or Task)
|
|
57
95
|
|
|
58
96
|
Returns:
|
|
97
|
+
-------
|
|
59
98
|
Created ticket with ID populated
|
|
60
99
|
|
|
61
100
|
"""
|
|
@@ -66,9 +105,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
66
105
|
"""Read a ticket by ID.
|
|
67
106
|
|
|
68
107
|
Args:
|
|
108
|
+
----
|
|
69
109
|
ticket_id: Unique ticket identifier
|
|
70
110
|
|
|
71
111
|
Returns:
|
|
112
|
+
-------
|
|
72
113
|
Ticket if found, None otherwise
|
|
73
114
|
|
|
74
115
|
"""
|
|
@@ -79,10 +120,12 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
79
120
|
"""Update a ticket.
|
|
80
121
|
|
|
81
122
|
Args:
|
|
123
|
+
----
|
|
82
124
|
ticket_id: Ticket identifier
|
|
83
125
|
updates: Fields to update
|
|
84
126
|
|
|
85
127
|
Returns:
|
|
128
|
+
-------
|
|
86
129
|
Updated ticket if successful, None otherwise
|
|
87
130
|
|
|
88
131
|
"""
|
|
@@ -93,9 +136,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
93
136
|
"""Delete a ticket.
|
|
94
137
|
|
|
95
138
|
Args:
|
|
139
|
+
----
|
|
96
140
|
ticket_id: Ticket identifier
|
|
97
141
|
|
|
98
142
|
Returns:
|
|
143
|
+
-------
|
|
99
144
|
True if deleted, False otherwise
|
|
100
145
|
|
|
101
146
|
"""
|
|
@@ -108,11 +153,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
108
153
|
"""List tickets with pagination and filters.
|
|
109
154
|
|
|
110
155
|
Args:
|
|
156
|
+
----
|
|
111
157
|
limit: Maximum number of tickets
|
|
112
158
|
offset: Skip this many tickets
|
|
113
159
|
filters: Optional filter criteria
|
|
114
160
|
|
|
115
161
|
Returns:
|
|
162
|
+
-------
|
|
116
163
|
List of tickets matching criteria
|
|
117
164
|
|
|
118
165
|
"""
|
|
@@ -123,9 +170,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
123
170
|
"""Search tickets using advanced query.
|
|
124
171
|
|
|
125
172
|
Args:
|
|
173
|
+
----
|
|
126
174
|
query: Search parameters
|
|
127
175
|
|
|
128
176
|
Returns:
|
|
177
|
+
-------
|
|
129
178
|
List of tickets matching search criteria
|
|
130
179
|
|
|
131
180
|
"""
|
|
@@ -138,10 +187,12 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
138
187
|
"""Transition ticket to a new state.
|
|
139
188
|
|
|
140
189
|
Args:
|
|
190
|
+
----
|
|
141
191
|
ticket_id: Ticket identifier
|
|
142
192
|
target_state: Target state
|
|
143
193
|
|
|
144
194
|
Returns:
|
|
195
|
+
-------
|
|
145
196
|
Updated ticket if transition successful, None otherwise
|
|
146
197
|
|
|
147
198
|
"""
|
|
@@ -152,9 +203,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
152
203
|
"""Add a comment to a ticket.
|
|
153
204
|
|
|
154
205
|
Args:
|
|
206
|
+
----
|
|
155
207
|
comment: Comment to add
|
|
156
208
|
|
|
157
209
|
Returns:
|
|
210
|
+
-------
|
|
158
211
|
Created comment with ID populated
|
|
159
212
|
|
|
160
213
|
"""
|
|
@@ -167,11 +220,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
167
220
|
"""Get comments for a ticket.
|
|
168
221
|
|
|
169
222
|
Args:
|
|
223
|
+
----
|
|
170
224
|
ticket_id: Ticket identifier
|
|
171
225
|
limit: Maximum number of comments
|
|
172
226
|
offset: Skip this many comments
|
|
173
227
|
|
|
174
228
|
Returns:
|
|
229
|
+
-------
|
|
175
230
|
List of comments for the ticket
|
|
176
231
|
|
|
177
232
|
"""
|
|
@@ -181,9 +236,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
181
236
|
"""Map universal state to system-specific state.
|
|
182
237
|
|
|
183
238
|
Args:
|
|
239
|
+
----
|
|
184
240
|
state: Universal ticket state
|
|
185
241
|
|
|
186
242
|
Returns:
|
|
243
|
+
-------
|
|
187
244
|
System-specific state string
|
|
188
245
|
|
|
189
246
|
"""
|
|
@@ -193,31 +250,88 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
193
250
|
"""Map system-specific state to universal state.
|
|
194
251
|
|
|
195
252
|
Args:
|
|
253
|
+
----
|
|
196
254
|
system_state: System-specific state string
|
|
197
255
|
|
|
198
256
|
Returns:
|
|
257
|
+
-------
|
|
199
258
|
Universal ticket state
|
|
200
259
|
|
|
201
260
|
"""
|
|
202
261
|
reverse_mapping = {v: k for k, v in self._state_mapping.items()}
|
|
203
262
|
return reverse_mapping.get(system_state, TicketState.OPEN)
|
|
204
263
|
|
|
264
|
+
def get_available_states(self) -> list[str]:
|
|
265
|
+
"""Get list of adapter-specific available states.
|
|
266
|
+
|
|
267
|
+
Returns adapter-specific state names that can be used for
|
|
268
|
+
semantic state matching. Override in subclasses to provide
|
|
269
|
+
platform-specific state names.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
-------
|
|
273
|
+
List of adapter-specific state names
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
-------
|
|
277
|
+
>>> # Linear adapter override
|
|
278
|
+
>>> def get_available_states(self):
|
|
279
|
+
... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
|
|
280
|
+
|
|
281
|
+
"""
|
|
282
|
+
# Default: return universal state values
|
|
283
|
+
return [state.value for state in TicketState]
|
|
284
|
+
|
|
285
|
+
def resolve_state(self, user_input: str) -> TicketState:
|
|
286
|
+
"""Resolve user input to universal state using semantic matcher.
|
|
287
|
+
|
|
288
|
+
Uses the semantic state matcher to interpret natural language
|
|
289
|
+
inputs and resolve them to universal TicketState values.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
----
|
|
293
|
+
user_input: Natural language state input (e.g., "working on it")
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
-------
|
|
297
|
+
Resolved universal TicketState
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
-------
|
|
301
|
+
>>> adapter = get_adapter()
|
|
302
|
+
>>> state = adapter.resolve_state("working on it")
|
|
303
|
+
>>> print(state)
|
|
304
|
+
TicketState.IN_PROGRESS
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
matcher = get_state_matcher()
|
|
308
|
+
adapter_states = self.get_available_states()
|
|
309
|
+
result = matcher.match_state(user_input, adapter_states)
|
|
310
|
+
return result.state
|
|
311
|
+
|
|
205
312
|
async def validate_transition(
|
|
206
313
|
self, ticket_id: str, target_state: TicketState
|
|
207
314
|
) -> bool:
|
|
208
315
|
"""Validate if state transition is allowed.
|
|
209
316
|
|
|
317
|
+
Validates both workflow rules and parent/child state constraints:
|
|
318
|
+
- Parent issues must remain at least as complete as their most complete child
|
|
319
|
+
- Standard workflow transitions must be valid
|
|
320
|
+
|
|
210
321
|
Args:
|
|
322
|
+
----
|
|
211
323
|
ticket_id: Ticket identifier
|
|
212
324
|
target_state: Target state
|
|
213
325
|
|
|
214
326
|
Returns:
|
|
327
|
+
-------
|
|
215
328
|
True if transition is valid
|
|
216
329
|
|
|
217
330
|
"""
|
|
218
331
|
ticket = await self.read(ticket_id)
|
|
219
332
|
if not ticket:
|
|
220
333
|
return False
|
|
334
|
+
|
|
221
335
|
# Handle case where state might be stored as string due to use_enum_values=True
|
|
222
336
|
current_state = ticket.state
|
|
223
337
|
if isinstance(current_state, str):
|
|
@@ -225,7 +339,35 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
225
339
|
current_state = TicketState(current_state)
|
|
226
340
|
except ValueError:
|
|
227
341
|
return False
|
|
228
|
-
|
|
342
|
+
|
|
343
|
+
# Check workflow transition validity
|
|
344
|
+
if not current_state.can_transition_to(target_state):
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
# Check parent/child state constraint
|
|
348
|
+
# If this ticket has children, ensure target state >= max child state
|
|
349
|
+
if isinstance(ticket, Task):
|
|
350
|
+
# Get all children
|
|
351
|
+
children = await self.list_tasks_by_issue(ticket_id)
|
|
352
|
+
if children:
|
|
353
|
+
# Find max child completion level
|
|
354
|
+
max_child_level = 0
|
|
355
|
+
for child in children:
|
|
356
|
+
child_state = child.state
|
|
357
|
+
if isinstance(child_state, str):
|
|
358
|
+
try:
|
|
359
|
+
child_state = TicketState(child_state)
|
|
360
|
+
except ValueError:
|
|
361
|
+
continue
|
|
362
|
+
max_child_level = max(
|
|
363
|
+
max_child_level, child_state.completion_level()
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Target state must be at least as complete as most complete child
|
|
367
|
+
if target_state.completion_level() < max_child_level:
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
return True
|
|
229
371
|
|
|
230
372
|
# Epic/Issue/Task Hierarchy Methods
|
|
231
373
|
|
|
@@ -235,11 +377,13 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
235
377
|
"""Create epic (top-level grouping).
|
|
236
378
|
|
|
237
379
|
Args:
|
|
380
|
+
----
|
|
238
381
|
title: Epic title
|
|
239
382
|
description: Epic description
|
|
240
383
|
**kwargs: Additional adapter-specific fields
|
|
241
384
|
|
|
242
385
|
Returns:
|
|
386
|
+
-------
|
|
243
387
|
Created epic or None if failed
|
|
244
388
|
|
|
245
389
|
"""
|
|
@@ -258,9 +402,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
258
402
|
"""Get epic by ID.
|
|
259
403
|
|
|
260
404
|
Args:
|
|
405
|
+
----
|
|
261
406
|
epic_id: Epic identifier
|
|
262
407
|
|
|
263
408
|
Returns:
|
|
409
|
+
-------
|
|
264
410
|
Epic if found, None otherwise
|
|
265
411
|
|
|
266
412
|
"""
|
|
@@ -274,9 +420,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
274
420
|
"""List all epics.
|
|
275
421
|
|
|
276
422
|
Args:
|
|
423
|
+
----
|
|
277
424
|
**kwargs: Adapter-specific filter parameters
|
|
278
425
|
|
|
279
426
|
Returns:
|
|
427
|
+
-------
|
|
280
428
|
List of epics
|
|
281
429
|
|
|
282
430
|
"""
|
|
@@ -296,12 +444,14 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
296
444
|
"""Create issue, optionally linked to epic.
|
|
297
445
|
|
|
298
446
|
Args:
|
|
447
|
+
----
|
|
299
448
|
title: Issue title
|
|
300
449
|
description: Issue description
|
|
301
450
|
epic_id: Optional parent epic ID
|
|
302
451
|
**kwargs: Additional adapter-specific fields
|
|
303
452
|
|
|
304
453
|
Returns:
|
|
454
|
+
-------
|
|
305
455
|
Created issue or None if failed
|
|
306
456
|
|
|
307
457
|
"""
|
|
@@ -318,9 +468,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
318
468
|
"""List all issues in epic.
|
|
319
469
|
|
|
320
470
|
Args:
|
|
471
|
+
----
|
|
321
472
|
epic_id: Epic identifier
|
|
322
473
|
|
|
323
474
|
Returns:
|
|
475
|
+
-------
|
|
324
476
|
List of issues belonging to epic
|
|
325
477
|
|
|
326
478
|
"""
|
|
@@ -335,15 +487,18 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
335
487
|
"""Create task as sub-ticket of parent issue.
|
|
336
488
|
|
|
337
489
|
Args:
|
|
490
|
+
----
|
|
338
491
|
title: Task title
|
|
339
492
|
parent_id: Required parent issue ID
|
|
340
493
|
description: Task description
|
|
341
494
|
**kwargs: Additional adapter-specific fields
|
|
342
495
|
|
|
343
496
|
Returns:
|
|
497
|
+
-------
|
|
344
498
|
Created task or None if failed
|
|
345
499
|
|
|
346
500
|
Raises:
|
|
501
|
+
------
|
|
347
502
|
ValueError: If parent_id is not provided
|
|
348
503
|
|
|
349
504
|
"""
|
|
@@ -369,9 +524,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
369
524
|
"""List all tasks under an issue.
|
|
370
525
|
|
|
371
526
|
Args:
|
|
527
|
+
----
|
|
372
528
|
issue_id: Issue identifier
|
|
373
529
|
|
|
374
530
|
Returns:
|
|
531
|
+
-------
|
|
375
532
|
List of tasks belonging to issue
|
|
376
533
|
|
|
377
534
|
"""
|
|
@@ -390,14 +547,17 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
390
547
|
"""Attach a file to a ticket.
|
|
391
548
|
|
|
392
549
|
Args:
|
|
550
|
+
----
|
|
393
551
|
ticket_id: Ticket identifier
|
|
394
552
|
file_path: Local file path to upload
|
|
395
553
|
description: Optional attachment description
|
|
396
554
|
|
|
397
555
|
Returns:
|
|
556
|
+
-------
|
|
398
557
|
Created Attachment with metadata
|
|
399
558
|
|
|
400
559
|
Raises:
|
|
560
|
+
------
|
|
401
561
|
NotImplementedError: If adapter doesn't support attachments
|
|
402
562
|
FileNotFoundError: If file doesn't exist
|
|
403
563
|
ValueError: If ticket doesn't exist or upload fails
|
|
@@ -412,9 +572,11 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
412
572
|
"""Get all attachments for a ticket.
|
|
413
573
|
|
|
414
574
|
Args:
|
|
575
|
+
----
|
|
415
576
|
ticket_id: Ticket identifier
|
|
416
577
|
|
|
417
578
|
Returns:
|
|
579
|
+
-------
|
|
418
580
|
List of attachments (empty if none or not supported)
|
|
419
581
|
|
|
420
582
|
"""
|
|
@@ -430,13 +592,16 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
430
592
|
"""Delete an attachment (optional implementation).
|
|
431
593
|
|
|
432
594
|
Args:
|
|
595
|
+
----
|
|
433
596
|
ticket_id: Ticket identifier
|
|
434
597
|
attachment_id: Attachment identifier
|
|
435
598
|
|
|
436
599
|
Returns:
|
|
600
|
+
-------
|
|
437
601
|
True if deleted, False otherwise
|
|
438
602
|
|
|
439
603
|
Raises:
|
|
604
|
+
------
|
|
440
605
|
NotImplementedError: If adapter doesn't support deletion
|
|
441
606
|
|
|
442
607
|
"""
|
mcp_ticketer/core/config.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Optional
|
|
9
|
+
from typing import Any, Optional, cast
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -38,9 +38,9 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
38
38
|
"""GitHub adapter configuration."""
|
|
39
39
|
|
|
40
40
|
type: AdapterType = AdapterType.GITHUB
|
|
41
|
-
token: str | None = Field(None
|
|
42
|
-
owner: str | None = Field(None
|
|
43
|
-
repo: str | None = Field(None
|
|
41
|
+
token: str | None = Field(default=None)
|
|
42
|
+
owner: str | None = Field(default=None)
|
|
43
|
+
repo: str | None = Field(default=None)
|
|
44
44
|
api_url: str = "https://api.github.com"
|
|
45
45
|
use_projects_v2: bool = False
|
|
46
46
|
custom_priority_scheme: dict[str, list[str]] | None = None
|
|
@@ -53,7 +53,7 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
53
53
|
v = os.getenv("GITHUB_TOKEN")
|
|
54
54
|
if not v:
|
|
55
55
|
raise ValueError("GitHub token is required")
|
|
56
|
-
return v
|
|
56
|
+
return cast(str, v)
|
|
57
57
|
|
|
58
58
|
@field_validator("owner", mode="before")
|
|
59
59
|
@classmethod
|
|
@@ -63,7 +63,7 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
63
63
|
v = os.getenv("GITHUB_OWNER")
|
|
64
64
|
if not v:
|
|
65
65
|
raise ValueError("GitHub owner is required")
|
|
66
|
-
return v
|
|
66
|
+
return cast(str, v)
|
|
67
67
|
|
|
68
68
|
@field_validator("repo", mode="before")
|
|
69
69
|
@classmethod
|
|
@@ -73,17 +73,17 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
73
73
|
v = os.getenv("GITHUB_REPO")
|
|
74
74
|
if not v:
|
|
75
75
|
raise ValueError("GitHub repo is required")
|
|
76
|
-
return v
|
|
76
|
+
return cast(str, v)
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
class JiraConfig(BaseAdapterConfig):
|
|
80
80
|
"""JIRA adapter configuration."""
|
|
81
81
|
|
|
82
82
|
type: AdapterType = AdapterType.JIRA
|
|
83
|
-
server: str | None = Field(None
|
|
84
|
-
email: str | None = Field(None
|
|
85
|
-
api_token: str | None = Field(None
|
|
86
|
-
project_key: str | None = Field(None
|
|
83
|
+
server: str | None = Field(default=None)
|
|
84
|
+
email: str | None = Field(default=None)
|
|
85
|
+
api_token: str | None = Field(default=None)
|
|
86
|
+
project_key: str | None = Field(default=None)
|
|
87
87
|
cloud: bool = True
|
|
88
88
|
verify_ssl: bool = True
|
|
89
89
|
|
|
@@ -95,7 +95,7 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
95
95
|
v = os.getenv("JIRA_SERVER")
|
|
96
96
|
if not v:
|
|
97
97
|
raise ValueError("JIRA server URL is required")
|
|
98
|
-
return v.rstrip("/")
|
|
98
|
+
return cast(str, v).rstrip("/")
|
|
99
99
|
|
|
100
100
|
@field_validator("email", mode="before")
|
|
101
101
|
@classmethod
|
|
@@ -105,7 +105,7 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
105
105
|
v = os.getenv("JIRA_EMAIL")
|
|
106
106
|
if not v:
|
|
107
107
|
raise ValueError("JIRA email is required")
|
|
108
|
-
return v
|
|
108
|
+
return cast(str, v)
|
|
109
109
|
|
|
110
110
|
@field_validator("api_token", mode="before")
|
|
111
111
|
@classmethod
|
|
@@ -115,14 +115,14 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
115
115
|
v = os.getenv("JIRA_API_TOKEN")
|
|
116
116
|
if not v:
|
|
117
117
|
raise ValueError("JIRA API token is required")
|
|
118
|
-
return v
|
|
118
|
+
return cast(str, v)
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
class LinearConfig(BaseAdapterConfig):
|
|
122
122
|
"""Linear adapter configuration."""
|
|
123
123
|
|
|
124
124
|
type: AdapterType = AdapterType.LINEAR
|
|
125
|
-
api_key: str | None = Field(None
|
|
125
|
+
api_key: str | None = Field(default=None)
|
|
126
126
|
workspace: str | None = None
|
|
127
127
|
team_key: str | None = None # Short team key like "BTA"
|
|
128
128
|
team_id: str | None = None # UUID team identifier
|
|
@@ -143,7 +143,7 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
143
143
|
v = os.getenv("LINEAR_API_KEY")
|
|
144
144
|
if not v:
|
|
145
145
|
raise ValueError("Linear API key is required")
|
|
146
|
-
return v
|
|
146
|
+
return cast(str, v)
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
class AITrackdownConfig(BaseAdapterConfig):
|
|
@@ -343,16 +343,16 @@ class ConfigurationManager:
|
|
|
343
343
|
try:
|
|
344
344
|
with open(config_path, encoding="utf-8") as file:
|
|
345
345
|
if config_path.suffix.lower() in [".yaml", ".yml"]:
|
|
346
|
-
return yaml.safe_load(file) or {}
|
|
346
|
+
return cast(dict[str, Any], yaml.safe_load(file) or {})
|
|
347
347
|
elif config_path.suffix.lower() == ".json":
|
|
348
|
-
return json.load(file)
|
|
348
|
+
return cast(dict[str, Any], json.load(file))
|
|
349
349
|
else:
|
|
350
350
|
# Try YAML first, then JSON
|
|
351
351
|
content = file.read()
|
|
352
352
|
try:
|
|
353
|
-
return yaml.safe_load(content) or {}
|
|
353
|
+
return cast(dict[str, Any], yaml.safe_load(content) or {})
|
|
354
354
|
except yaml.YAMLError:
|
|
355
|
-
return json.loads(content)
|
|
355
|
+
return cast(dict[str, Any], json.loads(content))
|
|
356
356
|
except Exception as e:
|
|
357
357
|
logger.error(f"Error loading config file {config_path}: {e}")
|
|
358
358
|
return {}
|
|
@@ -382,7 +382,7 @@ class ConfigurationManager:
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
# Convert discovered adapters to config format
|
|
385
|
-
config_data = {"adapters": {}, "default_adapter": None}
|
|
385
|
+
config_data: dict[str, Any] = {"adapters": {}, "default_adapter": None}
|
|
386
386
|
|
|
387
387
|
for adapter in discovered.adapters:
|
|
388
388
|
adapter_config = {"type": adapter.adapter_type, "enabled": True}
|
mcp_ticketer/core/exceptions.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""Exception classes for MCP Ticketer.
|
|
1
|
+
"""Exception classes for MCP Ticketer.
|
|
2
|
+
|
|
3
|
+
Error Severity Classification:
|
|
4
|
+
CRITICAL - System-level issues (auth, config, network) → Always suggest diagnostics
|
|
5
|
+
MEDIUM - Resource issues (not found, permissions) → Suggest diagnostics
|
|
6
|
+
LOW - User input errors (validation, state transitions) → No diagnostics
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|