mcp-ticketer 0.4.1__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/auggie_configure.py +66 -0
- mcp_ticketer/cli/codex_configure.py +70 -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 +68 -2
- mcp_ticketer/cli/linear_commands.py +6 -7
- mcp_ticketer/cli/main.py +341 -203
- mcp_ticketer/cli/mcp_configure.py +61 -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 +93 -0
- mcp_ticketer/mcp/tools/__init__.py +36 -0
- mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
- mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
- mcp_ticketer/mcp/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
- 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.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
- mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
- mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
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)
|
|
@@ -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")
|