mcp-ticketer 0.1.11__py3-none-any.whl → 0.1.12__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/__init__.py +8 -1
- mcp_ticketer/adapters/aitrackdown.py +9 -5
- mcp_ticketer/adapters/hybrid.py +505 -0
- mcp_ticketer/cli/configure.py +532 -0
- mcp_ticketer/cli/main.py +81 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/project_config.py +553 -0
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/RECORD +15 -11
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.11.dist-info → mcp_ticketer-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""Project-level configuration management with hierarchical resolution.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive configuration system that supports:
|
|
4
|
+
- Project-specific configurations (.mcp-ticketer/config.json in project root)
|
|
5
|
+
- Global configurations (~/.mcp-ticketer/config.json)
|
|
6
|
+
- Environment variable overrides
|
|
7
|
+
- CLI flag overrides
|
|
8
|
+
- Hybrid mode for multi-platform synchronization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Dict, Any, Optional, List
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from dataclasses import dataclass, field, asdict
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AdapterType(str, Enum):
|
|
23
|
+
"""Supported adapter types."""
|
|
24
|
+
AITRACKDOWN = "aitrackdown"
|
|
25
|
+
LINEAR = "linear"
|
|
26
|
+
JIRA = "jira"
|
|
27
|
+
GITHUB = "github"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SyncStrategy(str, Enum):
|
|
31
|
+
"""Hybrid mode synchronization strategies."""
|
|
32
|
+
PRIMARY_SOURCE = "primary_source" # One adapter is source of truth
|
|
33
|
+
BIDIRECTIONAL = "bidirectional" # Two-way sync between adapters
|
|
34
|
+
MIRROR = "mirror" # Clone tickets across all adapters
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AdapterConfig:
|
|
39
|
+
"""Base configuration for a single adapter instance."""
|
|
40
|
+
adapter: str
|
|
41
|
+
enabled: bool = True
|
|
42
|
+
|
|
43
|
+
# Common fields (not all adapters use all fields)
|
|
44
|
+
api_key: Optional[str] = None
|
|
45
|
+
token: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
# Linear-specific
|
|
48
|
+
team_id: Optional[str] = None
|
|
49
|
+
team_key: Optional[str] = None
|
|
50
|
+
workspace: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
# JIRA-specific
|
|
53
|
+
server: Optional[str] = None
|
|
54
|
+
email: Optional[str] = None
|
|
55
|
+
api_token: Optional[str] = None
|
|
56
|
+
project_key: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
# GitHub-specific
|
|
59
|
+
owner: Optional[str] = None
|
|
60
|
+
repo: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
# AITrackdown-specific
|
|
63
|
+
base_path: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
# Project ID (can be used by any adapter for scoping)
|
|
66
|
+
project_id: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
# Additional adapter-specific configuration
|
|
69
|
+
additional_config: Dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
72
|
+
"""Convert to dictionary, filtering None values."""
|
|
73
|
+
result = {}
|
|
74
|
+
for key, value in asdict(self).items():
|
|
75
|
+
if value is not None:
|
|
76
|
+
result[key] = value
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'AdapterConfig':
|
|
81
|
+
"""Create from dictionary."""
|
|
82
|
+
# Extract known fields
|
|
83
|
+
known_fields = {
|
|
84
|
+
'adapter', 'enabled', 'api_key', 'token', 'team_id', 'team_key',
|
|
85
|
+
'workspace', 'server', 'email', 'api_token', 'project_key',
|
|
86
|
+
'owner', 'repo', 'base_path', 'project_id'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
kwargs = {}
|
|
90
|
+
additional = {}
|
|
91
|
+
|
|
92
|
+
for key, value in data.items():
|
|
93
|
+
if key in known_fields:
|
|
94
|
+
kwargs[key] = value
|
|
95
|
+
elif key != 'additional_config':
|
|
96
|
+
additional[key] = value
|
|
97
|
+
|
|
98
|
+
# Merge explicit additional_config
|
|
99
|
+
if 'additional_config' in data:
|
|
100
|
+
additional.update(data['additional_config'])
|
|
101
|
+
|
|
102
|
+
kwargs['additional_config'] = additional
|
|
103
|
+
return cls(**kwargs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ProjectConfig:
|
|
108
|
+
"""Configuration for a specific project."""
|
|
109
|
+
adapter: str
|
|
110
|
+
api_key: Optional[str] = None
|
|
111
|
+
project_id: Optional[str] = None
|
|
112
|
+
team_id: Optional[str] = None
|
|
113
|
+
additional_config: Dict[str, Any] = field(default_factory=dict)
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
116
|
+
"""Convert to dictionary."""
|
|
117
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'ProjectConfig':
|
|
121
|
+
"""Create from dictionary."""
|
|
122
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class HybridConfig:
|
|
127
|
+
"""Configuration for hybrid mode (multi-adapter sync)."""
|
|
128
|
+
enabled: bool = False
|
|
129
|
+
adapters: List[str] = field(default_factory=list)
|
|
130
|
+
primary_adapter: Optional[str] = None
|
|
131
|
+
sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
134
|
+
"""Convert to dictionary."""
|
|
135
|
+
result = asdict(self)
|
|
136
|
+
result['sync_strategy'] = self.sync_strategy.value
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'HybridConfig':
|
|
141
|
+
"""Create from dictionary."""
|
|
142
|
+
data = data.copy()
|
|
143
|
+
if 'sync_strategy' in data:
|
|
144
|
+
data['sync_strategy'] = SyncStrategy(data['sync_strategy'])
|
|
145
|
+
return cls(**data)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class TicketerConfig:
|
|
150
|
+
"""Complete ticketer configuration with hierarchical resolution."""
|
|
151
|
+
default_adapter: str = "aitrackdown"
|
|
152
|
+
project_configs: Dict[str, ProjectConfig] = field(default_factory=dict)
|
|
153
|
+
adapters: Dict[str, AdapterConfig] = field(default_factory=dict)
|
|
154
|
+
hybrid_mode: Optional[HybridConfig] = None
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
157
|
+
"""Convert to dictionary for JSON serialization."""
|
|
158
|
+
return {
|
|
159
|
+
"default_adapter": self.default_adapter,
|
|
160
|
+
"project_configs": {
|
|
161
|
+
path: config.to_dict()
|
|
162
|
+
for path, config in self.project_configs.items()
|
|
163
|
+
},
|
|
164
|
+
"adapters": {
|
|
165
|
+
name: config.to_dict()
|
|
166
|
+
for name, config in self.adapters.items()
|
|
167
|
+
},
|
|
168
|
+
"hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'TicketerConfig':
|
|
173
|
+
"""Create from dictionary."""
|
|
174
|
+
# Parse project configs
|
|
175
|
+
project_configs = {}
|
|
176
|
+
if "project_configs" in data:
|
|
177
|
+
for path, config_data in data["project_configs"].items():
|
|
178
|
+
project_configs[path] = ProjectConfig.from_dict(config_data)
|
|
179
|
+
|
|
180
|
+
# Parse adapter configs
|
|
181
|
+
adapters = {}
|
|
182
|
+
if "adapters" in data:
|
|
183
|
+
for name, adapter_data in data["adapters"].items():
|
|
184
|
+
adapters[name] = AdapterConfig.from_dict(adapter_data)
|
|
185
|
+
|
|
186
|
+
# Parse hybrid config
|
|
187
|
+
hybrid_mode = None
|
|
188
|
+
if "hybrid_mode" in data and data["hybrid_mode"]:
|
|
189
|
+
hybrid_mode = HybridConfig.from_dict(data["hybrid_mode"])
|
|
190
|
+
|
|
191
|
+
return cls(
|
|
192
|
+
default_adapter=data.get("default_adapter", "aitrackdown"),
|
|
193
|
+
project_configs=project_configs,
|
|
194
|
+
adapters=adapters,
|
|
195
|
+
hybrid_mode=hybrid_mode
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ConfigValidator:
|
|
200
|
+
"""Validate adapter configurations."""
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def validate_linear_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
204
|
+
"""Validate Linear adapter configuration.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (is_valid, error_message)
|
|
208
|
+
"""
|
|
209
|
+
required = ["api_key"]
|
|
210
|
+
for field in required:
|
|
211
|
+
if field not in config or not config[field]:
|
|
212
|
+
return False, f"Linear config missing required field: {field}"
|
|
213
|
+
|
|
214
|
+
# Warn if team_id is missing but don't fail
|
|
215
|
+
if not config.get("team_id"):
|
|
216
|
+
logger.warning("Linear config missing team_id - may be required for some operations")
|
|
217
|
+
|
|
218
|
+
return True, None
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def validate_github_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
222
|
+
"""Validate GitHub adapter configuration.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (is_valid, error_message)
|
|
226
|
+
"""
|
|
227
|
+
# token or api_key (aliases)
|
|
228
|
+
has_token = config.get("token") or config.get("api_key")
|
|
229
|
+
if not has_token:
|
|
230
|
+
return False, "GitHub config missing required field: token or api_key"
|
|
231
|
+
|
|
232
|
+
# project_id can be "owner/repo" format
|
|
233
|
+
if config.get("project_id"):
|
|
234
|
+
if "/" in config["project_id"]:
|
|
235
|
+
parts = config["project_id"].split("/")
|
|
236
|
+
if len(parts) == 2:
|
|
237
|
+
# Extract owner and repo from project_id
|
|
238
|
+
return True, None
|
|
239
|
+
|
|
240
|
+
# Otherwise need explicit owner and repo
|
|
241
|
+
required = ["owner", "repo"]
|
|
242
|
+
for field in required:
|
|
243
|
+
if field not in config or not config[field]:
|
|
244
|
+
return False, f"GitHub config missing required field: {field}"
|
|
245
|
+
|
|
246
|
+
return True, None
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def validate_jira_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
250
|
+
"""Validate JIRA adapter configuration.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Tuple of (is_valid, error_message)
|
|
254
|
+
"""
|
|
255
|
+
required = ["server", "email", "api_token"]
|
|
256
|
+
for field in required:
|
|
257
|
+
if field not in config or not config[field]:
|
|
258
|
+
return False, f"JIRA config missing required field: {field}"
|
|
259
|
+
|
|
260
|
+
# Validate server URL format
|
|
261
|
+
server = config["server"]
|
|
262
|
+
if not server.startswith(("http://", "https://")):
|
|
263
|
+
return False, "JIRA server must be a valid URL (http:// or https://)"
|
|
264
|
+
|
|
265
|
+
return True, None
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def validate_aitrackdown_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
269
|
+
"""Validate AITrackdown adapter configuration.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tuple of (is_valid, error_message)
|
|
273
|
+
"""
|
|
274
|
+
# AITrackdown has minimal requirements
|
|
275
|
+
# base_path is optional (defaults to .aitrackdown)
|
|
276
|
+
return True, None
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def validate(cls, adapter_type: str, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
280
|
+
"""Validate configuration for any adapter type.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
adapter_type: Type of adapter
|
|
284
|
+
config: Configuration dictionary
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Tuple of (is_valid, error_message)
|
|
288
|
+
"""
|
|
289
|
+
validators = {
|
|
290
|
+
AdapterType.LINEAR.value: cls.validate_linear_config,
|
|
291
|
+
AdapterType.GITHUB.value: cls.validate_github_config,
|
|
292
|
+
AdapterType.JIRA.value: cls.validate_jira_config,
|
|
293
|
+
AdapterType.AITRACKDOWN.value: cls.validate_aitrackdown_config,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
validator = validators.get(adapter_type)
|
|
297
|
+
if not validator:
|
|
298
|
+
return False, f"Unknown adapter type: {adapter_type}"
|
|
299
|
+
|
|
300
|
+
return validator(config)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ConfigResolver:
|
|
304
|
+
"""Resolve configuration from multiple sources with hierarchical precedence."""
|
|
305
|
+
|
|
306
|
+
# Global config location
|
|
307
|
+
GLOBAL_CONFIG_PATH = Path.home() / ".mcp-ticketer" / "config.json"
|
|
308
|
+
|
|
309
|
+
# Project config location (relative to project root)
|
|
310
|
+
PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
|
|
311
|
+
|
|
312
|
+
def __init__(self, project_path: Optional[Path] = None):
|
|
313
|
+
"""Initialize config resolver.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
project_path: Path to project root (defaults to cwd)
|
|
317
|
+
"""
|
|
318
|
+
self.project_path = project_path or Path.cwd()
|
|
319
|
+
self._global_config: Optional[TicketerConfig] = None
|
|
320
|
+
self._project_config: Optional[TicketerConfig] = None
|
|
321
|
+
|
|
322
|
+
def load_global_config(self) -> TicketerConfig:
|
|
323
|
+
"""Load global configuration from ~/.mcp-ticketer/config.json."""
|
|
324
|
+
if self.GLOBAL_CONFIG_PATH.exists():
|
|
325
|
+
try:
|
|
326
|
+
with open(self.GLOBAL_CONFIG_PATH, 'r') as f:
|
|
327
|
+
data = json.load(f)
|
|
328
|
+
return TicketerConfig.from_dict(data)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(f"Failed to load global config: {e}")
|
|
331
|
+
|
|
332
|
+
# Return default config
|
|
333
|
+
return TicketerConfig()
|
|
334
|
+
|
|
335
|
+
def load_project_config(self, project_path: Optional[Path] = None) -> Optional[TicketerConfig]:
|
|
336
|
+
"""Load project-specific configuration.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
project_path: Path to project root (defaults to self.project_path)
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Project config if exists, None otherwise
|
|
343
|
+
"""
|
|
344
|
+
proj_path = project_path or self.project_path
|
|
345
|
+
config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
|
|
346
|
+
|
|
347
|
+
if config_path.exists():
|
|
348
|
+
try:
|
|
349
|
+
with open(config_path, 'r') as f:
|
|
350
|
+
data = json.load(f)
|
|
351
|
+
return TicketerConfig.from_dict(data)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Failed to load project config from {config_path}: {e}")
|
|
354
|
+
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
def save_global_config(self, config: TicketerConfig) -> None:
|
|
358
|
+
"""Save global configuration.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
config: Configuration to save
|
|
362
|
+
"""
|
|
363
|
+
self.GLOBAL_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
364
|
+
with open(self.GLOBAL_CONFIG_PATH, 'w') as f:
|
|
365
|
+
json.dump(config.to_dict(), f, indent=2)
|
|
366
|
+
logger.info(f"Saved global config to {self.GLOBAL_CONFIG_PATH}")
|
|
367
|
+
|
|
368
|
+
def save_project_config(self, config: TicketerConfig, project_path: Optional[Path] = None) -> None:
|
|
369
|
+
"""Save project-specific configuration.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
config: Configuration to save
|
|
373
|
+
project_path: Path to project root (defaults to self.project_path)
|
|
374
|
+
"""
|
|
375
|
+
proj_path = project_path or self.project_path
|
|
376
|
+
config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
|
|
377
|
+
|
|
378
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
379
|
+
with open(config_path, 'w') as f:
|
|
380
|
+
json.dump(config.to_dict(), f, indent=2)
|
|
381
|
+
logger.info(f"Saved project config to {config_path}")
|
|
382
|
+
|
|
383
|
+
def resolve_adapter_config(
|
|
384
|
+
self,
|
|
385
|
+
adapter_name: Optional[str] = None,
|
|
386
|
+
cli_overrides: Optional[Dict[str, Any]] = None
|
|
387
|
+
) -> Dict[str, Any]:
|
|
388
|
+
"""Resolve adapter configuration with hierarchical precedence.
|
|
389
|
+
|
|
390
|
+
Precedence (highest to lowest):
|
|
391
|
+
1. CLI overrides
|
|
392
|
+
2. Environment variables
|
|
393
|
+
3. Project-specific config
|
|
394
|
+
4. Global config
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
adapter_name: Name of adapter to configure (defaults to default_adapter)
|
|
398
|
+
cli_overrides: CLI flag overrides
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Resolved configuration dictionary
|
|
402
|
+
"""
|
|
403
|
+
# Load configs
|
|
404
|
+
global_config = self.load_global_config()
|
|
405
|
+
project_config = self.load_project_config()
|
|
406
|
+
|
|
407
|
+
# Determine which adapter to use
|
|
408
|
+
if adapter_name:
|
|
409
|
+
target_adapter = adapter_name
|
|
410
|
+
elif project_config and project_config.default_adapter:
|
|
411
|
+
target_adapter = project_config.default_adapter
|
|
412
|
+
else:
|
|
413
|
+
target_adapter = global_config.default_adapter
|
|
414
|
+
|
|
415
|
+
# Start with empty config
|
|
416
|
+
resolved_config = {"adapter": target_adapter}
|
|
417
|
+
|
|
418
|
+
# 1. Apply global adapter config
|
|
419
|
+
if target_adapter in global_config.adapters:
|
|
420
|
+
global_adapter_config = global_config.adapters[target_adapter].to_dict()
|
|
421
|
+
resolved_config.update(global_adapter_config)
|
|
422
|
+
|
|
423
|
+
# 2. Apply project-specific config if exists
|
|
424
|
+
if project_config:
|
|
425
|
+
# Check if this project has specific adapter config
|
|
426
|
+
project_path_str = str(self.project_path)
|
|
427
|
+
if project_path_str in project_config.project_configs:
|
|
428
|
+
proj_adapter_config = project_config.project_configs[project_path_str].to_dict()
|
|
429
|
+
resolved_config.update(proj_adapter_config)
|
|
430
|
+
|
|
431
|
+
# Also check if project has adapter-level overrides
|
|
432
|
+
if target_adapter in project_config.adapters:
|
|
433
|
+
proj_global_adapter_config = project_config.adapters[target_adapter].to_dict()
|
|
434
|
+
resolved_config.update(proj_global_adapter_config)
|
|
435
|
+
|
|
436
|
+
# 3. Apply environment variable overrides
|
|
437
|
+
env_overrides = self._get_env_overrides(target_adapter)
|
|
438
|
+
resolved_config.update(env_overrides)
|
|
439
|
+
|
|
440
|
+
# 4. Apply CLI overrides
|
|
441
|
+
if cli_overrides:
|
|
442
|
+
resolved_config.update(cli_overrides)
|
|
443
|
+
|
|
444
|
+
return resolved_config
|
|
445
|
+
|
|
446
|
+
def _get_env_overrides(self, adapter_type: str) -> Dict[str, Any]:
|
|
447
|
+
"""Get configuration overrides from environment variables.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
adapter_type: Type of adapter
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Dictionary of overrides from environment
|
|
454
|
+
"""
|
|
455
|
+
overrides = {}
|
|
456
|
+
|
|
457
|
+
# Override adapter type
|
|
458
|
+
if os.getenv("MCP_TICKETER_ADAPTER"):
|
|
459
|
+
overrides["adapter"] = os.getenv("MCP_TICKETER_ADAPTER")
|
|
460
|
+
|
|
461
|
+
# Common overrides
|
|
462
|
+
if os.getenv("MCP_TICKETER_API_KEY"):
|
|
463
|
+
overrides["api_key"] = os.getenv("MCP_TICKETER_API_KEY")
|
|
464
|
+
|
|
465
|
+
# Adapter-specific overrides
|
|
466
|
+
if adapter_type == AdapterType.LINEAR.value:
|
|
467
|
+
if os.getenv("MCP_TICKETER_LINEAR_API_KEY"):
|
|
468
|
+
overrides["api_key"] = os.getenv("MCP_TICKETER_LINEAR_API_KEY")
|
|
469
|
+
if os.getenv("MCP_TICKETER_LINEAR_TEAM_ID"):
|
|
470
|
+
overrides["team_id"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_ID")
|
|
471
|
+
if os.getenv("LINEAR_API_KEY"):
|
|
472
|
+
overrides["api_key"] = os.getenv("LINEAR_API_KEY")
|
|
473
|
+
|
|
474
|
+
elif adapter_type == AdapterType.GITHUB.value:
|
|
475
|
+
if os.getenv("MCP_TICKETER_GITHUB_TOKEN"):
|
|
476
|
+
overrides["token"] = os.getenv("MCP_TICKETER_GITHUB_TOKEN")
|
|
477
|
+
if os.getenv("GITHUB_TOKEN"):
|
|
478
|
+
overrides["token"] = os.getenv("GITHUB_TOKEN")
|
|
479
|
+
if os.getenv("MCP_TICKETER_GITHUB_OWNER"):
|
|
480
|
+
overrides["owner"] = os.getenv("MCP_TICKETER_GITHUB_OWNER")
|
|
481
|
+
if os.getenv("MCP_TICKETER_GITHUB_REPO"):
|
|
482
|
+
overrides["repo"] = os.getenv("MCP_TICKETER_GITHUB_REPO")
|
|
483
|
+
|
|
484
|
+
elif adapter_type == AdapterType.JIRA.value:
|
|
485
|
+
if os.getenv("MCP_TICKETER_JIRA_SERVER"):
|
|
486
|
+
overrides["server"] = os.getenv("MCP_TICKETER_JIRA_SERVER")
|
|
487
|
+
if os.getenv("MCP_TICKETER_JIRA_EMAIL"):
|
|
488
|
+
overrides["email"] = os.getenv("MCP_TICKETER_JIRA_EMAIL")
|
|
489
|
+
if os.getenv("MCP_TICKETER_JIRA_TOKEN"):
|
|
490
|
+
overrides["api_token"] = os.getenv("MCP_TICKETER_JIRA_TOKEN")
|
|
491
|
+
if os.getenv("JIRA_SERVER"):
|
|
492
|
+
overrides["server"] = os.getenv("JIRA_SERVER")
|
|
493
|
+
if os.getenv("JIRA_EMAIL"):
|
|
494
|
+
overrides["email"] = os.getenv("JIRA_EMAIL")
|
|
495
|
+
if os.getenv("JIRA_API_TOKEN"):
|
|
496
|
+
overrides["api_token"] = os.getenv("JIRA_API_TOKEN")
|
|
497
|
+
|
|
498
|
+
elif adapter_type == AdapterType.AITRACKDOWN.value:
|
|
499
|
+
if os.getenv("MCP_TICKETER_AITRACKDOWN_BASE_PATH"):
|
|
500
|
+
overrides["base_path"] = os.getenv("MCP_TICKETER_AITRACKDOWN_BASE_PATH")
|
|
501
|
+
|
|
502
|
+
# Hybrid mode
|
|
503
|
+
if os.getenv("MCP_TICKETER_HYBRID_MODE"):
|
|
504
|
+
overrides["hybrid_mode_enabled"] = os.getenv("MCP_TICKETER_HYBRID_MODE").lower() == "true"
|
|
505
|
+
if os.getenv("MCP_TICKETER_HYBRID_ADAPTERS"):
|
|
506
|
+
overrides["hybrid_adapters"] = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS").split(",")
|
|
507
|
+
|
|
508
|
+
return overrides
|
|
509
|
+
|
|
510
|
+
def get_hybrid_config(self) -> Optional[HybridConfig]:
|
|
511
|
+
"""Get hybrid mode configuration if enabled.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
HybridConfig if hybrid mode is enabled, None otherwise
|
|
515
|
+
"""
|
|
516
|
+
# Check environment first
|
|
517
|
+
if os.getenv("MCP_TICKETER_HYBRID_MODE", "").lower() == "true":
|
|
518
|
+
adapters = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS", "").split(",")
|
|
519
|
+
return HybridConfig(
|
|
520
|
+
enabled=True,
|
|
521
|
+
adapters=[a.strip() for a in adapters if a.strip()]
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Check project config
|
|
525
|
+
project_config = self.load_project_config()
|
|
526
|
+
if project_config and project_config.hybrid_mode and project_config.hybrid_mode.enabled:
|
|
527
|
+
return project_config.hybrid_mode
|
|
528
|
+
|
|
529
|
+
# Check global config
|
|
530
|
+
global_config = self.load_global_config()
|
|
531
|
+
if global_config.hybrid_mode and global_config.hybrid_mode.enabled:
|
|
532
|
+
return global_config.hybrid_mode
|
|
533
|
+
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# Singleton instance for global access
|
|
538
|
+
_default_resolver: Optional[ConfigResolver] = None
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def get_config_resolver(project_path: Optional[Path] = None) -> ConfigResolver:
|
|
542
|
+
"""Get the global config resolver instance.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
project_path: Path to project root (defaults to cwd)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
ConfigResolver instance
|
|
549
|
+
"""
|
|
550
|
+
global _default_resolver
|
|
551
|
+
if _default_resolver is None or project_path is not None:
|
|
552
|
+
_default_resolver = ConfigResolver(project_path)
|
|
553
|
+
return _default_resolver
|
mcp_ticketer/queue/queue.py
CHANGED
|
@@ -173,7 +173,10 @@ class Queue:
|
|
|
173
173
|
''', (QueueStatus.PROCESSING.value, row[0]))
|
|
174
174
|
conn.commit()
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
# Create QueueItem from row and update status
|
|
177
|
+
item = QueueItem.from_row(row)
|
|
178
|
+
item.status = QueueStatus.PROCESSING
|
|
179
|
+
return item
|
|
177
180
|
|
|
178
181
|
return None
|
|
179
182
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=ayPQdFr6msypD06_G96a1H0bdFCT1m1wDtv8MZBpY4I,496
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=2z2sS0X87sc9LRMGkMvT42tCltfdduz3SdYRWzVz6sg,1115
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
mcp_ticketer/adapters/__init__.py,sha256=
|
|
5
|
-
mcp_ticketer/adapters/aitrackdown.py,sha256=
|
|
4
|
+
mcp_ticketer/adapters/__init__.py,sha256=K_1egvhHb5F_7yFceUx2YzPGEoc7vX-q8dMVaS4K6gw,356
|
|
5
|
+
mcp_ticketer/adapters/aitrackdown.py,sha256=gqS_N6VGLoG5itUu17ANG5SefaAITYoW-t2xL9SrY-Y,15372
|
|
6
6
|
mcp_ticketer/adapters/github.py,sha256=onT8NhYaf9fIw2eCOTbZSkk7q4IoM7ZADRvRl9qrUz8,43850
|
|
7
|
+
mcp_ticketer/adapters/hybrid.py,sha256=H9B-pfWmDKXO3GgzxB8undEcZTMzLz_1a6zWhj7xfR0,18556
|
|
7
8
|
mcp_ticketer/adapters/jira.py,sha256=rd-8PseTsRyQNPjsrUJ8vJ8vfBpa6HWFBieOUyvw0Tg,28954
|
|
8
9
|
mcp_ticketer/adapters/linear.py,sha256=neTxVy-QD23tTI7XKtnc5CBCpm3yVCULlgxG5oFSQI4,51752
|
|
9
10
|
mcp_ticketer/cache/__init__.py,sha256=MSi3GLXancfP2-edPC9TFAJk7r0j6H5-XmpMHnkGPbI,137
|
|
10
11
|
mcp_ticketer/cache/memory.py,sha256=gTzv-xF7qGfiYVUjG7lnzo0ZcqgXQajMl4NAYUcaytg,5133
|
|
11
12
|
mcp_ticketer/cli/__init__.py,sha256=YeljyLtv906TqkvRuEPhmKO-Uk0CberQ9I6kx1tx2UA,88
|
|
12
|
-
mcp_ticketer/cli/
|
|
13
|
+
mcp_ticketer/cli/configure.py,sha256=etFutvc0QpaVDMOsZiiN7wKuaT98Od1Tj9W6lsEWw5A,16351
|
|
14
|
+
mcp_ticketer/cli/main.py,sha256=K-Onbn5xla4SGpB1SD1tlby7cdEH44w4le-oKeDt6uI,30338
|
|
15
|
+
mcp_ticketer/cli/migrate_config.py,sha256=iZIstnlr9vkhiW_MlnSyJOkMi4KHQqrZ6Hz1ECD_VUk,6045
|
|
13
16
|
mcp_ticketer/cli/queue_commands.py,sha256=f3pEHKZ43dBHEIoCBvdfvjfMB9_WJltps9ATwTzorY0,8160
|
|
14
17
|
mcp_ticketer/cli/utils.py,sha256=NxsS91vKA8xZnDXKU2y0Gcyc4I_ctRyJj-wT7Xd1Q_Q,18589
|
|
15
18
|
mcp_ticketer/core/__init__.py,sha256=NA-rDvwuAOZ9sUZVYJOWp8bR6mOBG8w_5lpWTT75JNI,318
|
|
@@ -18,18 +21,19 @@ mcp_ticketer/core/config.py,sha256=9a2bksbcFr7KXeHSPY6KoSP5Pzt54utYPCmbM-1QKmk,1
|
|
|
18
21
|
mcp_ticketer/core/http_client.py,sha256=RM9CEMNcuRb-FxhAijmM_FeBMgxgh1OII9HIPBdJue0,13855
|
|
19
22
|
mcp_ticketer/core/mappers.py,sha256=8I4jcqDqoQEdWlteDMpVeVF3Wo0fDCkmFPRr8oNv8gA,16933
|
|
20
23
|
mcp_ticketer/core/models.py,sha256=K-bLuU_DNNVgjHnVFzAIbSa0kJwT2I3Hj24sCklwIYo,4374
|
|
24
|
+
mcp_ticketer/core/project_config.py,sha256=uVhlA9r6GI7h-fWCJeM_s4VMbbj8E7t4QGGOIoC3jd0,19913
|
|
21
25
|
mcp_ticketer/core/registry.py,sha256=fwje0fnjp0YKPZ0SrVWk82SMNLs7CD0JlHQmx7SigNo,3537
|
|
22
26
|
mcp_ticketer/mcp/__init__.py,sha256=Bvzof9vBu6VwcXcIZK8RgKv6ycRV9tDlO-9TUmd8zqQ,122
|
|
23
27
|
mcp_ticketer/mcp/server.py,sha256=TDuU8ChZC2QYSRo0uGHkVReblTf--hriOjxo-pSAF_Y,34068
|
|
24
28
|
mcp_ticketer/queue/__init__.py,sha256=xHBoUwor8ZdO8bIHc7nP25EsAp5Si5Co4g_8ybb7fes,230
|
|
25
29
|
mcp_ticketer/queue/__main__.py,sha256=kQd6iOCKbbFqpRdbIRavuI4_G7-oE898JE4a0yLEYPE,108
|
|
26
30
|
mcp_ticketer/queue/manager.py,sha256=79AH9oUxdBXH3lmJ3kIlFf2GQkWHL6XB6u5JqVWPq60,7571
|
|
27
|
-
mcp_ticketer/queue/queue.py,sha256=
|
|
31
|
+
mcp_ticketer/queue/queue.py,sha256=z4aivQCtsH5_OUr2OfXSfnFKzugTahNnwHw0LS3ZhZc,11549
|
|
28
32
|
mcp_ticketer/queue/run_worker.py,sha256=HFoykfDpOoz8OUxWbQ2Fka_UlGrYwjPVZ-DEimGFH9o,802
|
|
29
33
|
mcp_ticketer/queue/worker.py,sha256=cVjHR_kfnGKAkiUg0HuXCnbKeKNBBEuj0XZHgIuIn4k,14017
|
|
30
|
-
mcp_ticketer-0.1.
|
|
31
|
-
mcp_ticketer-0.1.
|
|
32
|
-
mcp_ticketer-0.1.
|
|
33
|
-
mcp_ticketer-0.1.
|
|
34
|
-
mcp_ticketer-0.1.
|
|
35
|
-
mcp_ticketer-0.1.
|
|
34
|
+
mcp_ticketer-0.1.12.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
35
|
+
mcp_ticketer-0.1.12.dist-info/METADATA,sha256=7acr6QDWin0CGPNxlHTjzcCgVQ12fERtS68xhFQW-No,11211
|
|
36
|
+
mcp_ticketer-0.1.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
37
|
+
mcp_ticketer-0.1.12.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
38
|
+
mcp_ticketer-0.1.12.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
39
|
+
mcp_ticketer-0.1.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|