mcp-ticketer 0.1.22__py3-none-any.whl → 0.1.24__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 +15 -14
- mcp_ticketer/adapters/github.py +21 -20
- mcp_ticketer/adapters/hybrid.py +13 -12
- mcp_ticketer/adapters/jira.py +32 -27
- mcp_ticketer/adapters/linear.py +29 -26
- mcp_ticketer/cache/memory.py +2 -2
- mcp_ticketer/cli/auggie_configure.py +237 -0
- mcp_ticketer/cli/codex_configure.py +257 -0
- mcp_ticketer/cli/gemini_configure.py +261 -0
- mcp_ticketer/cli/main.py +171 -10
- mcp_ticketer/cli/migrate_config.py +3 -7
- mcp_ticketer/cli/utils.py +8 -8
- mcp_ticketer/core/adapter.py +12 -11
- mcp_ticketer/core/config.py +17 -17
- mcp_ticketer/core/env_discovery.py +24 -24
- mcp_ticketer/core/http_client.py +13 -13
- mcp_ticketer/core/mappers.py +25 -25
- mcp_ticketer/core/models.py +10 -10
- mcp_ticketer/core/project_config.py +25 -22
- mcp_ticketer/core/registry.py +7 -7
- mcp_ticketer/mcp/server.py +18 -18
- mcp_ticketer/queue/manager.py +2 -2
- mcp_ticketer/queue/queue.py +7 -7
- mcp_ticketer/queue/worker.py +8 -8
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/METADATA +58 -8
- mcp_ticketer-0.1.24.dist-info/RECORD +45 -0
- mcp_ticketer-0.1.22.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/top_level.txt +0 -0
mcp_ticketer/__init__.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
2
2
|
|
|
3
3
|
from .__version__ import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
__author__,
|
|
5
|
+
__author_email__,
|
|
6
|
+
__copyright__,
|
|
7
|
+
__description__,
|
|
8
|
+
__license__,
|
|
9
|
+
__title__,
|
|
10
|
+
__version__,
|
|
11
|
+
__version_info__,
|
|
12
|
+
get_user_agent,
|
|
13
|
+
get_version,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
mcp_ticketer/__version__.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""AI-Trackdown adapter implementation."""
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
3
4
|
import json
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, Optional, Union
|
|
7
8
|
|
|
8
9
|
from ..core.adapter import BaseAdapter
|
|
9
10
|
from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
|
|
@@ -24,7 +25,7 @@ except ImportError:
|
|
|
24
25
|
class AITrackdownAdapter(BaseAdapter[Task]):
|
|
25
26
|
"""Adapter for AI-Trackdown ticket system."""
|
|
26
27
|
|
|
27
|
-
def __init__(self, config:
|
|
28
|
+
def __init__(self, config: dict[str, Any]):
|
|
28
29
|
"""Initialize AI-Trackdown adapter.
|
|
29
30
|
|
|
30
31
|
Args:
|
|
@@ -58,7 +59,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
58
59
|
return False, "AITrackdown base_path is required in configuration"
|
|
59
60
|
return True, ""
|
|
60
61
|
|
|
61
|
-
def _get_state_mapping(self) ->
|
|
62
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
62
63
|
"""Map universal states to AI-Trackdown states."""
|
|
63
64
|
return {
|
|
64
65
|
TicketState.OPEN: "open",
|
|
@@ -84,7 +85,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
84
85
|
except ValueError:
|
|
85
86
|
return Priority.MEDIUM
|
|
86
87
|
|
|
87
|
-
def _task_from_ai_ticket(self, ai_ticket:
|
|
88
|
+
def _task_from_ai_ticket(self, ai_ticket: dict[str, Any]) -> Task:
|
|
88
89
|
"""Convert AI-Trackdown ticket to universal Task."""
|
|
89
90
|
return Task(
|
|
90
91
|
id=ai_ticket.get("id"),
|
|
@@ -109,7 +110,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
109
110
|
metadata={"ai_trackdown": ai_ticket},
|
|
110
111
|
)
|
|
111
112
|
|
|
112
|
-
def _epic_from_ai_ticket(self, ai_ticket:
|
|
113
|
+
def _epic_from_ai_ticket(self, ai_ticket: dict[str, Any]) -> Epic:
|
|
113
114
|
"""Convert AI-Trackdown ticket to universal Epic."""
|
|
114
115
|
return Epic(
|
|
115
116
|
id=ai_ticket.get("id"),
|
|
@@ -132,7 +133,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
132
133
|
metadata={"ai_trackdown": ai_ticket},
|
|
133
134
|
)
|
|
134
135
|
|
|
135
|
-
def _task_to_ai_ticket(self, task: Task) ->
|
|
136
|
+
def _task_to_ai_ticket(self, task: Task) -> dict[str, Any]:
|
|
136
137
|
"""Convert universal Task to AI-Trackdown ticket."""
|
|
137
138
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
138
139
|
state_value = task.state
|
|
@@ -159,7 +160,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
159
160
|
"type": "task",
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
def _epic_to_ai_ticket(self, epic: Epic) ->
|
|
163
|
+
def _epic_to_ai_ticket(self, epic: Epic) -> dict[str, Any]:
|
|
163
164
|
"""Convert universal Epic to AI-Trackdown ticket."""
|
|
164
165
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
165
166
|
state_value = epic.state
|
|
@@ -184,7 +185,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
184
185
|
"type": "epic",
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
def _read_ticket_file(self, ticket_id: str) -> Optional[
|
|
188
|
+
def _read_ticket_file(self, ticket_id: str) -> Optional[dict[str, Any]]:
|
|
188
189
|
"""Read ticket from file system."""
|
|
189
190
|
ticket_file = self.tickets_dir / f"{ticket_id}.json"
|
|
190
191
|
if ticket_file.exists():
|
|
@@ -192,7 +193,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
192
193
|
return json.load(f)
|
|
193
194
|
return None
|
|
194
195
|
|
|
195
|
-
def _write_ticket_file(self, ticket_id: str, data:
|
|
196
|
+
def _write_ticket_file(self, ticket_id: str, data: dict[str, Any]) -> None:
|
|
196
197
|
"""Write ticket to file system."""
|
|
197
198
|
ticket_file = self.tickets_dir / f"{ticket_id}.json"
|
|
198
199
|
with open(ticket_file, "w") as f:
|
|
@@ -250,7 +251,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
250
251
|
return None
|
|
251
252
|
|
|
252
253
|
async def update(
|
|
253
|
-
self, ticket_id: str, updates: Union[
|
|
254
|
+
self, ticket_id: str, updates: Union[dict[str, Any], Task]
|
|
254
255
|
) -> Optional[Task]:
|
|
255
256
|
"""Update a task."""
|
|
256
257
|
# Read existing ticket
|
|
@@ -297,8 +298,8 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
297
298
|
return False
|
|
298
299
|
|
|
299
300
|
async def list(
|
|
300
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
301
|
-
) ->
|
|
301
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
302
|
+
) -> list[Task]:
|
|
302
303
|
"""List tasks with pagination."""
|
|
303
304
|
tasks = []
|
|
304
305
|
|
|
@@ -342,7 +343,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
342
343
|
|
|
343
344
|
return tasks
|
|
344
345
|
|
|
345
|
-
async def search(self, query: SearchQuery) ->
|
|
346
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task]:
|
|
346
347
|
"""Search tasks using query parameters."""
|
|
347
348
|
filters = {}
|
|
348
349
|
if query.state:
|
|
@@ -410,7 +411,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
410
411
|
|
|
411
412
|
async def get_comments(
|
|
412
413
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
413
|
-
) ->
|
|
414
|
+
) -> builtins.list[Comment]:
|
|
414
415
|
"""Get comments for a task."""
|
|
415
416
|
comments = []
|
|
416
417
|
comments_dir = self.base_path / "comments"
|
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""GitHub adapter implementation using REST API v3 and GraphQL API v4."""
|
|
2
2
|
|
|
3
|
+
import builtins
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
from datetime import datetime
|
|
6
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, Optional
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
|
|
@@ -135,7 +136,7 @@ class GitHubGraphQLQueries:
|
|
|
135
136
|
class GitHubAdapter(BaseAdapter[Task]):
|
|
136
137
|
"""Adapter for GitHub Issues tracking system."""
|
|
137
138
|
|
|
138
|
-
def __init__(self, config:
|
|
139
|
+
def __init__(self, config: dict[str, Any]):
|
|
139
140
|
"""Initialize GitHub adapter.
|
|
140
141
|
|
|
141
142
|
Args:
|
|
@@ -192,9 +193,9 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
192
193
|
)
|
|
193
194
|
|
|
194
195
|
# Cache for labels and milestones
|
|
195
|
-
self._labels_cache: Optional[
|
|
196
|
-
self._milestones_cache: Optional[
|
|
197
|
-
self._rate_limit:
|
|
196
|
+
self._labels_cache: Optional[list[dict[str, Any]]] = None
|
|
197
|
+
self._milestones_cache: Optional[list[dict[str, Any]]] = None
|
|
198
|
+
self._rate_limit: dict[str, Any] = {}
|
|
198
199
|
|
|
199
200
|
def validate_credentials(self) -> tuple[bool, str]:
|
|
200
201
|
"""Validate that required credentials are present.
|
|
@@ -220,7 +221,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
220
221
|
)
|
|
221
222
|
return True, ""
|
|
222
223
|
|
|
223
|
-
def _get_state_mapping(self) ->
|
|
224
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
224
225
|
"""Map universal states to GitHub states."""
|
|
225
226
|
return {
|
|
226
227
|
TicketState.OPEN: GitHubStateMapping.OPEN,
|
|
@@ -237,7 +238,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
237
238
|
"""Get the label name for extended states."""
|
|
238
239
|
return GitHubStateMapping.STATE_LABELS.get(state)
|
|
239
240
|
|
|
240
|
-
def _get_priority_from_labels(self, labels:
|
|
241
|
+
def _get_priority_from_labels(self, labels: list[str]) -> Priority:
|
|
241
242
|
"""Extract priority from issue labels."""
|
|
242
243
|
label_names = [label.lower() for label in labels]
|
|
243
244
|
|
|
@@ -272,7 +273,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
272
273
|
else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
|
|
273
274
|
)
|
|
274
275
|
|
|
275
|
-
def _extract_state_from_issue(self, issue:
|
|
276
|
+
def _extract_state_from_issue(self, issue: dict[str, Any]) -> TicketState:
|
|
276
277
|
"""Extract ticket state from GitHub issue data."""
|
|
277
278
|
# Check if closed
|
|
278
279
|
if issue["state"] == "closed":
|
|
@@ -298,7 +299,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
298
299
|
|
|
299
300
|
return TicketState.OPEN
|
|
300
301
|
|
|
301
|
-
def _task_from_github_issue(self, issue:
|
|
302
|
+
def _task_from_github_issue(self, issue: dict[str, Any]) -> Task:
|
|
302
303
|
"""Convert GitHub issue to universal Task."""
|
|
303
304
|
# Extract labels
|
|
304
305
|
labels = []
|
|
@@ -415,8 +416,8 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
415
416
|
self._labels_cache.append(response.json())
|
|
416
417
|
|
|
417
418
|
async def _graphql_request(
|
|
418
|
-
self, query: str, variables:
|
|
419
|
-
) ->
|
|
419
|
+
self, query: str, variables: dict[str, Any]
|
|
420
|
+
) -> dict[str, Any]:
|
|
420
421
|
"""Execute a GraphQL query."""
|
|
421
422
|
response = await self.client.post(
|
|
422
423
|
self.graphql_url, json={"query": query, "variables": variables}
|
|
@@ -527,7 +528,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
527
528
|
except httpx.HTTPError:
|
|
528
529
|
return None
|
|
529
530
|
|
|
530
|
-
async def update(self, ticket_id: str, updates:
|
|
531
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[Task]:
|
|
531
532
|
"""Update a GitHub issue."""
|
|
532
533
|
# Validate credentials before attempting operation
|
|
533
534
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -679,8 +680,8 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
679
680
|
return False
|
|
680
681
|
|
|
681
682
|
async def list(
|
|
682
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
683
|
-
) ->
|
|
683
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
684
|
+
) -> list[Task]:
|
|
684
685
|
"""List GitHub issues with filters."""
|
|
685
686
|
# Build query parameters
|
|
686
687
|
params = {
|
|
@@ -743,7 +744,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
743
744
|
|
|
744
745
|
return [self._task_from_github_issue(issue) for issue in issues]
|
|
745
746
|
|
|
746
|
-
async def search(self, query: SearchQuery) ->
|
|
747
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task]:
|
|
747
748
|
"""Search GitHub issues using advanced search syntax."""
|
|
748
749
|
# Build GitHub search query
|
|
749
750
|
search_parts = [f"repo:{self.owner}/{self.repo}", "is:issue"]
|
|
@@ -875,7 +876,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
875
876
|
|
|
876
877
|
async def get_comments(
|
|
877
878
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
878
|
-
) ->
|
|
879
|
+
) -> builtins.list[Comment]:
|
|
879
880
|
"""Get comments for a GitHub issue."""
|
|
880
881
|
try:
|
|
881
882
|
issue_number = int(ticket_id)
|
|
@@ -919,7 +920,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
919
920
|
except httpx.HTTPError:
|
|
920
921
|
return []
|
|
921
922
|
|
|
922
|
-
async def get_rate_limit(self) ->
|
|
923
|
+
async def get_rate_limit(self) -> dict[str, Any]:
|
|
923
924
|
"""Get current rate limit status."""
|
|
924
925
|
response = await self.client.get("/rate_limit")
|
|
925
926
|
response.raise_for_status()
|
|
@@ -1006,7 +1007,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
1006
1007
|
|
|
1007
1008
|
async def list_milestones(
|
|
1008
1009
|
self, state: str = "open", limit: int = 10, offset: int = 0
|
|
1009
|
-
) ->
|
|
1010
|
+
) -> builtins.list[Epic]:
|
|
1010
1011
|
"""List GitHub milestones as Epics."""
|
|
1011
1012
|
params = {
|
|
1012
1013
|
"state": state,
|
|
@@ -1071,7 +1072,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
1071
1072
|
title: Optional[str] = None,
|
|
1072
1073
|
body: Optional[str] = None,
|
|
1073
1074
|
draft: bool = False,
|
|
1074
|
-
) ->
|
|
1075
|
+
) -> dict[str, Any]:
|
|
1075
1076
|
"""Create a pull request linked to an issue.
|
|
1076
1077
|
|
|
1077
1078
|
Args:
|
|
@@ -1246,7 +1247,7 @@ Fixes #{issue_number}
|
|
|
1246
1247
|
self,
|
|
1247
1248
|
ticket_id: str,
|
|
1248
1249
|
pr_url: str,
|
|
1249
|
-
) ->
|
|
1250
|
+
) -> dict[str, Any]:
|
|
1250
1251
|
"""Link an existing pull request to a ticket.
|
|
1251
1252
|
|
|
1252
1253
|
Args:
|
mcp_ticketer/adapters/hybrid.py
CHANGED
|
@@ -4,10 +4,11 @@ This adapter enables synchronization across multiple ticketing systems
|
|
|
4
4
|
(Linear, JIRA, GitHub, AITrackdown) with configurable sync strategies.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import builtins
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Any,
|
|
11
|
+
from typing import Any, Optional
|
|
11
12
|
|
|
12
13
|
from ..core.adapter import BaseAdapter
|
|
13
14
|
from ..core.models import Comment, Epic, SearchQuery, Task, TicketState
|
|
@@ -27,7 +28,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
27
28
|
Maintains mapping between ticket IDs across different systems.
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
|
-
def __init__(self, config:
|
|
31
|
+
def __init__(self, config: dict[str, Any]):
|
|
31
32
|
"""Initialize hybrid adapter.
|
|
32
33
|
|
|
33
34
|
Args:
|
|
@@ -40,7 +41,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
40
41
|
"""
|
|
41
42
|
super().__init__(config)
|
|
42
43
|
|
|
43
|
-
self.adapters:
|
|
44
|
+
self.adapters: dict[str, BaseAdapter] = {}
|
|
44
45
|
self.primary_adapter_name = config.get("primary_adapter")
|
|
45
46
|
self.sync_strategy = config.get("sync_strategy", "primary_source")
|
|
46
47
|
|
|
@@ -70,12 +71,12 @@ class HybridAdapter(BaseAdapter):
|
|
|
70
71
|
)
|
|
71
72
|
self.id_mapping = self._load_mapping()
|
|
72
73
|
|
|
73
|
-
def _get_state_mapping(self) ->
|
|
74
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
74
75
|
"""Get state mapping from primary adapter."""
|
|
75
76
|
primary = self.adapters[self.primary_adapter_name]
|
|
76
77
|
return primary._get_state_mapping()
|
|
77
78
|
|
|
78
|
-
def _load_mapping(self) ->
|
|
79
|
+
def _load_mapping(self) -> dict[str, dict[str, str]]:
|
|
79
80
|
"""Load ID mapping from file.
|
|
80
81
|
|
|
81
82
|
Mapping format:
|
|
@@ -207,7 +208,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
207
208
|
return primary_ticket
|
|
208
209
|
|
|
209
210
|
def _add_cross_references(
|
|
210
|
-
self, ticket: Task | Epic, results:
|
|
211
|
+
self, ticket: Task | Epic, results: list[tuple[str, Task | Epic]]
|
|
211
212
|
) -> None:
|
|
212
213
|
"""Add cross-references to ticket description.
|
|
213
214
|
|
|
@@ -253,7 +254,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
253
254
|
return await primary.read(ticket_id)
|
|
254
255
|
|
|
255
256
|
async def update(
|
|
256
|
-
self, ticket_id: str, updates:
|
|
257
|
+
self, ticket_id: str, updates: dict[str, Any]
|
|
257
258
|
) -> Optional[Task | Epic]:
|
|
258
259
|
"""Update ticket across all adapters.
|
|
259
260
|
|
|
@@ -358,8 +359,8 @@ class HybridAdapter(BaseAdapter):
|
|
|
358
359
|
return success_count > 0
|
|
359
360
|
|
|
360
361
|
async def list(
|
|
361
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
362
|
-
) ->
|
|
362
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
363
|
+
) -> list[Task | Epic]:
|
|
363
364
|
"""List tickets from primary adapter.
|
|
364
365
|
|
|
365
366
|
Args:
|
|
@@ -374,7 +375,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
374
375
|
primary = self.adapters[self.primary_adapter_name]
|
|
375
376
|
return await primary.list(limit, offset, filters)
|
|
376
377
|
|
|
377
|
-
async def search(self, query: SearchQuery) ->
|
|
378
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task | Epic]:
|
|
378
379
|
"""Search tickets in primary adapter.
|
|
379
380
|
|
|
380
381
|
Args:
|
|
@@ -488,7 +489,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
488
489
|
|
|
489
490
|
async def get_comments(
|
|
490
491
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
491
|
-
) ->
|
|
492
|
+
) -> builtins.list[Comment]:
|
|
492
493
|
"""Get comments from primary adapter.
|
|
493
494
|
|
|
494
495
|
Args:
|
|
@@ -520,7 +521,7 @@ class HybridAdapter(BaseAdapter):
|
|
|
520
521
|
except Exception as e:
|
|
521
522
|
logger.error(f"Error closing adapter: {e}")
|
|
522
523
|
|
|
523
|
-
async def sync_status(self) ->
|
|
524
|
+
async def sync_status(self) -> dict[str, Any]:
|
|
524
525
|
"""Get synchronization status across all adapters.
|
|
525
526
|
|
|
526
527
|
Returns:
|
mcp_ticketer/adapters/jira.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""JIRA adapter implementation using REST API v3."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import builtins
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from enum import Enum
|
|
8
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, Optional, Union
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
from httpx import AsyncClient, HTTPStatusError, TimeoutException
|
|
@@ -42,7 +43,7 @@ class JiraPriority(str, Enum):
|
|
|
42
43
|
class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
43
44
|
"""Adapter for JIRA using REST API v3."""
|
|
44
45
|
|
|
45
|
-
def __init__(self, config:
|
|
46
|
+
def __init__(self, config: dict[str, Any]):
|
|
46
47
|
"""Initialize JIRA adapter.
|
|
47
48
|
|
|
48
49
|
Args:
|
|
@@ -93,10 +94,10 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
# Cache for workflow states and transitions
|
|
96
|
-
self._workflow_cache:
|
|
97
|
-
self._priority_cache:
|
|
98
|
-
self._issue_types_cache:
|
|
99
|
-
self._custom_fields_cache:
|
|
97
|
+
self._workflow_cache: dict[str, Any] = {}
|
|
98
|
+
self._priority_cache: list[dict[str, Any]] = []
|
|
99
|
+
self._issue_types_cache: dict[str, Any] = {}
|
|
100
|
+
self._custom_fields_cache: dict[str, Any] = {}
|
|
100
101
|
|
|
101
102
|
def validate_credentials(self) -> tuple[bool, str]:
|
|
102
103
|
"""Validate that required credentials are present.
|
|
@@ -122,7 +123,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
122
123
|
)
|
|
123
124
|
return True, ""
|
|
124
125
|
|
|
125
|
-
def _get_state_mapping(self) ->
|
|
126
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
126
127
|
"""Map universal states to common JIRA workflow states."""
|
|
127
128
|
return {
|
|
128
129
|
TicketState.OPEN: "To Do",
|
|
@@ -148,10 +149,10 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
148
149
|
self,
|
|
149
150
|
method: str,
|
|
150
151
|
endpoint: str,
|
|
151
|
-
data: Optional[
|
|
152
|
-
params: Optional[
|
|
152
|
+
data: Optional[dict[str, Any]] = None,
|
|
153
|
+
params: Optional[dict[str, Any]] = None,
|
|
153
154
|
retry_count: int = 0,
|
|
154
|
-
) ->
|
|
155
|
+
) -> dict[str, Any]:
|
|
155
156
|
"""Make HTTP request to JIRA API with retry logic.
|
|
156
157
|
|
|
157
158
|
Args:
|
|
@@ -207,7 +208,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
207
208
|
)
|
|
208
209
|
raise e
|
|
209
210
|
|
|
210
|
-
async def _get_priorities(self) ->
|
|
211
|
+
async def _get_priorities(self) -> list[dict[str, Any]]:
|
|
211
212
|
"""Get available priorities from JIRA."""
|
|
212
213
|
if not self._priority_cache:
|
|
213
214
|
self._priority_cache = await self._make_request("GET", "priority")
|
|
@@ -215,7 +216,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
215
216
|
|
|
216
217
|
async def _get_issue_types(
|
|
217
218
|
self, project_key: Optional[str] = None
|
|
218
|
-
) ->
|
|
219
|
+
) -> list[dict[str, Any]]:
|
|
219
220
|
"""Get available issue types for a project."""
|
|
220
221
|
key = project_key or self.project_key
|
|
221
222
|
if key not in self._issue_types_cache:
|
|
@@ -223,12 +224,12 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
223
224
|
self._issue_types_cache[key] = data.get("issueTypes", [])
|
|
224
225
|
return self._issue_types_cache[key]
|
|
225
226
|
|
|
226
|
-
async def _get_transitions(self, issue_key: str) ->
|
|
227
|
+
async def _get_transitions(self, issue_key: str) -> list[dict[str, Any]]:
|
|
227
228
|
"""Get available transitions for an issue."""
|
|
228
229
|
data = await self._make_request("GET", f"issue/{issue_key}/transitions")
|
|
229
230
|
return data.get("transitions", [])
|
|
230
231
|
|
|
231
|
-
async def _get_custom_fields(self) ->
|
|
232
|
+
async def _get_custom_fields(self) -> dict[str, str]:
|
|
232
233
|
"""Get custom field definitions."""
|
|
233
234
|
if not self._custom_fields_cache:
|
|
234
235
|
fields = await self._make_request("GET", "field")
|
|
@@ -274,7 +275,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
274
275
|
|
|
275
276
|
return "\n".join(lines)
|
|
276
277
|
|
|
277
|
-
def _convert_to_adf(self, text: str) ->
|
|
278
|
+
def _convert_to_adf(self, text: str) -> dict[str, Any]:
|
|
278
279
|
"""Convert plain text to Atlassian Document Format (ADF).
|
|
279
280
|
|
|
280
281
|
ADF is required for JIRA Cloud description fields.
|
|
@@ -308,7 +309,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
308
309
|
return mapping.get(priority, JiraPriority.MEDIUM)
|
|
309
310
|
|
|
310
311
|
def _map_priority_from_jira(
|
|
311
|
-
self, jira_priority: Optional[
|
|
312
|
+
self, jira_priority: Optional[dict[str, Any]]
|
|
312
313
|
) -> Priority:
|
|
313
314
|
"""Map JIRA priority to universal priority."""
|
|
314
315
|
if not jira_priority:
|
|
@@ -325,7 +326,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
325
326
|
else:
|
|
326
327
|
return Priority.MEDIUM
|
|
327
328
|
|
|
328
|
-
def _map_state_from_jira(self, status:
|
|
329
|
+
def _map_state_from_jira(self, status: dict[str, Any]) -> TicketState:
|
|
329
330
|
"""Map JIRA status to universal state."""
|
|
330
331
|
if not status:
|
|
331
332
|
return TicketState.OPEN
|
|
@@ -359,7 +360,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
359
360
|
else:
|
|
360
361
|
return TicketState.OPEN
|
|
361
362
|
|
|
362
|
-
def _issue_to_ticket(self, issue:
|
|
363
|
+
def _issue_to_ticket(self, issue: dict[str, Any]) -> Union[Epic, Task]:
|
|
363
364
|
"""Convert JIRA issue to universal ticket model."""
|
|
364
365
|
fields = issue.get("fields", {})
|
|
365
366
|
|
|
@@ -443,7 +444,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
443
444
|
|
|
444
445
|
def _ticket_to_issue_fields(
|
|
445
446
|
self, ticket: Union[Epic, Task], issue_type: Optional[str] = None
|
|
446
|
-
) ->
|
|
447
|
+
) -> dict[str, Any]:
|
|
447
448
|
"""Convert universal ticket to JIRA issue fields."""
|
|
448
449
|
# Convert description to ADF format for JIRA Cloud
|
|
449
450
|
description = (
|
|
@@ -526,7 +527,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
526
527
|
raise
|
|
527
528
|
|
|
528
529
|
async def update(
|
|
529
|
-
self, ticket_id: str, updates:
|
|
530
|
+
self, ticket_id: str, updates: dict[str, Any]
|
|
530
531
|
) -> Optional[Union[Epic, Task]]:
|
|
531
532
|
"""Update a JIRA issue."""
|
|
532
533
|
# Validate credentials before attempting operation
|
|
@@ -584,8 +585,8 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
584
585
|
raise
|
|
585
586
|
|
|
586
587
|
async def list(
|
|
587
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
588
|
-
) ->
|
|
588
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
589
|
+
) -> list[Union[Epic, Task]]:
|
|
589
590
|
"""List JIRA issues with pagination."""
|
|
590
591
|
# Build JQL query
|
|
591
592
|
jql_parts = []
|
|
@@ -624,7 +625,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
624
625
|
issues = data.get("issues", [])
|
|
625
626
|
return [self._issue_to_ticket(issue) for issue in issues]
|
|
626
627
|
|
|
627
|
-
async def search(self, query: SearchQuery) ->
|
|
628
|
+
async def search(self, query: SearchQuery) -> builtins.list[Union[Epic, Task]]:
|
|
628
629
|
"""Search JIRA issues using JQL."""
|
|
629
630
|
# Build JQL query
|
|
630
631
|
jql_parts = []
|
|
@@ -749,7 +750,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
749
750
|
|
|
750
751
|
async def get_comments(
|
|
751
752
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
752
|
-
) ->
|
|
753
|
+
) -> builtins.list[Comment]:
|
|
753
754
|
"""Get comments for a JIRA issue."""
|
|
754
755
|
# Fetch issue with comments
|
|
755
756
|
params = {"expand": "comments", "fields": "comment"}
|
|
@@ -785,7 +786,7 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
785
786
|
|
|
786
787
|
async def get_project_info(
|
|
787
788
|
self, project_key: Optional[str] = None
|
|
788
|
-
) ->
|
|
789
|
+
) -> dict[str, Any]:
|
|
789
790
|
"""Get JIRA project information including workflows and fields."""
|
|
790
791
|
key = project_key or self.project_key
|
|
791
792
|
if not key:
|
|
@@ -805,7 +806,9 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
805
806
|
"custom_fields": custom_fields,
|
|
806
807
|
}
|
|
807
808
|
|
|
808
|
-
async def execute_jql(
|
|
809
|
+
async def execute_jql(
|
|
810
|
+
self, jql: str, limit: int = 50
|
|
811
|
+
) -> builtins.list[Union[Epic, Task]]:
|
|
809
812
|
"""Execute a raw JQL query.
|
|
810
813
|
|
|
811
814
|
Args:
|
|
@@ -830,7 +833,9 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
830
833
|
issues = data.get("issues", [])
|
|
831
834
|
return [self._issue_to_ticket(issue) for issue in issues]
|
|
832
835
|
|
|
833
|
-
async def get_sprints(
|
|
836
|
+
async def get_sprints(
|
|
837
|
+
self, board_id: Optional[int] = None
|
|
838
|
+
) -> builtins.list[dict[str, Any]]:
|
|
834
839
|
"""Get active sprints for a board (requires JIRA Software).
|
|
835
840
|
|
|
836
841
|
Args:
|