mcp-ticketer 0.1.22__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/__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 +28 -27
- mcp_ticketer/adapters/linear.py +26 -25
- mcp_ticketer/cache/memory.py +2 -2
- mcp_ticketer/cli/main.py +36 -8
- mcp_ticketer/cli/migrate_config.py +2 -2
- 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 +22 -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.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.22.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/linear.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Linear adapter implementation using native GraphQL API with full feature support."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import builtins
|
|
4
5
|
import os
|
|
5
6
|
from datetime import date, datetime
|
|
6
7
|
from enum import Enum
|
|
7
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Optional, Union
|
|
8
9
|
|
|
9
10
|
from gql import Client, gql
|
|
10
11
|
from gql.transport.exceptions import TransportQueryError
|
|
@@ -278,7 +279,7 @@ ISSUE_LIST_FRAGMENTS = (
|
|
|
278
279
|
class LinearAdapter(BaseAdapter[Task]):
|
|
279
280
|
"""Adapter for Linear issue tracking system using native GraphQL API."""
|
|
280
281
|
|
|
281
|
-
def __init__(self, config:
|
|
282
|
+
def __init__(self, config: dict[str, Any]):
|
|
282
283
|
"""Initialize Linear adapter.
|
|
283
284
|
|
|
284
285
|
Args:
|
|
@@ -315,9 +316,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
315
316
|
|
|
316
317
|
# Caches for frequently used data
|
|
317
318
|
self._team_id: Optional[str] = None
|
|
318
|
-
self._workflow_states: Optional[
|
|
319
|
-
self._labels: Optional[
|
|
320
|
-
self._users: Optional[
|
|
319
|
+
self._workflow_states: Optional[dict[str, dict[str, Any]]] = None
|
|
320
|
+
self._labels: Optional[dict[str, str]] = None # name -> id
|
|
321
|
+
self._users: Optional[dict[str, str]] = None # email -> id
|
|
321
322
|
|
|
322
323
|
# Initialize state mapping
|
|
323
324
|
self._state_mapping = self._get_state_mapping()
|
|
@@ -435,7 +436,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
435
436
|
|
|
436
437
|
async def _fetch_workflow_states_data(
|
|
437
438
|
self, team_id: str
|
|
438
|
-
) ->
|
|
439
|
+
) -> dict[str, dict[str, Any]]:
|
|
439
440
|
"""Fetch workflow states data."""
|
|
440
441
|
query = gql(
|
|
441
442
|
"""
|
|
@@ -467,7 +468,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
467
468
|
|
|
468
469
|
return workflow_states
|
|
469
470
|
|
|
470
|
-
async def _fetch_labels_data(self, team_id: str) ->
|
|
471
|
+
async def _fetch_labels_data(self, team_id: str) -> dict[str, str]:
|
|
471
472
|
"""Fetch labels data."""
|
|
472
473
|
query = gql(
|
|
473
474
|
"""
|
|
@@ -498,7 +499,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
498
499
|
await self._ensure_initialized()
|
|
499
500
|
return self._team_id
|
|
500
501
|
|
|
501
|
-
async def _get_workflow_states(self) ->
|
|
502
|
+
async def _get_workflow_states(self) -> dict[str, dict[str, Any]]:
|
|
502
503
|
"""Get cached workflow states from Linear."""
|
|
503
504
|
await self._ensure_initialized()
|
|
504
505
|
return self._workflow_states
|
|
@@ -619,7 +620,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
619
620
|
)
|
|
620
621
|
return True, ""
|
|
621
622
|
|
|
622
|
-
def _get_state_mapping(self) ->
|
|
623
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
623
624
|
"""Get mapping from universal states to Linear state types.
|
|
624
625
|
|
|
625
626
|
Required by BaseAdapter abstract method.
|
|
@@ -643,7 +644,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
643
644
|
return self._state_mapping.get(state, LinearStateType.BACKLOG)
|
|
644
645
|
|
|
645
646
|
def _map_linear_state(
|
|
646
|
-
self, state_data:
|
|
647
|
+
self, state_data: dict[str, Any], labels: list[str]
|
|
647
648
|
) -> TicketState:
|
|
648
649
|
"""Map Linear state and labels to universal state."""
|
|
649
650
|
state_type = state_data.get("type", "").lower()
|
|
@@ -669,7 +670,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
669
670
|
}
|
|
670
671
|
return state_mapping.get(state_type, TicketState.OPEN)
|
|
671
672
|
|
|
672
|
-
def _task_from_linear_issue(self, issue:
|
|
673
|
+
def _task_from_linear_issue(self, issue: dict[str, Any]) -> Task:
|
|
673
674
|
"""Convert Linear issue to universal Task."""
|
|
674
675
|
# Extract labels
|
|
675
676
|
tags = []
|
|
@@ -786,7 +787,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
786
787
|
metadata=metadata,
|
|
787
788
|
)
|
|
788
789
|
|
|
789
|
-
def _epic_from_linear_project(self, project:
|
|
790
|
+
def _epic_from_linear_project(self, project: dict[str, Any]) -> Epic:
|
|
790
791
|
"""Convert Linear project to universal Epic."""
|
|
791
792
|
# Map project state to ticket state
|
|
792
793
|
project_state = project.get("state", "planned").lower()
|
|
@@ -1005,7 +1006,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1005
1006
|
|
|
1006
1007
|
return None
|
|
1007
1008
|
|
|
1008
|
-
async def update(self, ticket_id: str, updates:
|
|
1009
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[Task]:
|
|
1009
1010
|
"""Update a Linear issue with comprehensive field support."""
|
|
1010
1011
|
# Validate credentials before attempting operation
|
|
1011
1012
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -1162,8 +1163,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1162
1163
|
return result.get("issueArchive", {}).get("success", False)
|
|
1163
1164
|
|
|
1164
1165
|
async def list(
|
|
1165
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
1166
|
-
) ->
|
|
1166
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
1167
|
+
) -> list[Task]:
|
|
1167
1168
|
"""List Linear issues with comprehensive filtering."""
|
|
1168
1169
|
team_id = await self._ensure_team_id()
|
|
1169
1170
|
|
|
@@ -1271,7 +1272,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1271
1272
|
|
|
1272
1273
|
return tasks
|
|
1273
1274
|
|
|
1274
|
-
async def search(self, query: SearchQuery) ->
|
|
1275
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task]:
|
|
1275
1276
|
"""Search Linear issues with advanced filtering and text search."""
|
|
1276
1277
|
team_id = await self._ensure_team_id()
|
|
1277
1278
|
|
|
@@ -1445,7 +1446,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1445
1446
|
|
|
1446
1447
|
async def get_comments(
|
|
1447
1448
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
1448
|
-
) ->
|
|
1449
|
+
) -> builtins.list[Comment]:
|
|
1449
1450
|
"""Get comments for a Linear issue with pagination."""
|
|
1450
1451
|
query = gql(
|
|
1451
1452
|
USER_FRAGMENT
|
|
@@ -1546,7 +1547,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1546
1547
|
|
|
1547
1548
|
return result["projectCreate"]["project"]["id"]
|
|
1548
1549
|
|
|
1549
|
-
async def get_cycles(self, active_only: bool = True) ->
|
|
1550
|
+
async def get_cycles(self, active_only: bool = True) -> builtins.list[dict[str, Any]]:
|
|
1550
1551
|
"""Get Linear cycles (sprints) for the team."""
|
|
1551
1552
|
team_id = await self._ensure_team_id()
|
|
1552
1553
|
|
|
@@ -1638,7 +1639,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1638
1639
|
ticket_id: str,
|
|
1639
1640
|
pr_url: str,
|
|
1640
1641
|
pr_number: Optional[int] = None,
|
|
1641
|
-
) ->
|
|
1642
|
+
) -> dict[str, Any]:
|
|
1642
1643
|
"""Link a Linear issue to a GitHub pull request.
|
|
1643
1644
|
|
|
1644
1645
|
Args:
|
|
@@ -1741,8 +1742,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1741
1742
|
async def create_pull_request_for_issue(
|
|
1742
1743
|
self,
|
|
1743
1744
|
ticket_id: str,
|
|
1744
|
-
github_config:
|
|
1745
|
-
) ->
|
|
1745
|
+
github_config: dict[str, Any],
|
|
1746
|
+
) -> dict[str, Any]:
|
|
1746
1747
|
"""Create a GitHub PR for a Linear issue using GitHub integration.
|
|
1747
1748
|
|
|
1748
1749
|
This requires GitHub integration to be configured in Linear.
|
|
@@ -1837,7 +1838,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1837
1838
|
else:
|
|
1838
1839
|
raise ValueError(f"Failed to update issue {ticket_id} with branch name")
|
|
1839
1840
|
|
|
1840
|
-
async def _search_by_identifier(self, identifier: str) -> Optional[
|
|
1841
|
+
async def _search_by_identifier(self, identifier: str) -> Optional[dict[str, Any]]:
|
|
1841
1842
|
"""Search for an issue by its identifier."""
|
|
1842
1843
|
search_query = gql(
|
|
1843
1844
|
"""
|
|
@@ -1950,7 +1951,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1950
1951
|
|
|
1951
1952
|
return None
|
|
1952
1953
|
|
|
1953
|
-
async def list_epics(self, **kwargs) ->
|
|
1954
|
+
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
1954
1955
|
"""List all Linear Projects (Epics).
|
|
1955
1956
|
|
|
1956
1957
|
Args:
|
|
@@ -2037,7 +2038,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2037
2038
|
# The existing create method handles project association via parent_epic field
|
|
2038
2039
|
return await self.create(task)
|
|
2039
2040
|
|
|
2040
|
-
async def list_issues_by_epic(self, epic_id: str) ->
|
|
2041
|
+
async def list_issues_by_epic(self, epic_id: str) -> builtins.list[Task]:
|
|
2041
2042
|
"""List all issues in a Linear project (epic).
|
|
2042
2043
|
|
|
2043
2044
|
Args:
|
|
@@ -2197,7 +2198,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2197
2198
|
created_issue = result["issueCreate"]["issue"]
|
|
2198
2199
|
return self._task_from_linear_issue(created_issue)
|
|
2199
2200
|
|
|
2200
|
-
async def list_tasks_by_issue(self, issue_id: str) ->
|
|
2201
|
+
async def list_tasks_by_issue(self, issue_id: str) -> builtins.list[Task]:
|
|
2201
2202
|
"""List all tasks (sub-issues) under an issue.
|
|
2202
2203
|
|
|
2203
2204
|
Args:
|
mcp_ticketer/cache/memory.py
CHANGED
|
@@ -5,7 +5,7 @@ import hashlib
|
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
7
|
from functools import wraps
|
|
8
|
-
from typing import Any, Callable,
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class CacheEntry:
|
|
@@ -37,7 +37,7 @@ class MemoryCache:
|
|
|
37
37
|
default_ttl: Default TTL in seconds (5 minutes)
|
|
38
38
|
|
|
39
39
|
"""
|
|
40
|
-
self._cache:
|
|
40
|
+
self._cache: dict[str, CacheEntry] = {}
|
|
41
41
|
self._default_ttl = default_ttl
|
|
42
42
|
self._lock = asyncio.Lock()
|
|
43
43
|
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from dotenv import load_dotenv
|
|
@@ -59,8 +59,7 @@ def main_callback(
|
|
|
59
59
|
help="Show version and exit",
|
|
60
60
|
),
|
|
61
61
|
):
|
|
62
|
-
"""MCP Ticketer - Universal ticket management interface.
|
|
63
|
-
"""
|
|
62
|
+
"""MCP Ticketer - Universal ticket management interface."""
|
|
64
63
|
pass
|
|
65
64
|
|
|
66
65
|
|
|
@@ -801,7 +800,7 @@ def create(
|
|
|
801
800
|
priority: Priority = typer.Option(
|
|
802
801
|
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
803
802
|
),
|
|
804
|
-
tags: Optional[
|
|
803
|
+
tags: Optional[list[str]] = typer.Option(
|
|
805
804
|
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
806
805
|
),
|
|
807
806
|
assignee: Optional[str] = typer.Option(
|
|
@@ -1008,12 +1007,39 @@ def update(
|
|
|
1008
1007
|
@app.command()
|
|
1009
1008
|
def transition(
|
|
1010
1009
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1011
|
-
|
|
1010
|
+
state_positional: Optional[TicketState] = typer.Argument(
|
|
1011
|
+
None, help="Target state (positional - deprecated, use --state instead)"
|
|
1012
|
+
),
|
|
1013
|
+
state: Optional[TicketState] = typer.Option(
|
|
1014
|
+
None, "--state", "-s", help="Target state (recommended)"
|
|
1015
|
+
),
|
|
1012
1016
|
adapter: Optional[AdapterType] = typer.Option(
|
|
1013
1017
|
None, "--adapter", help="Override default adapter"
|
|
1014
1018
|
),
|
|
1015
1019
|
) -> None:
|
|
1016
|
-
"""Change ticket state with validation.
|
|
1020
|
+
"""Change ticket state with validation.
|
|
1021
|
+
|
|
1022
|
+
Examples:
|
|
1023
|
+
# Recommended syntax with flag:
|
|
1024
|
+
mcp-ticketer transition BTA-215 --state done
|
|
1025
|
+
mcp-ticketer transition BTA-215 -s in_progress
|
|
1026
|
+
|
|
1027
|
+
# Legacy positional syntax (still supported):
|
|
1028
|
+
mcp-ticketer transition BTA-215 done
|
|
1029
|
+
|
|
1030
|
+
"""
|
|
1031
|
+
# Determine which state to use (prefer flag over positional)
|
|
1032
|
+
target_state = state if state is not None else state_positional
|
|
1033
|
+
|
|
1034
|
+
if target_state is None:
|
|
1035
|
+
console.print("[red]Error: State is required[/red]")
|
|
1036
|
+
console.print(
|
|
1037
|
+
"Use either:\n"
|
|
1038
|
+
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1039
|
+
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1040
|
+
)
|
|
1041
|
+
raise typer.Exit(1)
|
|
1042
|
+
|
|
1017
1043
|
# Get the adapter name
|
|
1018
1044
|
config = load_config()
|
|
1019
1045
|
adapter_name = (
|
|
@@ -1025,14 +1051,16 @@ def transition(
|
|
|
1025
1051
|
queue_id = queue.add(
|
|
1026
1052
|
ticket_data={
|
|
1027
1053
|
"ticket_id": ticket_id,
|
|
1028
|
-
"state":
|
|
1054
|
+
"state": (
|
|
1055
|
+
target_state.value if hasattr(target_state, "value") else target_state
|
|
1056
|
+
),
|
|
1029
1057
|
},
|
|
1030
1058
|
adapter=adapter_name,
|
|
1031
1059
|
operation="transition",
|
|
1032
1060
|
)
|
|
1033
1061
|
|
|
1034
1062
|
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
1035
|
-
console.print(f" Ticket: {ticket_id} → {
|
|
1063
|
+
console.print(f" Ticket: {ticket_id} → {target_state}")
|
|
1036
1064
|
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
1037
1065
|
|
|
1038
1066
|
# Start worker if needed
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import shutil
|
|
5
5
|
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.prompt import Confirm
|
|
@@ -96,7 +96,7 @@ def migrate_config_command(dry_run: bool = False) -> None:
|
|
|
96
96
|
console.print(f"[yellow]Old config backed up at: {backup_path}[/yellow]")
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def _migrate_old_to_new(old_config:
|
|
99
|
+
def _migrate_old_to_new(old_config: dict[str, Any]) -> TicketerConfig:
|
|
100
100
|
"""Migrate old configuration format to new format.
|
|
101
101
|
|
|
102
102
|
Old format examples:
|
mcp_ticketer/cli/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
from functools import wraps
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Callable,
|
|
9
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
from rich.console import Console
|
|
@@ -166,7 +166,7 @@ class CommonPatterns:
|
|
|
166
166
|
|
|
167
167
|
@staticmethod
|
|
168
168
|
def queue_operation(
|
|
169
|
-
ticket_data:
|
|
169
|
+
ticket_data: dict[str, Any],
|
|
170
170
|
operation: str,
|
|
171
171
|
adapter_name: Optional[str] = None,
|
|
172
172
|
show_progress: bool = True,
|
|
@@ -197,7 +197,7 @@ class CommonPatterns:
|
|
|
197
197
|
return queue_id
|
|
198
198
|
|
|
199
199
|
@staticmethod
|
|
200
|
-
def display_ticket_table(tickets:
|
|
200
|
+
def display_ticket_table(tickets: list[Task], title: str = "Tickets") -> None:
|
|
201
201
|
"""Display tickets in a formatted table."""
|
|
202
202
|
if not tickets:
|
|
203
203
|
console.print("[yellow]No tickets found[/yellow]")
|
|
@@ -222,7 +222,7 @@ class CommonPatterns:
|
|
|
222
222
|
console.print(table)
|
|
223
223
|
|
|
224
224
|
@staticmethod
|
|
225
|
-
def display_ticket_details(ticket: Task, comments: Optional[
|
|
225
|
+
def display_ticket_details(ticket: Task, comments: Optional[list] = None) -> None:
|
|
226
226
|
"""Display detailed ticket information."""
|
|
227
227
|
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
228
228
|
console.print(f"Title: {ticket.title}")
|
|
@@ -367,7 +367,7 @@ class ConfigValidator:
|
|
|
367
367
|
"""Configuration validation utilities."""
|
|
368
368
|
|
|
369
369
|
@staticmethod
|
|
370
|
-
def validate_adapter_config(adapter_type: str, config: dict) ->
|
|
370
|
+
def validate_adapter_config(adapter_type: str, config: dict) -> list[str]:
|
|
371
371
|
"""Validate adapter configuration and return list of issues."""
|
|
372
372
|
issues = []
|
|
373
373
|
|
|
@@ -483,7 +483,7 @@ def create_standard_ticket_command(operation: str):
|
|
|
483
483
|
priority: Optional[Priority] = None,
|
|
484
484
|
state: Optional[TicketState] = None,
|
|
485
485
|
assignee: Optional[str] = None,
|
|
486
|
-
tags: Optional[
|
|
486
|
+
tags: Optional[list[str]] = None,
|
|
487
487
|
adapter: Optional[str] = None,
|
|
488
488
|
):
|
|
489
489
|
"""Template for ticket commands."""
|
|
@@ -562,7 +562,7 @@ class TicketCommands:
|
|
|
562
562
|
title: str,
|
|
563
563
|
description: Optional[str] = None,
|
|
564
564
|
priority: Priority = Priority.MEDIUM,
|
|
565
|
-
tags: Optional[
|
|
565
|
+
tags: Optional[list[str]] = None,
|
|
566
566
|
assignee: Optional[str] = None,
|
|
567
567
|
adapter: Optional[str] = None,
|
|
568
568
|
) -> str:
|
|
@@ -579,7 +579,7 @@ class TicketCommands:
|
|
|
579
579
|
|
|
580
580
|
@staticmethod
|
|
581
581
|
def update_ticket(
|
|
582
|
-
ticket_id: str, updates:
|
|
582
|
+
ticket_id: str, updates: dict[str, Any], adapter: Optional[str] = None
|
|
583
583
|
) -> str:
|
|
584
584
|
"""Update a ticket."""
|
|
585
585
|
if not updates:
|
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 Any,
|
|
5
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
5
6
|
|
|
6
7
|
from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
|
|
7
8
|
|
|
@@ -12,7 +13,7 @@ T = TypeVar("T", Epic, Task)
|
|
|
12
13
|
class BaseAdapter(ABC, Generic[T]):
|
|
13
14
|
"""Abstract base class for all ticket system adapters."""
|
|
14
15
|
|
|
15
|
-
def __init__(self, config:
|
|
16
|
+
def __init__(self, config: dict[str, Any]):
|
|
16
17
|
"""Initialize adapter with configuration.
|
|
17
18
|
|
|
18
19
|
Args:
|
|
@@ -23,7 +24,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
23
24
|
self._state_mapping = self._get_state_mapping()
|
|
24
25
|
|
|
25
26
|
@abstractmethod
|
|
26
|
-
def _get_state_mapping(self) ->
|
|
27
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
27
28
|
"""Get mapping from universal states to system-specific states.
|
|
28
29
|
|
|
29
30
|
Returns:
|
|
@@ -69,7 +70,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
69
70
|
pass
|
|
70
71
|
|
|
71
72
|
@abstractmethod
|
|
72
|
-
async def update(self, ticket_id: str, updates:
|
|
73
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[T]:
|
|
73
74
|
"""Update a ticket.
|
|
74
75
|
|
|
75
76
|
Args:
|
|
@@ -97,8 +98,8 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
97
98
|
|
|
98
99
|
@abstractmethod
|
|
99
100
|
async def list(
|
|
100
|
-
self, limit: int = 10, offset: int = 0, filters: Optional[
|
|
101
|
-
) ->
|
|
101
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
102
|
+
) -> list[T]:
|
|
102
103
|
"""List tickets with pagination and filters.
|
|
103
104
|
|
|
104
105
|
Args:
|
|
@@ -113,7 +114,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
113
114
|
pass
|
|
114
115
|
|
|
115
116
|
@abstractmethod
|
|
116
|
-
async def search(self, query: SearchQuery) ->
|
|
117
|
+
async def search(self, query: SearchQuery) -> builtins.list[T]:
|
|
117
118
|
"""Search tickets using advanced query.
|
|
118
119
|
|
|
119
120
|
Args:
|
|
@@ -157,7 +158,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
157
158
|
@abstractmethod
|
|
158
159
|
async def get_comments(
|
|
159
160
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
160
|
-
) ->
|
|
161
|
+
) -> builtins.list[Comment]:
|
|
161
162
|
"""Get comments for a ticket.
|
|
162
163
|
|
|
163
164
|
Args:
|
|
@@ -264,7 +265,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
264
265
|
return result
|
|
265
266
|
return None
|
|
266
267
|
|
|
267
|
-
async def list_epics(self, **kwargs) ->
|
|
268
|
+
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
268
269
|
"""List all epics.
|
|
269
270
|
|
|
270
271
|
Args:
|
|
@@ -308,7 +309,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
308
309
|
)
|
|
309
310
|
return await self.create(task)
|
|
310
311
|
|
|
311
|
-
async def list_issues_by_epic(self, epic_id: str) ->
|
|
312
|
+
async def list_issues_by_epic(self, epic_id: str) -> builtins.list[Task]:
|
|
312
313
|
"""List all issues in epic.
|
|
313
314
|
|
|
314
315
|
Args:
|
|
@@ -359,7 +360,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
359
360
|
|
|
360
361
|
return await self.create(task)
|
|
361
362
|
|
|
362
|
-
async def list_tasks_by_issue(self, issue_id: str) ->
|
|
363
|
+
async def list_tasks_by_issue(self, issue_id: str) -> builtins.list[Task]:
|
|
363
364
|
"""List all tasks under an issue.
|
|
364
365
|
|
|
365
366
|
Args:
|
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,
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
from pydantic import BaseModel, Field, root_validator, validator
|
|
@@ -31,7 +31,7 @@ class BaseAdapterConfig(BaseModel):
|
|
|
31
31
|
enabled: bool = True
|
|
32
32
|
timeout: float = 30.0
|
|
33
33
|
max_retries: int = 3
|
|
34
|
-
rate_limit: Optional[
|
|
34
|
+
rate_limit: Optional[dict[str, Any]] = None
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class GitHubConfig(BaseAdapterConfig):
|
|
@@ -43,10 +43,10 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
43
43
|
repo: Optional[str] = Field(None, env="GITHUB_REPO")
|
|
44
44
|
api_url: str = "https://api.github.com"
|
|
45
45
|
use_projects_v2: bool = False
|
|
46
|
-
custom_priority_scheme: Optional[
|
|
46
|
+
custom_priority_scheme: Optional[dict[str, list[str]]] = None
|
|
47
47
|
|
|
48
48
|
@validator("token", pre=True, always=True)
|
|
49
|
-
def validate_token(
|
|
49
|
+
def validate_token(self, v):
|
|
50
50
|
if not v:
|
|
51
51
|
v = os.getenv("GITHUB_TOKEN")
|
|
52
52
|
if not v:
|
|
@@ -54,7 +54,7 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
54
54
|
return v
|
|
55
55
|
|
|
56
56
|
@validator("owner", pre=True, always=True)
|
|
57
|
-
def validate_owner(
|
|
57
|
+
def validate_owner(self, v):
|
|
58
58
|
if not v:
|
|
59
59
|
v = os.getenv("GITHUB_OWNER")
|
|
60
60
|
if not v:
|
|
@@ -62,7 +62,7 @@ class GitHubConfig(BaseAdapterConfig):
|
|
|
62
62
|
return v
|
|
63
63
|
|
|
64
64
|
@validator("repo", pre=True, always=True)
|
|
65
|
-
def validate_repo(
|
|
65
|
+
def validate_repo(self, v):
|
|
66
66
|
if not v:
|
|
67
67
|
v = os.getenv("GITHUB_REPO")
|
|
68
68
|
if not v:
|
|
@@ -82,7 +82,7 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
82
82
|
verify_ssl: bool = True
|
|
83
83
|
|
|
84
84
|
@validator("server", pre=True, always=True)
|
|
85
|
-
def validate_server(
|
|
85
|
+
def validate_server(self, v):
|
|
86
86
|
if not v:
|
|
87
87
|
v = os.getenv("JIRA_SERVER")
|
|
88
88
|
if not v:
|
|
@@ -90,7 +90,7 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
90
90
|
return v.rstrip("/")
|
|
91
91
|
|
|
92
92
|
@validator("email", pre=True, always=True)
|
|
93
|
-
def validate_email(
|
|
93
|
+
def validate_email(self, v):
|
|
94
94
|
if not v:
|
|
95
95
|
v = os.getenv("JIRA_EMAIL")
|
|
96
96
|
if not v:
|
|
@@ -98,7 +98,7 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
98
98
|
return v
|
|
99
99
|
|
|
100
100
|
@validator("api_token", pre=True, always=True)
|
|
101
|
-
def validate_api_token(
|
|
101
|
+
def validate_api_token(self, v):
|
|
102
102
|
if not v:
|
|
103
103
|
v = os.getenv("JIRA_API_TOKEN")
|
|
104
104
|
if not v:
|
|
@@ -116,7 +116,7 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
116
116
|
api_url: str = "https://api.linear.app/graphql"
|
|
117
117
|
|
|
118
118
|
@validator("api_key", pre=True, always=True)
|
|
119
|
-
def validate_api_key(
|
|
119
|
+
def validate_api_key(self, v):
|
|
120
120
|
if not v:
|
|
121
121
|
v = os.getenv("LINEAR_API_KEY")
|
|
122
122
|
if not v:
|
|
@@ -155,7 +155,7 @@ class LoggingConfig(BaseModel):
|
|
|
155
155
|
class AppConfig(BaseModel):
|
|
156
156
|
"""Main application configuration."""
|
|
157
157
|
|
|
158
|
-
adapters:
|
|
158
|
+
adapters: dict[
|
|
159
159
|
str, Union[GitHubConfig, JiraConfig, LinearConfig, AITrackdownConfig]
|
|
160
160
|
] = {}
|
|
161
161
|
queue: QueueConfig = QueueConfig()
|
|
@@ -164,7 +164,7 @@ class AppConfig(BaseModel):
|
|
|
164
164
|
default_adapter: Optional[str] = None
|
|
165
165
|
|
|
166
166
|
@root_validator(skip_on_failure=True)
|
|
167
|
-
def validate_adapters(
|
|
167
|
+
def validate_adapters(self, values):
|
|
168
168
|
"""Validate adapter configurations."""
|
|
169
169
|
adapters = values.get("adapters", {})
|
|
170
170
|
|
|
@@ -185,7 +185,7 @@ class AppConfig(BaseModel):
|
|
|
185
185
|
"""Get configuration for a specific adapter."""
|
|
186
186
|
return self.adapters.get(adapter_name)
|
|
187
187
|
|
|
188
|
-
def get_enabled_adapters(self) ->
|
|
188
|
+
def get_enabled_adapters(self) -> dict[str, BaseAdapterConfig]:
|
|
189
189
|
"""Get all enabled adapters."""
|
|
190
190
|
return {
|
|
191
191
|
name: config for name, config in self.adapters.items() if config.enabled
|
|
@@ -197,7 +197,7 @@ class ConfigurationManager:
|
|
|
197
197
|
|
|
198
198
|
_instance: Optional["ConfigurationManager"] = None
|
|
199
199
|
_config: Optional[AppConfig] = None
|
|
200
|
-
_config_file_paths:
|
|
200
|
+
_config_file_paths: list[Path] = []
|
|
201
201
|
|
|
202
202
|
def __new__(cls) -> "ConfigurationManager":
|
|
203
203
|
"""Singleton pattern for global config access."""
|
|
@@ -209,7 +209,7 @@ class ConfigurationManager:
|
|
|
209
209
|
"""Initialize configuration manager."""
|
|
210
210
|
if not hasattr(self, "_initialized"):
|
|
211
211
|
self._initialized = True
|
|
212
|
-
self._config_cache:
|
|
212
|
+
self._config_cache: dict[str, Any] = {}
|
|
213
213
|
self._find_config_files()
|
|
214
214
|
|
|
215
215
|
def _find_config_files(self) -> None:
|
|
@@ -302,7 +302,7 @@ class ConfigurationManager:
|
|
|
302
302
|
self._config = AppConfig(**config_data)
|
|
303
303
|
return self._config
|
|
304
304
|
|
|
305
|
-
def _load_config_file(self, config_path: Path) ->
|
|
305
|
+
def _load_config_file(self, config_path: Path) -> dict[str, Any]:
|
|
306
306
|
"""Load configuration from YAML or JSON file."""
|
|
307
307
|
try:
|
|
308
308
|
with open(config_path, encoding="utf-8") as file:
|
|
@@ -332,7 +332,7 @@ class ConfigurationManager:
|
|
|
332
332
|
config = self.get_config()
|
|
333
333
|
return config.get_adapter_config(adapter_name)
|
|
334
334
|
|
|
335
|
-
def get_enabled_adapters(self) ->
|
|
335
|
+
def get_enabled_adapters(self) -> dict[str, BaseAdapterConfig]:
|
|
336
336
|
"""Get all enabled adapter configurations."""
|
|
337
337
|
config = self.get_config()
|
|
338
338
|
return config.get_enabled_adapters()
|