mcp-ticketer 0.4.2__py3-none-any.whl → 0.4.4__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 +254 -11
- mcp_ticketer/adapters/github.py +13 -13
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +20 -24
- mcp_ticketer/cache/memory.py +6 -5
- mcp_ticketer/cli/codex_configure.py +2 -2
- mcp_ticketer/cli/configure.py +4 -5
- mcp_ticketer/cli/diagnostics.py +2 -2
- mcp_ticketer/cli/discover.py +4 -5
- mcp_ticketer/cli/gemini_configure.py +2 -2
- mcp_ticketer/cli/linear_commands.py +6 -7
- mcp_ticketer/cli/main.py +341 -250
- mcp_ticketer/cli/mcp_configure.py +1 -2
- mcp_ticketer/cli/ticket_commands.py +27 -30
- mcp_ticketer/cli/utils.py +23 -22
- mcp_ticketer/core/__init__.py +2 -1
- mcp_ticketer/core/adapter.py +82 -13
- mcp_ticketer/core/config.py +27 -29
- mcp_ticketer/core/env_discovery.py +10 -10
- mcp_ticketer/core/env_loader.py +8 -8
- mcp_ticketer/core/http_client.py +16 -16
- mcp_ticketer/core/mappers.py +10 -10
- mcp_ticketer/core/models.py +50 -20
- mcp_ticketer/core/project_config.py +40 -34
- mcp_ticketer/core/registry.py +2 -2
- mcp_ticketer/mcp/dto.py +32 -32
- mcp_ticketer/mcp/response_builder.py +2 -2
- mcp_ticketer/mcp/server.py +3 -3
- mcp_ticketer/mcp/server_sdk.py +2 -2
- mcp_ticketer/mcp/tools/attachment_tools.py +3 -4
- mcp_ticketer/mcp/tools/comment_tools.py +2 -2
- mcp_ticketer/mcp/tools/hierarchy_tools.py +8 -8
- mcp_ticketer/mcp/tools/pr_tools.py +2 -2
- mcp_ticketer/mcp/tools/search_tools.py +6 -6
- mcp_ticketer/mcp/tools/ticket_tools.py +12 -12
- mcp_ticketer/queue/health_monitor.py +4 -4
- mcp_ticketer/queue/manager.py +2 -2
- mcp_ticketer/queue/queue.py +16 -16
- mcp_ticketer/queue/ticket_registry.py +7 -7
- mcp_ticketer/queue/worker.py +2 -2
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/METADATA +61 -2
- mcp_ticketer-0.4.4.dist-info/RECORD +73 -0
- mcp_ticketer-0.4.2.dist-info/RECORD +0 -73
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.4.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,6 @@ import os
|
|
|
5
5
|
import shutil
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
9
8
|
|
|
10
9
|
from rich.console import Console
|
|
11
10
|
|
|
@@ -171,7 +170,7 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
|
|
|
171
170
|
|
|
172
171
|
|
|
173
172
|
def create_mcp_server_config(
|
|
174
|
-
binary_path: str, project_config: dict, cwd:
|
|
173
|
+
binary_path: str, project_config: dict, cwd: str | None = None
|
|
175
174
|
) -> dict:
|
|
176
175
|
"""Create MCP server configuration for mcp-ticketer.
|
|
177
176
|
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
9
8
|
|
|
10
9
|
import typer
|
|
11
10
|
from rich.console import Console
|
|
@@ -36,7 +35,7 @@ console = Console()
|
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
# Configuration functions (moved from main.py to avoid circular import)
|
|
39
|
-
def load_config(project_dir:
|
|
38
|
+
def load_config(project_dir: Path | None = None) -> dict:
|
|
40
39
|
"""Load configuration from project-local config file."""
|
|
41
40
|
import logging
|
|
42
41
|
|
|
@@ -73,7 +72,7 @@ def save_config(config: dict) -> None:
|
|
|
73
72
|
|
|
74
73
|
|
|
75
74
|
def get_adapter(
|
|
76
|
-
override_adapter:
|
|
75
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
77
76
|
):
|
|
78
77
|
"""Get configured adapter instance."""
|
|
79
78
|
config = load_config()
|
|
@@ -108,7 +107,7 @@ def get_adapter(
|
|
|
108
107
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
109
108
|
|
|
110
109
|
|
|
111
|
-
def _discover_from_env_files() ->
|
|
110
|
+
def _discover_from_env_files() -> str | None:
|
|
112
111
|
"""Discover adapter configuration from .env or .env.local files.
|
|
113
112
|
|
|
114
113
|
Returns:
|
|
@@ -191,29 +190,29 @@ def _save_adapter_to_config(adapter_name: str) -> None:
|
|
|
191
190
|
@app.command()
|
|
192
191
|
def create(
|
|
193
192
|
title: str = typer.Argument(..., help="Ticket title"),
|
|
194
|
-
description:
|
|
193
|
+
description: str | None = typer.Option(
|
|
195
194
|
None, "--description", "-d", help="Ticket description"
|
|
196
195
|
),
|
|
197
196
|
priority: Priority = typer.Option(
|
|
198
197
|
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
199
198
|
),
|
|
200
|
-
tags:
|
|
199
|
+
tags: list[str] | None = typer.Option(
|
|
201
200
|
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
202
201
|
),
|
|
203
|
-
assignee:
|
|
202
|
+
assignee: str | None = typer.Option(
|
|
204
203
|
None, "--assignee", "-a", help="Assignee username"
|
|
205
204
|
),
|
|
206
|
-
project:
|
|
205
|
+
project: str | None = typer.Option(
|
|
207
206
|
None,
|
|
208
207
|
"--project",
|
|
209
208
|
help="Parent project/epic ID (synonym for --epic)",
|
|
210
209
|
),
|
|
211
|
-
epic:
|
|
210
|
+
epic: str | None = typer.Option(
|
|
212
211
|
None,
|
|
213
212
|
"--epic",
|
|
214
213
|
help="Parent epic/project ID (synonym for --project)",
|
|
215
214
|
),
|
|
216
|
-
adapter:
|
|
215
|
+
adapter: AdapterType | None = typer.Option(
|
|
217
216
|
None, "--adapter", help="Override default adapter"
|
|
218
217
|
),
|
|
219
218
|
) -> None:
|
|
@@ -417,14 +416,14 @@ def create(
|
|
|
417
416
|
|
|
418
417
|
@app.command("list")
|
|
419
418
|
def list_tickets(
|
|
420
|
-
state:
|
|
419
|
+
state: TicketState | None = typer.Option(
|
|
421
420
|
None, "--state", "-s", help="Filter by state"
|
|
422
421
|
),
|
|
423
|
-
priority:
|
|
422
|
+
priority: Priority | None = typer.Option(
|
|
424
423
|
None, "--priority", "-p", help="Filter by priority"
|
|
425
424
|
),
|
|
426
425
|
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
427
|
-
adapter:
|
|
426
|
+
adapter: AdapterType | None = typer.Option(
|
|
428
427
|
None, "--adapter", help="Override default adapter"
|
|
429
428
|
),
|
|
430
429
|
) -> None:
|
|
@@ -474,7 +473,7 @@ def list_tickets(
|
|
|
474
473
|
def show(
|
|
475
474
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
476
475
|
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
477
|
-
adapter:
|
|
476
|
+
adapter: AdapterType | None = typer.Option(
|
|
478
477
|
None, "--adapter", help="Override default adapter"
|
|
479
478
|
),
|
|
480
479
|
) -> None:
|
|
@@ -524,7 +523,7 @@ def show(
|
|
|
524
523
|
def comment(
|
|
525
524
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
526
525
|
content: str = typer.Argument(..., help="Comment content"),
|
|
527
|
-
adapter:
|
|
526
|
+
adapter: AdapterType | None = typer.Option(
|
|
528
527
|
None, "--adapter", help="Override default adapter"
|
|
529
528
|
),
|
|
530
529
|
) -> None:
|
|
@@ -559,17 +558,15 @@ def comment(
|
|
|
559
558
|
@app.command()
|
|
560
559
|
def update(
|
|
561
560
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
562
|
-
title:
|
|
563
|
-
description:
|
|
561
|
+
title: str | None = typer.Option(None, "--title", help="New title"),
|
|
562
|
+
description: str | None = typer.Option(
|
|
564
563
|
None, "--description", "-d", help="New description"
|
|
565
564
|
),
|
|
566
|
-
priority:
|
|
565
|
+
priority: Priority | None = typer.Option(
|
|
567
566
|
None, "--priority", "-p", help="New priority"
|
|
568
567
|
),
|
|
569
|
-
assignee:
|
|
570
|
-
|
|
571
|
-
),
|
|
572
|
-
adapter: Optional[AdapterType] = typer.Option(
|
|
568
|
+
assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
|
|
569
|
+
adapter: AdapterType | None = typer.Option(
|
|
573
570
|
None, "--adapter", help="Override default adapter"
|
|
574
571
|
),
|
|
575
572
|
) -> None:
|
|
@@ -625,13 +622,13 @@ def update(
|
|
|
625
622
|
@app.command()
|
|
626
623
|
def transition(
|
|
627
624
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
628
|
-
state_positional:
|
|
625
|
+
state_positional: TicketState | None = typer.Argument(
|
|
629
626
|
None, help="Target state (positional - deprecated, use --state instead)"
|
|
630
627
|
),
|
|
631
|
-
state:
|
|
628
|
+
state: TicketState | None = typer.Option(
|
|
632
629
|
None, "--state", "-s", help="Target state (recommended)"
|
|
633
630
|
),
|
|
634
|
-
adapter:
|
|
631
|
+
adapter: AdapterType | None = typer.Option(
|
|
635
632
|
None, "--adapter", help="Override default adapter"
|
|
636
633
|
),
|
|
637
634
|
) -> None:
|
|
@@ -692,12 +689,12 @@ def transition(
|
|
|
692
689
|
|
|
693
690
|
@app.command()
|
|
694
691
|
def search(
|
|
695
|
-
query:
|
|
696
|
-
state:
|
|
697
|
-
priority:
|
|
698
|
-
assignee:
|
|
692
|
+
query: str | None = typer.Argument(None, help="Search query"),
|
|
693
|
+
state: TicketState | None = typer.Option(None, "--state", "-s"),
|
|
694
|
+
priority: Priority | None = typer.Option(None, "--priority", "-p"),
|
|
695
|
+
assignee: str | None = typer.Option(None, "--assignee", "-a"),
|
|
699
696
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
700
|
-
adapter:
|
|
697
|
+
adapter: AdapterType | None = typer.Option(
|
|
701
698
|
None, "--adapter", help="Override default adapter"
|
|
702
699
|
),
|
|
703
700
|
) -> None:
|
mcp_ticketer/cli/utils.py
CHANGED
|
@@ -4,9 +4,10 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from functools import wraps
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, TypeVar
|
|
10
11
|
|
|
11
12
|
import typer
|
|
12
13
|
from rich.console import Console
|
|
@@ -154,7 +155,7 @@ class CommonPatterns:
|
|
|
154
155
|
|
|
155
156
|
@staticmethod
|
|
156
157
|
def get_adapter(
|
|
157
|
-
override_adapter:
|
|
158
|
+
override_adapter: str | None = None, override_config: dict | None = None
|
|
158
159
|
):
|
|
159
160
|
"""Get configured adapter instance with environment variable support."""
|
|
160
161
|
config = CommonPatterns.load_config()
|
|
@@ -206,7 +207,7 @@ class CommonPatterns:
|
|
|
206
207
|
def queue_operation(
|
|
207
208
|
ticket_data: dict[str, Any],
|
|
208
209
|
operation: str,
|
|
209
|
-
adapter_name:
|
|
210
|
+
adapter_name: str | None = None,
|
|
210
211
|
show_progress: bool = True,
|
|
211
212
|
) -> str:
|
|
212
213
|
"""Queue an operation and optionally start the worker."""
|
|
@@ -265,7 +266,7 @@ class CommonPatterns:
|
|
|
265
266
|
console.print(table)
|
|
266
267
|
|
|
267
268
|
@staticmethod
|
|
268
|
-
def display_ticket_details(ticket: Task, comments:
|
|
269
|
+
def display_ticket_details(ticket: Task, comments: list | None = None) -> None:
|
|
269
270
|
"""Display detailed ticket information."""
|
|
270
271
|
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
271
272
|
console.print(f"Title: {ticket.title}")
|
|
@@ -334,7 +335,7 @@ def with_adapter(f: Callable) -> Callable:
|
|
|
334
335
|
"""Decorator to inject adapter instance into CLI commands."""
|
|
335
336
|
|
|
336
337
|
@wraps(f)
|
|
337
|
-
def wrapper(adapter:
|
|
338
|
+
def wrapper(adapter: str | None = None, *args, **kwargs):
|
|
338
339
|
adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
|
|
339
340
|
return f(adapter_instance, *args, **kwargs)
|
|
340
341
|
|
|
@@ -446,7 +447,7 @@ class ConfigValidator:
|
|
|
446
447
|
return issues
|
|
447
448
|
|
|
448
449
|
@staticmethod
|
|
449
|
-
def _get_env_var(adapter_type: str, field: str) ->
|
|
450
|
+
def _get_env_var(adapter_type: str, field: str) -> str | None:
|
|
450
451
|
"""Get corresponding environment variable name for a config field."""
|
|
451
452
|
env_mapping = {
|
|
452
453
|
"github": {
|
|
@@ -520,14 +521,14 @@ def create_standard_ticket_command(operation: str):
|
|
|
520
521
|
"""Create a standard ticket operation command."""
|
|
521
522
|
|
|
522
523
|
def command_template(
|
|
523
|
-
ticket_id:
|
|
524
|
-
title:
|
|
525
|
-
description:
|
|
526
|
-
priority:
|
|
527
|
-
state:
|
|
528
|
-
assignee:
|
|
529
|
-
tags:
|
|
530
|
-
adapter:
|
|
524
|
+
ticket_id: str | None = None,
|
|
525
|
+
title: str | None = None,
|
|
526
|
+
description: str | None = None,
|
|
527
|
+
priority: Priority | None = None,
|
|
528
|
+
state: TicketState | None = None,
|
|
529
|
+
assignee: str | None = None,
|
|
530
|
+
tags: list[str] | None = None,
|
|
531
|
+
adapter: str | None = None,
|
|
531
532
|
):
|
|
532
533
|
"""Template for ticket commands."""
|
|
533
534
|
# Build ticket data
|
|
@@ -568,8 +569,8 @@ class TicketCommands:
|
|
|
568
569
|
@handle_adapter_errors
|
|
569
570
|
async def list_tickets(
|
|
570
571
|
adapter_instance,
|
|
571
|
-
state:
|
|
572
|
-
priority:
|
|
572
|
+
state: TicketState | None = None,
|
|
573
|
+
priority: Priority | None = None,
|
|
573
574
|
limit: int = 10,
|
|
574
575
|
):
|
|
575
576
|
"""List tickets with filters."""
|
|
@@ -603,11 +604,11 @@ class TicketCommands:
|
|
|
603
604
|
@staticmethod
|
|
604
605
|
def create_ticket(
|
|
605
606
|
title: str,
|
|
606
|
-
description:
|
|
607
|
+
description: str | None = None,
|
|
607
608
|
priority: Priority = Priority.MEDIUM,
|
|
608
|
-
tags:
|
|
609
|
-
assignee:
|
|
610
|
-
adapter:
|
|
609
|
+
tags: list[str] | None = None,
|
|
610
|
+
assignee: str | None = None,
|
|
611
|
+
adapter: str | None = None,
|
|
611
612
|
) -> str:
|
|
612
613
|
"""Create a new ticket."""
|
|
613
614
|
ticket_data = {
|
|
@@ -622,7 +623,7 @@ class TicketCommands:
|
|
|
622
623
|
|
|
623
624
|
@staticmethod
|
|
624
625
|
def update_ticket(
|
|
625
|
-
ticket_id: str, updates: dict[str, Any], adapter:
|
|
626
|
+
ticket_id: str, updates: dict[str, Any], adapter: str | None = None
|
|
626
627
|
) -> str:
|
|
627
628
|
"""Update a ticket."""
|
|
628
629
|
if not updates:
|
|
@@ -634,7 +635,7 @@ class TicketCommands:
|
|
|
634
635
|
|
|
635
636
|
@staticmethod
|
|
636
637
|
def transition_ticket(
|
|
637
|
-
ticket_id: str, state: TicketState, adapter:
|
|
638
|
+
ticket_id: str, state: TicketState, adapter: str | None = None
|
|
638
639
|
) -> str:
|
|
639
640
|
"""Transition ticket state."""
|
|
640
641
|
ticket_data = {
|
mcp_ticketer/core/__init__.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Core models and abstractions for MCP Ticketer."""
|
|
2
2
|
|
|
3
3
|
from .adapter import BaseAdapter
|
|
4
|
-
from .models import Comment, Epic, Priority, Task, TicketState, TicketType
|
|
4
|
+
from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
|
|
5
5
|
from .registry import AdapterRegistry
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"Epic",
|
|
9
9
|
"Task",
|
|
10
10
|
"Comment",
|
|
11
|
+
"Attachment",
|
|
11
12
|
"TicketState",
|
|
12
13
|
"Priority",
|
|
13
14
|
"TicketType",
|
mcp_ticketer/core/adapter.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Base adapter abstract class for ticket systems."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import builtins
|
|
4
6
|
from abc import ABC, abstractmethod
|
|
5
|
-
from typing import Any, Generic,
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
6
8
|
|
|
7
9
|
from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
|
|
8
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .models import Attachment
|
|
13
|
+
|
|
9
14
|
# Generic type for tickets
|
|
10
15
|
T = TypeVar("T", Epic, Task)
|
|
11
16
|
|
|
@@ -57,7 +62,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
57
62
|
pass
|
|
58
63
|
|
|
59
64
|
@abstractmethod
|
|
60
|
-
async def read(self, ticket_id: str) ->
|
|
65
|
+
async def read(self, ticket_id: str) -> T | None:
|
|
61
66
|
"""Read a ticket by ID.
|
|
62
67
|
|
|
63
68
|
Args:
|
|
@@ -70,7 +75,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
70
75
|
pass
|
|
71
76
|
|
|
72
77
|
@abstractmethod
|
|
73
|
-
async def update(self, ticket_id: str, updates: dict[str, Any]) ->
|
|
78
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
|
|
74
79
|
"""Update a ticket.
|
|
75
80
|
|
|
76
81
|
Args:
|
|
@@ -98,7 +103,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
98
103
|
|
|
99
104
|
@abstractmethod
|
|
100
105
|
async def list(
|
|
101
|
-
self, limit: int = 10, offset: int = 0, filters:
|
|
106
|
+
self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
|
|
102
107
|
) -> list[T]:
|
|
103
108
|
"""List tickets with pagination and filters.
|
|
104
109
|
|
|
@@ -129,7 +134,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
129
134
|
@abstractmethod
|
|
130
135
|
async def transition_state(
|
|
131
136
|
self, ticket_id: str, target_state: TicketState
|
|
132
|
-
) ->
|
|
137
|
+
) -> T | None:
|
|
133
138
|
"""Transition ticket to a new state.
|
|
134
139
|
|
|
135
140
|
Args:
|
|
@@ -225,8 +230,8 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
225
230
|
# Epic/Issue/Task Hierarchy Methods
|
|
226
231
|
|
|
227
232
|
async def create_epic(
|
|
228
|
-
self, title: str, description:
|
|
229
|
-
) ->
|
|
233
|
+
self, title: str, description: str | None = None, **kwargs
|
|
234
|
+
) -> Epic | None:
|
|
230
235
|
"""Create epic (top-level grouping).
|
|
231
236
|
|
|
232
237
|
Args:
|
|
@@ -249,7 +254,7 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
249
254
|
return result
|
|
250
255
|
return None
|
|
251
256
|
|
|
252
|
-
async def get_epic(self, epic_id: str) ->
|
|
257
|
+
async def get_epic(self, epic_id: str) -> Epic | None:
|
|
253
258
|
"""Get epic by ID.
|
|
254
259
|
|
|
255
260
|
Args:
|
|
@@ -284,10 +289,10 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
284
289
|
async def create_issue(
|
|
285
290
|
self,
|
|
286
291
|
title: str,
|
|
287
|
-
description:
|
|
288
|
-
epic_id:
|
|
292
|
+
description: str | None = None,
|
|
293
|
+
epic_id: str | None = None,
|
|
289
294
|
**kwargs,
|
|
290
|
-
) ->
|
|
295
|
+
) -> Task | None:
|
|
291
296
|
"""Create issue, optionally linked to epic.
|
|
292
297
|
|
|
293
298
|
Args:
|
|
@@ -325,8 +330,8 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
325
330
|
return [r for r in results if isinstance(r, Task) and r.is_issue()]
|
|
326
331
|
|
|
327
332
|
async def create_task(
|
|
328
|
-
self, title: str, parent_id: str, description:
|
|
329
|
-
) ->
|
|
333
|
+
self, title: str, parent_id: str, description: str | None = None, **kwargs
|
|
334
|
+
) -> Task | None:
|
|
330
335
|
"""Create task as sub-ticket of parent issue.
|
|
331
336
|
|
|
332
337
|
Args:
|
|
@@ -375,6 +380,70 @@ class BaseAdapter(ABC, Generic[T]):
|
|
|
375
380
|
results = await self.list(filters=filters)
|
|
376
381
|
return [r for r in results if isinstance(r, Task) and r.is_task()]
|
|
377
382
|
|
|
383
|
+
# Attachment methods
|
|
384
|
+
async def add_attachment(
|
|
385
|
+
self,
|
|
386
|
+
ticket_id: str,
|
|
387
|
+
file_path: str,
|
|
388
|
+
description: str | None = None,
|
|
389
|
+
) -> Attachment:
|
|
390
|
+
"""Attach a file to a ticket.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
ticket_id: Ticket identifier
|
|
394
|
+
file_path: Local file path to upload
|
|
395
|
+
description: Optional attachment description
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Created Attachment with metadata
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
NotImplementedError: If adapter doesn't support attachments
|
|
402
|
+
FileNotFoundError: If file doesn't exist
|
|
403
|
+
ValueError: If ticket doesn't exist or upload fails
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
raise NotImplementedError(
|
|
407
|
+
f"{self.__class__.__name__} does not support file attachments. "
|
|
408
|
+
"Use comments to reference external files instead."
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
async def get_attachments(self, ticket_id: str) -> list[Attachment]:
|
|
412
|
+
"""Get all attachments for a ticket.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
ticket_id: Ticket identifier
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
List of attachments (empty if none or not supported)
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
raise NotImplementedError(
|
|
422
|
+
f"{self.__class__.__name__} does not support file attachments."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def delete_attachment(
|
|
426
|
+
self,
|
|
427
|
+
ticket_id: str,
|
|
428
|
+
attachment_id: str,
|
|
429
|
+
) -> bool:
|
|
430
|
+
"""Delete an attachment (optional implementation).
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
ticket_id: Ticket identifier
|
|
434
|
+
attachment_id: Attachment identifier
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
True if deleted, False otherwise
|
|
438
|
+
|
|
439
|
+
Raises:
|
|
440
|
+
NotImplementedError: If adapter doesn't support deletion
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
raise NotImplementedError(
|
|
444
|
+
f"{self.__class__.__name__} does not support attachment deletion."
|
|
445
|
+
)
|
|
446
|
+
|
|
378
447
|
async def close(self) -> None:
|
|
379
448
|
"""Close adapter and cleanup resources."""
|
|
380
449
|
pass
|
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
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -27,23 +27,23 @@ class BaseAdapterConfig(BaseModel):
|
|
|
27
27
|
"""Base configuration for all adapters."""
|
|
28
28
|
|
|
29
29
|
type: AdapterType
|
|
30
|
-
name:
|
|
30
|
+
name: str | None = None
|
|
31
31
|
enabled: bool = True
|
|
32
32
|
timeout: float = 30.0
|
|
33
33
|
max_retries: int = 3
|
|
34
|
-
rate_limit:
|
|
34
|
+
rate_limit: dict[str, Any] | None = None
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class GitHubConfig(BaseAdapterConfig):
|
|
38
38
|
"""GitHub adapter configuration."""
|
|
39
39
|
|
|
40
40
|
type: AdapterType = AdapterType.GITHUB
|
|
41
|
-
token:
|
|
42
|
-
owner:
|
|
43
|
-
repo:
|
|
41
|
+
token: str | None = Field(None, env="GITHUB_TOKEN")
|
|
42
|
+
owner: str | None = Field(None, env="GITHUB_OWNER")
|
|
43
|
+
repo: str | None = 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:
|
|
46
|
+
custom_priority_scheme: dict[str, list[str]] | None = None
|
|
47
47
|
|
|
48
48
|
@field_validator("token", mode="before")
|
|
49
49
|
@classmethod
|
|
@@ -77,10 +77,10 @@ class JiraConfig(BaseAdapterConfig):
|
|
|
77
77
|
"""JIRA adapter configuration."""
|
|
78
78
|
|
|
79
79
|
type: AdapterType = AdapterType.JIRA
|
|
80
|
-
server:
|
|
81
|
-
email:
|
|
82
|
-
api_token:
|
|
83
|
-
project_key:
|
|
80
|
+
server: str | None = Field(None, env="JIRA_SERVER")
|
|
81
|
+
email: str | None = Field(None, env="JIRA_EMAIL")
|
|
82
|
+
api_token: str | None = Field(None, env="JIRA_API_TOKEN")
|
|
83
|
+
project_key: str | None = Field(None, env="JIRA_PROJECT_KEY")
|
|
84
84
|
cloud: bool = True
|
|
85
85
|
verify_ssl: bool = True
|
|
86
86
|
|
|
@@ -116,10 +116,10 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
116
116
|
"""Linear adapter configuration."""
|
|
117
117
|
|
|
118
118
|
type: AdapterType = AdapterType.LINEAR
|
|
119
|
-
api_key:
|
|
120
|
-
workspace:
|
|
121
|
-
team_key:
|
|
122
|
-
team_id:
|
|
119
|
+
api_key: str | None = Field(None, env="LINEAR_API_KEY")
|
|
120
|
+
workspace: str | None = None
|
|
121
|
+
team_key: str | None = None # Short team key like "BTA"
|
|
122
|
+
team_id: str | None = None # UUID team identifier
|
|
123
123
|
api_url: str = "https://api.linear.app/graphql"
|
|
124
124
|
|
|
125
125
|
@model_validator(mode="after")
|
|
@@ -150,7 +150,7 @@ class QueueConfig(BaseModel):
|
|
|
150
150
|
"""Queue configuration."""
|
|
151
151
|
|
|
152
152
|
provider: str = "sqlite"
|
|
153
|
-
connection_string:
|
|
153
|
+
connection_string: str | None = None
|
|
154
154
|
batch_size: int = 10
|
|
155
155
|
max_concurrent: int = 5
|
|
156
156
|
retry_attempts: int = 3
|
|
@@ -162,7 +162,7 @@ class LoggingConfig(BaseModel):
|
|
|
162
162
|
|
|
163
163
|
level: str = "INFO"
|
|
164
164
|
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
165
|
-
file:
|
|
165
|
+
file: str | None = None
|
|
166
166
|
max_size: str = "10MB"
|
|
167
167
|
backup_count: int = 5
|
|
168
168
|
|
|
@@ -171,12 +171,12 @@ class AppConfig(BaseModel):
|
|
|
171
171
|
"""Main application configuration."""
|
|
172
172
|
|
|
173
173
|
adapters: dict[
|
|
174
|
-
str,
|
|
174
|
+
str, GitHubConfig | JiraConfig | LinearConfig | AITrackdownConfig
|
|
175
175
|
] = {}
|
|
176
176
|
queue: QueueConfig = QueueConfig()
|
|
177
177
|
logging: LoggingConfig = LoggingConfig()
|
|
178
178
|
cache_ttl: int = 300 # Cache TTL in seconds
|
|
179
|
-
default_adapter:
|
|
179
|
+
default_adapter: str | None = None
|
|
180
180
|
|
|
181
181
|
@model_validator(mode="after")
|
|
182
182
|
def validate_adapters(self):
|
|
@@ -196,7 +196,7 @@ class AppConfig(BaseModel):
|
|
|
196
196
|
|
|
197
197
|
return self
|
|
198
198
|
|
|
199
|
-
def get_adapter_config(self, adapter_name: str) ->
|
|
199
|
+
def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
|
|
200
200
|
"""Get configuration for a specific adapter."""
|
|
201
201
|
return self.adapters.get(adapter_name)
|
|
202
202
|
|
|
@@ -211,7 +211,7 @@ class ConfigurationManager:
|
|
|
211
211
|
"""Centralized configuration management with caching and validation."""
|
|
212
212
|
|
|
213
213
|
_instance: Optional["ConfigurationManager"] = None
|
|
214
|
-
_config:
|
|
214
|
+
_config: AppConfig | None = None
|
|
215
215
|
_config_file_paths: list[Path] = []
|
|
216
216
|
|
|
217
217
|
def __new__(cls) -> "ConfigurationManager":
|
|
@@ -266,7 +266,7 @@ class ConfigurationManager:
|
|
|
266
266
|
logger.debug("No project-local config files found, will use defaults")
|
|
267
267
|
|
|
268
268
|
@lru_cache(maxsize=1)
|
|
269
|
-
def load_config(self, config_file:
|
|
269
|
+
def load_config(self, config_file: str | Path | None = None) -> AppConfig:
|
|
270
270
|
"""Load and validate configuration from file and environment.
|
|
271
271
|
|
|
272
272
|
Args:
|
|
@@ -437,7 +437,7 @@ class ConfigurationManager:
|
|
|
437
437
|
return self.load_config()
|
|
438
438
|
return self._config
|
|
439
439
|
|
|
440
|
-
def get_adapter_config(self, adapter_name: str) ->
|
|
440
|
+
def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
|
|
441
441
|
"""Get configuration for a specific adapter."""
|
|
442
442
|
config = self.get_config()
|
|
443
443
|
return config.get_adapter_config(adapter_name)
|
|
@@ -457,9 +457,7 @@ class ConfigurationManager:
|
|
|
457
457
|
config = self.get_config()
|
|
458
458
|
return config.logging
|
|
459
459
|
|
|
460
|
-
def reload_config(
|
|
461
|
-
self, config_file: Optional[Union[str, Path]] = None
|
|
462
|
-
) -> AppConfig:
|
|
460
|
+
def reload_config(self, config_file: str | Path | None = None) -> AppConfig:
|
|
463
461
|
"""Reload configuration from file."""
|
|
464
462
|
# Clear cache
|
|
465
463
|
self.load_config.cache_clear()
|
|
@@ -492,7 +490,7 @@ class ConfigurationManager:
|
|
|
492
490
|
self._config_cache[key] = value
|
|
493
491
|
return value
|
|
494
492
|
|
|
495
|
-
def create_sample_config(self, output_path:
|
|
493
|
+
def create_sample_config(self, output_path: str | Path) -> None:
|
|
496
494
|
"""Create a sample configuration file."""
|
|
497
495
|
sample_config = {
|
|
498
496
|
"adapters": {
|
|
@@ -539,11 +537,11 @@ def get_config() -> AppConfig:
|
|
|
539
537
|
return config_manager.get_config()
|
|
540
538
|
|
|
541
539
|
|
|
542
|
-
def get_adapter_config(adapter_name: str) ->
|
|
540
|
+
def get_adapter_config(adapter_name: str) -> BaseAdapterConfig | None:
|
|
543
541
|
"""Get configuration for a specific adapter."""
|
|
544
542
|
return config_manager.get_adapter_config(adapter_name)
|
|
545
543
|
|
|
546
544
|
|
|
547
|
-
def reload_config(config_file:
|
|
545
|
+
def reload_config(config_file: str | Path | None = None) -> AppConfig:
|
|
548
546
|
"""Reload the global configuration."""
|
|
549
547
|
return config_manager.reload_config(config_file)
|