mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
|
@@ -8,19 +8,20 @@ This module provides a comprehensive configuration system that supports:
|
|
|
8
8
|
- Hybrid mode for multi-platform synchronization
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import os
|
|
12
11
|
import json
|
|
13
12
|
import logging
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
from dataclasses import dataclass, field, asdict
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
17
15
|
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class AdapterType(str, Enum):
|
|
23
23
|
"""Supported adapter types."""
|
|
24
|
+
|
|
24
25
|
AITRACKDOWN = "aitrackdown"
|
|
25
26
|
LINEAR = "linear"
|
|
26
27
|
JIRA = "jira"
|
|
@@ -29,14 +30,16 @@ class AdapterType(str, Enum):
|
|
|
29
30
|
|
|
30
31
|
class SyncStrategy(str, Enum):
|
|
31
32
|
"""Hybrid mode synchronization strategies."""
|
|
33
|
+
|
|
32
34
|
PRIMARY_SOURCE = "primary_source" # One adapter is source of truth
|
|
33
|
-
BIDIRECTIONAL = "bidirectional"
|
|
34
|
-
MIRROR = "mirror"
|
|
35
|
+
BIDIRECTIONAL = "bidirectional" # Two-way sync between adapters
|
|
36
|
+
MIRROR = "mirror" # Clone tickets across all adapters
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
@dataclass
|
|
38
40
|
class AdapterConfig:
|
|
39
41
|
"""Base configuration for a single adapter instance."""
|
|
42
|
+
|
|
40
43
|
adapter: str
|
|
41
44
|
enabled: bool = True
|
|
42
45
|
|
|
@@ -66,9 +69,9 @@ class AdapterConfig:
|
|
|
66
69
|
project_id: Optional[str] = None
|
|
67
70
|
|
|
68
71
|
# Additional adapter-specific configuration
|
|
69
|
-
additional_config:
|
|
72
|
+
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
70
73
|
|
|
71
|
-
def to_dict(self) ->
|
|
74
|
+
def to_dict(self) -> dict[str, Any]:
|
|
72
75
|
"""Convert to dictionary, filtering None values."""
|
|
73
76
|
result = {}
|
|
74
77
|
for key, value in asdict(self).items():
|
|
@@ -77,13 +80,25 @@ class AdapterConfig:
|
|
|
77
80
|
return result
|
|
78
81
|
|
|
79
82
|
@classmethod
|
|
80
|
-
def from_dict(cls, data:
|
|
83
|
+
def from_dict(cls, data: dict[str, Any]) -> "AdapterConfig":
|
|
81
84
|
"""Create from dictionary."""
|
|
82
85
|
# Extract known fields
|
|
83
86
|
known_fields = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
"adapter",
|
|
88
|
+
"enabled",
|
|
89
|
+
"api_key",
|
|
90
|
+
"token",
|
|
91
|
+
"team_id",
|
|
92
|
+
"team_key",
|
|
93
|
+
"workspace",
|
|
94
|
+
"server",
|
|
95
|
+
"email",
|
|
96
|
+
"api_token",
|
|
97
|
+
"project_key",
|
|
98
|
+
"owner",
|
|
99
|
+
"repo",
|
|
100
|
+
"base_path",
|
|
101
|
+
"project_id",
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
kwargs = {}
|
|
@@ -92,32 +107,33 @@ class AdapterConfig:
|
|
|
92
107
|
for key, value in data.items():
|
|
93
108
|
if key in known_fields:
|
|
94
109
|
kwargs[key] = value
|
|
95
|
-
elif key !=
|
|
110
|
+
elif key != "additional_config":
|
|
96
111
|
additional[key] = value
|
|
97
112
|
|
|
98
113
|
# Merge explicit additional_config
|
|
99
|
-
if
|
|
100
|
-
additional.update(data[
|
|
114
|
+
if "additional_config" in data:
|
|
115
|
+
additional.update(data["additional_config"])
|
|
101
116
|
|
|
102
|
-
kwargs[
|
|
117
|
+
kwargs["additional_config"] = additional
|
|
103
118
|
return cls(**kwargs)
|
|
104
119
|
|
|
105
120
|
|
|
106
121
|
@dataclass
|
|
107
122
|
class ProjectConfig:
|
|
108
123
|
"""Configuration for a specific project."""
|
|
124
|
+
|
|
109
125
|
adapter: str
|
|
110
126
|
api_key: Optional[str] = None
|
|
111
127
|
project_id: Optional[str] = None
|
|
112
128
|
team_id: Optional[str] = None
|
|
113
|
-
additional_config:
|
|
129
|
+
additional_config: dict[str, Any] = field(default_factory=dict)
|
|
114
130
|
|
|
115
|
-
def to_dict(self) ->
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
116
132
|
"""Convert to dictionary."""
|
|
117
133
|
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
118
134
|
|
|
119
135
|
@classmethod
|
|
120
|
-
def from_dict(cls, data:
|
|
136
|
+
def from_dict(cls, data: dict[str, Any]) -> "ProjectConfig":
|
|
121
137
|
"""Create from dictionary."""
|
|
122
138
|
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
|
|
123
139
|
|
|
@@ -125,51 +141,51 @@ class ProjectConfig:
|
|
|
125
141
|
@dataclass
|
|
126
142
|
class HybridConfig:
|
|
127
143
|
"""Configuration for hybrid mode (multi-adapter sync)."""
|
|
144
|
+
|
|
128
145
|
enabled: bool = False
|
|
129
|
-
adapters:
|
|
146
|
+
adapters: list[str] = field(default_factory=list)
|
|
130
147
|
primary_adapter: Optional[str] = None
|
|
131
148
|
sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
|
|
132
149
|
|
|
133
|
-
def to_dict(self) ->
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
134
151
|
"""Convert to dictionary."""
|
|
135
152
|
result = asdict(self)
|
|
136
|
-
result[
|
|
153
|
+
result["sync_strategy"] = self.sync_strategy.value
|
|
137
154
|
return result
|
|
138
155
|
|
|
139
156
|
@classmethod
|
|
140
|
-
def from_dict(cls, data:
|
|
157
|
+
def from_dict(cls, data: dict[str, Any]) -> "HybridConfig":
|
|
141
158
|
"""Create from dictionary."""
|
|
142
159
|
data = data.copy()
|
|
143
|
-
if
|
|
144
|
-
data[
|
|
160
|
+
if "sync_strategy" in data:
|
|
161
|
+
data["sync_strategy"] = SyncStrategy(data["sync_strategy"])
|
|
145
162
|
return cls(**data)
|
|
146
163
|
|
|
147
164
|
|
|
148
165
|
@dataclass
|
|
149
166
|
class TicketerConfig:
|
|
150
167
|
"""Complete ticketer configuration with hierarchical resolution."""
|
|
168
|
+
|
|
151
169
|
default_adapter: str = "aitrackdown"
|
|
152
|
-
project_configs:
|
|
153
|
-
adapters:
|
|
170
|
+
project_configs: dict[str, ProjectConfig] = field(default_factory=dict)
|
|
171
|
+
adapters: dict[str, AdapterConfig] = field(default_factory=dict)
|
|
154
172
|
hybrid_mode: Optional[HybridConfig] = None
|
|
155
173
|
|
|
156
|
-
def to_dict(self) ->
|
|
174
|
+
def to_dict(self) -> dict[str, Any]:
|
|
157
175
|
"""Convert to dictionary for JSON serialization."""
|
|
158
176
|
return {
|
|
159
177
|
"default_adapter": self.default_adapter,
|
|
160
178
|
"project_configs": {
|
|
161
|
-
path: config.to_dict()
|
|
162
|
-
for path, config in self.project_configs.items()
|
|
179
|
+
path: config.to_dict() for path, config in self.project_configs.items()
|
|
163
180
|
},
|
|
164
181
|
"adapters": {
|
|
165
|
-
name: config.to_dict()
|
|
166
|
-
for name, config in self.adapters.items()
|
|
182
|
+
name: config.to_dict() for name, config in self.adapters.items()
|
|
167
183
|
},
|
|
168
|
-
"hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None
|
|
184
|
+
"hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None,
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
@classmethod
|
|
172
|
-
def from_dict(cls, data:
|
|
188
|
+
def from_dict(cls, data: dict[str, Any]) -> "TicketerConfig":
|
|
173
189
|
"""Create from dictionary."""
|
|
174
190
|
# Parse project configs
|
|
175
191
|
project_configs = {}
|
|
@@ -192,7 +208,7 @@ class TicketerConfig:
|
|
|
192
208
|
default_adapter=data.get("default_adapter", "aitrackdown"),
|
|
193
209
|
project_configs=project_configs,
|
|
194
210
|
adapters=adapters,
|
|
195
|
-
hybrid_mode=hybrid_mode
|
|
211
|
+
hybrid_mode=hybrid_mode,
|
|
196
212
|
)
|
|
197
213
|
|
|
198
214
|
|
|
@@ -200,11 +216,12 @@ class ConfigValidator:
|
|
|
200
216
|
"""Validate adapter configurations."""
|
|
201
217
|
|
|
202
218
|
@staticmethod
|
|
203
|
-
def validate_linear_config(config:
|
|
219
|
+
def validate_linear_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
204
220
|
"""Validate Linear adapter configuration.
|
|
205
221
|
|
|
206
222
|
Returns:
|
|
207
223
|
Tuple of (is_valid, error_message)
|
|
224
|
+
|
|
208
225
|
"""
|
|
209
226
|
required = ["api_key"]
|
|
210
227
|
for field in required:
|
|
@@ -213,16 +230,20 @@ class ConfigValidator:
|
|
|
213
230
|
|
|
214
231
|
# Require either team_key or team_id (team_id is preferred)
|
|
215
232
|
if not config.get("team_key") and not config.get("team_id"):
|
|
216
|
-
return
|
|
233
|
+
return (
|
|
234
|
+
False,
|
|
235
|
+
"Linear config requires either team_key (short key like 'BTA') or team_id (UUID)",
|
|
236
|
+
)
|
|
217
237
|
|
|
218
238
|
return True, None
|
|
219
239
|
|
|
220
240
|
@staticmethod
|
|
221
|
-
def validate_github_config(config:
|
|
241
|
+
def validate_github_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
222
242
|
"""Validate GitHub adapter configuration.
|
|
223
243
|
|
|
224
244
|
Returns:
|
|
225
245
|
Tuple of (is_valid, error_message)
|
|
246
|
+
|
|
226
247
|
"""
|
|
227
248
|
# token or api_key (aliases)
|
|
228
249
|
has_token = config.get("token") or config.get("api_key")
|
|
@@ -246,11 +267,12 @@ class ConfigValidator:
|
|
|
246
267
|
return True, None
|
|
247
268
|
|
|
248
269
|
@staticmethod
|
|
249
|
-
def validate_jira_config(config:
|
|
270
|
+
def validate_jira_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
|
|
250
271
|
"""Validate JIRA adapter configuration.
|
|
251
272
|
|
|
252
273
|
Returns:
|
|
253
274
|
Tuple of (is_valid, error_message)
|
|
275
|
+
|
|
254
276
|
"""
|
|
255
277
|
required = ["server", "email", "api_token"]
|
|
256
278
|
for field in required:
|
|
@@ -265,18 +287,23 @@ class ConfigValidator:
|
|
|
265
287
|
return True, None
|
|
266
288
|
|
|
267
289
|
@staticmethod
|
|
268
|
-
def validate_aitrackdown_config(
|
|
290
|
+
def validate_aitrackdown_config(
|
|
291
|
+
config: dict[str, Any],
|
|
292
|
+
) -> tuple[bool, Optional[str]]:
|
|
269
293
|
"""Validate AITrackdown adapter configuration.
|
|
270
294
|
|
|
271
295
|
Returns:
|
|
272
296
|
Tuple of (is_valid, error_message)
|
|
297
|
+
|
|
273
298
|
"""
|
|
274
299
|
# AITrackdown has minimal requirements
|
|
275
300
|
# base_path is optional (defaults to .aitrackdown)
|
|
276
301
|
return True, None
|
|
277
302
|
|
|
278
303
|
@classmethod
|
|
279
|
-
def validate(
|
|
304
|
+
def validate(
|
|
305
|
+
cls, adapter_type: str, config: dict[str, Any]
|
|
306
|
+
) -> tuple[bool, Optional[str]]:
|
|
280
307
|
"""Validate configuration for any adapter type.
|
|
281
308
|
|
|
282
309
|
Args:
|
|
@@ -285,6 +312,7 @@ class ConfigValidator:
|
|
|
285
312
|
|
|
286
313
|
Returns:
|
|
287
314
|
Tuple of (is_valid, error_message)
|
|
315
|
+
|
|
288
316
|
"""
|
|
289
317
|
validators = {
|
|
290
318
|
AdapterType.LINEAR.value: cls.validate_linear_config,
|
|
@@ -303,47 +331,56 @@ class ConfigValidator:
|
|
|
303
331
|
class ConfigResolver:
|
|
304
332
|
"""Resolve configuration from multiple sources with hierarchical precedence.
|
|
305
333
|
|
|
334
|
+
SECURITY: This class ONLY reads from project-local configurations
|
|
335
|
+
to prevent configuration leakage across projects. It will NEVER read
|
|
336
|
+
from user home directory or system-wide locations.
|
|
337
|
+
|
|
306
338
|
Resolution order (highest to lowest priority):
|
|
307
339
|
1. CLI overrides
|
|
308
340
|
2. Environment variables
|
|
309
341
|
3. Project-specific config (.mcp-ticketer/config.json)
|
|
310
342
|
4. Auto-discovered .env files
|
|
311
|
-
5.
|
|
343
|
+
5. Default to aitrackdown adapter
|
|
312
344
|
"""
|
|
313
345
|
|
|
314
|
-
#
|
|
315
|
-
GLOBAL_CONFIG_PATH = Path.home() / ".mcp-ticketer" / "config.json"
|
|
316
|
-
|
|
317
|
-
# Project config location (relative to project root)
|
|
346
|
+
# Project config location (relative to project root) - PROJECT-LOCAL ONLY
|
|
318
347
|
PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
|
|
319
348
|
|
|
320
|
-
def __init__(
|
|
349
|
+
def __init__(
|
|
350
|
+
self, project_path: Optional[Path] = None, enable_env_discovery: bool = True
|
|
351
|
+
):
|
|
321
352
|
"""Initialize config resolver.
|
|
322
353
|
|
|
323
354
|
Args:
|
|
324
355
|
project_path: Path to project root (defaults to cwd)
|
|
325
356
|
enable_env_discovery: Enable auto-discovery from .env files (default: True)
|
|
357
|
+
|
|
326
358
|
"""
|
|
327
359
|
self.project_path = project_path or Path.cwd()
|
|
328
360
|
self.enable_env_discovery = enable_env_discovery
|
|
329
|
-
self._global_config: Optional[TicketerConfig] = None
|
|
330
361
|
self._project_config: Optional[TicketerConfig] = None
|
|
331
|
-
self._discovered_config: Optional[
|
|
362
|
+
self._discovered_config: Optional[DiscoveryResult] = None
|
|
332
363
|
|
|
333
364
|
def load_global_config(self) -> TicketerConfig:
|
|
334
|
-
"""Load
|
|
335
|
-
if self.GLOBAL_CONFIG_PATH.exists():
|
|
336
|
-
try:
|
|
337
|
-
with open(self.GLOBAL_CONFIG_PATH, 'r') as f:
|
|
338
|
-
data = json.load(f)
|
|
339
|
-
return TicketerConfig.from_dict(data)
|
|
340
|
-
except Exception as e:
|
|
341
|
-
logger.error(f"Failed to load global config: {e}")
|
|
365
|
+
"""Load default configuration (global config loading removed for security).
|
|
342
366
|
|
|
343
|
-
|
|
344
|
-
|
|
367
|
+
DEPRECATED: Global config loading has been removed for security reasons.
|
|
368
|
+
This method now only returns default configuration.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Default TicketerConfig with aitrackdown adapter
|
|
345
372
|
|
|
346
|
-
|
|
373
|
+
"""
|
|
374
|
+
logger.info("Global config loading disabled for security, using defaults")
|
|
375
|
+
# Return default config with aitrackdown adapter
|
|
376
|
+
default_config = TicketerConfig()
|
|
377
|
+
if not default_config.default_adapter:
|
|
378
|
+
default_config.default_adapter = "aitrackdown"
|
|
379
|
+
return default_config
|
|
380
|
+
|
|
381
|
+
def load_project_config(
|
|
382
|
+
self, project_path: Optional[Path] = None
|
|
383
|
+
) -> Optional[TicketerConfig]:
|
|
347
384
|
"""Load project-specific configuration.
|
|
348
385
|
|
|
349
386
|
Args:
|
|
@@ -351,13 +388,14 @@ class ConfigResolver:
|
|
|
351
388
|
|
|
352
389
|
Returns:
|
|
353
390
|
Project config if exists, None otherwise
|
|
391
|
+
|
|
354
392
|
"""
|
|
355
393
|
proj_path = project_path or self.project_path
|
|
356
394
|
config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
|
|
357
395
|
|
|
358
396
|
if config_path.exists():
|
|
359
397
|
try:
|
|
360
|
-
with open(config_path
|
|
398
|
+
with open(config_path) as f:
|
|
361
399
|
data = json.load(f)
|
|
362
400
|
return TicketerConfig.from_dict(data)
|
|
363
401
|
except Exception as e:
|
|
@@ -366,36 +404,46 @@ class ConfigResolver:
|
|
|
366
404
|
return None
|
|
367
405
|
|
|
368
406
|
def save_global_config(self, config: TicketerConfig) -> None:
|
|
369
|
-
"""Save global
|
|
407
|
+
"""Save configuration to project-local location (global config disabled).
|
|
408
|
+
|
|
409
|
+
DEPRECATED: Global config saving has been removed for security reasons.
|
|
410
|
+
This method now saves to project-local config instead.
|
|
370
411
|
|
|
371
412
|
Args:
|
|
372
413
|
config: Configuration to save
|
|
414
|
+
|
|
373
415
|
"""
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
416
|
+
logger.warning(
|
|
417
|
+
"save_global_config is deprecated and now saves to project-local config. "
|
|
418
|
+
"Use save_project_config instead."
|
|
419
|
+
)
|
|
420
|
+
# Save to project config instead
|
|
421
|
+
self.save_project_config(config)
|
|
378
422
|
|
|
379
|
-
def save_project_config(
|
|
423
|
+
def save_project_config(
|
|
424
|
+
self, config: TicketerConfig, project_path: Optional[Path] = None
|
|
425
|
+
) -> None:
|
|
380
426
|
"""Save project-specific configuration.
|
|
381
427
|
|
|
382
428
|
Args:
|
|
383
429
|
config: Configuration to save
|
|
384
430
|
project_path: Path to project root (defaults to self.project_path)
|
|
431
|
+
|
|
385
432
|
"""
|
|
386
433
|
proj_path = project_path or self.project_path
|
|
387
434
|
config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
|
|
388
435
|
|
|
389
436
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
390
|
-
with open(config_path,
|
|
437
|
+
with open(config_path, "w") as f:
|
|
391
438
|
json.dump(config.to_dict(), f, indent=2)
|
|
392
439
|
logger.info(f"Saved project config to {config_path}")
|
|
393
440
|
|
|
394
|
-
def get_discovered_config(self) -> Optional[
|
|
441
|
+
def get_discovered_config(self) -> Optional["DiscoveryResult"]:
|
|
395
442
|
"""Get auto-discovered configuration from .env files.
|
|
396
443
|
|
|
397
444
|
Returns:
|
|
398
445
|
DiscoveryResult if env discovery is enabled, None otherwise
|
|
446
|
+
|
|
399
447
|
"""
|
|
400
448
|
if not self.enable_env_discovery:
|
|
401
449
|
return None
|
|
@@ -403,6 +451,7 @@ class ConfigResolver:
|
|
|
403
451
|
if self._discovered_config is None:
|
|
404
452
|
# Import here to avoid circular dependency
|
|
405
453
|
from .env_discovery import discover_config
|
|
454
|
+
|
|
406
455
|
self._discovered_config = discover_config(self.project_path)
|
|
407
456
|
|
|
408
457
|
return self._discovered_config
|
|
@@ -410,8 +459,8 @@ class ConfigResolver:
|
|
|
410
459
|
def resolve_adapter_config(
|
|
411
460
|
self,
|
|
412
461
|
adapter_name: Optional[str] = None,
|
|
413
|
-
cli_overrides: Optional[
|
|
414
|
-
) ->
|
|
462
|
+
cli_overrides: Optional[dict[str, Any]] = None,
|
|
463
|
+
) -> dict[str, Any]:
|
|
415
464
|
"""Resolve adapter configuration with hierarchical precedence.
|
|
416
465
|
|
|
417
466
|
Resolution order (highest to lowest priority):
|
|
@@ -427,6 +476,7 @@ class ConfigResolver:
|
|
|
427
476
|
|
|
428
477
|
Returns:
|
|
429
478
|
Resolved configuration dictionary
|
|
479
|
+
|
|
430
480
|
"""
|
|
431
481
|
# Load configs
|
|
432
482
|
global_config = self.load_global_config()
|
|
@@ -465,7 +515,8 @@ class ConfigResolver:
|
|
|
465
515
|
if discovered_adapter:
|
|
466
516
|
# Merge discovered config
|
|
467
517
|
discovered_dict = {
|
|
468
|
-
k: v
|
|
518
|
+
k: v
|
|
519
|
+
for k, v in discovered_adapter.config.items()
|
|
469
520
|
if k != "adapter" # Don't override adapter type
|
|
470
521
|
}
|
|
471
522
|
resolved_config.update(discovered_dict)
|
|
@@ -478,12 +529,16 @@ class ConfigResolver:
|
|
|
478
529
|
# Check if this project has specific adapter config
|
|
479
530
|
project_path_str = str(self.project_path)
|
|
480
531
|
if project_path_str in project_config.project_configs:
|
|
481
|
-
proj_adapter_config = project_config.project_configs[
|
|
532
|
+
proj_adapter_config = project_config.project_configs[
|
|
533
|
+
project_path_str
|
|
534
|
+
].to_dict()
|
|
482
535
|
resolved_config.update(proj_adapter_config)
|
|
483
536
|
|
|
484
537
|
# Also check if project has adapter-level overrides
|
|
485
538
|
if target_adapter in project_config.adapters:
|
|
486
|
-
proj_global_adapter_config = project_config.adapters[
|
|
539
|
+
proj_global_adapter_config = project_config.adapters[
|
|
540
|
+
target_adapter
|
|
541
|
+
].to_dict()
|
|
487
542
|
resolved_config.update(proj_global_adapter_config)
|
|
488
543
|
|
|
489
544
|
# 4. Apply environment variable overrides (os.getenv - HIGHER PRIORITY)
|
|
@@ -496,7 +551,7 @@ class ConfigResolver:
|
|
|
496
551
|
|
|
497
552
|
return resolved_config
|
|
498
553
|
|
|
499
|
-
def _get_env_overrides(self, adapter_type: str) ->
|
|
554
|
+
def _get_env_overrides(self, adapter_type: str) -> dict[str, Any]:
|
|
500
555
|
"""Get configuration overrides from environment variables.
|
|
501
556
|
|
|
502
557
|
Args:
|
|
@@ -504,6 +559,7 @@ class ConfigResolver:
|
|
|
504
559
|
|
|
505
560
|
Returns:
|
|
506
561
|
Dictionary of overrides from environment
|
|
562
|
+
|
|
507
563
|
"""
|
|
508
564
|
overrides = {}
|
|
509
565
|
|
|
@@ -554,9 +610,13 @@ class ConfigResolver:
|
|
|
554
610
|
|
|
555
611
|
# Hybrid mode
|
|
556
612
|
if os.getenv("MCP_TICKETER_HYBRID_MODE"):
|
|
557
|
-
overrides["hybrid_mode_enabled"] =
|
|
613
|
+
overrides["hybrid_mode_enabled"] = (
|
|
614
|
+
os.getenv("MCP_TICKETER_HYBRID_MODE").lower() == "true"
|
|
615
|
+
)
|
|
558
616
|
if os.getenv("MCP_TICKETER_HYBRID_ADAPTERS"):
|
|
559
|
-
overrides["hybrid_adapters"] = os.getenv(
|
|
617
|
+
overrides["hybrid_adapters"] = os.getenv(
|
|
618
|
+
"MCP_TICKETER_HYBRID_ADAPTERS"
|
|
619
|
+
).split(",")
|
|
560
620
|
|
|
561
621
|
return overrides
|
|
562
622
|
|
|
@@ -565,18 +625,22 @@ class ConfigResolver:
|
|
|
565
625
|
|
|
566
626
|
Returns:
|
|
567
627
|
HybridConfig if hybrid mode is enabled, None otherwise
|
|
628
|
+
|
|
568
629
|
"""
|
|
569
630
|
# Check environment first
|
|
570
631
|
if os.getenv("MCP_TICKETER_HYBRID_MODE", "").lower() == "true":
|
|
571
632
|
adapters = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS", "").split(",")
|
|
572
633
|
return HybridConfig(
|
|
573
|
-
enabled=True,
|
|
574
|
-
adapters=[a.strip() for a in adapters if a.strip()]
|
|
634
|
+
enabled=True, adapters=[a.strip() for a in adapters if a.strip()]
|
|
575
635
|
)
|
|
576
636
|
|
|
577
637
|
# Check project config
|
|
578
638
|
project_config = self.load_project_config()
|
|
579
|
-
if
|
|
639
|
+
if (
|
|
640
|
+
project_config
|
|
641
|
+
and project_config.hybrid_mode
|
|
642
|
+
and project_config.hybrid_mode.enabled
|
|
643
|
+
):
|
|
580
644
|
return project_config.hybrid_mode
|
|
581
645
|
|
|
582
646
|
# Check global config
|
|
@@ -599,6 +663,7 @@ def get_config_resolver(project_path: Optional[Path] = None) -> ConfigResolver:
|
|
|
599
663
|
|
|
600
664
|
Returns:
|
|
601
665
|
ConfigResolver instance
|
|
666
|
+
|
|
602
667
|
"""
|
|
603
668
|
global _default_resolver
|
|
604
669
|
if _default_resolver is None or project_path is not None:
|
mcp_ticketer/core/registry.py
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
"""Adapter registry for dynamic adapter management."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
4
5
|
from .adapter import BaseAdapter
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class AdapterRegistry:
|
|
8
9
|
"""Registry for managing ticket system adapters."""
|
|
9
10
|
|
|
10
|
-
_adapters:
|
|
11
|
-
_instances:
|
|
11
|
+
_adapters: dict[str, type[BaseAdapter]] = {}
|
|
12
|
+
_instances: dict[str, BaseAdapter] = {}
|
|
12
13
|
|
|
13
14
|
@classmethod
|
|
14
|
-
def register(cls, name: str, adapter_class:
|
|
15
|
+
def register(cls, name: str, adapter_class: type[BaseAdapter]) -> None:
|
|
15
16
|
"""Register an adapter class.
|
|
16
17
|
|
|
17
18
|
Args:
|
|
18
19
|
name: Unique name for the adapter
|
|
19
20
|
adapter_class: Adapter class to register
|
|
21
|
+
|
|
20
22
|
"""
|
|
21
23
|
if not issubclass(adapter_class, BaseAdapter):
|
|
22
24
|
raise TypeError(f"{adapter_class} must be a subclass of BaseAdapter")
|
|
@@ -28,16 +30,14 @@ class AdapterRegistry:
|
|
|
28
30
|
|
|
29
31
|
Args:
|
|
30
32
|
name: Name of adapter to unregister
|
|
33
|
+
|
|
31
34
|
"""
|
|
32
35
|
cls._adapters.pop(name, None)
|
|
33
36
|
cls._instances.pop(name, None)
|
|
34
37
|
|
|
35
38
|
@classmethod
|
|
36
39
|
def get_adapter(
|
|
37
|
-
cls,
|
|
38
|
-
name: str,
|
|
39
|
-
config: Optional[Dict[str, Any]] = None,
|
|
40
|
-
force_new: bool = False
|
|
40
|
+
cls, name: str, config: Optional[dict[str, Any]] = None, force_new: bool = False
|
|
41
41
|
) -> BaseAdapter:
|
|
42
42
|
"""Get or create an adapter instance.
|
|
43
43
|
|
|
@@ -53,12 +53,12 @@ class AdapterRegistry:
|
|
|
53
53
|
|
|
54
54
|
Raises:
|
|
55
55
|
ValueError: If adapter not registered
|
|
56
|
+
|
|
56
57
|
"""
|
|
57
58
|
if name not in cls._adapters:
|
|
58
59
|
available = ", ".join(cls._adapters.keys())
|
|
59
60
|
raise ValueError(
|
|
60
|
-
f"Adapter '{name}' not registered. "
|
|
61
|
-
f"Available adapters: {available}"
|
|
61
|
+
f"Adapter '{name}' not registered. " f"Available adapters: {available}"
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
# Return cached instance if exists and not forcing new
|
|
@@ -75,11 +75,12 @@ class AdapterRegistry:
|
|
|
75
75
|
return instance
|
|
76
76
|
|
|
77
77
|
@classmethod
|
|
78
|
-
def list_adapters(cls) ->
|
|
78
|
+
def list_adapters(cls) -> dict[str, type[BaseAdapter]]:
|
|
79
79
|
"""List all registered adapters.
|
|
80
80
|
|
|
81
81
|
Returns:
|
|
82
82
|
Dictionary of adapter names to classes
|
|
83
|
+
|
|
83
84
|
"""
|
|
84
85
|
return cls._adapters.copy()
|
|
85
86
|
|
|
@@ -92,6 +93,7 @@ class AdapterRegistry:
|
|
|
92
93
|
|
|
93
94
|
Returns:
|
|
94
95
|
True if registered
|
|
96
|
+
|
|
95
97
|
"""
|
|
96
98
|
return name in cls._adapters
|
|
97
99
|
|
|
@@ -112,10 +114,7 @@ class AdapterRegistry:
|
|
|
112
114
|
cls._instances.clear()
|
|
113
115
|
|
|
114
116
|
|
|
115
|
-
def adapter_factory(
|
|
116
|
-
adapter_type: str,
|
|
117
|
-
config: Dict[str, Any]
|
|
118
|
-
) -> BaseAdapter:
|
|
117
|
+
def adapter_factory(adapter_type: str, config: dict[str, Any]) -> BaseAdapter:
|
|
119
118
|
"""Factory function for creating adapters.
|
|
120
119
|
|
|
121
120
|
Args:
|
|
@@ -124,5 +123,6 @@ def adapter_factory(
|
|
|
124
123
|
|
|
125
124
|
Returns:
|
|
126
125
|
Configured adapter instance
|
|
126
|
+
|
|
127
127
|
"""
|
|
128
|
-
return AdapterRegistry.get_adapter(adapter_type, config)
|
|
128
|
+
return AdapterRegistry.get_adapter(adapter_type, config)
|
mcp_ticketer/mcp/__init__.py
CHANGED