mcp-ticketer 0.2.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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -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
- from pathlib import Path
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: List[str]
25
+ aliases: list[str]
26
26
  description: str
27
27
  required: bool = False
28
- default: Optional[str] = None
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
- Unified environment loader that handles multiple naming conventions
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=["JIRA_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_AUTH_TOKEN", "JIRA_PASSWORD"],
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: Optional[Path] = None):
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: Dict[str, str] = {}
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, 'r') as f:
148
- for line_num, line in enumerate(f, 1):
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 '=' in line:
157
- key, value = line.split('=', 1)
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(self, config_key: str, config: Optional[Dict[str, Any]] = None) -> Optional[str]:
177
- """
178
- Get a configuration value using the key alias system.
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 = config_key.split('_', 1)[1] if '_' in config_key else config_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(f"Required configuration key {config_key} not found. Tried: {all_keys}")
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(self, adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
224
- """
225
- Get complete configuration for an adapter with environment variable resolution.
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 = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
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('_', 1)[1]
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(self, adapter_name: str, config: Dict[str, Any]) -> List[str]:
252
- """
253
- Validate that all required configuration is present for an adapter.
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 = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
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('_', 1)[1]
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) -> Dict[str, Any]:
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: Optional[UnifiedEnvLoader] = None
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(adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
301
- """
302
- Convenience function to load adapter configuration with environment variables.
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: Dict[str, Any]) -> List[str]:
315
- """
316
- Convenience function to validate adapter configuration.
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)
@@ -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, Optional
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: Optional[Exception] = None
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
- """String representation of the error."""
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: Optional[int] = None,
56
- original_error: Optional[Exception] = None
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
- """String representation of the error."""
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
- """String representation of the error."""
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