mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/core/env_loader.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Unified Environment Loading System for MCP Ticketer.
|
|
2
|
+
"""Unified Environment Loading System for MCP Ticketer.
|
|
4
3
|
|
|
5
4
|
This module provides a resilient environment loading system that:
|
|
6
5
|
1. Supports multiple naming conventions for each configuration key
|
|
@@ -9,11 +8,11 @@ This module provides a resilient environment loading system that:
|
|
|
9
8
|
4. Provides fallback mechanisms for different key aliases
|
|
10
9
|
"""
|
|
11
10
|
|
|
12
|
-
import os
|
|
13
11
|
import logging
|
|
14
|
-
|
|
15
|
-
from typing import Dict, Any, List, Optional, Union
|
|
12
|
+
import os
|
|
16
13
|
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger(__name__)
|
|
19
18
|
|
|
@@ -21,19 +20,20 @@ logger = logging.getLogger(__name__)
|
|
|
21
20
|
@dataclass
|
|
22
21
|
class EnvKeyConfig:
|
|
23
22
|
"""Configuration for environment variable key aliases."""
|
|
23
|
+
|
|
24
24
|
primary_key: str
|
|
25
|
-
aliases:
|
|
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:
|
|
32
|
+
"""Unified environment loader that handles multiple naming conventions.
|
|
33
|
+
|
|
34
|
+
Provides consistent environment loading across all contexts.
|
|
32
35
|
"""
|
|
33
|
-
|
|
34
|
-
and provides consistent environment loading across all contexts.
|
|
35
|
-
"""
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
# Define key aliases for all adapters
|
|
38
38
|
KEY_MAPPINGS = {
|
|
39
39
|
# Linear adapter keys
|
|
@@ -41,237 +41,259 @@ class UnifiedEnvLoader:
|
|
|
41
41
|
primary_key="LINEAR_API_KEY",
|
|
42
42
|
aliases=["LINEAR_TOKEN", "LINEAR_ACCESS_TOKEN", "LINEAR_AUTH_TOKEN"],
|
|
43
43
|
description="Linear API key",
|
|
44
|
-
required=True
|
|
44
|
+
required=True,
|
|
45
45
|
),
|
|
46
46
|
"linear_team_id": EnvKeyConfig(
|
|
47
47
|
primary_key="LINEAR_TEAM_ID",
|
|
48
48
|
aliases=["LINEAR_TEAM_UUID", "LINEAR_TEAM_IDENTIFIER"],
|
|
49
49
|
description="Linear team ID (UUID)",
|
|
50
|
-
required=False
|
|
50
|
+
required=False,
|
|
51
51
|
),
|
|
52
52
|
"linear_team_key": EnvKeyConfig(
|
|
53
53
|
primary_key="LINEAR_TEAM_KEY",
|
|
54
54
|
aliases=["LINEAR_TEAM_IDENTIFIER", "LINEAR_TEAM_NAME"],
|
|
55
55
|
description="Linear team key (short name)",
|
|
56
|
-
required=False
|
|
56
|
+
required=False,
|
|
57
57
|
),
|
|
58
|
-
|
|
59
58
|
# JIRA adapter keys
|
|
60
59
|
"jira_server": EnvKeyConfig(
|
|
61
60
|
primary_key="JIRA_SERVER",
|
|
62
61
|
aliases=["JIRA_URL", "JIRA_HOST", "JIRA_BASE_URL"],
|
|
63
62
|
description="JIRA server URL",
|
|
64
|
-
required=True
|
|
63
|
+
required=True,
|
|
65
64
|
),
|
|
66
65
|
"jira_email": EnvKeyConfig(
|
|
67
66
|
primary_key="JIRA_EMAIL",
|
|
68
67
|
aliases=["JIRA_USER", "JIRA_USERNAME", "JIRA_ACCESS_USER"],
|
|
69
68
|
description="JIRA user email",
|
|
70
|
-
required=True
|
|
69
|
+
required=True,
|
|
71
70
|
),
|
|
72
71
|
"jira_api_token": EnvKeyConfig(
|
|
73
72
|
primary_key="JIRA_API_TOKEN",
|
|
74
|
-
aliases=[
|
|
73
|
+
aliases=[
|
|
74
|
+
"JIRA_TOKEN",
|
|
75
|
+
"JIRA_ACCESS_TOKEN",
|
|
76
|
+
"JIRA_AUTH_TOKEN",
|
|
77
|
+
"JIRA_PASSWORD",
|
|
78
|
+
],
|
|
75
79
|
description="JIRA API token",
|
|
76
|
-
required=True
|
|
80
|
+
required=True,
|
|
77
81
|
),
|
|
78
82
|
"jira_project_key": EnvKeyConfig(
|
|
79
83
|
primary_key="JIRA_PROJECT_KEY",
|
|
80
84
|
aliases=["JIRA_PROJECT", "JIRA_PROJECT_ID"],
|
|
81
85
|
description="JIRA project key",
|
|
82
|
-
required=False
|
|
86
|
+
required=False,
|
|
83
87
|
),
|
|
84
|
-
|
|
85
88
|
# GitHub adapter keys
|
|
86
89
|
"github_token": EnvKeyConfig(
|
|
87
90
|
primary_key="GITHUB_TOKEN",
|
|
88
91
|
aliases=["GITHUB_ACCESS_TOKEN", "GITHUB_API_TOKEN", "GITHUB_AUTH_TOKEN"],
|
|
89
92
|
description="GitHub access token",
|
|
90
|
-
required=True
|
|
93
|
+
required=True,
|
|
91
94
|
),
|
|
92
95
|
"github_owner": EnvKeyConfig(
|
|
93
96
|
primary_key="GITHUB_OWNER",
|
|
94
97
|
aliases=["GITHUB_USER", "GITHUB_USERNAME", "GITHUB_ORG"],
|
|
95
98
|
description="GitHub repository owner",
|
|
96
|
-
required=True
|
|
99
|
+
required=True,
|
|
97
100
|
),
|
|
98
101
|
"github_repo": EnvKeyConfig(
|
|
99
102
|
primary_key="GITHUB_REPO",
|
|
100
103
|
aliases=["GITHUB_REPOSITORY", "GITHUB_REPO_NAME"],
|
|
101
104
|
description="GitHub repository name",
|
|
102
|
-
required=True
|
|
105
|
+
required=True,
|
|
103
106
|
),
|
|
104
107
|
}
|
|
105
|
-
|
|
106
|
-
def __init__(self, project_root:
|
|
108
|
+
|
|
109
|
+
def __init__(self, project_root: Path | None = None):
|
|
107
110
|
"""Initialize the environment loader.
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
Args:
|
|
110
113
|
project_root: Project root directory. If None, will auto-detect.
|
|
114
|
+
|
|
111
115
|
"""
|
|
112
116
|
self.project_root = project_root or self._find_project_root()
|
|
113
|
-
self._env_cache:
|
|
117
|
+
self._env_cache: dict[str, str] = {}
|
|
114
118
|
self._load_env_files()
|
|
115
|
-
|
|
119
|
+
|
|
116
120
|
def _find_project_root(self) -> Path:
|
|
117
121
|
"""Find the project root directory."""
|
|
118
122
|
current = Path.cwd()
|
|
119
|
-
|
|
123
|
+
|
|
120
124
|
# Look for common project indicators
|
|
121
125
|
indicators = [".mcp-ticketer", ".git", "pyproject.toml", "setup.py"]
|
|
122
|
-
|
|
126
|
+
|
|
123
127
|
while current != current.parent:
|
|
124
128
|
if any((current / indicator).exists() for indicator in indicators):
|
|
125
129
|
return current
|
|
126
130
|
current = current.parent
|
|
127
|
-
|
|
131
|
+
|
|
128
132
|
# Fallback to current directory
|
|
129
133
|
return Path.cwd()
|
|
130
|
-
|
|
131
|
-
def _load_env_files(self):
|
|
134
|
+
|
|
135
|
+
def _load_env_files(self) -> None:
|
|
132
136
|
"""Load environment variables from .env files."""
|
|
133
137
|
env_files = [
|
|
134
138
|
self.project_root / ".env.local",
|
|
135
139
|
self.project_root / ".env",
|
|
136
140
|
Path.home() / ".mcp-ticketer" / ".env",
|
|
137
141
|
]
|
|
138
|
-
|
|
142
|
+
|
|
139
143
|
for env_file in env_files:
|
|
140
144
|
if env_file.exists():
|
|
141
145
|
logger.debug(f"Loading environment from: {env_file}")
|
|
142
146
|
self._load_env_file(env_file)
|
|
143
|
-
|
|
144
|
-
def _load_env_file(self, env_file: Path):
|
|
147
|
+
|
|
148
|
+
def _load_env_file(self, env_file: Path) -> None:
|
|
145
149
|
"""Load variables from a single .env file."""
|
|
146
150
|
try:
|
|
147
|
-
with open(env_file
|
|
148
|
-
for
|
|
151
|
+
with open(env_file) as f:
|
|
152
|
+
for _line_num, line in enumerate(f, 1):
|
|
149
153
|
line = line.strip()
|
|
150
|
-
|
|
154
|
+
|
|
151
155
|
# Skip empty lines and comments
|
|
152
|
-
if not line or line.startswith(
|
|
156
|
+
if not line or line.startswith("#"):
|
|
153
157
|
continue
|
|
154
|
-
|
|
158
|
+
|
|
155
159
|
# Parse KEY=VALUE format
|
|
156
|
-
if
|
|
157
|
-
key, value = line.split(
|
|
160
|
+
if "=" in line:
|
|
161
|
+
key, value = line.split("=", 1)
|
|
158
162
|
key = key.strip()
|
|
159
163
|
value = value.strip()
|
|
160
|
-
|
|
164
|
+
|
|
161
165
|
# Remove quotes if present
|
|
162
166
|
if value.startswith('"') and value.endswith('"'):
|
|
163
167
|
value = value[1:-1]
|
|
164
168
|
elif value.startswith("'") and value.endswith("'"):
|
|
165
169
|
value = value[1:-1]
|
|
166
|
-
|
|
170
|
+
|
|
167
171
|
# Only set if not already in environment
|
|
168
172
|
if key not in os.environ:
|
|
169
173
|
os.environ[key] = value
|
|
170
174
|
self._env_cache[key] = value
|
|
171
175
|
logger.debug(f"Loaded {key} from {env_file}")
|
|
172
|
-
|
|
176
|
+
|
|
173
177
|
except Exception as e:
|
|
174
178
|
logger.warning(f"Failed to load {env_file}: {e}")
|
|
175
|
-
|
|
176
|
-
def get_value(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
|
|
180
|
+
def get_value(
|
|
181
|
+
self, config_key: str, config: dict[str, Any] | None = None
|
|
182
|
+
) -> str | None:
|
|
183
|
+
"""Get a configuration value using the key alias system.
|
|
184
|
+
|
|
180
185
|
Args:
|
|
181
186
|
config_key: The configuration key (e.g., 'linear_api_key')
|
|
182
187
|
config: Optional configuration dictionary to check first
|
|
183
|
-
|
|
188
|
+
|
|
184
189
|
Returns:
|
|
185
190
|
The value if found, None otherwise
|
|
191
|
+
|
|
186
192
|
"""
|
|
187
193
|
if config_key not in self.KEY_MAPPINGS:
|
|
188
194
|
logger.warning(f"Unknown configuration key: {config_key}")
|
|
189
195
|
return None
|
|
190
|
-
|
|
196
|
+
|
|
191
197
|
key_config = self.KEY_MAPPINGS[config_key]
|
|
192
|
-
|
|
198
|
+
|
|
193
199
|
# 1. Check provided config dictionary first
|
|
194
200
|
if config:
|
|
195
201
|
# Check for the config key itself (without adapter prefix)
|
|
196
|
-
simple_key =
|
|
202
|
+
simple_key = (
|
|
203
|
+
config_key.split("_", 1)[1] if "_" in config_key else config_key
|
|
204
|
+
)
|
|
197
205
|
if simple_key in config:
|
|
198
206
|
value = config[simple_key]
|
|
199
207
|
if value:
|
|
200
208
|
logger.debug(f"Found {config_key} in config as {simple_key}")
|
|
201
209
|
return str(value)
|
|
202
|
-
|
|
210
|
+
|
|
203
211
|
# 2. Check environment variables (primary key first, then aliases)
|
|
204
212
|
all_keys = [key_config.primary_key] + key_config.aliases
|
|
205
|
-
|
|
213
|
+
|
|
206
214
|
for env_key in all_keys:
|
|
207
215
|
value = os.getenv(env_key)
|
|
208
216
|
if value:
|
|
209
217
|
logger.debug(f"Found {config_key} as {env_key}")
|
|
210
218
|
return value
|
|
211
|
-
|
|
219
|
+
|
|
212
220
|
# 3. Return default if available
|
|
213
221
|
if key_config.default:
|
|
214
222
|
logger.debug(f"Using default for {config_key}")
|
|
215
223
|
return key_config.default
|
|
216
|
-
|
|
224
|
+
|
|
217
225
|
# 4. Log if required key is missing
|
|
218
226
|
if key_config.required:
|
|
219
|
-
logger.warning(
|
|
220
|
-
|
|
227
|
+
logger.warning(
|
|
228
|
+
f"Required configuration key {config_key} not found. Tried: {all_keys}"
|
|
229
|
+
)
|
|
230
|
+
|
|
221
231
|
return None
|
|
222
|
-
|
|
223
|
-
def get_adapter_config(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
|
|
233
|
+
def get_adapter_config(
|
|
234
|
+
self, adapter_name: str, base_config: dict[str, Any] | None = None
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""Get complete configuration for an adapter with environment variable resolution.
|
|
237
|
+
|
|
227
238
|
Args:
|
|
228
239
|
adapter_name: Name of the adapter ('linear', 'jira', 'github')
|
|
229
240
|
base_config: Base configuration dictionary
|
|
230
|
-
|
|
241
|
+
|
|
231
242
|
Returns:
|
|
232
243
|
Complete configuration with environment variables resolved
|
|
244
|
+
|
|
233
245
|
"""
|
|
234
246
|
config = base_config.copy() if base_config else {}
|
|
235
|
-
|
|
247
|
+
|
|
236
248
|
# Get adapter-specific keys
|
|
237
|
-
adapter_keys = [
|
|
238
|
-
|
|
249
|
+
adapter_keys = [
|
|
250
|
+
key
|
|
251
|
+
for key in self.KEY_MAPPINGS.keys()
|
|
252
|
+
if key.startswith(f"{adapter_name}_")
|
|
253
|
+
]
|
|
254
|
+
|
|
239
255
|
for config_key in adapter_keys:
|
|
240
256
|
# Remove adapter prefix for the config key
|
|
241
|
-
simple_key = config_key.split(
|
|
242
|
-
|
|
257
|
+
simple_key = config_key.split("_", 1)[1]
|
|
258
|
+
|
|
243
259
|
# Only set if not already in config or if config value is empty
|
|
244
260
|
if simple_key not in config or not config[simple_key]:
|
|
245
261
|
value = self.get_value(config_key, config)
|
|
246
262
|
if value:
|
|
247
263
|
config[simple_key] = value
|
|
248
|
-
|
|
264
|
+
|
|
249
265
|
return config
|
|
250
|
-
|
|
251
|
-
def validate_adapter_config(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
|
|
267
|
+
def validate_adapter_config(
|
|
268
|
+
self, adapter_name: str, config: dict[str, Any]
|
|
269
|
+
) -> list[str]:
|
|
270
|
+
"""Validate that all required configuration is present for an adapter.
|
|
271
|
+
|
|
255
272
|
Args:
|
|
256
273
|
adapter_name: Name of the adapter
|
|
257
274
|
config: Configuration dictionary
|
|
258
|
-
|
|
275
|
+
|
|
259
276
|
Returns:
|
|
260
277
|
List of missing required keys (empty if all required keys are present)
|
|
278
|
+
|
|
261
279
|
"""
|
|
262
280
|
missing_keys = []
|
|
263
|
-
adapter_keys = [
|
|
264
|
-
|
|
281
|
+
adapter_keys = [
|
|
282
|
+
key
|
|
283
|
+
for key in self.KEY_MAPPINGS.keys()
|
|
284
|
+
if key.startswith(f"{adapter_name}_")
|
|
285
|
+
]
|
|
286
|
+
|
|
265
287
|
for config_key in adapter_keys:
|
|
266
288
|
key_config = self.KEY_MAPPINGS[config_key]
|
|
267
289
|
if key_config.required:
|
|
268
|
-
simple_key = config_key.split(
|
|
290
|
+
simple_key = config_key.split("_", 1)[1]
|
|
269
291
|
if simple_key not in config or not config[simple_key]:
|
|
270
292
|
missing_keys.append(f"{simple_key} ({key_config.description})")
|
|
271
|
-
|
|
293
|
+
|
|
272
294
|
return missing_keys
|
|
273
|
-
|
|
274
|
-
def get_debug_info(self) ->
|
|
295
|
+
|
|
296
|
+
def get_debug_info(self) -> dict[str, Any]:
|
|
275
297
|
"""Get debug information about environment loading."""
|
|
276
298
|
return {
|
|
277
299
|
"project_root": str(self.project_root),
|
|
@@ -286,7 +308,7 @@ class UnifiedEnvLoader:
|
|
|
286
308
|
|
|
287
309
|
|
|
288
310
|
# Global instance
|
|
289
|
-
_env_loader:
|
|
311
|
+
_env_loader: UnifiedEnvLoader | None = None
|
|
290
312
|
|
|
291
313
|
|
|
292
314
|
def get_env_loader() -> UnifiedEnvLoader:
|
|
@@ -297,29 +319,31 @@ def get_env_loader() -> UnifiedEnvLoader:
|
|
|
297
319
|
return _env_loader
|
|
298
320
|
|
|
299
321
|
|
|
300
|
-
def load_adapter_config(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
322
|
+
def load_adapter_config(
|
|
323
|
+
adapter_name: str, base_config: dict[str, Any] | None = None
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
"""Load adapter configuration with environment variables.
|
|
326
|
+
|
|
304
327
|
Args:
|
|
305
328
|
adapter_name: Name of the adapter ('linear', 'jira', 'github')
|
|
306
329
|
base_config: Base configuration dictionary
|
|
307
|
-
|
|
330
|
+
|
|
308
331
|
Returns:
|
|
309
332
|
Complete configuration with environment variables resolved
|
|
333
|
+
|
|
310
334
|
"""
|
|
311
335
|
return get_env_loader().get_adapter_config(adapter_name, base_config)
|
|
312
336
|
|
|
313
337
|
|
|
314
|
-
def validate_adapter_config(adapter_name: str, config:
|
|
315
|
-
"""
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
def validate_adapter_config(adapter_name: str, config: dict[str, Any]) -> list[str]:
|
|
339
|
+
"""Validate adapter configuration.
|
|
340
|
+
|
|
318
341
|
Args:
|
|
319
342
|
adapter_name: Name of the adapter
|
|
320
343
|
config: Configuration dictionary
|
|
321
|
-
|
|
344
|
+
|
|
322
345
|
Returns:
|
|
323
346
|
List of missing required keys (empty if all required keys are present)
|
|
347
|
+
|
|
324
348
|
"""
|
|
325
349
|
return get_env_loader().validate_adapter_config(adapter_name, config)
|
mcp_ticketer/core/exceptions.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
"""Exception classes for MCP Ticketer.
|
|
1
|
+
"""Exception classes for MCP Ticketer.
|
|
2
|
+
|
|
3
|
+
Error Severity Classification:
|
|
4
|
+
CRITICAL - System-level issues (auth, config, network) → Always suggest diagnostics
|
|
5
|
+
MEDIUM - Resource issues (not found, permissions) → Suggest diagnostics
|
|
6
|
+
LOW - User input errors (validation, state transitions) → No diagnostics
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
5
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
6
12
|
|
|
7
13
|
from .models import TicketState
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class MCPTicketerError(Exception):
|
|
11
17
|
"""Base exception for MCP Ticketer."""
|
|
18
|
+
|
|
12
19
|
pass
|
|
13
20
|
|
|
14
21
|
|
|
@@ -19,21 +26,22 @@ class AdapterError(MCPTicketerError):
|
|
|
19
26
|
self,
|
|
20
27
|
message: str,
|
|
21
28
|
adapter_name: str,
|
|
22
|
-
original_error:
|
|
29
|
+
original_error: Exception | None = None,
|
|
23
30
|
):
|
|
24
31
|
"""Initialize adapter error.
|
|
25
|
-
|
|
32
|
+
|
|
26
33
|
Args:
|
|
27
34
|
message: Error message
|
|
28
35
|
adapter_name: Name of the adapter that raised the error
|
|
29
36
|
original_error: Original exception that caused this error
|
|
37
|
+
|
|
30
38
|
"""
|
|
31
39
|
super().__init__(message)
|
|
32
40
|
self.adapter_name = adapter_name
|
|
33
41
|
self.original_error = original_error
|
|
34
42
|
|
|
35
43
|
def __str__(self) -> str:
|
|
36
|
-
"""
|
|
44
|
+
"""Return string representation of the error."""
|
|
37
45
|
base_msg = f"[{self.adapter_name}] {super().__str__()}"
|
|
38
46
|
if self.original_error:
|
|
39
47
|
base_msg += f" (caused by: {self.original_error})"
|
|
@@ -42,6 +50,7 @@ class AdapterError(MCPTicketerError):
|
|
|
42
50
|
|
|
43
51
|
class AuthenticationError(AdapterError):
|
|
44
52
|
"""Authentication failed with external service."""
|
|
53
|
+
|
|
45
54
|
pass
|
|
46
55
|
|
|
47
56
|
|
|
@@ -52,16 +61,17 @@ class RateLimitError(AdapterError):
|
|
|
52
61
|
self,
|
|
53
62
|
message: str,
|
|
54
63
|
adapter_name: str,
|
|
55
|
-
retry_after:
|
|
56
|
-
original_error:
|
|
64
|
+
retry_after: int | None = None,
|
|
65
|
+
original_error: Exception | None = None,
|
|
57
66
|
):
|
|
58
67
|
"""Initialize rate limit error.
|
|
59
|
-
|
|
68
|
+
|
|
60
69
|
Args:
|
|
61
70
|
message: Error message
|
|
62
71
|
adapter_name: Name of the adapter
|
|
63
72
|
retry_after: Seconds to wait before retrying
|
|
64
73
|
original_error: Original exception
|
|
74
|
+
|
|
65
75
|
"""
|
|
66
76
|
super().__init__(message, adapter_name, original_error)
|
|
67
77
|
self.retry_after = retry_after
|
|
@@ -70,25 +80,21 @@ class RateLimitError(AdapterError):
|
|
|
70
80
|
class ValidationError(MCPTicketerError):
|
|
71
81
|
"""Data validation error."""
|
|
72
82
|
|
|
73
|
-
def __init__(
|
|
74
|
-
self,
|
|
75
|
-
message: str,
|
|
76
|
-
field: Optional[str] = None,
|
|
77
|
-
value: Any = None
|
|
78
|
-
):
|
|
83
|
+
def __init__(self, message: str, field: str | None = None, value: Any = None):
|
|
79
84
|
"""Initialize validation error.
|
|
80
|
-
|
|
85
|
+
|
|
81
86
|
Args:
|
|
82
87
|
message: Error message
|
|
83
88
|
field: Field that failed validation
|
|
84
89
|
value: Value that failed validation
|
|
90
|
+
|
|
85
91
|
"""
|
|
86
92
|
super().__init__(message)
|
|
87
93
|
self.field = field
|
|
88
94
|
self.value = value
|
|
89
95
|
|
|
90
96
|
def __str__(self) -> str:
|
|
91
|
-
"""
|
|
97
|
+
"""Return string representation of the error."""
|
|
92
98
|
base_msg = super().__str__()
|
|
93
99
|
if self.field:
|
|
94
100
|
base_msg += f" (field: {self.field})"
|
|
@@ -99,54 +105,56 @@ class ValidationError(MCPTicketerError):
|
|
|
99
105
|
|
|
100
106
|
class ConfigurationError(MCPTicketerError):
|
|
101
107
|
"""Configuration error."""
|
|
108
|
+
|
|
102
109
|
pass
|
|
103
110
|
|
|
104
111
|
|
|
105
112
|
class CacheError(MCPTicketerError):
|
|
106
113
|
"""Cache operation error."""
|
|
114
|
+
|
|
107
115
|
pass
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
class StateTransitionError(MCPTicketerError):
|
|
111
119
|
"""Invalid state transition."""
|
|
112
120
|
|
|
113
|
-
def __init__(
|
|
114
|
-
self,
|
|
115
|
-
message: str,
|
|
116
|
-
from_state: TicketState,
|
|
117
|
-
to_state: TicketState
|
|
118
|
-
):
|
|
121
|
+
def __init__(self, message: str, from_state: TicketState, to_state: TicketState):
|
|
119
122
|
"""Initialize state transition error.
|
|
120
|
-
|
|
123
|
+
|
|
121
124
|
Args:
|
|
122
125
|
message: Error message
|
|
123
126
|
from_state: Current state
|
|
124
127
|
to_state: Target state
|
|
128
|
+
|
|
125
129
|
"""
|
|
126
130
|
super().__init__(message)
|
|
127
131
|
self.from_state = from_state
|
|
128
132
|
self.to_state = to_state
|
|
129
133
|
|
|
130
134
|
def __str__(self) -> str:
|
|
131
|
-
"""
|
|
135
|
+
"""Return string representation of the error."""
|
|
132
136
|
return f"{super().__str__()} ({self.from_state} -> {self.to_state})"
|
|
133
137
|
|
|
134
138
|
|
|
135
139
|
class NetworkError(AdapterError):
|
|
136
140
|
"""Network-related error."""
|
|
141
|
+
|
|
137
142
|
pass
|
|
138
143
|
|
|
139
144
|
|
|
140
145
|
class TimeoutError(AdapterError):
|
|
141
146
|
"""Request timeout error."""
|
|
147
|
+
|
|
142
148
|
pass
|
|
143
149
|
|
|
144
150
|
|
|
145
151
|
class NotFoundError(AdapterError):
|
|
146
152
|
"""Resource not found error."""
|
|
153
|
+
|
|
147
154
|
pass
|
|
148
155
|
|
|
149
156
|
|
|
150
157
|
class PermissionError(AdapterError):
|
|
151
158
|
"""Permission denied error."""
|
|
159
|
+
|
|
152
160
|
pass
|