mcp-ticketer 0.1.31__py3-none-any.whl → 0.1.34__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.

@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.1.31"
3
+ __version__ = "0.1.34"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
@@ -18,22 +18,50 @@ from rich.text import Text
18
18
  def safe_import_config():
19
19
  """Safely import configuration with fallback."""
20
20
  try:
21
- from ..core.config import get_config
22
- return get_config
21
+ from ..core.config import get_config as real_get_config
22
+
23
+ # Test if the real config system works
24
+ try:
25
+ config = real_get_config()
26
+ # If we get here, the real config system is working
27
+ return real_get_config
28
+ except Exception:
29
+ # Real config system failed, use fallback
30
+ pass
31
+
23
32
  except ImportError:
24
- # Create a minimal config fallback
25
- class MockConfig:
26
- def get_enabled_adapters(self):
27
- return {}
33
+ pass
34
+
35
+ # Create a minimal config fallback
36
+ class MockConfig:
37
+ def get_enabled_adapters(self):
38
+ # Try to detect adapters from environment even in fallback
39
+ import os
40
+ adapters = {}
41
+
42
+ # Check for environment variables
43
+ if os.getenv("LINEAR_API_KEY"):
44
+ adapters["linear"] = {"type": "linear", "enabled": True}
45
+ if os.getenv("GITHUB_TOKEN"):
46
+ adapters["github"] = {"type": "github", "enabled": True}
47
+ if os.getenv("JIRA_SERVER"):
48
+ adapters["jira"] = {"type": "jira", "enabled": True}
28
49
 
29
- @property
30
- def default_adapter(self):
31
- return "aitrackdown"
50
+ # Always include aitrackdown as fallback
51
+ if not adapters:
52
+ adapters["aitrackdown"] = {"type": "aitrackdown", "enabled": True}
53
+
54
+ return adapters
55
+
56
+ @property
57
+ def default_adapter(self):
58
+ adapters = self.get_enabled_adapters()
59
+ return list(adapters.keys())[0] if adapters else "aitrackdown"
32
60
 
33
- def get_config():
34
- return MockConfig()
61
+ def get_config():
62
+ return MockConfig()
35
63
 
36
- return get_config
64
+ return get_config
37
65
 
38
66
  def safe_import_registry():
39
67
  """Safely import adapter registry with fallback."""
@@ -51,17 +79,32 @@ def safe_import_registry():
51
79
  def safe_import_queue_manager():
52
80
  """Safely import queue manager with fallback."""
53
81
  try:
54
- from ..queue.manager import QueueManager
55
- return QueueManager
82
+ from ..queue.manager import QueueManager as RealQueueManager
83
+
84
+ # Test if the real queue manager works
85
+ try:
86
+ qm = RealQueueManager()
87
+ # Test a basic operation
88
+ qm.get_worker_status()
89
+ return RealQueueManager
90
+ except Exception:
91
+ # Real queue manager failed, use fallback
92
+ pass
93
+
56
94
  except ImportError:
57
- class MockQueueManager:
58
- def get_worker_status(self):
59
- return {"running": False, "pid": None}
95
+ pass
96
+
97
+ class MockQueueManager:
98
+ def get_worker_status(self):
99
+ return {"running": False, "pid": None, "status": "fallback_mode"}
60
100
 
61
- def get_queue_stats(self):
62
- return {"total": 0, "failed": 0}
101
+ def get_queue_stats(self):
102
+ return {"total": 0, "failed": 0, "pending": 0, "completed": 0}
63
103
 
64
- return MockQueueManager
104
+ def health_check(self):
105
+ return {"status": "degraded", "score": 50, "details": "Running in fallback mode"}
106
+
107
+ return MockQueueManager
65
108
 
66
109
  # Initialize with safe imports
67
110
  get_config = safe_import_config()
@@ -209,7 +252,9 @@ class SystemDiagnostics:
209
252
  for name, adapter_config in adapters.items():
210
253
  try:
211
254
  adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
212
- adapter = adapter_class(adapter_config.dict())
255
+ # Convert Pydantic model to dict, excluding None values
256
+ config_dict = adapter_config.model_dump(exclude_none=False)
257
+ adapter = adapter_class(config_dict)
213
258
 
214
259
  # Test adapter validation if available
215
260
  if hasattr(adapter, 'validate_credentials'):
@@ -256,16 +301,40 @@ class SystemDiagnostics:
256
301
  adapter_status["total_adapters"] = len(adapters)
257
302
 
258
303
  for name, adapter_config in adapters.items():
304
+ # Handle both dict and object adapter configs
305
+ if isinstance(adapter_config, dict):
306
+ adapter_type = adapter_config.get("type", "unknown")
307
+ config_dict = adapter_config
308
+ else:
309
+ adapter_type = adapter_config.type.value if hasattr(adapter_config, 'type') else "unknown"
310
+ # Use model_dump for Pydantic v2 compatibility
311
+ if hasattr(adapter_config, 'model_dump'):
312
+ config_dict = adapter_config.model_dump(exclude_none=False)
313
+ elif hasattr(adapter_config, 'dict'):
314
+ config_dict = adapter_config.dict()
315
+ else:
316
+ config_dict = adapter_config
317
+
259
318
  details = {
260
- "type": adapter_config.type.value,
319
+ "type": adapter_type,
261
320
  "status": "unknown",
262
321
  "last_test": None,
263
322
  "error": None,
264
323
  }
265
324
 
266
325
  try:
267
- adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
268
- adapter = adapter_class(adapter_config.dict())
326
+ # Import AdapterRegistry safely
327
+ try:
328
+ from ..core.registry import AdapterRegistry
329
+ except ImportError:
330
+ details["status"] = "failed"
331
+ details["error"] = "AdapterRegistry not available"
332
+ adapter_status["failed_adapters"] += 1
333
+ adapter_status["adapter_details"][name] = details
334
+ continue
335
+
336
+ adapter_class = AdapterRegistry.get_adapter(adapter_type)
337
+ adapter = adapter_class(config_dict)
269
338
 
270
339
  # Test basic adapter functionality
271
340
  test_start = datetime.now()
@@ -303,9 +372,9 @@ class SystemDiagnostics:
303
372
  return adapter_status
304
373
 
305
374
  async def _diagnose_queue_system(self) -> Dict[str, Any]:
306
- """Diagnose queue system health."""
375
+ """Diagnose queue system health with active testing."""
307
376
  console.print("\n⚡ [yellow]Queue System Diagnosis[/yellow]")
308
-
377
+
309
378
  queue_status = {
310
379
  "worker_running": False,
311
380
  "worker_pid": None,
@@ -313,19 +382,24 @@ class SystemDiagnostics:
313
382
  "recent_failures": [],
314
383
  "failure_rate": 0.0,
315
384
  "health_score": 0,
385
+ "worker_start_test": {"attempted": False, "success": False, "error": None},
386
+ "queue_operation_test": {"attempted": False, "success": False, "error": None},
316
387
  }
317
388
 
318
389
  try:
319
390
  if not self.queue_available:
320
- warning = "Queue system in fallback mode - limited functionality"
391
+ warning = "Queue system in fallback mode - testing basic functionality"
321
392
  self.warnings.append(warning)
322
393
  console.print(f"⚠️ {warning}")
323
- queue_status["worker_running"] = False
324
- queue_status["worker_pid"] = None
325
- queue_status["health_score"] = 50 # Degraded but not critical
394
+
395
+ # Even in fallback mode, test if we can create a basic queue operation
396
+ test_result = await self._test_basic_queue_functionality()
397
+ queue_status["queue_operation_test"] = test_result
398
+ queue_status["health_score"] = 50 if test_result["success"] else 25
326
399
  return queue_status
327
400
 
328
- # Check worker status
401
+ # Test 1: Check current worker status
402
+ console.print("🔍 Checking current worker status...")
329
403
  worker_status = self.queue_manager.get_worker_status()
330
404
  queue_status["worker_running"] = worker_status.get("running", False)
331
405
  queue_status["worker_pid"] = worker_status.get("pid")
@@ -334,21 +408,32 @@ class SystemDiagnostics:
334
408
  console.print(f"✅ Queue worker running (PID: {queue_status['worker_pid']})")
335
409
  self.successes.append("Queue worker is running")
336
410
  else:
337
- issue = "Queue worker not running"
338
- self.issues.append(issue)
339
- console.print(f"❌ {issue}")
411
+ console.print("⚠️ Queue worker not running - attempting to start...")
340
412
 
341
- # Get queue statistics
413
+ # Test 2: Try to start worker
414
+ start_test = await self._test_worker_startup()
415
+ queue_status["worker_start_test"] = start_test
416
+
417
+ if start_test["success"]:
418
+ console.print("✅ Successfully started queue worker")
419
+ queue_status["worker_running"] = True
420
+ self.successes.append("Queue worker started successfully")
421
+ else:
422
+ console.print(f"❌ Failed to start queue worker: {start_test['error']}")
423
+ self.issues.append(f"Queue worker startup failed: {start_test['error']}")
424
+
425
+ # Test 3: Get queue statistics
426
+ console.print("🔍 Analyzing queue statistics...")
342
427
  stats = self.queue_manager.get_queue_stats()
343
428
  queue_status["queue_stats"] = stats
344
-
429
+
345
430
  total_items = stats.get("total", 0)
346
431
  failed_items = stats.get("failed", 0)
347
-
432
+
348
433
  if total_items > 0:
349
434
  failure_rate = (failed_items / total_items) * 100
350
435
  queue_status["failure_rate"] = failure_rate
351
-
436
+
352
437
  if failure_rate > 50:
353
438
  issue = f"High failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
354
439
  self.issues.append(issue)
@@ -360,14 +445,30 @@ class SystemDiagnostics:
360
445
  else:
361
446
  console.print(f"✅ Queue failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})")
362
447
 
363
- # Calculate health score
448
+ # Test 4: Test actual queue operations
449
+ console.print("🔍 Testing queue operations...")
450
+ operation_test = await self._test_queue_operations()
451
+ queue_status["queue_operation_test"] = operation_test
452
+
453
+ if operation_test["success"]:
454
+ console.print("✅ Queue operations test passed")
455
+ self.successes.append("Queue operations working correctly")
456
+ else:
457
+ console.print(f"❌ Queue operations test failed: {operation_test['error']}")
458
+ self.issues.append(f"Queue operations failed: {operation_test['error']}")
459
+
460
+ # Calculate health score based on actual tests
364
461
  health_score = 100
365
462
  if not queue_status["worker_running"]:
366
- health_score -= 50
367
- health_score -= min(queue_status["failure_rate"], 50)
463
+ health_score -= 30
464
+ if not queue_status["worker_start_test"]["success"] and queue_status["worker_start_test"]["attempted"]:
465
+ health_score -= 20
466
+ if not queue_status["queue_operation_test"]["success"]:
467
+ health_score -= 30
468
+ health_score -= min(queue_status["failure_rate"], 20)
368
469
  queue_status["health_score"] = max(0, health_score)
369
470
 
370
- console.print(f"📊 Queue health score: {queue_status['health_score']}/100")
471
+ console.print(f"📊 Queue health score: {queue_status['health_score']}/100 (based on active testing)")
371
472
 
372
473
  except Exception as e:
373
474
  issue = f"Queue system diagnosis failed: {str(e)}"
@@ -376,6 +477,116 @@ class SystemDiagnostics:
376
477
 
377
478
  return queue_status
378
479
 
480
+ async def _test_worker_startup(self) -> Dict[str, Any]:
481
+ """Test starting a queue worker."""
482
+ test_result = {
483
+ "attempted": True,
484
+ "success": False,
485
+ "error": None,
486
+ "details": None
487
+ }
488
+
489
+ try:
490
+ # Try to start worker using the queue manager
491
+ if hasattr(self.queue_manager, 'start_worker'):
492
+ result = await self.queue_manager.start_worker()
493
+ test_result["success"] = True
494
+ test_result["details"] = "Worker started successfully"
495
+ else:
496
+ # Try alternative method - use CLI command
497
+ import subprocess
498
+ result = subprocess.run(
499
+ ["mcp-ticketer", "queue", "worker", "start"],
500
+ capture_output=True,
501
+ text=True,
502
+ timeout=10
503
+ )
504
+ if result.returncode == 0:
505
+ test_result["success"] = True
506
+ test_result["details"] = "Worker started via CLI"
507
+ else:
508
+ test_result["error"] = f"CLI start failed: {result.stderr}"
509
+
510
+ except subprocess.TimeoutExpired:
511
+ test_result["error"] = "Worker startup timed out"
512
+ except Exception as e:
513
+ test_result["error"] = str(e)
514
+
515
+ return test_result
516
+
517
+ async def _test_queue_operations(self) -> Dict[str, Any]:
518
+ """Test basic queue operations."""
519
+ test_result = {
520
+ "attempted": True,
521
+ "success": False,
522
+ "error": None,
523
+ "details": None
524
+ }
525
+
526
+ try:
527
+ # Test creating a simple queue item (diagnostic test)
528
+ from ..core.models import Task, Priority
529
+
530
+ test_task = Task(
531
+ title="[DIAGNOSTIC TEST] Queue functionality test",
532
+ description="This is a diagnostic test - safe to ignore",
533
+ priority=Priority.LOW
534
+ )
535
+
536
+ # Try to queue the test task
537
+ if hasattr(self.queue_manager, 'queue_task'):
538
+ queue_id = await self.queue_manager.queue_task("create", test_task, "aitrackdown")
539
+ test_result["success"] = True
540
+ test_result["details"] = f"Test task queued successfully: {queue_id}"
541
+ else:
542
+ test_result["error"] = "Queue manager doesn't support task queuing"
543
+
544
+ except Exception as e:
545
+ test_result["error"] = str(e)
546
+
547
+ return test_result
548
+
549
+ async def _test_basic_queue_functionality(self) -> Dict[str, Any]:
550
+ """Test basic queue functionality in fallback mode."""
551
+ test_result = {
552
+ "attempted": True,
553
+ "success": False,
554
+ "error": None,
555
+ "details": None
556
+ }
557
+
558
+ try:
559
+ # Test if we can at least create a task directly (bypass queue)
560
+ from ..core.models import Task, Priority
561
+ from ..adapters.aitrackdown import AITrackdownAdapter
562
+
563
+ test_task = Task(
564
+ title="[DIAGNOSTIC TEST] Direct adapter test",
565
+ description="Testing direct adapter functionality",
566
+ priority=Priority.LOW
567
+ )
568
+
569
+ # Try direct adapter creation
570
+ adapter_config = {
571
+ "type": "aitrackdown",
572
+ "enabled": True,
573
+ "base_path": "/tmp/mcp-ticketer-diagnostic-test"
574
+ }
575
+
576
+ adapter = AITrackdownAdapter(adapter_config)
577
+ result = await adapter.create(test_task)
578
+
579
+ test_result["success"] = True
580
+ test_result["details"] = f"Direct adapter test passed: {result.id}"
581
+
582
+ # Clean up test
583
+ await adapter.delete(result.id)
584
+
585
+ except Exception as e:
586
+ test_result["error"] = str(e)
587
+
588
+ return test_result
589
+
379
590
  async def _analyze_recent_logs(self) -> Dict[str, Any]:
380
591
  """Analyze recent log entries for issues."""
381
592
  console.print("\n📝 [yellow]Recent Log Analysis[/yellow]")
mcp_ticketer/cli/main.py CHANGED
@@ -144,6 +144,83 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
144
144
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
145
145
 
146
146
 
147
+ def _discover_from_env_files() -> Optional[str]:
148
+ """Discover adapter configuration from .env or .env.local files.
149
+
150
+ Returns:
151
+ Adapter name if discovered, None otherwise
152
+ """
153
+ import os
154
+ import logging
155
+ from pathlib import Path
156
+
157
+ logger = logging.getLogger(__name__)
158
+
159
+ # Check .env.local first, then .env
160
+ env_files = [".env.local", ".env"]
161
+
162
+ for env_file in env_files:
163
+ env_path = Path.cwd() / env_file
164
+ if env_path.exists():
165
+ try:
166
+ # Simple .env parsing (key=value format)
167
+ env_vars = {}
168
+ with open(env_path, 'r') as f:
169
+ for line in f:
170
+ line = line.strip()
171
+ if line and not line.startswith('#') and '=' in line:
172
+ key, value = line.split('=', 1)
173
+ env_vars[key.strip()] = value.strip().strip('"\'')
174
+
175
+ # Check for adapter-specific variables
176
+ if env_vars.get("LINEAR_API_KEY"):
177
+ logger.info(f"Discovered Linear configuration in {env_file}")
178
+ return "linear"
179
+ elif env_vars.get("GITHUB_TOKEN"):
180
+ logger.info(f"Discovered GitHub configuration in {env_file}")
181
+ return "github"
182
+ elif env_vars.get("JIRA_SERVER"):
183
+ logger.info(f"Discovered JIRA configuration in {env_file}")
184
+ return "jira"
185
+
186
+ except Exception as e:
187
+ logger.warning(f"Could not read {env_file}: {e}")
188
+
189
+ return None
190
+
191
+
192
+ def _save_adapter_to_config(adapter_name: str) -> None:
193
+ """Save adapter configuration to config file.
194
+
195
+ Args:
196
+ adapter_name: Name of the adapter to save as default
197
+ """
198
+ import logging
199
+
200
+ logger = logging.getLogger(__name__)
201
+
202
+ try:
203
+ config = load_config()
204
+ config["default_adapter"] = adapter_name
205
+
206
+ # Ensure adapters section exists
207
+ if "adapters" not in config:
208
+ config["adapters"] = {}
209
+
210
+ # Add basic adapter config if not exists
211
+ if adapter_name not in config["adapters"]:
212
+ if adapter_name == "aitrackdown":
213
+ config["adapters"][adapter_name] = {"base_path": ".aitrackdown"}
214
+ else:
215
+ config["adapters"][adapter_name] = {"type": adapter_name}
216
+
217
+ save_config(config)
218
+ logger.info(f"Saved {adapter_name} as default adapter")
219
+
220
+ except Exception as e:
221
+ logger.warning(f"Could not save adapter configuration: {e}")
222
+
223
+
147
224
  def save_config(config: dict) -> None:
148
225
  """Save configuration to project-local config file ONLY.
149
226
 
@@ -926,11 +1003,25 @@ def create(
926
1003
  console.print(f"[yellow] • {alert['message']}[/yellow]")
927
1004
  console.print("[yellow]Proceeding with ticket creation...[/yellow]")
928
1005
 
929
- # Get the adapter name
930
- config = load_config()
931
- adapter_name = (
932
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
933
- )
1006
+ # Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
1007
+ if adapter:
1008
+ # Priority 1: Command-line argument - save to config for future use
1009
+ adapter_name = adapter.value
1010
+ _save_adapter_to_config(adapter_name)
1011
+ else:
1012
+ # Priority 2: Check existing config
1013
+ config = load_config()
1014
+ adapter_name = config.get("default_adapter")
1015
+
1016
+ if not adapter_name or adapter_name == "aitrackdown":
1017
+ # Priority 3: Check .env files and save if found
1018
+ env_adapter = _discover_from_env_files()
1019
+ if env_adapter:
1020
+ adapter_name = env_adapter
1021
+ _save_adapter_to_config(adapter_name)
1022
+ else:
1023
+ # Priority 4: Default
1024
+ adapter_name = "aitrackdown"
934
1025
 
935
1026
  # Create task data
936
1027
  task_data = {
mcp_ticketer/cli/utils.py CHANGED
@@ -31,7 +31,7 @@ class CommonPatterns:
31
31
 
32
32
  @staticmethod
33
33
  def load_config() -> dict:
34
- """Load configuration from project-local config file ONLY.
34
+ """Load configuration from project-local config file with environment discovery fallback.
35
35
 
36
36
  SECURITY: This method ONLY reads from the current project directory
37
37
  to prevent configuration leakage across projects. It will NEVER read
@@ -39,7 +39,8 @@ class CommonPatterns:
39
39
 
40
40
  Resolution order:
41
41
  1. Project-specific config (.mcp-ticketer/config.json in cwd)
42
- 2. Default to aitrackdown adapter
42
+ 2. Environment discovery (environment variables and .env files in cwd)
43
+ 3. Default to aitrackdown adapter
43
44
 
44
45
  Returns:
45
46
  Configuration dictionary with adapter and config keys.
@@ -77,6 +78,39 @@ class CommonPatterns:
77
78
  f"[yellow]Warning: Could not load project config: {e}[/yellow]"
78
79
  )
79
80
 
81
+ # Try environment discovery as fallback
82
+ try:
83
+ from ..core.config import ConfigurationManager
84
+ config_manager = ConfigurationManager()
85
+ app_config = config_manager.load_config()
86
+
87
+ # Convert AppConfig to legacy dict format for CLI compatibility
88
+ enabled_adapters = app_config.get_enabled_adapters()
89
+ if enabled_adapters:
90
+ # Use the first enabled adapter as default
91
+ default_adapter = app_config.default_adapter or list(enabled_adapters.keys())[0]
92
+
93
+ # Convert to legacy format
94
+ legacy_config = {
95
+ "default_adapter": default_adapter,
96
+ "adapters": {}
97
+ }
98
+
99
+ # Convert adapter configs to dict format
100
+ for name, adapter_config in enabled_adapters.items():
101
+ if hasattr(adapter_config, 'model_dump'):
102
+ legacy_config["adapters"][name] = adapter_config.model_dump(exclude_none=False)
103
+ elif hasattr(adapter_config, 'dict'):
104
+ legacy_config["adapters"][name] = adapter_config.dict()
105
+ else:
106
+ legacy_config["adapters"][name] = adapter_config
107
+
108
+ logger.info(f"Loaded configuration from environment discovery: {list(enabled_adapters.keys())}")
109
+ return legacy_config
110
+
111
+ except Exception as e:
112
+ logger.warning(f"Environment discovery failed: {e}")
113
+
80
114
  # Default to aitrackdown with local base path
81
115
  logger.info("No project-local config found, defaulting to aitrackdown adapter")
82
116
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
@@ -118,9 +118,17 @@ class LinearConfig(BaseAdapterConfig):
118
118
  type: AdapterType = AdapterType.LINEAR
119
119
  api_key: Optional[str] = Field(None, env="LINEAR_API_KEY")
120
120
  workspace: Optional[str] = None
121
- team_key: str
121
+ team_key: Optional[str] = None # Short team key like "BTA"
122
+ team_id: Optional[str] = None # UUID team identifier
122
123
  api_url: str = "https://api.linear.app/graphql"
123
124
 
125
+ @model_validator(mode="after")
126
+ def validate_team_identifier(self):
127
+ """Ensure either team_key or team_id is provided."""
128
+ if not self.team_key and not self.team_id:
129
+ raise ValueError("Either team_key or team_id is required")
130
+ return self
131
+
124
132
  @field_validator("api_key", mode="before")
125
133
  @classmethod
126
134
  def validate_api_key(cls, v):
@@ -284,9 +292,27 @@ class ConfigurationManager:
284
292
  config_data = self._load_config_file(self._config_file_paths[0])
285
293
  logger.info(f"Loaded configuration from: {self._config_file_paths[0]}")
286
294
  else:
287
- # No config file found - try environment discovery
288
- logger.info("No configuration file found, attempting environment discovery")
289
- config_data = self._discover_from_environment()
295
+ # No config file found - use empty config
296
+ config_data = {"adapters": {}, "default_adapter": None}
297
+
298
+ # Always try environment discovery and merge with file-based config
299
+ logger.info("Attempting environment discovery to supplement configuration")
300
+ env_config_data = self._discover_from_environment()
301
+
302
+ # Merge environment-discovered adapters with file-based config
303
+ if env_config_data and "adapters" in env_config_data:
304
+ if "adapters" not in config_data:
305
+ config_data["adapters"] = {}
306
+
307
+ # Add discovered adapters that aren't already configured
308
+ for adapter_name, adapter_config in env_config_data["adapters"].items():
309
+ if adapter_name not in config_data["adapters"]:
310
+ config_data["adapters"][adapter_name] = adapter_config
311
+ logger.info(f"Added environment-discovered adapter: {adapter_name}")
312
+
313
+ # Set default adapter if not already set
314
+ if not config_data.get("default_adapter") and env_config_data.get("default_adapter"):
315
+ config_data["default_adapter"] = env_config_data["default_adapter"]
290
316
 
291
317
  # Parse adapter configurations
292
318
  if "adapters" in config_data:
@@ -294,13 +320,21 @@ class ConfigurationManager:
294
320
  for name, adapter_config in config_data["adapters"].items():
295
321
  adapter_type = adapter_config.get("type", "").lower()
296
322
 
323
+ # If no type specified, try to infer from adapter name
324
+ if not adapter_type:
325
+ adapter_type = name.lower()
326
+
297
327
  if adapter_type == "github":
328
+ adapter_config["type"] = "github"
298
329
  parsed_adapters[name] = GitHubConfig(**adapter_config)
299
330
  elif adapter_type == "jira":
331
+ adapter_config["type"] = "jira"
300
332
  parsed_adapters[name] = JiraConfig(**adapter_config)
301
333
  elif adapter_type == "linear":
334
+ adapter_config["type"] = "linear"
302
335
  parsed_adapters[name] = LinearConfig(**adapter_config)
303
336
  elif adapter_type == "aitrackdown":
337
+ adapter_config["type"] = "aitrackdown"
304
338
  parsed_adapters[name] = AITrackdownConfig(**adapter_config)
305
339
  else:
306
340
  logger.warning(
@@ -335,10 +369,10 @@ class ConfigurationManager:
335
369
  def _discover_from_environment(self) -> dict[str, Any]:
336
370
  """Discover configuration from environment variables."""
337
371
  try:
338
- from .env_discovery import EnvironmentDiscovery
372
+ from .env_discovery import EnvDiscovery
339
373
 
340
- discovery = EnvironmentDiscovery()
341
- discovered = discovery.discover_all()
374
+ discovery = EnvDiscovery()
375
+ discovered = discovery.discover()
342
376
 
343
377
  if not discovered.adapters:
344
378
  logger.info("No adapters discovered from environment variables")
@@ -362,36 +396,24 @@ class ConfigurationManager:
362
396
 
363
397
  for adapter in discovered.adapters:
364
398
  adapter_config = {
365
- "type": adapter.type.value,
399
+ "type": adapter.adapter_type,
366
400
  "enabled": True
367
401
  }
368
402
 
369
- # Add adapter-specific configuration
370
- if adapter.type.value == "linear":
371
- adapter_config.update({
372
- "api_key": adapter.credentials.get("api_key"),
373
- "team_id": adapter.credentials.get("team_id"),
374
- "project_id": adapter.credentials.get("project_id")
375
- })
376
- elif adapter.type.value == "github":
377
- adapter_config.update({
378
- "token": adapter.credentials.get("token"),
379
- "repo": adapter.credentials.get("repo"),
380
- "owner": adapter.credentials.get("owner")
381
- })
382
- elif adapter.type.value == "jira":
383
- adapter_config.update({
384
- "server": adapter.credentials.get("server"),
385
- "email": adapter.credentials.get("email"),
386
- "api_token": adapter.credentials.get("api_token"),
387
- "project_key": adapter.credentials.get("project_key")
388
- })
389
-
390
- config_data["adapters"][adapter.name] = adapter_config
403
+ # Add adapter-specific configuration from discovered config
404
+ adapter_config.update(adapter.config)
405
+
406
+ # Ensure type is set correctly (remove 'adapter' key if present)
407
+ if 'adapter' in adapter_config:
408
+ del adapter_config['adapter']
409
+
410
+ # Use adapter type as the key name
411
+ adapter_name = adapter.adapter_type
412
+ config_data["adapters"][adapter_name] = adapter_config
391
413
 
392
414
  # Set first discovered adapter as default
393
415
  if config_data["default_adapter"] is None:
394
- config_data["default_adapter"] = adapter.name
416
+ config_data["default_adapter"] = adapter.adapter_type
395
417
 
396
418
  logger.info(f"Discovered {len(config_data['adapters'])} adapter(s) from environment")
397
419
  return config_data
@@ -30,8 +30,10 @@ LINEAR_KEY_PATTERNS = [
30
30
 
31
31
  LINEAR_TEAM_PATTERNS = [
32
32
  "LINEAR_TEAM_ID",
33
+ "LINEAR_TEAM_KEY", # Added support for team key (e.g., "BTA")
33
34
  "LINEAR_TEAM",
34
35
  "MCP_TICKETER_LINEAR_TEAM_ID",
36
+ "MCP_TICKETER_LINEAR_TEAM_KEY",
35
37
  ]
36
38
 
37
39
  LINEAR_PROJECT_PATTERNS = [
@@ -210,7 +212,11 @@ class EnvDiscovery:
210
212
  return result
211
213
 
212
214
  def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
213
- """Load environment variables from files.
215
+ """Load environment variables from files and actual environment.
216
+
217
+ Priority order (highest to lowest):
218
+ 1. .env files (highest priority)
219
+ 2. Environment variables (lowest priority)
214
220
 
215
221
  Args:
216
222
  result: DiscoveryResult to update with found files
@@ -221,7 +227,15 @@ class EnvDiscovery:
221
227
  """
222
228
  merged_env: dict[str, str] = {}
223
229
 
224
- # Load files in reverse order (lowest priority first)
230
+ # First, load from actual environment variables (lowest priority)
231
+ import os
232
+ actual_env = {k: v for k, v in os.environ.items() if v}
233
+ merged_env.update(actual_env)
234
+ if actual_env:
235
+ result.env_files_found.append("environment")
236
+ logger.debug(f"Loaded {len(actual_env)} variables from environment")
237
+
238
+ # Load files in reverse order (higher priority than environment)
225
239
  for env_file in reversed(self.ENV_FILE_ORDER):
226
240
  file_path = self.project_path / env_file
227
241
  if file_path.exists():
@@ -282,13 +296,19 @@ class EnvDiscovery:
282
296
  missing_fields: list[str] = []
283
297
  confidence = 0.6 # Has API key
284
298
 
285
- # Extract team ID (recommended but not required)
286
- team_id = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
287
- if team_id:
288
- config["team_id"] = team_id
299
+ # Extract team identifier (either team_id or team_key is required)
300
+ team_identifier = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
301
+ if team_identifier:
302
+ # Determine if it's a team_id (UUID format) or team_key (short string)
303
+ if len(team_identifier) > 20 and '-' in team_identifier:
304
+ # Looks like a UUID (team_id)
305
+ config["team_id"] = team_identifier
306
+ else:
307
+ # Looks like a short key (team_key)
308
+ config["team_key"] = team_identifier
289
309
  confidence += 0.3
290
310
  else:
291
- missing_fields.append("team_id (recommended)")
311
+ missing_fields.append("team_id or team_key (required)")
292
312
 
293
313
  # Extract project ID (optional)
294
314
  project_id = self._find_key_value(env_vars, LINEAR_PROJECT_PATTERNS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-ticketer
3
- Version: 0.1.31
3
+ Version: 0.1.34
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,5 +1,5 @@
1
1
  mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
2
- mcp_ticketer/__version__.py,sha256=FF4HV-lsroMPtYJb5y6GRAhnAUrJmzbKonXKGSwOiL4,1118
2
+ mcp_ticketer/__version__.py,sha256=NKKpxMKtYBzkS7DVcj0AKzeO_ZpeIyXvB1W2qzW1TS4,1118
3
3
  mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
5
5
  mcp_ticketer/adapters/aitrackdown.py,sha256=stlbge8K6w-EyQkw_vEQNSXQgCOWN5tOlQUgGWZQNMQ,17936
@@ -13,19 +13,19 @@ mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,
13
13
  mcp_ticketer/cli/auggie_configure.py,sha256=MXKzLtqe3K_UTQ2GacHAWbvf_B0779KL325smiAKE0Q,8212
14
14
  mcp_ticketer/cli/codex_configure.py,sha256=xDppHouT6_-cYXswyAggoPX5bSlRXMvCoM_x9PQ-42A,9086
15
15
  mcp_ticketer/cli/configure.py,sha256=BsA_pSHQMQS0t1bJO_wMM8LWsd5sWJDASjEPRHvwC18,16198
16
- mcp_ticketer/cli/diagnostics.py,sha256=edTmM0OEDNiMYTvQ56HK304EUNUBtYejMaKmpJTpomY,22821
16
+ mcp_ticketer/cli/diagnostics.py,sha256=V4b6qD_eHG1K6QLBi3-y4JIqzqnJdSWaH6k96NPZiB8,31540
17
17
  mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
18
18
  mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
19
- mcp_ticketer/cli/main.py,sha256=TxhCdmEi97rrDkCd0nT4KYRtZSboTfPSrw7SL607goA,54970
19
+ mcp_ticketer/cli/main.py,sha256=pzwjcXCbUFQCvTM5sN-dKZcnMruSImuYPcC7__DEYZg,58182
20
20
  mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
21
21
  mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
22
22
  mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
23
23
  mcp_ticketer/cli/simple_health.py,sha256=FIMHbrSNHpNJHXx7wtH8HzQXmPlcF9HQE9ngxTbxhMM,8035
24
- mcp_ticketer/cli/utils.py,sha256=2ptUrp2ELZsox0kSxAI5DFrHonOU999qh4MxbLv6VBQ,21155
24
+ mcp_ticketer/cli/utils.py,sha256=RRoUMJbw0isyzlRhZwPcbmYQXl1PGYDx9dy1zRctljA,22789
25
25
  mcp_ticketer/core/__init__.py,sha256=eXovsaJymQRP2AwOBuOy6mFtI3I68D7gGenZ5V-IMqo,349
26
26
  mcp_ticketer/core/adapter.py,sha256=q64LxOInIno7EIbmuxItf8KEsd-g9grCs__Z4uwZHto,10273
27
- mcp_ticketer/core/config.py,sha256=A3K2JXn65qwpI80_J05oA-6DRjcONi6ZqemU02O4L8U,19120
28
- mcp_ticketer/core/env_discovery.py,sha256=wKp2Pi5vQMGOTrM1690IBv_eoABly-pD8ah7n1zSWDc,17710
27
+ mcp_ticketer/core/config.py,sha256=P6Y_i6xmcLb9xtD3VHkXfujIU6VlQlUoteDdS2d0C-w,20144
28
+ mcp_ticketer/core/env_discovery.py,sha256=It62C97UBt96CgVbZCql5NQtONmCHMkX3c8W2kEl-Qk,18716
29
29
  mcp_ticketer/core/http_client.py,sha256=s5ikMiwEJ8TJjNn73wu3gv3OdAtyBEpAqPnSroRMW2k,13971
30
30
  mcp_ticketer/core/mappers.py,sha256=1aG1jFsHTCwmGRVgOlXW-VOSTGzc86gv7qjDfiR1ups,17462
31
31
  mcp_ticketer/core/models.py,sha256=DRuJoYbjp9fcPV9GwQfhVcNUB0XmwQB3vuqW8hQWZ_k,6491
@@ -41,9 +41,9 @@ mcp_ticketer/queue/queue.py,sha256=jSAkYNEIbNH1cbYuF8s6eFuZmXqn8WHXx3mbfMU2Ud8,1
41
41
  mcp_ticketer/queue/run_worker.py,sha256=_IBezjvhbJJ7gn0evTBIMbSPjvfFZwxEdT-1DLo_bRk,799
42
42
  mcp_ticketer/queue/ticket_registry.py,sha256=k8FYg2cFYsI4POb94-o-fTrIVr-ttfi60r0O5YhJYck,15321
43
43
  mcp_ticketer/queue/worker.py,sha256=TLXXXTAQT1k9Oiw2WjSd8bzT3rr8TQ8NLt9JBovGQEA,18679
44
- mcp_ticketer-0.1.31.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
45
- mcp_ticketer-0.1.31.dist-info/METADATA,sha256=JR2x-cFldb4sWX5lKGGD3sLTWFriAV2LXjboPR9tKUY,13191
46
- mcp_ticketer-0.1.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- mcp_ticketer-0.1.31.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
48
- mcp_ticketer-0.1.31.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
49
- mcp_ticketer-0.1.31.dist-info/RECORD,,
44
+ mcp_ticketer-0.1.34.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
45
+ mcp_ticketer-0.1.34.dist-info/METADATA,sha256=kKvvgMiqCmqPceTqnhV-InFn1jkNcvZfM_-yoKHyzR4,13191
46
+ mcp_ticketer-0.1.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ mcp_ticketer-0.1.34.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
48
+ mcp_ticketer-0.1.34.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
49
+ mcp_ticketer-0.1.34.dist-info/RECORD,,