mcp-ticketer 0.4.2__py3-none-any.whl → 0.4.3__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 +3 -12
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +243 -11
- mcp_ticketer/adapters/github.py +15 -14
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +22 -25
- mcp_ticketer/adapters/linear/adapter.py +9 -21
- mcp_ticketer/adapters/linear/client.py +2 -1
- mcp_ticketer/adapters/linear/mappers.py +2 -1
- mcp_ticketer/cache/memory.py +6 -5
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/codex_configure.py +2 -2
- mcp_ticketer/cli/configure.py +7 -14
- mcp_ticketer/cli/diagnostics.py +2 -2
- mcp_ticketer/cli/discover.py +6 -11
- mcp_ticketer/cli/gemini_configure.py +2 -2
- mcp_ticketer/cli/linear_commands.py +6 -7
- mcp_ticketer/cli/main.py +218 -242
- 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 +3 -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 +17 -37
- mcp_ticketer/mcp/server_sdk.py +2 -2
- mcp_ticketer/mcp/tools/__init__.py +7 -9
- 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.3.dist-info}/METADATA +61 -2
- mcp_ticketer-0.4.3.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.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.2.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@ environment files, including:
|
|
|
11
11
|
import logging
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
from dotenv import dotenv_values
|
|
17
17
|
|
|
@@ -125,7 +125,7 @@ class DiscoveryResult:
|
|
|
125
125
|
warnings: list[str] = field(default_factory=list)
|
|
126
126
|
env_files_found: list[str] = field(default_factory=list)
|
|
127
127
|
|
|
128
|
-
def get_primary_adapter(self) ->
|
|
128
|
+
def get_primary_adapter(self) -> DiscoveredAdapter | None:
|
|
129
129
|
"""Get the adapter with highest confidence and completeness."""
|
|
130
130
|
if not self.adapters:
|
|
131
131
|
return None
|
|
@@ -136,7 +136,7 @@ class DiscoveryResult:
|
|
|
136
136
|
)
|
|
137
137
|
return sorted_adapters[0]
|
|
138
138
|
|
|
139
|
-
def get_adapter_by_type(self, adapter_type: str) ->
|
|
139
|
+
def get_adapter_by_type(self, adapter_type: str) -> DiscoveredAdapter | None:
|
|
140
140
|
"""Get discovered adapter by type."""
|
|
141
141
|
for adapter in self.adapters:
|
|
142
142
|
if adapter.adapter_type == adapter_type:
|
|
@@ -155,7 +155,7 @@ class EnvDiscovery:
|
|
|
155
155
|
".env.development",
|
|
156
156
|
]
|
|
157
157
|
|
|
158
|
-
def __init__(self, project_path:
|
|
158
|
+
def __init__(self, project_path: Path | None = None):
|
|
159
159
|
"""Initialize discovery.
|
|
160
160
|
|
|
161
161
|
Args:
|
|
@@ -255,7 +255,7 @@ class EnvDiscovery:
|
|
|
255
255
|
|
|
256
256
|
def _find_key_value(
|
|
257
257
|
self, env_vars: dict[str, str], patterns: list[str]
|
|
258
|
-
) ->
|
|
258
|
+
) -> str | None:
|
|
259
259
|
"""Find first matching key value from patterns.
|
|
260
260
|
|
|
261
261
|
Args:
|
|
@@ -273,7 +273,7 @@ class EnvDiscovery:
|
|
|
273
273
|
|
|
274
274
|
def _detect_linear(
|
|
275
275
|
self, env_vars: dict[str, str], found_in: str
|
|
276
|
-
) ->
|
|
276
|
+
) -> DiscoveredAdapter | None:
|
|
277
277
|
"""Detect Linear adapter configuration.
|
|
278
278
|
|
|
279
279
|
Args:
|
|
@@ -327,7 +327,7 @@ class EnvDiscovery:
|
|
|
327
327
|
|
|
328
328
|
def _detect_github(
|
|
329
329
|
self, env_vars: dict[str, str], found_in: str
|
|
330
|
-
) ->
|
|
330
|
+
) -> DiscoveredAdapter | None:
|
|
331
331
|
"""Detect GitHub adapter configuration.
|
|
332
332
|
|
|
333
333
|
Args:
|
|
@@ -386,7 +386,7 @@ class EnvDiscovery:
|
|
|
386
386
|
|
|
387
387
|
def _detect_jira(
|
|
388
388
|
self, env_vars: dict[str, str], found_in: str
|
|
389
|
-
) ->
|
|
389
|
+
) -> DiscoveredAdapter | None:
|
|
390
390
|
"""Detect JIRA adapter configuration.
|
|
391
391
|
|
|
392
392
|
Args:
|
|
@@ -442,7 +442,7 @@ class EnvDiscovery:
|
|
|
442
442
|
|
|
443
443
|
def _detect_aitrackdown(
|
|
444
444
|
self, env_vars: dict[str, str], found_in: str
|
|
445
|
-
) ->
|
|
445
|
+
) -> DiscoveredAdapter | None:
|
|
446
446
|
"""Detect AITrackdown adapter configuration.
|
|
447
447
|
|
|
448
448
|
Args:
|
|
@@ -622,7 +622,7 @@ class EnvDiscovery:
|
|
|
622
622
|
return warnings
|
|
623
623
|
|
|
624
624
|
|
|
625
|
-
def discover_config(project_path:
|
|
625
|
+
def discover_config(project_path: Path | None = None) -> DiscoveryResult:
|
|
626
626
|
"""Convenience function to discover configuration.
|
|
627
627
|
|
|
628
628
|
Args:
|
mcp_ticketer/core/env_loader.py
CHANGED
|
@@ -12,7 +12,7 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -25,7 +25,7 @@ class EnvKeyConfig:
|
|
|
25
25
|
aliases: list[str]
|
|
26
26
|
description: str
|
|
27
27
|
required: bool = False
|
|
28
|
-
default:
|
|
28
|
+
default: str | None = None
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class UnifiedEnvLoader:
|
|
@@ -105,7 +105,7 @@ class UnifiedEnvLoader:
|
|
|
105
105
|
),
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
def __init__(self, project_root:
|
|
108
|
+
def __init__(self, project_root: Path | None = None):
|
|
109
109
|
"""Initialize the environment loader.
|
|
110
110
|
|
|
111
111
|
Args:
|
|
@@ -177,8 +177,8 @@ class UnifiedEnvLoader:
|
|
|
177
177
|
logger.warning(f"Failed to load {env_file}: {e}")
|
|
178
178
|
|
|
179
179
|
def get_value(
|
|
180
|
-
self, config_key: str, config:
|
|
181
|
-
) ->
|
|
180
|
+
self, config_key: str, config: dict[str, Any] | None = None
|
|
181
|
+
) -> str | None:
|
|
182
182
|
"""Get a configuration value using the key alias system.
|
|
183
183
|
|
|
184
184
|
Args:
|
|
@@ -230,7 +230,7 @@ class UnifiedEnvLoader:
|
|
|
230
230
|
return None
|
|
231
231
|
|
|
232
232
|
def get_adapter_config(
|
|
233
|
-
self, adapter_name: str, base_config:
|
|
233
|
+
self, adapter_name: str, base_config: dict[str, Any] | None = None
|
|
234
234
|
) -> dict[str, Any]:
|
|
235
235
|
"""Get complete configuration for an adapter with environment variable resolution.
|
|
236
236
|
|
|
@@ -307,7 +307,7 @@ class UnifiedEnvLoader:
|
|
|
307
307
|
|
|
308
308
|
|
|
309
309
|
# Global instance
|
|
310
|
-
_env_loader:
|
|
310
|
+
_env_loader: UnifiedEnvLoader | None = None
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
def get_env_loader() -> UnifiedEnvLoader:
|
|
@@ -319,7 +319,7 @@ def get_env_loader() -> UnifiedEnvLoader:
|
|
|
319
319
|
|
|
320
320
|
|
|
321
321
|
def load_adapter_config(
|
|
322
|
-
adapter_name: str, base_config:
|
|
322
|
+
adapter_name: str, base_config: dict[str, Any] | None = None
|
|
323
323
|
) -> dict[str, Any]:
|
|
324
324
|
"""Convenience function to load adapter configuration with environment variables.
|
|
325
325
|
|
mcp_ticketer/core/http_client.py
CHANGED
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from httpx import AsyncClient, TimeoutException
|
|
@@ -32,8 +32,8 @@ class RetryConfig:
|
|
|
32
32
|
max_delay: float = 60.0,
|
|
33
33
|
exponential_base: float = 2.0,
|
|
34
34
|
jitter: bool = True,
|
|
35
|
-
retry_on_status:
|
|
36
|
-
retry_on_exceptions:
|
|
35
|
+
retry_on_status: list[int] | None = None,
|
|
36
|
+
retry_on_exceptions: list[type] | None = None,
|
|
37
37
|
):
|
|
38
38
|
self.max_retries = max_retries
|
|
39
39
|
self.initial_delay = initial_delay
|
|
@@ -94,11 +94,11 @@ class BaseHTTPClient:
|
|
|
94
94
|
def __init__(
|
|
95
95
|
self,
|
|
96
96
|
base_url: str,
|
|
97
|
-
headers:
|
|
98
|
-
auth:
|
|
97
|
+
headers: dict[str, str] | None = None,
|
|
98
|
+
auth: httpx.Auth | tuple | None = None,
|
|
99
99
|
timeout: float = 30.0,
|
|
100
|
-
retry_config:
|
|
101
|
-
rate_limiter:
|
|
100
|
+
retry_config: RetryConfig | None = None,
|
|
101
|
+
rate_limiter: RateLimiter | None = None,
|
|
102
102
|
verify_ssl: bool = True,
|
|
103
103
|
follow_redirects: bool = True,
|
|
104
104
|
):
|
|
@@ -132,7 +132,7 @@ class BaseHTTPClient:
|
|
|
132
132
|
"errors": 0,
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
self._client:
|
|
135
|
+
self._client: AsyncClient | None = None
|
|
136
136
|
|
|
137
137
|
async def _get_client(self) -> AsyncClient:
|
|
138
138
|
"""Get or create HTTP client instance."""
|
|
@@ -148,7 +148,7 @@ class BaseHTTPClient:
|
|
|
148
148
|
return self._client
|
|
149
149
|
|
|
150
150
|
async def _calculate_delay(
|
|
151
|
-
self, attempt: int, response:
|
|
151
|
+
self, attempt: int, response: httpx.Response | None = None
|
|
152
152
|
) -> float:
|
|
153
153
|
"""Calculate delay for retry attempt."""
|
|
154
154
|
if response and response.status_code == 429:
|
|
@@ -178,7 +178,7 @@ class BaseHTTPClient:
|
|
|
178
178
|
def _should_retry(
|
|
179
179
|
self,
|
|
180
180
|
exception: Exception,
|
|
181
|
-
response:
|
|
181
|
+
response: httpx.Response | None = None,
|
|
182
182
|
attempt: int = 1,
|
|
183
183
|
) -> bool:
|
|
184
184
|
"""Determine if request should be retried."""
|
|
@@ -198,13 +198,13 @@ class BaseHTTPClient:
|
|
|
198
198
|
|
|
199
199
|
async def request(
|
|
200
200
|
self,
|
|
201
|
-
method:
|
|
201
|
+
method: HTTPMethod | str,
|
|
202
202
|
endpoint: str,
|
|
203
|
-
data:
|
|
204
|
-
json:
|
|
205
|
-
params:
|
|
206
|
-
headers:
|
|
207
|
-
timeout:
|
|
203
|
+
data: dict[str, Any] | None = None,
|
|
204
|
+
json: dict[str, Any] | None = None,
|
|
205
|
+
params: dict[str, Any] | None = None,
|
|
206
|
+
headers: dict[str, str] | None = None,
|
|
207
|
+
timeout: float | None = None,
|
|
208
208
|
retry_count: int = 0,
|
|
209
209
|
**kwargs,
|
|
210
210
|
) -> httpx.Response:
|
mcp_ticketer/core/mappers.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from typing import Any, Generic,
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
7
7
|
|
|
8
8
|
from .models import Priority, TicketState
|
|
9
9
|
|
|
@@ -27,11 +27,11 @@ class BiDirectionalDict(Generic[T, U]):
|
|
|
27
27
|
self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
|
|
28
28
|
self._cache: dict[str, Any] = {}
|
|
29
29
|
|
|
30
|
-
def get_forward(self, key: T, default:
|
|
30
|
+
def get_forward(self, key: T, default: U | None = None) -> U | None:
|
|
31
31
|
"""Get value by forward key."""
|
|
32
32
|
return self._forward.get(key, default)
|
|
33
33
|
|
|
34
|
-
def get_reverse(self, key: U, default:
|
|
34
|
+
def get_reverse(self, key: U, default: T | None = None) -> T | None:
|
|
35
35
|
"""Get value by reverse key."""
|
|
36
36
|
return self._reverse.get(key, default)
|
|
37
37
|
|
|
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
|
|
|
83
83
|
"""Universal state mapping utility."""
|
|
84
84
|
|
|
85
85
|
def __init__(
|
|
86
|
-
self, adapter_type: str, custom_mappings:
|
|
86
|
+
self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
87
87
|
):
|
|
88
88
|
"""Initialize state mapper.
|
|
89
89
|
|
|
@@ -95,7 +95,7 @@ class StateMapper(BaseMapper):
|
|
|
95
95
|
super().__init__()
|
|
96
96
|
self.adapter_type = adapter_type
|
|
97
97
|
self.custom_mappings = custom_mappings or {}
|
|
98
|
-
self._mapping:
|
|
98
|
+
self._mapping: BiDirectionalDict | None = None
|
|
99
99
|
|
|
100
100
|
@lru_cache(maxsize=1)
|
|
101
101
|
def get_mapping(self) -> BiDirectionalDict:
|
|
@@ -229,7 +229,7 @@ class StateMapper(BaseMapper):
|
|
|
229
229
|
"""Check if adapter uses labels for extended states."""
|
|
230
230
|
return self.adapter_type in ["github", "linear"]
|
|
231
231
|
|
|
232
|
-
def get_state_label(self, state: TicketState) ->
|
|
232
|
+
def get_state_label(self, state: TicketState) -> str | None:
|
|
233
233
|
"""Get label name for extended states that require labels.
|
|
234
234
|
|
|
235
235
|
Args:
|
|
@@ -258,7 +258,7 @@ class PriorityMapper(BaseMapper):
|
|
|
258
258
|
"""Universal priority mapping utility."""
|
|
259
259
|
|
|
260
260
|
def __init__(
|
|
261
|
-
self, adapter_type: str, custom_mappings:
|
|
261
|
+
self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
262
262
|
):
|
|
263
263
|
"""Initialize priority mapper.
|
|
264
264
|
|
|
@@ -270,7 +270,7 @@ class PriorityMapper(BaseMapper):
|
|
|
270
270
|
super().__init__()
|
|
271
271
|
self.adapter_type = adapter_type
|
|
272
272
|
self.custom_mappings = custom_mappings or {}
|
|
273
|
-
self._mapping:
|
|
273
|
+
self._mapping: BiDirectionalDict | None = None
|
|
274
274
|
|
|
275
275
|
@lru_cache(maxsize=1)
|
|
276
276
|
def get_mapping(self) -> BiDirectionalDict:
|
|
@@ -483,7 +483,7 @@ class MapperRegistry:
|
|
|
483
483
|
|
|
484
484
|
@classmethod
|
|
485
485
|
def get_state_mapper(
|
|
486
|
-
cls, adapter_type: str, custom_mappings:
|
|
486
|
+
cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
487
487
|
) -> StateMapper:
|
|
488
488
|
"""Get or create state mapper for adapter type.
|
|
489
489
|
|
|
@@ -502,7 +502,7 @@ class MapperRegistry:
|
|
|
502
502
|
|
|
503
503
|
@classmethod
|
|
504
504
|
def get_priority_mapper(
|
|
505
|
-
cls, adapter_type: str, custom_mappings:
|
|
505
|
+
cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
|
|
506
506
|
) -> PriorityMapper:
|
|
507
507
|
"""Get or create priority mapper for adapter type.
|
|
508
508
|
|
mcp_ticketer/core/models.py
CHANGED
|
@@ -25,7 +25,7 @@ Example:
|
|
|
25
25
|
|
|
26
26
|
from datetime import datetime
|
|
27
27
|
from enum import Enum
|
|
28
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
29
29
|
|
|
30
30
|
from pydantic import BaseModel, ConfigDict, Field
|
|
31
31
|
|
|
@@ -194,14 +194,14 @@ class BaseTicket(BaseModel):
|
|
|
194
194
|
|
|
195
195
|
model_config = ConfigDict(use_enum_values=True)
|
|
196
196
|
|
|
197
|
-
id:
|
|
197
|
+
id: str | None = Field(None, description="Unique identifier")
|
|
198
198
|
title: str = Field(..., min_length=1, description="Ticket title")
|
|
199
|
-
description:
|
|
199
|
+
description: str | None = Field(None, description="Detailed description")
|
|
200
200
|
state: TicketState = Field(TicketState.OPEN, description="Current state")
|
|
201
201
|
priority: Priority = Field(Priority.MEDIUM, description="Priority level")
|
|
202
202
|
tags: list[str] = Field(default_factory=list, description="Tags/labels")
|
|
203
|
-
created_at:
|
|
204
|
-
updated_at:
|
|
203
|
+
created_at: datetime | None = Field(None, description="Creation timestamp")
|
|
204
|
+
updated_at: datetime | None = Field(None, description="Last update timestamp")
|
|
205
205
|
|
|
206
206
|
# Metadata for field mapping to different systems
|
|
207
207
|
metadata: dict[str, Any] = Field(
|
|
@@ -270,20 +270,20 @@ class Task(BaseTicket):
|
|
|
270
270
|
ticket_type: TicketType = Field(
|
|
271
271
|
default=TicketType.ISSUE, description="Ticket type in hierarchy"
|
|
272
272
|
)
|
|
273
|
-
parent_issue:
|
|
274
|
-
parent_epic:
|
|
273
|
+
parent_issue: str | None = Field(None, description="Parent issue ID (for tasks)")
|
|
274
|
+
parent_epic: str | None = Field(
|
|
275
275
|
None,
|
|
276
276
|
description="Parent epic/project ID (for issues). Synonym: 'project'",
|
|
277
277
|
)
|
|
278
|
-
assignee:
|
|
278
|
+
assignee: str | None = Field(None, description="Assigned user")
|
|
279
279
|
children: list[str] = Field(default_factory=list, description="Child task IDs")
|
|
280
280
|
|
|
281
281
|
# Additional fields common across systems
|
|
282
|
-
estimated_hours:
|
|
283
|
-
actual_hours:
|
|
282
|
+
estimated_hours: float | None = Field(None, description="Time estimate")
|
|
283
|
+
actual_hours: float | None = Field(None, description="Actual time spent")
|
|
284
284
|
|
|
285
285
|
@property
|
|
286
|
-
def project(self) ->
|
|
286
|
+
def project(self) -> str | None:
|
|
287
287
|
"""Synonym for parent_epic.
|
|
288
288
|
|
|
289
289
|
Returns:
|
|
@@ -293,7 +293,7 @@ class Task(BaseTicket):
|
|
|
293
293
|
return self.parent_epic
|
|
294
294
|
|
|
295
295
|
@project.setter
|
|
296
|
-
def project(self, value:
|
|
296
|
+
def project(self, value: str | None) -> None:
|
|
297
297
|
"""Set parent_epic via project synonym.
|
|
298
298
|
|
|
299
299
|
Args:
|
|
@@ -345,23 +345,53 @@ class Comment(BaseModel):
|
|
|
345
345
|
|
|
346
346
|
model_config = ConfigDict(use_enum_values=True)
|
|
347
347
|
|
|
348
|
-
id:
|
|
348
|
+
id: str | None = Field(None, description="Comment ID")
|
|
349
349
|
ticket_id: str = Field(..., description="Parent ticket ID")
|
|
350
|
-
author:
|
|
350
|
+
author: str | None = Field(None, description="Comment author")
|
|
351
351
|
content: str = Field(..., min_length=1, description="Comment text")
|
|
352
|
-
created_at:
|
|
352
|
+
created_at: datetime | None = Field(None, description="Creation timestamp")
|
|
353
353
|
metadata: dict[str, Any] = Field(
|
|
354
354
|
default_factory=dict, description="System-specific metadata"
|
|
355
355
|
)
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
class Attachment(BaseModel):
|
|
359
|
+
"""File attachment metadata for tickets.
|
|
360
|
+
|
|
361
|
+
Represents a file attached to a ticket across all adapters.
|
|
362
|
+
Each adapter maps its native attachment format to this model.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
366
|
+
|
|
367
|
+
id: str | None = Field(None, description="Attachment unique identifier")
|
|
368
|
+
ticket_id: str = Field(..., description="Parent ticket identifier")
|
|
369
|
+
filename: str = Field(..., description="Original filename")
|
|
370
|
+
url: str | None = Field(None, description="Download URL or file path")
|
|
371
|
+
content_type: str | None = Field(
|
|
372
|
+
None, description="MIME type (e.g., 'application/pdf', 'image/png')"
|
|
373
|
+
)
|
|
374
|
+
size_bytes: int | None = Field(None, description="File size in bytes")
|
|
375
|
+
created_at: datetime | None = Field(None, description="Upload timestamp")
|
|
376
|
+
created_by: str | None = Field(None, description="User who uploaded the attachment")
|
|
377
|
+
description: str | None = Field(None, description="Attachment description or notes")
|
|
378
|
+
metadata: dict[str, Any] = Field(
|
|
379
|
+
default_factory=dict, description="Adapter-specific attachment metadata"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def __str__(self) -> str:
|
|
383
|
+
"""String representation showing filename and size."""
|
|
384
|
+
size_str = f" ({self.size_bytes} bytes)" if self.size_bytes else ""
|
|
385
|
+
return f"Attachment({self.filename}{size_str})"
|
|
386
|
+
|
|
387
|
+
|
|
358
388
|
class SearchQuery(BaseModel):
|
|
359
389
|
"""Search query parameters."""
|
|
360
390
|
|
|
361
|
-
query:
|
|
362
|
-
state:
|
|
363
|
-
priority:
|
|
364
|
-
tags:
|
|
365
|
-
assignee:
|
|
391
|
+
query: str | None = Field(None, description="Text search query")
|
|
392
|
+
state: TicketState | None = Field(None, description="Filter by state")
|
|
393
|
+
priority: Priority | None = Field(None, description="Filter by priority")
|
|
394
|
+
tags: list[str] | None = Field(None, description="Filter by tags")
|
|
395
|
+
assignee: str | None = Field(None, description="Filter by assignee")
|
|
366
396
|
limit: int = Field(10, gt=0, le=100, description="Maximum results")
|
|
367
397
|
offset: int = Field(0, ge=0, description="Result offset for pagination")
|
|
@@ -47,29 +47,29 @@ class AdapterConfig:
|
|
|
47
47
|
enabled: bool = True
|
|
48
48
|
|
|
49
49
|
# Common fields (not all adapters use all fields)
|
|
50
|
-
api_key:
|
|
51
|
-
token:
|
|
50
|
+
api_key: str | None = None
|
|
51
|
+
token: str | None = None
|
|
52
52
|
|
|
53
53
|
# Linear-specific
|
|
54
|
-
team_id:
|
|
55
|
-
team_key:
|
|
56
|
-
workspace:
|
|
54
|
+
team_id: str | None = None
|
|
55
|
+
team_key: str | None = None
|
|
56
|
+
workspace: str | None = None
|
|
57
57
|
|
|
58
58
|
# JIRA-specific
|
|
59
|
-
server:
|
|
60
|
-
email:
|
|
61
|
-
api_token:
|
|
62
|
-
project_key:
|
|
59
|
+
server: str | None = None
|
|
60
|
+
email: str | None = None
|
|
61
|
+
api_token: str | None = None
|
|
62
|
+
project_key: str | None = None
|
|
63
63
|
|
|
64
64
|
# GitHub-specific
|
|
65
|
-
owner:
|
|
66
|
-
repo:
|
|
65
|
+
owner: str | None = None
|
|
66
|
+
repo: str | None = None
|
|
67
67
|
|
|
68
68
|
# AITrackdown-specific
|
|
69
|
-
base_path:
|
|
69
|
+
base_path: str | None = None
|
|
70
70
|
|
|
71
71
|
# Project ID (can be used by any adapter for scoping)
|
|
72
|
-
project_id:
|
|
72
|
+
project_id: str | None = None
|
|
73
73
|
|
|
74
74
|
# Additional adapter-specific configuration
|
|
75
75
|
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
@@ -126,9 +126,9 @@ class ProjectConfig:
|
|
|
126
126
|
"""Configuration for a specific project."""
|
|
127
127
|
|
|
128
128
|
adapter: str
|
|
129
|
-
api_key:
|
|
130
|
-
project_id:
|
|
131
|
-
team_id:
|
|
129
|
+
api_key: str | None = None
|
|
130
|
+
project_id: str | None = None
|
|
131
|
+
team_id: str | None = None
|
|
132
132
|
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
133
133
|
|
|
134
134
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -147,7 +147,7 @@ class HybridConfig:
|
|
|
147
147
|
|
|
148
148
|
enabled: bool = False
|
|
149
149
|
adapters: list[str] = field(default_factory=list)
|
|
150
|
-
primary_adapter:
|
|
150
|
+
primary_adapter: str | None = None
|
|
151
151
|
sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
|
|
152
152
|
|
|
153
153
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -172,7 +172,7 @@ class TicketerConfig:
|
|
|
172
172
|
default_adapter: str = "aitrackdown"
|
|
173
173
|
project_configs: dict[str, ProjectConfig] = field(default_factory=dict)
|
|
174
174
|
adapters: dict[str, AdapterConfig] = field(default_factory=dict)
|
|
175
|
-
hybrid_mode:
|
|
175
|
+
hybrid_mode: HybridConfig | None = None
|
|
176
176
|
|
|
177
177
|
def to_dict(self) -> dict[str, Any]:
|
|
178
178
|
"""Convert to dictionary for JSON serialization."""
|
|
@@ -219,7 +219,7 @@ class ConfigValidator:
|
|
|
219
219
|
"""Validate adapter configurations."""
|
|
220
220
|
|
|
221
221
|
@staticmethod
|
|
222
|
-
def validate_linear_config(config: dict[str, Any]) -> tuple[bool,
|
|
222
|
+
def validate_linear_config(config: dict[str, Any]) -> tuple[bool, str | None]:
|
|
223
223
|
"""Validate Linear adapter configuration.
|
|
224
224
|
|
|
225
225
|
Returns:
|
|
@@ -241,7 +241,7 @@ class ConfigValidator:
|
|
|
241
241
|
return True, None
|
|
242
242
|
|
|
243
243
|
@staticmethod
|
|
244
|
-
def validate_github_config(config: dict[str, Any]) -> tuple[bool,
|
|
244
|
+
def validate_github_config(config: dict[str, Any]) -> tuple[bool, str | None]:
|
|
245
245
|
"""Validate GitHub adapter configuration.
|
|
246
246
|
|
|
247
247
|
Returns:
|
|
@@ -270,7 +270,7 @@ class ConfigValidator:
|
|
|
270
270
|
return True, None
|
|
271
271
|
|
|
272
272
|
@staticmethod
|
|
273
|
-
def validate_jira_config(config: dict[str, Any]) -> tuple[bool,
|
|
273
|
+
def validate_jira_config(config: dict[str, Any]) -> tuple[bool, str | None]:
|
|
274
274
|
"""Validate JIRA adapter configuration.
|
|
275
275
|
|
|
276
276
|
Returns:
|
|
@@ -292,7 +292,7 @@ class ConfigValidator:
|
|
|
292
292
|
@staticmethod
|
|
293
293
|
def validate_aitrackdown_config(
|
|
294
294
|
config: dict[str, Any],
|
|
295
|
-
) -> tuple[bool,
|
|
295
|
+
) -> tuple[bool, str | None]:
|
|
296
296
|
"""Validate AITrackdown adapter configuration.
|
|
297
297
|
|
|
298
298
|
Returns:
|
|
@@ -306,7 +306,7 @@ class ConfigValidator:
|
|
|
306
306
|
@classmethod
|
|
307
307
|
def validate(
|
|
308
308
|
cls, adapter_type: str, config: dict[str, Any]
|
|
309
|
-
) -> tuple[bool,
|
|
309
|
+
) -> tuple[bool, str | None]:
|
|
310
310
|
"""Validate configuration for any adapter type.
|
|
311
311
|
|
|
312
312
|
Args:
|
|
@@ -350,7 +350,7 @@ class ConfigResolver:
|
|
|
350
350
|
PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
|
|
351
351
|
|
|
352
352
|
def __init__(
|
|
353
|
-
self, project_path:
|
|
353
|
+
self, project_path: Path | None = None, enable_env_discovery: bool = True
|
|
354
354
|
):
|
|
355
355
|
"""Initialize config resolver.
|
|
356
356
|
|
|
@@ -361,8 +361,8 @@ class ConfigResolver:
|
|
|
361
361
|
"""
|
|
362
362
|
self.project_path = project_path or Path.cwd()
|
|
363
363
|
self.enable_env_discovery = enable_env_discovery
|
|
364
|
-
self._project_config:
|
|
365
|
-
self._discovered_config:
|
|
364
|
+
self._project_config: TicketerConfig | None = None
|
|
365
|
+
self._discovered_config: DiscoveryResult | None = None
|
|
366
366
|
|
|
367
367
|
def load_global_config(self) -> TicketerConfig:
|
|
368
368
|
"""Load default configuration (global config loading removed for security).
|
|
@@ -382,8 +382,8 @@ class ConfigResolver:
|
|
|
382
382
|
return default_config
|
|
383
383
|
|
|
384
384
|
def load_project_config(
|
|
385
|
-
self, project_path:
|
|
386
|
-
) ->
|
|
385
|
+
self, project_path: Path | None = None
|
|
386
|
+
) -> TicketerConfig | None:
|
|
387
387
|
"""Load project-specific configuration.
|
|
388
388
|
|
|
389
389
|
Args:
|
|
@@ -424,7 +424,7 @@ class ConfigResolver:
|
|
|
424
424
|
self.save_project_config(config)
|
|
425
425
|
|
|
426
426
|
def save_project_config(
|
|
427
|
-
self, config: TicketerConfig, project_path:
|
|
427
|
+
self, config: TicketerConfig, project_path: Path | None = None
|
|
428
428
|
) -> None:
|
|
429
429
|
"""Save project-specific configuration.
|
|
430
430
|
|
|
@@ -461,8 +461,8 @@ class ConfigResolver:
|
|
|
461
461
|
|
|
462
462
|
def resolve_adapter_config(
|
|
463
463
|
self,
|
|
464
|
-
adapter_name:
|
|
465
|
-
cli_overrides:
|
|
464
|
+
adapter_name: str | None = None,
|
|
465
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
466
466
|
) -> dict[str, Any]:
|
|
467
467
|
"""Resolve adapter configuration with hierarchical precedence.
|
|
468
468
|
|
|
@@ -582,6 +582,12 @@ class ConfigResolver:
|
|
|
582
582
|
overrides["team_id"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_ID")
|
|
583
583
|
if os.getenv("LINEAR_API_KEY"):
|
|
584
584
|
overrides["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
585
|
+
if os.getenv("LINEAR_TEAM_ID"):
|
|
586
|
+
overrides["team_id"] = os.getenv("LINEAR_TEAM_ID")
|
|
587
|
+
if os.getenv("LINEAR_TEAM_KEY"):
|
|
588
|
+
overrides["team_key"] = os.getenv("LINEAR_TEAM_KEY")
|
|
589
|
+
if os.getenv("MCP_TICKETER_LINEAR_TEAM_KEY"):
|
|
590
|
+
overrides["team_key"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_KEY")
|
|
585
591
|
|
|
586
592
|
elif adapter_type == AdapterType.GITHUB.value:
|
|
587
593
|
if os.getenv("MCP_TICKETER_GITHUB_TOKEN"):
|
|
@@ -623,7 +629,7 @@ class ConfigResolver:
|
|
|
623
629
|
|
|
624
630
|
return overrides
|
|
625
631
|
|
|
626
|
-
def get_hybrid_config(self) ->
|
|
632
|
+
def get_hybrid_config(self) -> HybridConfig | None:
|
|
627
633
|
"""Get hybrid mode configuration if enabled.
|
|
628
634
|
|
|
629
635
|
Returns:
|
|
@@ -655,10 +661,10 @@ class ConfigResolver:
|
|
|
655
661
|
|
|
656
662
|
|
|
657
663
|
# Singleton instance for global access
|
|
658
|
-
_default_resolver:
|
|
664
|
+
_default_resolver: ConfigResolver | None = None
|
|
659
665
|
|
|
660
666
|
|
|
661
|
-
def get_config_resolver(project_path:
|
|
667
|
+
def get_config_resolver(project_path: Path | None = None) -> ConfigResolver:
|
|
662
668
|
"""Get the global config resolver instance.
|
|
663
669
|
|
|
664
670
|
Args:
|
mcp_ticketer/core/registry.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Adapter registry for dynamic adapter management."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from .adapter import BaseAdapter
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ class AdapterRegistry:
|
|
|
37
37
|
|
|
38
38
|
@classmethod
|
|
39
39
|
def get_adapter(
|
|
40
|
-
cls, name: str, config:
|
|
40
|
+
cls, name: str, config: dict[str, Any] | None = None, force_new: bool = False
|
|
41
41
|
) -> BaseAdapter:
|
|
42
42
|
"""Get or create an adapter instance.
|
|
43
43
|
|