mcp-ticketer 0.4.11__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 +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- 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/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- 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/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- 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 +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- 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 +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- 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.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.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,21 +339,51 @@ 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
|
|
|
232
374
|
async def create_epic(
|
|
233
|
-
self, title: str, description: str | None = None, **kwargs
|
|
375
|
+
self, title: str, description: str | None = None, **kwargs: Any
|
|
234
376
|
) -> Epic | None:
|
|
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
|
"""
|
|
@@ -270,13 +416,15 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
270
416
|
return result
|
|
271
417
|
return None
|
|
272
418
|
|
|
273
|
-
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
419
|
+
async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
|
|
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
|
"""
|
|
@@ -291,17 +439,19 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
291
439
|
title: str,
|
|
292
440
|
description: str | None = None,
|
|
293
441
|
epic_id: str | None = None,
|
|
294
|
-
**kwargs,
|
|
442
|
+
**kwargs: Any,
|
|
295
443
|
) -> Task | None:
|
|
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
|
"""
|
|
@@ -330,20 +482,23 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
330
482
|
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
331
483
|
|
|
332
484
|
async def create_task(
|
|
333
|
-
self, title: str, parent_id: str, description: str | None = None, **kwargs
|
|
485
|
+
self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
|
|
334
486
|
) -> Task | None:
|
|
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,92 +38,98 @@ 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
|
|
47
47
|
|
|
48
48
|
@field_validator("token", mode="before")
|
|
49
49
|
@classmethod
|
|
50
|
-
def validate_token(cls, v):
|
|
50
|
+
def validate_token(cls, v: Any) -> str:
|
|
51
|
+
"""Validate GitHub token from config or environment."""
|
|
51
52
|
if not v:
|
|
52
53
|
v = os.getenv("GITHUB_TOKEN")
|
|
53
54
|
if not v:
|
|
54
55
|
raise ValueError("GitHub token is required")
|
|
55
|
-
return v
|
|
56
|
+
return cast(str, v)
|
|
56
57
|
|
|
57
58
|
@field_validator("owner", mode="before")
|
|
58
59
|
@classmethod
|
|
59
|
-
def validate_owner(cls, v):
|
|
60
|
+
def validate_owner(cls, v: Any) -> str:
|
|
61
|
+
"""Validate GitHub repository owner from config or environment."""
|
|
60
62
|
if not v:
|
|
61
63
|
v = os.getenv("GITHUB_OWNER")
|
|
62
64
|
if not v:
|
|
63
65
|
raise ValueError("GitHub owner is required")
|
|
64
|
-
return v
|
|
66
|
+
return cast(str, v)
|
|
65
67
|
|
|
66
68
|
@field_validator("repo", mode="before")
|
|
67
69
|
@classmethod
|
|
68
|
-
def validate_repo(cls, v):
|
|
70
|
+
def validate_repo(cls, v: Any) -> str:
|
|
71
|
+
"""Validate GitHub repository name from config or environment."""
|
|
69
72
|
if not v:
|
|
70
73
|
v = os.getenv("GITHUB_REPO")
|
|
71
74
|
if not v:
|
|
72
75
|
raise ValueError("GitHub repo is required")
|
|
73
|
-
return v
|
|
76
|
+
return cast(str, v)
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
class JiraConfig(BaseAdapterConfig):
|
|
77
80
|
"""JIRA adapter configuration."""
|
|
78
81
|
|
|
79
82
|
type: AdapterType = AdapterType.JIRA
|
|
80
|
-
server: str | None = Field(None
|
|
81
|
-
email: str | None = Field(None
|
|
82
|
-
api_token: str | None = Field(None
|
|
83
|
-
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)
|
|
84
87
|
cloud: bool = True
|
|
85
88
|
verify_ssl: bool = True
|
|
86
89
|
|
|
87
90
|
@field_validator("server", mode="before")
|
|
88
91
|
@classmethod
|
|
89
|
-
def validate_server(cls, v):
|
|
92
|
+
def validate_server(cls, v: Any) -> str:
|
|
93
|
+
"""Validate JIRA server URL from config or environment."""
|
|
90
94
|
if not v:
|
|
91
95
|
v = os.getenv("JIRA_SERVER")
|
|
92
96
|
if not v:
|
|
93
97
|
raise ValueError("JIRA server URL is required")
|
|
94
|
-
return v.rstrip("/")
|
|
98
|
+
return cast(str, v).rstrip("/")
|
|
95
99
|
|
|
96
100
|
@field_validator("email", mode="before")
|
|
97
101
|
@classmethod
|
|
98
|
-
def validate_email(cls, v):
|
|
102
|
+
def validate_email(cls, v: Any) -> str:
|
|
103
|
+
"""Validate JIRA user email from config or environment."""
|
|
99
104
|
if not v:
|
|
100
105
|
v = os.getenv("JIRA_EMAIL")
|
|
101
106
|
if not v:
|
|
102
107
|
raise ValueError("JIRA email is required")
|
|
103
|
-
return v
|
|
108
|
+
return cast(str, v)
|
|
104
109
|
|
|
105
110
|
@field_validator("api_token", mode="before")
|
|
106
111
|
@classmethod
|
|
107
|
-
def validate_api_token(cls, v):
|
|
112
|
+
def validate_api_token(cls, v: Any) -> str:
|
|
113
|
+
"""Validate JIRA API token from config or environment."""
|
|
108
114
|
if not v:
|
|
109
115
|
v = os.getenv("JIRA_API_TOKEN")
|
|
110
116
|
if not v:
|
|
111
117
|
raise ValueError("JIRA API token is required")
|
|
112
|
-
return v
|
|
118
|
+
return cast(str, v)
|
|
113
119
|
|
|
114
120
|
|
|
115
121
|
class LinearConfig(BaseAdapterConfig):
|
|
116
122
|
"""Linear adapter configuration."""
|
|
117
123
|
|
|
118
124
|
type: AdapterType = AdapterType.LINEAR
|
|
119
|
-
api_key: str | None = Field(None
|
|
125
|
+
api_key: str | None = Field(default=None)
|
|
120
126
|
workspace: str | None = None
|
|
121
127
|
team_key: str | None = None # Short team key like "BTA"
|
|
122
128
|
team_id: str | None = None # UUID team identifier
|
|
123
129
|
api_url: str = "https://api.linear.app/graphql"
|
|
124
130
|
|
|
125
131
|
@model_validator(mode="after")
|
|
126
|
-
def validate_team_identifier(self):
|
|
132
|
+
def validate_team_identifier(self) -> "LinearConfig":
|
|
127
133
|
"""Ensure either team_key or team_id is provided."""
|
|
128
134
|
if not self.team_key and not self.team_id:
|
|
129
135
|
raise ValueError("Either team_key or team_id is required")
|
|
@@ -131,12 +137,13 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
131
137
|
|
|
132
138
|
@field_validator("api_key", mode="before")
|
|
133
139
|
@classmethod
|
|
134
|
-
def validate_api_key(cls, v):
|
|
140
|
+
def validate_api_key(cls, v: Any) -> str:
|
|
141
|
+
"""Validate Linear API key from config or environment."""
|
|
135
142
|
if not v:
|
|
136
143
|
v = os.getenv("LINEAR_API_KEY")
|
|
137
144
|
if not v:
|
|
138
145
|
raise ValueError("Linear API key is required")
|
|
139
|
-
return v
|
|
146
|
+
return cast(str, v)
|
|
140
147
|
|
|
141
148
|
|
|
142
149
|
class AITrackdownConfig(BaseAdapterConfig):
|
|
@@ -179,7 +186,7 @@ class AppConfig(BaseModel):
|
|
|
179
186
|
default_adapter: str | None = None
|
|
180
187
|
|
|
181
188
|
@model_validator(mode="after")
|
|
182
|
-
def validate_adapters(self):
|
|
189
|
+
def validate_adapters(self) -> "AppConfig":
|
|
183
190
|
"""Validate adapter configurations."""
|
|
184
191
|
adapters = self.adapters
|
|
185
192
|
|
|
@@ -220,7 +227,7 @@ class ConfigurationManager:
|
|
|
220
227
|
cls._instance = super().__new__(cls)
|
|
221
228
|
return cls._instance
|
|
222
229
|
|
|
223
|
-
def __init__(self):
|
|
230
|
+
def __init__(self) -> None:
|
|
224
231
|
"""Initialize configuration manager."""
|
|
225
232
|
if not hasattr(self, "_initialized"):
|
|
226
233
|
self._initialized = True
|
|
@@ -336,16 +343,16 @@ class ConfigurationManager:
|
|
|
336
343
|
try:
|
|
337
344
|
with open(config_path, encoding="utf-8") as file:
|
|
338
345
|
if config_path.suffix.lower() in [".yaml", ".yml"]:
|
|
339
|
-
return yaml.safe_load(file) or {}
|
|
346
|
+
return cast(dict[str, Any], yaml.safe_load(file) or {})
|
|
340
347
|
elif config_path.suffix.lower() == ".json":
|
|
341
|
-
return json.load(file)
|
|
348
|
+
return cast(dict[str, Any], json.load(file))
|
|
342
349
|
else:
|
|
343
350
|
# Try YAML first, then JSON
|
|
344
351
|
content = file.read()
|
|
345
352
|
try:
|
|
346
|
-
return yaml.safe_load(content) or {}
|
|
353
|
+
return cast(dict[str, Any], yaml.safe_load(content) or {})
|
|
347
354
|
except yaml.YAMLError:
|
|
348
|
-
return json.loads(content)
|
|
355
|
+
return cast(dict[str, Any], json.loads(content))
|
|
349
356
|
except Exception as e:
|
|
350
357
|
logger.error(f"Error loading config file {config_path}: {e}")
|
|
351
358
|
return {}
|
|
@@ -375,7 +382,7 @@ class ConfigurationManager:
|
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
# Convert discovered adapters to config format
|
|
378
|
-
config_data = {"adapters": {}, "default_adapter": None}
|
|
385
|
+
config_data: dict[str, Any] = {"adapters": {}, "default_adapter": None}
|
|
379
386
|
|
|
380
387
|
for adapter in discovered.adapters:
|
|
381
388
|
adapter_config = {"type": adapter.adapter_type, "enabled": True}
|