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

Files changed (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,6 +5,7 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import typer
10
11
  from dotenv import load_dotenv
@@ -23,6 +24,7 @@ from ..queue.ticket_registry import TicketRegistry
23
24
  from .configure import configure_wizard, set_adapter_config, show_current_config
24
25
  from .diagnostics import run_diagnostics
25
26
  from .discover import app as discover_app
27
+ from .instruction_commands import app as instruction_app
26
28
  from .migrate_config import migrate_config_command
27
29
  from .platform_commands import app as platform_app
28
30
  from .queue_commands import app as queue_app
@@ -48,11 +50,11 @@ app = typer.Typer(
48
50
  console = Console()
49
51
 
50
52
 
51
- def version_callback(value: bool):
53
+ def version_callback(value: bool) -> None:
52
54
  """Print version and exit."""
53
55
  if value:
54
56
  console.print(f"mcp-ticketer version {__version__}")
55
- raise typer.Exit()
57
+ raise typer.Exit() from None
56
58
 
57
59
 
58
60
  @app.callback()
@@ -65,7 +67,7 @@ def main_callback(
65
67
  is_eager=True,
66
68
  help="Show version and exit",
67
69
  ),
68
- ):
70
+ ) -> None:
69
71
  """MCP Ticketer - Universal ticket management interface."""
70
72
  pass
71
73
 
@@ -270,7 +272,7 @@ def merge_config(updates: dict) -> dict:
270
272
 
271
273
  def get_adapter(
272
274
  override_adapter: str | None = None, override_config: dict | None = None
273
- ):
275
+ ) -> Any:
274
276
  """Get configured adapter instance.
275
277
 
276
278
  Args:
@@ -318,6 +320,416 @@ def get_adapter(
318
320
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
319
321
 
320
322
 
323
+ async def _validate_adapter_credentials(
324
+ adapter_type: str, config_file_path: Path
325
+ ) -> list[str]:
326
+ """Validate adapter credentials by performing real connectivity tests.
327
+
328
+ Args:
329
+ adapter_type: Type of adapter to validate
330
+ config_file_path: Path to config file
331
+
332
+ Returns:
333
+ List of validation issues (empty if valid)
334
+
335
+ """
336
+ import json
337
+
338
+ issues = []
339
+
340
+ try:
341
+ # Load config
342
+ with open(config_file_path) as f:
343
+ config = json.load(f)
344
+
345
+ adapter_config = config.get("adapters", {}).get(adapter_type, {})
346
+
347
+ if not adapter_config:
348
+ issues.append(f"No configuration found for {adapter_type}")
349
+ return issues
350
+
351
+ # Validate based on adapter type
352
+ if adapter_type == "linear":
353
+ api_key = adapter_config.get("api_key")
354
+
355
+ # Check API key format
356
+ if not api_key:
357
+ issues.append("Linear API key is missing")
358
+ return issues
359
+
360
+ if not api_key.startswith("lin_api_"):
361
+ issues.append(
362
+ "Invalid Linear API key format (should start with 'lin_api_')"
363
+ )
364
+ return issues
365
+
366
+ # Test actual connectivity
367
+ try:
368
+ from ..adapters.linear import LinearAdapter
369
+
370
+ adapter = LinearAdapter(adapter_config)
371
+ # Try to list one ticket to verify connectivity
372
+ await adapter.list(limit=1)
373
+ except Exception as e:
374
+ error_msg = str(e)
375
+ if "401" in error_msg or "Unauthorized" in error_msg:
376
+ issues.append(
377
+ "Failed to authenticate with Linear API - invalid API key"
378
+ )
379
+ elif "403" in error_msg or "Forbidden" in error_msg:
380
+ issues.append("Linear API key lacks required permissions")
381
+ elif "team" in error_msg.lower():
382
+ issues.append(f"Linear team configuration error: {error_msg}")
383
+ else:
384
+ issues.append(f"Failed to connect to Linear API: {error_msg}")
385
+
386
+ elif adapter_type == "jira":
387
+ server = adapter_config.get("server")
388
+ email = adapter_config.get("email")
389
+ api_token = adapter_config.get("api_token")
390
+
391
+ # Check required fields
392
+ if not server:
393
+ issues.append("JIRA server URL is missing")
394
+ if not email:
395
+ issues.append("JIRA email is missing")
396
+ if not api_token:
397
+ issues.append("JIRA API token is missing")
398
+
399
+ if issues:
400
+ return issues
401
+
402
+ # Test actual connectivity
403
+ try:
404
+ from ..adapters.jira import JiraAdapter
405
+
406
+ adapter = JiraAdapter(adapter_config)
407
+ await adapter.list(limit=1)
408
+ except Exception as e:
409
+ error_msg = str(e)
410
+ if "401" in error_msg or "Unauthorized" in error_msg:
411
+ issues.append(
412
+ "Failed to authenticate with JIRA - invalid credentials"
413
+ )
414
+ elif "403" in error_msg or "Forbidden" in error_msg:
415
+ issues.append("JIRA credentials lack required permissions")
416
+ else:
417
+ issues.append(f"Failed to connect to JIRA: {error_msg}")
418
+
419
+ elif adapter_type == "github":
420
+ token = adapter_config.get("token") or adapter_config.get("api_key")
421
+ owner = adapter_config.get("owner")
422
+ repo = adapter_config.get("repo")
423
+
424
+ # Check required fields
425
+ if not token:
426
+ issues.append("GitHub token is missing")
427
+ if not owner:
428
+ issues.append("GitHub owner is missing")
429
+ if not repo:
430
+ issues.append("GitHub repo is missing")
431
+
432
+ if issues:
433
+ return issues
434
+
435
+ # Test actual connectivity
436
+ try:
437
+ from ..adapters.github import GitHubAdapter
438
+
439
+ adapter = GitHubAdapter(adapter_config)
440
+ await adapter.list(limit=1)
441
+ except Exception as e:
442
+ error_msg = str(e)
443
+ if (
444
+ "401" in error_msg
445
+ or "Unauthorized" in error_msg
446
+ or "Bad credentials" in error_msg
447
+ ):
448
+ issues.append("Failed to authenticate with GitHub - invalid token")
449
+ elif "404" in error_msg or "Not Found" in error_msg:
450
+ issues.append(f"GitHub repository not found: {owner}/{repo}")
451
+ elif "403" in error_msg or "Forbidden" in error_msg:
452
+ issues.append("GitHub token lacks required permissions")
453
+ else:
454
+ issues.append(f"Failed to connect to GitHub: {error_msg}")
455
+
456
+ elif adapter_type == "aitrackdown":
457
+ # AITrackdown doesn't require credentials, just check base_path is set
458
+ base_path = adapter_config.get("base_path")
459
+ if not base_path:
460
+ issues.append("AITrackdown base_path is missing")
461
+
462
+ except Exception as e:
463
+ issues.append(f"Validation error: {str(e)}")
464
+
465
+ return issues
466
+
467
+
468
+ async def _validate_configuration_with_retry(
469
+ console: Console, adapter_type: str, config_file_path: Path, proj_path: Path
470
+ ) -> bool:
471
+ """Validate configuration with retry loop for corrections.
472
+
473
+ Args:
474
+ console: Rich console for output
475
+ adapter_type: Type of adapter configured
476
+ config_file_path: Path to config file
477
+ proj_path: Project path
478
+
479
+ Returns:
480
+ True if validation passed or user chose to continue, False if user chose to exit
481
+
482
+ """
483
+ max_retries = 3
484
+ retry_count = 0
485
+
486
+ while retry_count < max_retries:
487
+ console.print("\n[cyan]🔍 Validating configuration...[/cyan]")
488
+
489
+ # Run real adapter validation (suppress verbose output)
490
+ import io
491
+ import sys
492
+
493
+ # Capture output to suppress verbose diagnostics output
494
+ old_stdout = sys.stdout
495
+ old_stderr = sys.stderr
496
+ sys.stdout = io.StringIO()
497
+ sys.stderr = io.StringIO()
498
+
499
+ try:
500
+ # Perform real adapter validation using diagnostics
501
+ validation_issues = await _validate_adapter_credentials(
502
+ adapter_type, config_file_path
503
+ )
504
+ finally:
505
+ # Restore stdout/stderr
506
+ sys.stdout = old_stdout
507
+ sys.stderr = old_stderr
508
+
509
+ # Check if there are issues
510
+ if not validation_issues:
511
+ console.print("[green]✓ Configuration validated successfully![/green]")
512
+ return True
513
+
514
+ # Display issues found
515
+ console.print("[yellow]⚠️ Configuration validation found issues:[/yellow]")
516
+ for issue in validation_issues:
517
+ console.print(f" [red]❌[/red] {issue}")
518
+
519
+ # Offer user options
520
+ console.print("\n[bold]What would you like to do?[/bold]")
521
+ console.print("1. [cyan]Re-enter configuration values[/cyan] (fix issues)")
522
+ console.print("2. [yellow]Continue anyway[/yellow] (skip validation)")
523
+ console.print("3. [red]Exit[/red] (fix manually later)")
524
+
525
+ try:
526
+ choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
527
+ except typer.Abort:
528
+ console.print("[yellow]Cancelled.[/yellow]")
529
+ return False
530
+
531
+ if choice == 1:
532
+ # Re-enter configuration
533
+ # Check BEFORE increment to fix off-by-one error
534
+ if retry_count >= max_retries:
535
+ console.print(
536
+ f"[red]Maximum retry attempts ({max_retries}) reached.[/red]"
537
+ )
538
+ console.print(
539
+ "[yellow]Please fix configuration manually and run 'mcp-ticketer doctor'[/yellow]"
540
+ )
541
+ return False
542
+ retry_count += 1
543
+
544
+ console.print(
545
+ f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
546
+ )
547
+
548
+ # Reload current config to get values
549
+ import json
550
+
551
+ with open(config_file_path) as f:
552
+ current_config = json.load(f)
553
+
554
+ # Re-prompt for adapter-specific configuration
555
+ if adapter_type == "linear":
556
+ console.print("\n[bold]Linear Configuration[/bold]")
557
+ console.print(
558
+ "[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
559
+ )
560
+
561
+ linear_api_key = typer.prompt(
562
+ "Enter your Linear API key", hide_input=True
563
+ )
564
+
565
+ console.print("\n[bold]Linear Team Configuration[/bold]")
566
+ console.print("You can provide either:")
567
+ console.print(
568
+ " 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
569
+ )
570
+ console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
571
+ console.print(" 3. Team ID (UUID)")
572
+ console.print(
573
+ "[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
574
+ )
575
+
576
+ team_input = typer.prompt("Team URL, key, or ID")
577
+
578
+ # Check if input is a URL
579
+ linear_team_id = None
580
+ linear_team_key = None
581
+
582
+ if team_input.startswith("https://linear.app/"):
583
+ console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
584
+ from .linear_commands import derive_team_from_url
585
+
586
+ derived_team_id, error = await derive_team_from_url(
587
+ linear_api_key, team_input
588
+ )
589
+
590
+ if derived_team_id:
591
+ linear_team_id = derived_team_id
592
+ console.print(
593
+ "[green]✓[/green] Successfully derived team ID from URL"
594
+ )
595
+ else:
596
+ console.print(f"[red]Error:[/red] {error}")
597
+ console.print("Please provide team key or ID manually instead.")
598
+ team_input = typer.prompt("Team key or ID")
599
+
600
+ if len(team_input) > 20: # Likely a UUID
601
+ linear_team_id = team_input
602
+ else:
603
+ linear_team_key = team_input
604
+ else:
605
+ # Input is team key or ID
606
+ if len(team_input) > 20: # Likely a UUID
607
+ linear_team_id = team_input
608
+ else:
609
+ linear_team_key = team_input
610
+
611
+ # Update config
612
+ linear_config = {
613
+ "api_key": linear_api_key,
614
+ "type": "linear",
615
+ }
616
+ if linear_team_key:
617
+ linear_config["team_key"] = linear_team_key
618
+ if linear_team_id:
619
+ linear_config["team_id"] = linear_team_id
620
+
621
+ current_config["adapters"]["linear"] = linear_config
622
+
623
+ elif adapter_type == "jira":
624
+ console.print("\n[bold]JIRA Configuration[/bold]")
625
+ console.print("Enter your JIRA server details.\n")
626
+
627
+ server = typer.prompt(
628
+ "JIRA server URL (e.g., https://company.atlassian.net)"
629
+ )
630
+ email = typer.prompt("Your JIRA email address")
631
+
632
+ console.print("\nYou need a JIRA API token.")
633
+ console.print(
634
+ "[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
635
+ )
636
+
637
+ token = typer.prompt("Enter your JIRA API token", hide_input=True)
638
+
639
+ project = typer.prompt(
640
+ "Default JIRA project key (optional, press Enter to skip)",
641
+ default="",
642
+ show_default=False,
643
+ )
644
+
645
+ # Update config
646
+ jira_config = {
647
+ "server": server,
648
+ "email": email,
649
+ "api_token": token,
650
+ "type": "jira",
651
+ }
652
+ if project:
653
+ jira_config["project_key"] = project
654
+
655
+ current_config["adapters"]["jira"] = jira_config
656
+
657
+ elif adapter_type == "github":
658
+ console.print("\n[bold]GitHub Configuration[/bold]")
659
+ console.print("Enter your GitHub repository details.\n")
660
+
661
+ owner = typer.prompt(
662
+ "GitHub repository owner (username or organization)"
663
+ )
664
+ repo = typer.prompt("GitHub repository name")
665
+
666
+ console.print("\nYou need a GitHub Personal Access Token.")
667
+ console.print(
668
+ "[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
669
+ )
670
+ console.print(
671
+ "[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
672
+ )
673
+
674
+ token = typer.prompt(
675
+ "Enter your GitHub Personal Access Token", hide_input=True
676
+ )
677
+
678
+ # Update config
679
+ current_config["adapters"]["github"] = {
680
+ "owner": owner,
681
+ "repo": repo,
682
+ "token": token,
683
+ "type": "github",
684
+ }
685
+
686
+ elif adapter_type == "aitrackdown":
687
+ # AITrackdown doesn't need credentials, but save config before returning
688
+ # Save updated configuration
689
+ with open(config_file_path, "w") as f:
690
+ json.dump(current_config, f, indent=2)
691
+
692
+ console.print(
693
+ "[yellow]AITrackdown doesn't require credentials. Continuing...[/yellow]"
694
+ )
695
+ console.print("[dim]✓ Configuration updated[/dim]")
696
+ return True
697
+
698
+ else:
699
+ console.print(f"[red]Unknown adapter type: {adapter_type}[/red]")
700
+ return False
701
+
702
+ # Save updated configuration
703
+ with open(config_file_path, "w") as f:
704
+ json.dump(current_config, f, indent=2)
705
+
706
+ console.print("[dim]✓ Configuration updated[/dim]")
707
+ # Loop will retry validation
708
+
709
+ elif choice == 2:
710
+ # Continue anyway
711
+ console.print(
712
+ "[yellow]⚠️ Continuing with potentially invalid configuration.[/yellow]"
713
+ )
714
+ console.print("[dim]You can validate later with: mcp-ticketer doctor[/dim]")
715
+ return True
716
+
717
+ elif choice == 3:
718
+ # Exit
719
+ console.print(
720
+ "[yellow]Configuration saved but not validated. Run 'mcp-ticketer doctor' to test.[/yellow]"
721
+ )
722
+ return False
723
+
724
+ else:
725
+ console.print(
726
+ f"[red]Invalid choice: {choice}. Please enter 1, 2, or 3.[/red]"
727
+ )
728
+ # Continue loop to ask again
729
+
730
+ return True
731
+
732
+
321
733
  def _prompt_for_adapter_selection(console: Console) -> str:
322
734
  """Interactive prompt for adapter selection.
323
735
 
@@ -381,90 +793,354 @@ def _prompt_for_adapter_selection(console: Console) -> str:
381
793
  )
382
794
  except (ValueError, typer.Abort):
383
795
  console.print("[yellow]Setup cancelled.[/yellow]")
384
- raise typer.Exit(0)
796
+ raise typer.Exit(0) from None
385
797
 
386
798
 
387
799
  @app.command()
388
800
  def setup(
389
- adapter: str | None = typer.Option(
390
- None,
391
- "--adapter",
392
- "-a",
393
- help="Adapter type to use (interactive prompt if not specified)",
394
- ),
395
801
  project_path: str | None = typer.Option(
396
802
  None, "--path", help="Project path (default: current directory)"
397
803
  ),
398
- global_config: bool = typer.Option(
804
+ skip_platforms: bool = typer.Option(
399
805
  False,
400
- "--global",
401
- "-g",
402
- help="Save to global config instead of project-specific",
806
+ "--skip-platforms",
807
+ help="Skip platform installation (only initialize adapter)",
403
808
  ),
404
- base_path: str | None = typer.Option(
405
- None,
406
- "--base-path",
407
- "-p",
408
- help="Base path for ticket storage (AITrackdown only)",
409
- ),
410
- api_key: str | None = typer.Option(
411
- None, "--api-key", help="API key for Linear or API token for JIRA"
412
- ),
413
- team_id: str | None = typer.Option(
414
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
415
- ),
416
- jira_server: str | None = typer.Option(
417
- None,
418
- "--jira-server",
419
- help="JIRA server URL (e.g., https://company.atlassian.net)",
420
- ),
421
- jira_email: str | None = typer.Option(
422
- None, "--jira-email", help="JIRA user email for authentication"
423
- ),
424
- jira_project: str | None = typer.Option(
425
- None, "--jira-project", help="Default JIRA project key"
426
- ),
427
- github_owner: str | None = typer.Option(
428
- None, "--github-owner", help="GitHub repository owner"
429
- ),
430
- github_repo: str | None = typer.Option(
431
- None, "--github-repo", help="GitHub repository name"
432
- ),
433
- github_token: str | None = typer.Option(
434
- None, "--github-token", help="GitHub Personal Access Token"
809
+ force_reinit: bool = typer.Option(
810
+ False,
811
+ "--force-reinit",
812
+ help="Force re-initialization even if config exists",
435
813
  ),
436
814
  ) -> None:
437
- """Interactive setup wizard for MCP Ticketer (alias for init).
815
+ """Smart setup command - combines init + platform installation.
816
+
817
+ This command intelligently detects your current setup state and only
818
+ performs necessary configuration. It's the recommended way to get started.
438
819
 
439
- This command provides a user-friendly setup experience with prompts
440
- to guide you through configuring MCP Ticketer for your preferred
441
- ticket management system. It's identical to 'init' and 'install'.
820
+ Detection & Smart Actions:
821
+ - First run: Full setup (init + platform installation)
822
+ - Existing config: Skip init, offer platform installation
823
+ - Detects changes: Offers to update configurations
824
+ - Respects existing: Won't overwrite without confirmation
442
825
 
443
826
  Examples:
444
- # Run interactive setup
827
+ # Smart setup (recommended for first-time setup)
445
828
  mcp-ticketer setup
446
829
 
447
- # Setup with specific adapter
448
- mcp-ticketer setup --adapter linear
449
-
450
830
  # Setup for different project
451
831
  mcp-ticketer setup --path /path/to/project
452
832
 
833
+ # Re-initialize configuration
834
+ mcp-ticketer setup --force-reinit
835
+
836
+ # Only init adapter, skip platform installation
837
+ mcp-ticketer setup --skip-platforms
838
+
839
+ Note: For advanced configuration, use 'init' and 'install' separately.
840
+
453
841
  """
454
- # Call init with all parameters
455
- init(
456
- adapter=adapter,
457
- project_path=project_path,
458
- global_config=global_config,
459
- base_path=base_path,
460
- api_key=api_key,
461
- team_id=team_id,
462
- jira_server=jira_server,
463
- jira_email=jira_email,
464
- jira_project=jira_project,
465
- github_owner=github_owner,
466
- github_repo=github_repo,
467
- github_token=github_token,
842
+ from .platform_detection import PlatformDetector
843
+
844
+ proj_path = Path(project_path) if project_path else Path.cwd()
845
+ config_path = proj_path / ".mcp-ticketer" / "config.json"
846
+
847
+ console.print("[bold cyan]🚀 MCP Ticketer Smart Setup[/bold cyan]\n")
848
+
849
+ # Step 1: Detect existing configuration
850
+ config_exists = config_path.exists()
851
+ config_valid = False
852
+ current_adapter = None
853
+
854
+ if config_exists and not force_reinit:
855
+ try:
856
+ with open(config_path) as f:
857
+ config = json.load(f)
858
+ current_adapter = config.get("default_adapter")
859
+ config_valid = bool(current_adapter and config.get("adapters"))
860
+ except (json.JSONDecodeError, OSError):
861
+ config_valid = False
862
+
863
+ if config_valid:
864
+ console.print("[green]✓[/green] Configuration detected")
865
+ console.print(f"[dim] Adapter: {current_adapter}[/dim]")
866
+ console.print(f"[dim] Location: {config_path}[/dim]\n")
867
+
868
+ # Offer to reconfigure
869
+ if not typer.confirm(
870
+ "Configuration already exists. Keep existing settings?", default=True
871
+ ):
872
+ console.print("[cyan]Re-initializing configuration...[/cyan]\n")
873
+ force_reinit = True
874
+ config_valid = False
875
+ else:
876
+ if config_exists:
877
+ console.print(
878
+ "[yellow]⚠[/yellow] Configuration file exists but is invalid\n"
879
+ )
880
+ else:
881
+ console.print("[yellow]⚠[/yellow] No configuration found\n")
882
+
883
+ # Step 2: Initialize adapter configuration if needed
884
+ if not config_valid or force_reinit:
885
+ console.print("[bold]Step 1/2: Adapter Configuration[/bold]\n")
886
+
887
+ # Run init command non-interactively through function call
888
+ # We'll use the discover and prompt flow from init
889
+ from ..core.env_discovery import discover_config
890
+
891
+ discovered = discover_config(proj_path)
892
+ adapter_type = None
893
+
894
+ # Try auto-discovery
895
+ if discovered and discovered.adapters:
896
+ primary = discovered.get_primary_adapter()
897
+ if primary:
898
+ adapter_type = primary.adapter_type
899
+ console.print(f"[green]✓ Auto-detected {adapter_type} adapter[/green]")
900
+ console.print(f"[dim] Source: {primary.found_in}[/dim]")
901
+ console.print(f"[dim] Confidence: {primary.confidence:.0%}[/dim]\n")
902
+
903
+ if not typer.confirm(
904
+ f"Use detected {adapter_type} adapter?", default=True
905
+ ):
906
+ adapter_type = None
907
+
908
+ # If no adapter detected, prompt for selection
909
+ if not adapter_type:
910
+ adapter_type = _prompt_for_adapter_selection(console)
911
+
912
+ # Now run the full init with the selected adapter
913
+ console.print(f"\n[cyan]Initializing {adapter_type} adapter...[/cyan]\n")
914
+
915
+ # Call init programmatically
916
+ init(
917
+ adapter=adapter_type,
918
+ project_path=str(proj_path),
919
+ global_config=False,
920
+ )
921
+
922
+ console.print("\n[green]✓ Adapter configuration complete[/green]\n")
923
+ else:
924
+ console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
925
+
926
+ # Step 3: Platform installation
927
+ if skip_platforms:
928
+ console.print(
929
+ "[yellow]⚠[/yellow] Skipping platform installation (--skip-platforms)\n"
930
+ )
931
+ _show_setup_complete_message(console, proj_path)
932
+ return
933
+
934
+ console.print("[bold]Step 2/2: Platform Installation[/bold]\n")
935
+
936
+ # Detect available platforms
937
+ detector = PlatformDetector()
938
+ detected = detector.detect_all(project_path=proj_path)
939
+
940
+ if not detected:
941
+ console.print("[yellow]No AI platforms detected on this system.[/yellow]")
942
+ console.print(
943
+ "\n[dim]Supported platforms: Claude Code, Claude Desktop, Gemini, Codex, Auggie[/dim]"
944
+ )
945
+ console.print(
946
+ "[dim]Install these platforms to use them with mcp-ticketer.[/dim]\n"
947
+ )
948
+ _show_setup_complete_message(console, proj_path)
949
+ return
950
+
951
+ # Filter to only installed platforms
952
+ installed = [p for p in detected if p.is_installed]
953
+
954
+ if not installed:
955
+ console.print(
956
+ "[yellow]AI platforms detected but have configuration issues.[/yellow]"
957
+ )
958
+ console.print(
959
+ "\n[dim]Run 'mcp-ticketer install --auto-detect' for details.[/dim]\n"
960
+ )
961
+ _show_setup_complete_message(console, proj_path)
962
+ return
963
+
964
+ # Show detected platforms
965
+ console.print(f"[green]✓[/green] Detected {len(installed)} platform(s):\n")
966
+ for plat in installed:
967
+ console.print(f" • {plat.display_name} ({plat.scope})")
968
+
969
+ console.print()
970
+
971
+ # Check if mcp-ticketer is already configured for these platforms
972
+ already_configured = _check_existing_platform_configs(installed, proj_path)
973
+
974
+ if already_configured:
975
+ console.print(
976
+ f"[green]✓[/green] mcp-ticketer already configured for {len(already_configured)} platform(s)\n"
977
+ )
978
+ for plat_name in already_configured:
979
+ console.print(f" • {plat_name}")
980
+ console.print()
981
+
982
+ if not typer.confirm("Update platform configurations anyway?", default=False):
983
+ console.print("[yellow]Skipping platform installation[/yellow]\n")
984
+ _show_setup_complete_message(console, proj_path)
985
+ return
986
+
987
+ # Offer to install for all or select specific
988
+ console.print("[bold]Platform Installation Options:[/bold]")
989
+ console.print("1. Install for all detected platforms")
990
+ console.print("2. Select specific platform")
991
+ console.print("3. Skip platform installation")
992
+
993
+ try:
994
+ choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
995
+ except typer.Abort:
996
+ console.print("[yellow]Setup cancelled[/yellow]")
997
+ raise typer.Exit(0) from None
998
+
999
+ if choice == 3:
1000
+ console.print("[yellow]Skipping platform installation[/yellow]\n")
1001
+ _show_setup_complete_message(console, proj_path)
1002
+ return
1003
+
1004
+ # Import configuration functions
1005
+ from .auggie_configure import configure_auggie_mcp
1006
+ from .codex_configure import configure_codex_mcp
1007
+ from .gemini_configure import configure_gemini_mcp
1008
+ from .mcp_configure import configure_claude_mcp
1009
+
1010
+ platform_mapping = {
1011
+ "claude-code": lambda: configure_claude_mcp(global_config=False, force=True),
1012
+ "claude-desktop": lambda: configure_claude_mcp(global_config=True, force=True),
1013
+ "auggie": lambda: configure_auggie_mcp(force=True),
1014
+ "gemini": lambda: configure_gemini_mcp(scope="project", force=True),
1015
+ "codex": lambda: configure_codex_mcp(force=True),
1016
+ }
1017
+
1018
+ platforms_to_install = []
1019
+
1020
+ if choice == 1:
1021
+ # Install for all
1022
+ platforms_to_install = installed
1023
+ elif choice == 2:
1024
+ # Select specific platform
1025
+ console.print("\n[bold]Select platform:[/bold]")
1026
+ for idx, plat in enumerate(installed, 1):
1027
+ console.print(f" {idx}. {plat.display_name} ({plat.scope})")
1028
+
1029
+ try:
1030
+ plat_choice = typer.prompt("\nSelect platform number", type=int)
1031
+ if 1 <= plat_choice <= len(installed):
1032
+ platforms_to_install = [installed[plat_choice - 1]]
1033
+ else:
1034
+ console.print("[red]Invalid selection[/red]")
1035
+ raise typer.Exit(1) from None
1036
+ except typer.Abort:
1037
+ console.print("[yellow]Setup cancelled[/yellow]")
1038
+ raise typer.Exit(0) from None
1039
+
1040
+ # Install for selected platforms
1041
+ console.print()
1042
+ success_count = 0
1043
+ failed = []
1044
+
1045
+ for plat in platforms_to_install:
1046
+ config_func = platform_mapping.get(plat.name)
1047
+ if not config_func:
1048
+ console.print(f"[yellow]⚠[/yellow] No installer for {plat.display_name}")
1049
+ continue
1050
+
1051
+ try:
1052
+ console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
1053
+ config_func()
1054
+ console.print(f"[green]✓[/green] {plat.display_name} configured\n")
1055
+ success_count += 1
1056
+ except Exception as e:
1057
+ console.print(
1058
+ f"[red]✗[/red] Failed to configure {plat.display_name}: {e}\n"
1059
+ )
1060
+ failed.append(plat.display_name)
1061
+
1062
+ # Summary
1063
+ console.print(
1064
+ f"[bold]Platform Installation:[/bold] {success_count}/{len(platforms_to_install)} succeeded"
1065
+ )
1066
+ if failed:
1067
+ console.print(f"[red]Failed:[/red] {', '.join(failed)}")
1068
+
1069
+ console.print()
1070
+ _show_setup_complete_message(console, proj_path)
1071
+
1072
+
1073
+ def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[str]:
1074
+ """Check if mcp-ticketer is already configured for given platforms.
1075
+
1076
+ Args:
1077
+ platforms: List of DetectedPlatform objects
1078
+ proj_path: Project path
1079
+
1080
+ Returns:
1081
+ List of platform display names that are already configured
1082
+
1083
+ """
1084
+ configured = []
1085
+
1086
+ for plat in platforms:
1087
+ try:
1088
+ if plat.name == "claude-code":
1089
+ config_path = Path.home() / ".claude.json"
1090
+ if config_path.exists():
1091
+ with open(config_path) as f:
1092
+ config = json.load(f)
1093
+ projects = config.get("projects", {})
1094
+ proj_key = str(proj_path)
1095
+ if proj_key in projects:
1096
+ mcp_servers = projects[proj_key].get("mcpServers", {})
1097
+ if "mcp-ticketer" in mcp_servers:
1098
+ configured.append(plat.display_name)
1099
+
1100
+ elif plat.name == "claude-desktop":
1101
+ if plat.config_path.exists():
1102
+ with open(plat.config_path) as f:
1103
+ config = json.load(f)
1104
+ if "mcp-ticketer" in config.get("mcpServers", {}):
1105
+ configured.append(plat.display_name)
1106
+
1107
+ elif plat.name in ["auggie", "codex", "gemini"]:
1108
+ if plat.config_path.exists():
1109
+ # Check if mcp-ticketer is configured
1110
+ # Implementation depends on each platform's config format
1111
+ # For now, just check if config exists (simplified)
1112
+ pass
1113
+
1114
+ except (json.JSONDecodeError, OSError):
1115
+ pass
1116
+
1117
+ return configured
1118
+
1119
+
1120
+ def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
1121
+ """Show setup complete message with next steps.
1122
+
1123
+ Args:
1124
+ console: Rich console for output
1125
+ proj_path: Project path
1126
+
1127
+ """
1128
+ console.print("[bold green]🎉 Setup Complete![/bold green]\n")
1129
+
1130
+ console.print("[bold]Quick Start:[/bold]")
1131
+ console.print("1. Create a test ticket:")
1132
+ console.print(" [cyan]mcp-ticketer create 'My first ticket'[/cyan]\n")
1133
+
1134
+ console.print("2. List tickets:")
1135
+ console.print(" [cyan]mcp-ticketer list[/cyan]\n")
1136
+
1137
+ console.print("[bold]Useful Commands:[/bold]")
1138
+ console.print(" [cyan]mcp-ticketer doctor[/cyan] - Validate configuration")
1139
+ console.print(" [cyan]mcp-ticketer install <platform>[/cyan] - Add more platforms")
1140
+ console.print(" [cyan]mcp-ticketer --help[/cyan] - See all commands\n")
1141
+
1142
+ console.print(
1143
+ f"[dim]Configuration: {proj_path / '.mcp-ticketer' / 'config.json'}[/dim]"
468
1144
  )
469
1145
 
470
1146
 
@@ -518,37 +1194,41 @@ def init(
518
1194
  None, "--github-token", help="GitHub Personal Access Token"
519
1195
  ),
520
1196
  ) -> None:
521
- """Initialize mcp-ticketer for the current project (synonymous with 'install' and 'setup').
1197
+ """Initialize adapter configuration only (without platform installation).
1198
+
1199
+ This command sets up adapter configuration with interactive prompts.
1200
+ It auto-detects adapter configuration from .env files or prompts for
1201
+ interactive setup if no configuration is found.
522
1202
 
523
- This command sets up MCP Ticketer configuration with interactive prompts
524
- to guide you through the process. It auto-detects adapter configuration
525
- from .env files or prompts for interactive setup if no configuration is found.
1203
+ Creates .mcp-ticketer/config.json in the current directory.
526
1204
 
527
- Creates .mcp-ticketer/config.json in the current directory with
528
- auto-detected or specified adapter configuration.
1205
+ RECOMMENDED: Use 'mcp-ticketer setup' instead for a complete setup
1206
+ experience that includes both adapter configuration and platform
1207
+ installation in one command.
529
1208
 
530
- Note: 'init', 'install', and 'setup' are all synonyms - use whichever feels natural.
1209
+ The init command automatically validates your configuration after setup:
1210
+ - If validation passes, setup completes
1211
+ - If issues are detected, you can re-enter credentials, continue anyway, or exit
1212
+ - You get up to 3 retry attempts to fix configuration issues
1213
+ - You can always re-validate later with 'mcp-ticketer doctor'
531
1214
 
532
1215
  Examples:
533
- # Interactive setup (all three commands are identical)
534
- mcp-ticketer init
535
- mcp-ticketer install
1216
+ # For first-time setup, use 'setup' instead (recommended)
536
1217
  mcp-ticketer setup
537
1218
 
1219
+ # Initialize adapter only (advanced usage)
1220
+ mcp-ticketer init
1221
+
538
1222
  # Force specific adapter
539
1223
  mcp-ticketer init --adapter linear
540
1224
 
541
1225
  # Initialize for different project
542
1226
  mcp-ticketer init --path /path/to/project
543
1227
 
544
- # Save globally (not recommended)
545
- mcp-ticketer init --global
546
-
547
1228
  """
548
1229
  from pathlib import Path
549
1230
 
550
1231
  from ..core.env_discovery import discover_config
551
- from ..core.project_config import ConfigResolver
552
1232
 
553
1233
  # Determine project path
554
1234
  proj_path = Path(project_path) if project_path else Path.cwd()
@@ -563,7 +1243,7 @@ def init(
563
1243
  default=False,
564
1244
  ):
565
1245
  console.print("[yellow]Initialization cancelled.[/yellow]")
566
- raise typer.Exit(0)
1246
+ raise typer.Exit(0) from None
567
1247
 
568
1248
  # 1. Try auto-discovery if no adapter specified
569
1249
  discovered = None
@@ -652,11 +1332,9 @@ def init(
652
1332
  elif adapter_type == "linear":
653
1333
  # If not auto-discovered, build from CLI params or prompt
654
1334
  if adapter_type not in config["adapters"]:
655
- linear_config = {}
656
-
657
1335
  # API Key
658
1336
  linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
659
- if not linear_api_key and not discovered:
1337
+ if not linear_api_key:
660
1338
  console.print("\n[bold]Linear Configuration[/bold]")
661
1339
  console.print("You need a Linear API key to connect to Linear.")
662
1340
  console.print(
@@ -667,22 +1345,74 @@ def init(
667
1345
  "Enter your Linear API key", hide_input=True
668
1346
  )
669
1347
 
670
- if linear_api_key:
671
- linear_config["api_key"] = linear_api_key
672
-
673
- # Team ID or Team Key
1348
+ # Team ID or Team Key or Team URL
674
1349
  # Try environment variables first
675
1350
  linear_team_key = os.getenv("LINEAR_TEAM_KEY")
676
1351
  linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
677
1352
 
678
- if not linear_team_key and not linear_team_id and not discovered:
1353
+ if not linear_team_key and not linear_team_id:
679
1354
  console.print("\n[bold]Linear Team Configuration[/bold]")
680
- console.print("Enter your team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
1355
+ console.print("You can provide either:")
1356
+ console.print(
1357
+ " 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
1358
+ )
1359
+ console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
1360
+ console.print(" 3. Team ID (UUID)")
681
1361
  console.print(
682
- "[dim]Find it in: Linear Settings Teams → Your Team → Key field[/dim]\n"
1362
+ "[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
683
1363
  )
684
1364
 
685
- linear_team_key = typer.prompt("Team key")
1365
+ team_input = typer.prompt("Team URL, key, or ID")
1366
+
1367
+ # Check if input is a URL
1368
+ if team_input.startswith("https://linear.app/"):
1369
+ console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
1370
+ import asyncio
1371
+
1372
+ from .linear_commands import derive_team_from_url
1373
+
1374
+ derived_team_id, error = asyncio.run(
1375
+ derive_team_from_url(linear_api_key, team_input)
1376
+ )
1377
+
1378
+ if derived_team_id:
1379
+ linear_team_id = derived_team_id
1380
+ console.print(
1381
+ "[green]✓[/green] Successfully derived team ID from URL"
1382
+ )
1383
+ else:
1384
+ console.print(f"[red]Error:[/red] {error}")
1385
+ console.print("Please provide team key or ID manually instead.")
1386
+ team_input = typer.prompt("Team key or ID")
1387
+
1388
+ # Store as either team_key or team_id based on format
1389
+ if len(team_input) > 20: # Likely a UUID
1390
+ linear_team_id = team_input
1391
+ else:
1392
+ linear_team_key = team_input
1393
+ else:
1394
+ # Input is team key or ID
1395
+ if len(team_input) > 20: # Likely a UUID
1396
+ linear_team_id = team_input
1397
+ else:
1398
+ linear_team_key = team_input
1399
+
1400
+ # Validate required fields (following JIRA pattern)
1401
+ if not linear_api_key:
1402
+ console.print("[red]Error:[/red] Linear API key is required")
1403
+ raise typer.Exit(1) from None
1404
+
1405
+ if not linear_team_id and not linear_team_key:
1406
+ console.print(
1407
+ "[red]Error:[/red] Linear requires either team ID or team key"
1408
+ )
1409
+ raise typer.Exit(1) from None
1410
+
1411
+ # Build configuration
1412
+ linear_config = {
1413
+ "api_key": linear_api_key,
1414
+ "type": "linear",
1415
+ }
686
1416
 
687
1417
  # Save whichever was provided
688
1418
  if linear_team_key:
@@ -690,18 +1420,6 @@ def init(
690
1420
  if linear_team_id:
691
1421
  linear_config["team_id"] = linear_team_id
692
1422
 
693
- if not linear_config.get("api_key") or (
694
- not linear_config.get("team_id") and not linear_config.get("team_key")
695
- ):
696
- console.print(
697
- "[red]Error:[/red] Linear requires both API key and team ID/key"
698
- )
699
- console.print(
700
- "Run 'mcp-ticketer init --adapter linear' with proper credentials"
701
- )
702
- raise typer.Exit(1)
703
-
704
- linear_config["type"] = "linear"
705
1423
  config["adapters"]["linear"] = linear_config
706
1424
 
707
1425
  elif adapter_type == "jira":
@@ -713,7 +1431,7 @@ def init(
713
1431
  project = jira_project or os.getenv("JIRA_PROJECT_KEY")
714
1432
 
715
1433
  # Interactive prompts for missing values
716
- if not server and not discovered:
1434
+ if not server:
717
1435
  console.print("\n[bold]JIRA Configuration[/bold]")
718
1436
  console.print("Enter your JIRA server details.\n")
719
1437
 
@@ -721,10 +1439,10 @@ def init(
721
1439
  "JIRA server URL (e.g., https://company.atlassian.net)"
722
1440
  )
723
1441
 
724
- if not email and not discovered:
1442
+ if not email:
725
1443
  email = typer.prompt("Your JIRA email address")
726
1444
 
727
- if not token and not discovered:
1445
+ if not token:
728
1446
  console.print("\nYou need a JIRA API token.")
729
1447
  console.print(
730
1448
  "[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
@@ -732,7 +1450,7 @@ def init(
732
1450
 
733
1451
  token = typer.prompt("Enter your JIRA API token", hide_input=True)
734
1452
 
735
- if not project and not discovered:
1453
+ if not project:
736
1454
  project = typer.prompt(
737
1455
  "Default JIRA project key (optional, press Enter to skip)",
738
1456
  default="",
@@ -742,15 +1460,15 @@ def init(
742
1460
  # Validate required fields
743
1461
  if not server:
744
1462
  console.print("[red]Error:[/red] JIRA server URL is required")
745
- raise typer.Exit(1)
1463
+ raise typer.Exit(1) from None
746
1464
 
747
1465
  if not email:
748
1466
  console.print("[red]Error:[/red] JIRA email is required")
749
- raise typer.Exit(1)
1467
+ raise typer.Exit(1) from None
750
1468
 
751
1469
  if not token:
752
1470
  console.print("[red]Error:[/red] JIRA API token is required")
753
- raise typer.Exit(1)
1471
+ raise typer.Exit(1) from None
754
1472
 
755
1473
  jira_config = {
756
1474
  "server": server,
@@ -772,7 +1490,7 @@ def init(
772
1490
  token = github_token or os.getenv("GITHUB_TOKEN")
773
1491
 
774
1492
  # Interactive prompts for missing values
775
- if not owner and not discovered:
1493
+ if not owner:
776
1494
  console.print("\n[bold]GitHub Configuration[/bold]")
777
1495
  console.print("Enter your GitHub repository details.\n")
778
1496
 
@@ -780,10 +1498,10 @@ def init(
780
1498
  "GitHub repository owner (username or organization)"
781
1499
  )
782
1500
 
783
- if not repo and not discovered:
1501
+ if not repo:
784
1502
  repo = typer.prompt("GitHub repository name")
785
1503
 
786
- if not token and not discovered:
1504
+ if not token:
787
1505
  console.print("\nYou need a GitHub Personal Access Token.")
788
1506
  console.print(
789
1507
  "[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
@@ -799,17 +1517,17 @@ def init(
799
1517
  # Validate required fields
800
1518
  if not owner:
801
1519
  console.print("[red]Error:[/red] GitHub repository owner is required")
802
- raise typer.Exit(1)
1520
+ raise typer.Exit(1) from None
803
1521
 
804
1522
  if not repo:
805
1523
  console.print("[red]Error:[/red] GitHub repository name is required")
806
- raise typer.Exit(1)
1524
+ raise typer.Exit(1) from None
807
1525
 
808
1526
  if not token:
809
1527
  console.print(
810
1528
  "[red]Error:[/red] GitHub Personal Access Token is required"
811
1529
  )
812
- raise typer.Exit(1)
1530
+ raise typer.Exit(1) from None
813
1531
 
814
1532
  config["adapters"]["github"] = {
815
1533
  "owner": owner,
@@ -818,42 +1536,46 @@ def init(
818
1536
  "type": "github",
819
1537
  }
820
1538
 
821
- # 5. Save to appropriate location
822
- if global_config:
823
- # Save to ~/.mcp-ticketer/config.json
824
- resolver = ConfigResolver(project_path=proj_path)
825
- config_file_path = resolver.GLOBAL_CONFIG_PATH
826
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
1539
+ # 5. Save to project-local config (global config deprecated for security)
1540
+ # Always save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
1541
+ config_file_path = proj_path / ".mcp-ticketer" / "config.json"
1542
+ config_file_path.parent.mkdir(parents=True, exist_ok=True)
827
1543
 
828
- with open(config_file_path, "w") as f:
829
- json.dump(config, f, indent=2)
1544
+ with open(config_file_path, "w") as f:
1545
+ json.dump(config, f, indent=2)
1546
+
1547
+ if global_config:
1548
+ console.print(
1549
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
1550
+ )
830
1551
 
831
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
832
- console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
1552
+ console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
1553
+ console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
1554
+
1555
+ # Add .mcp-ticketer to .gitignore if not already there
1556
+ gitignore_path = proj_path / ".gitignore"
1557
+ if gitignore_path.exists():
1558
+ gitignore_content = gitignore_path.read_text()
1559
+ if ".mcp-ticketer" not in gitignore_content:
1560
+ with open(gitignore_path, "a") as f:
1561
+ f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
1562
+ console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
833
1563
  else:
834
- # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
835
- config_file_path = proj_path / ".mcp-ticketer" / "config.json"
836
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
837
-
838
- with open(config_file_path, "w") as f:
839
- json.dump(config, f, indent=2)
840
-
841
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
842
- console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
843
-
844
- # Add .mcp-ticketer to .gitignore if not already there
845
- gitignore_path = proj_path / ".gitignore"
846
- if gitignore_path.exists():
847
- gitignore_content = gitignore_path.read_text()
848
- if ".mcp-ticketer" not in gitignore_content:
849
- with open(gitignore_path, "a") as f:
850
- f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
851
- console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
852
- else:
853
- # Create .gitignore if it doesn't exist
854
- with open(gitignore_path, "w") as f:
855
- f.write("# MCP Ticketer\n.mcp-ticketer/\n")
856
- console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
1564
+ # Create .gitignore if it doesn't exist
1565
+ with open(gitignore_path, "w") as f:
1566
+ f.write("# MCP Ticketer\n.mcp-ticketer/\n")
1567
+ console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
1568
+
1569
+ # Validate configuration with loop for corrections
1570
+ import asyncio
1571
+
1572
+ if not asyncio.run(
1573
+ _validate_configuration_with_retry(
1574
+ console, adapter_type, config_file_path, proj_path
1575
+ )
1576
+ ):
1577
+ # User chose to exit without valid configuration
1578
+ raise typer.Exit(1) from None
857
1579
 
858
1580
  # Show next steps
859
1581
  _show_next_steps(console, adapter_type, config_file_path)
@@ -874,16 +1596,13 @@ def _show_next_steps(
874
1596
  console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
875
1597
 
876
1598
  console.print("[bold]Next Steps:[/bold]")
877
- console.print("1. [cyan]Test your configuration:[/cyan]")
878
- console.print(" mcp-ticketer diagnose")
879
- console.print("\n2. [cyan]Create a test ticket:[/cyan]")
1599
+ console.print("1. [cyan]Create a test ticket:[/cyan]")
880
1600
  console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
881
1601
 
882
1602
  if adapter_type != "aitrackdown":
883
1603
  console.print(
884
- f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
1604
+ f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
885
1605
  )
886
-
887
1606
  if adapter_type == "linear":
888
1607
  console.print(" Check your Linear workspace for the new ticket")
889
1608
  elif adapter_type == "github":
@@ -891,16 +1610,19 @@ def _show_next_steps(
891
1610
  elif adapter_type == "jira":
892
1611
  console.print(" Check your JIRA project for the new ticket")
893
1612
  else:
894
- console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
1613
+ console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
895
1614
  console.print(" ls .aitrackdown/")
896
1615
 
897
- console.print("\n4. [cyan]Install MCP for AI clients (optional):[/cyan]")
1616
+ console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
898
1617
  console.print(" mcp-ticketer install claude-code # For Claude Code")
899
1618
  console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
900
1619
  console.print(" mcp-ticketer install auggie # For Auggie")
901
1620
  console.print(" mcp-ticketer install gemini # For Gemini CLI")
902
1621
 
903
1622
  console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
1623
+ console.print(
1624
+ "[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
1625
+ )
904
1626
  console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
905
1627
 
906
1628
 
@@ -1064,7 +1786,7 @@ def migrate_config(
1064
1786
 
1065
1787
 
1066
1788
  @app.command("queue-status", deprecated=True, hidden=True)
1067
- def old_queue_status_command():
1789
+ def old_queue_status_command() -> None:
1068
1790
  """Show queue and worker status.
1069
1791
 
1070
1792
  DEPRECATED: Use 'mcp-ticketer queue status' instead.
@@ -1180,9 +1902,9 @@ def old_queue_health_command(
1180
1902
 
1181
1903
  # Exit with appropriate code
1182
1904
  if health["status"] == HealthStatus.CRITICAL:
1183
- raise typer.Exit(1)
1905
+ raise typer.Exit(1) from None
1184
1906
  elif health["status"] == HealthStatus.WARNING:
1185
- raise typer.Exit(2)
1907
+ raise typer.Exit(2) from None
1186
1908
 
1187
1909
 
1188
1910
  @app.command(deprecated=True, hidden=True)
@@ -1250,7 +1972,7 @@ def create(
1250
1972
  console.print(
1251
1973
  "[red]Cannot safely create ticket. Please check system status.[/red]"
1252
1974
  )
1253
- raise typer.Exit(1)
1975
+ raise typer.Exit(1) from None
1254
1976
  else:
1255
1977
  console.print(
1256
1978
  "[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
@@ -1259,7 +1981,7 @@ def create(
1259
1981
  console.print(
1260
1982
  "[red]❌ No repair actions available. Manual intervention required.[/red]"
1261
1983
  )
1262
- raise typer.Exit(1)
1984
+ raise typer.Exit(1) from None
1263
1985
 
1264
1986
  elif health["status"] == HealthStatus.WARNING:
1265
1987
  console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
@@ -1438,7 +2160,7 @@ def list_tickets(
1438
2160
  "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
1439
2161
  )
1440
2162
 
1441
- async def _list():
2163
+ async def _list() -> None:
1442
2164
  adapter_instance = get_adapter(
1443
2165
  override_adapter=adapter.value if adapter else None
1444
2166
  )
@@ -1494,7 +2216,7 @@ def show(
1494
2216
  "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
1495
2217
  )
1496
2218
 
1497
- async def _show():
2219
+ async def _show() -> None:
1498
2220
  adapter_instance = get_adapter(
1499
2221
  override_adapter=adapter.value if adapter else None
1500
2222
  )
@@ -1508,7 +2230,7 @@ def show(
1508
2230
 
1509
2231
  if not ticket:
1510
2232
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
1511
- raise typer.Exit(1)
2233
+ raise typer.Exit(1) from None
1512
2234
 
1513
2235
  # Display ticket details
1514
2236
  console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
@@ -1550,7 +2272,7 @@ def comment(
1550
2272
  "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
1551
2273
  )
1552
2274
 
1553
- async def _comment():
2275
+ async def _comment() -> None:
1554
2276
  adapter_instance = get_adapter(
1555
2277
  override_adapter=adapter.value if adapter else None
1556
2278
  )
@@ -1573,7 +2295,7 @@ def comment(
1573
2295
  console.print(f"Content: {content}")
1574
2296
  except Exception as e:
1575
2297
  console.print(f"[red]✗[/red] Failed to add comment: {e}")
1576
- raise typer.Exit(1)
2298
+ raise typer.Exit(1) from e
1577
2299
 
1578
2300
 
1579
2301
  @app.command(deprecated=True, hidden=True)
@@ -1612,7 +2334,7 @@ def update(
1612
2334
 
1613
2335
  if not updates:
1614
2336
  console.print("[yellow]No updates specified[/yellow]")
1615
- raise typer.Exit(1)
2337
+ raise typer.Exit(1) from None
1616
2338
 
1617
2339
  # Get the adapter name
1618
2340
  config = load_config()
@@ -1684,7 +2406,7 @@ def transition(
1684
2406
  " - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
1685
2407
  " - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
1686
2408
  )
1687
- raise typer.Exit(1)
2409
+ raise typer.Exit(1) from None
1688
2410
 
1689
2411
  # Get the adapter name
1690
2412
  config = load_config()
@@ -1735,7 +2457,7 @@ def search(
1735
2457
  "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
1736
2458
  )
1737
2459
 
1738
- async def _search():
2460
+ async def _search() -> None:
1739
2461
  adapter_instance = get_adapter(
1740
2462
  override_adapter=adapter.value if adapter else None
1741
2463
  )
@@ -1777,10 +2499,13 @@ app.add_typer(queue_app, name="queue")
1777
2499
  # Add discover command to main app
1778
2500
  app.add_typer(discover_app, name="discover")
1779
2501
 
2502
+ # Add instructions command to main app
2503
+ app.add_typer(instruction_app, name="instructions")
2504
+
1780
2505
 
1781
2506
  # Add diagnostics command
1782
- @app.command("diagnose")
1783
- def diagnose_command(
2507
+ @app.command("doctor")
2508
+ def doctor_command(
1784
2509
  output_file: str | None = typer.Option(
1785
2510
  None, "--output", "-o", help="Save full report to file"
1786
2511
  ),
@@ -1791,7 +2516,7 @@ def diagnose_command(
1791
2516
  False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1792
2517
  ),
1793
2518
  ) -> None:
1794
- """Run comprehensive system diagnostics and health check (alias: doctor)."""
2519
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
1795
2520
  if simple:
1796
2521
  from .simple_health import simple_diagnose
1797
2522
 
@@ -1807,7 +2532,7 @@ def diagnose_command(
1807
2532
 
1808
2533
  console.print("\n" + json.dumps(report, indent=2))
1809
2534
  if report["issues"]:
1810
- raise typer.Exit(1)
2535
+ raise typer.Exit(1) from None
1811
2536
  else:
1812
2537
  try:
1813
2538
  asyncio.run(
@@ -1823,11 +2548,11 @@ def diagnose_command(
1823
2548
 
1824
2549
  report = simple_diagnose()
1825
2550
  if report["issues"]:
1826
- raise typer.Exit(1)
2551
+ raise typer.Exit(1) from None
1827
2552
 
1828
2553
 
1829
- @app.command("doctor")
1830
- def doctor_alias(
2554
+ @app.command("diagnose", hidden=True)
2555
+ def diagnose_alias(
1831
2556
  output_file: str | None = typer.Option(
1832
2557
  None, "--output", "-o", help="Save full report to file"
1833
2558
  ),
@@ -1838,9 +2563,9 @@ def doctor_alias(
1838
2563
  False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1839
2564
  ),
1840
2565
  ) -> None:
1841
- """Run comprehensive system diagnostics and health check (alias for diagnose)."""
1842
- # Call the diagnose_command function with the same parameters
1843
- diagnose_command(output_file=output_file, json_output=json_output, simple=simple)
2566
+ """Run comprehensive system diagnostics and health check (alias for doctor)."""
2567
+ # Call the doctor_command function with the same parameters
2568
+ doctor_command(output_file=output_file, json_output=json_output, simple=simple)
1844
2569
 
1845
2570
 
1846
2571
  @app.command("status")
@@ -1850,7 +2575,7 @@ def status_command() -> None:
1850
2575
 
1851
2576
  result = simple_health_check()
1852
2577
  if result != 0:
1853
- raise typer.Exit(result)
2578
+ raise typer.Exit(result) from None
1854
2579
 
1855
2580
 
1856
2581
  @app.command("health")
@@ -1860,7 +2585,7 @@ def health_alias() -> None:
1860
2585
 
1861
2586
  result = simple_health_check()
1862
2587
  if result != 0:
1863
- raise typer.Exit(result)
2588
+ raise typer.Exit(result) from None
1864
2589
 
1865
2590
 
1866
2591
  # Create MCP configuration command group
@@ -1875,11 +2600,20 @@ mcp_app = typer.Typer(
1875
2600
  @mcp_app.callback()
1876
2601
  def mcp_callback(
1877
2602
  ctx: typer.Context,
1878
- project_path: str | None = typer.Argument(
1879
- None, help="Project directory path (optional - uses cwd if not provided)"
2603
+ project_path: str | None = typer.Option(
2604
+ None, "--path", "-p", help="Project directory path (default: current directory)"
1880
2605
  ),
1881
- ):
1882
- """MCP command group - runs MCP server if no subcommand provided."""
2606
+ ) -> None:
2607
+ """MCP command group - runs MCP server if no subcommand provided.
2608
+
2609
+ Examples:
2610
+ mcp-ticketer mcp # Start server in current directory
2611
+ mcp-ticketer mcp --path /dir # Start server in specific directory
2612
+ mcp-ticketer mcp -p /dir # Start server (short form)
2613
+ mcp-ticketer mcp status # Check MCP status
2614
+ mcp-ticketer mcp serve # Explicitly start server
2615
+
2616
+ """
1883
2617
  if ctx.invoked_subcommand is None:
1884
2618
  # No subcommand provided, run the serve command
1885
2619
  # Change to project directory if provided
@@ -1897,6 +2631,17 @@ def install(
1897
2631
  None,
1898
2632
  help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
1899
2633
  ),
2634
+ auto_detect: bool = typer.Option(
2635
+ False,
2636
+ "--auto-detect",
2637
+ "-d",
2638
+ help="Auto-detect and show all installed AI platforms",
2639
+ ),
2640
+ install_all: bool = typer.Option(
2641
+ False,
2642
+ "--all",
2643
+ help="Install for all detected platforms",
2644
+ ),
1900
2645
  adapter: str | None = typer.Option(
1901
2646
  None,
1902
2647
  "--adapter",
@@ -1950,26 +2695,247 @@ def install(
1950
2695
  help="Show what would be done without making changes (for platform installation)",
1951
2696
  ),
1952
2697
  ) -> None:
1953
- """Install MCP for AI platforms OR initialize adapter setup.
2698
+ """Install MCP server configuration for AI platforms.
2699
+
2700
+ This command configures mcp-ticketer as an MCP server for various AI
2701
+ platforms. It updates platform-specific configuration files to enable
2702
+ mcp-ticketer integration.
2703
+
2704
+ RECOMMENDED: Use 'mcp-ticketer setup' for first-time setup, which
2705
+ handles both adapter configuration and platform installation together.
1954
2706
 
1955
- With platform argument (new syntax): Install MCP configuration for AI platforms
1956
- Without platform argument (legacy): Run adapter setup wizard (same as 'init' and 'setup')
2707
+ Platform Installation:
2708
+ # Auto-detect and prompt for platform selection
2709
+ mcp-ticketer install
2710
+
2711
+ # Show all detected platforms
2712
+ mcp-ticketer install --auto-detect
1957
2713
 
1958
- New Command Structure:
1959
- # Install MCP for AI platforms
2714
+ # Install for all detected platforms
2715
+ mcp-ticketer install --all
2716
+
2717
+ # Install for specific platform
1960
2718
  mcp-ticketer install claude-code # Claude Code (project-level)
1961
2719
  mcp-ticketer install claude-desktop # Claude Desktop (global)
1962
2720
  mcp-ticketer install gemini # Gemini CLI
1963
2721
  mcp-ticketer install codex # Codex
1964
2722
  mcp-ticketer install auggie # Auggie
1965
2723
 
1966
- Legacy Adapter Setup (still supported):
1967
- mcp-ticketer install # Interactive setup wizard
1968
- mcp-ticketer install --adapter linear
2724
+ Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
2725
+ mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
1969
2726
 
1970
2727
  """
2728
+ from .platform_detection import PlatformDetector, get_platform_by_name
2729
+
2730
+ detector = PlatformDetector()
2731
+
2732
+ # Handle auto-detect flag (just show detected platforms and exit)
2733
+ if auto_detect:
2734
+ detected = detector.detect_all(
2735
+ project_path=Path(project_path) if project_path else Path.cwd()
2736
+ )
2737
+
2738
+ if not detected:
2739
+ console.print("[yellow]No AI platforms detected.[/yellow]")
2740
+ console.print("\n[bold]Supported platforms:[/bold]")
2741
+ console.print(" • Claude Code - Project-level configuration")
2742
+ console.print(" • Claude Desktop - Global GUI application")
2743
+ console.print(" • Auggie - CLI tool with global config")
2744
+ console.print(" • Codex - CLI tool with global config")
2745
+ console.print(" • Gemini - CLI tool with project/global config")
2746
+ console.print(
2747
+ "\n[dim]Install these platforms to use them with mcp-ticketer.[/dim]"
2748
+ )
2749
+ return
2750
+
2751
+ console.print("[bold]Detected AI platforms:[/bold]\n")
2752
+ table = Table(show_header=True, header_style="bold cyan")
2753
+ table.add_column("Platform", style="green")
2754
+ table.add_column("Status", style="yellow")
2755
+ table.add_column("Scope", style="blue")
2756
+ table.add_column("Config Path", style="dim")
2757
+
2758
+ for plat in detected:
2759
+ status = "✓ Installed" if plat.is_installed else "⚠ Config Issue"
2760
+ table.add_row(plat.display_name, status, plat.scope, str(plat.config_path))
2761
+
2762
+ console.print(table)
2763
+ console.print(
2764
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a specific platform[/dim]"
2765
+ )
2766
+ console.print(
2767
+ "[dim]Run 'mcp-ticketer install --all' to configure all detected platforms[/dim]"
2768
+ )
2769
+ return
2770
+
2771
+ # Handle --all flag (install for all detected platforms)
2772
+ if install_all:
2773
+ detected = detector.detect_all(
2774
+ project_path=Path(project_path) if project_path else Path.cwd()
2775
+ )
2776
+
2777
+ if not detected:
2778
+ console.print("[yellow]No AI platforms detected.[/yellow]")
2779
+ console.print(
2780
+ "Run 'mcp-ticketer install --auto-detect' to see supported platforms."
2781
+ )
2782
+ return
2783
+
2784
+ # Handle dry-run mode - show what would be installed without actually installing
2785
+ if dry_run:
2786
+ console.print(
2787
+ "\n[yellow]DRY RUN - The following platforms would be configured:[/yellow]\n"
2788
+ )
2789
+
2790
+ installable_count = 0
2791
+ for plat in detected:
2792
+ if plat.is_installed:
2793
+ console.print(f" ✓ {plat.display_name} ({plat.scope})")
2794
+ installable_count += 1
2795
+ else:
2796
+ console.print(
2797
+ f" ⚠ {plat.display_name} ({plat.scope}) - would be skipped (configuration issue)"
2798
+ )
2799
+
2800
+ console.print(
2801
+ f"\n[dim]Would configure {installable_count} platform(s)[/dim]"
2802
+ )
2803
+ return
2804
+
2805
+ console.print(
2806
+ f"[bold]Installing for {len(detected)} detected platform(s)...[/bold]\n"
2807
+ )
2808
+
2809
+ # Import configuration functions
2810
+ from .auggie_configure import configure_auggie_mcp
2811
+ from .codex_configure import configure_codex_mcp
2812
+ from .gemini_configure import configure_gemini_mcp
2813
+ from .mcp_configure import configure_claude_mcp
2814
+
2815
+ # Map platform names to configuration functions
2816
+ platform_mapping = {
2817
+ "claude-code": lambda: configure_claude_mcp(
2818
+ global_config=False, force=True
2819
+ ),
2820
+ "claude-desktop": lambda: configure_claude_mcp(
2821
+ global_config=True, force=True
2822
+ ),
2823
+ "auggie": lambda: configure_auggie_mcp(force=True),
2824
+ "gemini": lambda: configure_gemini_mcp(scope="project", force=True),
2825
+ "codex": lambda: configure_codex_mcp(force=True),
2826
+ }
2827
+
2828
+ success_count = 0
2829
+ failed = []
2830
+
2831
+ for plat in detected:
2832
+ if not plat.is_installed:
2833
+ console.print(
2834
+ f"[yellow]⚠[/yellow] Skipping {plat.display_name} (configuration issue)"
2835
+ )
2836
+ continue
2837
+
2838
+ config_func = platform_mapping.get(plat.name)
2839
+ if not config_func:
2840
+ console.print(
2841
+ f"[yellow]⚠[/yellow] No installer for {plat.display_name}"
2842
+ )
2843
+ continue
2844
+
2845
+ try:
2846
+ console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
2847
+ config_func()
2848
+ success_count += 1
2849
+ except Exception as e:
2850
+ console.print(
2851
+ f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
2852
+ )
2853
+ failed.append(plat.display_name)
2854
+
2855
+ console.print(
2856
+ f"\n[bold]Installation complete:[/bold] {success_count} succeeded"
2857
+ )
2858
+ if failed:
2859
+ console.print(f"[red]Failed:[/red] {', '.join(failed)}")
2860
+ return
2861
+
2862
+ # If no platform argument and no adapter flag, auto-detect and prompt
2863
+ if platform is None and adapter is None:
2864
+ detected = detector.detect_all(
2865
+ project_path=Path(project_path) if project_path else Path.cwd()
2866
+ )
2867
+
2868
+ # Filter to only installed platforms
2869
+ installed = [p for p in detected if p.is_installed]
2870
+
2871
+ if not installed:
2872
+ console.print("[yellow]No AI platforms detected.[/yellow]")
2873
+ console.print("\n[bold]To see supported platforms:[/bold]")
2874
+ console.print(" mcp-ticketer install --auto-detect")
2875
+ console.print("\n[bold]Or run legacy adapter setup:[/bold]")
2876
+ console.print(" mcp-ticketer install --adapter <adapter-type>")
2877
+ return
2878
+
2879
+ # Show detected platforms and prompt for selection
2880
+ console.print("[bold]Detected AI platforms:[/bold]\n")
2881
+ for idx, plat in enumerate(installed, 1):
2882
+ console.print(f" {idx}. {plat.display_name} ({plat.scope})")
2883
+
2884
+ console.print(
2885
+ "\n[dim]Enter the number of the platform to configure, or 'q' to quit:[/dim]"
2886
+ )
2887
+ choice = typer.prompt("Select platform")
2888
+
2889
+ if choice.lower() == "q":
2890
+ console.print("Installation cancelled.")
2891
+ return
2892
+
2893
+ try:
2894
+ idx = int(choice) - 1
2895
+ if idx < 0 or idx >= len(installed):
2896
+ console.print("[red]Invalid selection.[/red]")
2897
+ raise typer.Exit(1) from None
2898
+ platform = installed[idx].name
2899
+ except ValueError as e:
2900
+ console.print("[red]Invalid input. Please enter a number.[/red]")
2901
+ raise typer.Exit(1) from e
2902
+
1971
2903
  # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
1972
2904
  if platform is not None:
2905
+ # Validate that the platform is actually installed
2906
+ platform_info = get_platform_by_name(
2907
+ platform, project_path=Path(project_path) if project_path else Path.cwd()
2908
+ )
2909
+
2910
+ if platform_info and not platform_info.is_installed:
2911
+ console.print(
2912
+ f"[yellow]⚠[/yellow] {platform_info.display_name} was detected but has a configuration issue."
2913
+ )
2914
+ console.print(f"[dim]Config path: {platform_info.config_path}[/dim]\n")
2915
+
2916
+ proceed = typer.confirm(
2917
+ "Do you want to proceed with installation anyway?", default=False
2918
+ )
2919
+ if not proceed:
2920
+ console.print("Installation cancelled.")
2921
+ return
2922
+
2923
+ elif not platform_info:
2924
+ # Platform not detected at all - warn but allow proceeding
2925
+ console.print(
2926
+ f"[yellow]⚠[/yellow] Platform '{platform}' not detected on this system."
2927
+ )
2928
+ console.print(
2929
+ "[dim]Run 'mcp-ticketer install --auto-detect' to see detected platforms.[/dim]\n"
2930
+ )
2931
+
2932
+ proceed = typer.confirm(
2933
+ "Do you want to proceed with installation anyway?", default=False
2934
+ )
2935
+ if not proceed:
2936
+ console.print("Installation cancelled.")
2937
+ return
2938
+
1973
2939
  # Import configuration functions
1974
2940
  from .auggie_configure import configure_auggie_mcp
1975
2941
  from .codex_configure import configure_codex_mcp
@@ -2005,7 +2971,7 @@ def install(
2005
2971
  console.print("\n[bold]Available platforms:[/bold]")
2006
2972
  for p in platform_mapping.keys():
2007
2973
  console.print(f" • {p}")
2008
- raise typer.Exit(1)
2974
+ raise typer.Exit(1) from None
2009
2975
 
2010
2976
  config = platform_mapping[platform]
2011
2977
 
@@ -2017,7 +2983,7 @@ def install(
2017
2983
  config["func"]()
2018
2984
  except Exception as e:
2019
2985
  console.print(f"[red]Installation failed: {e}[/red]")
2020
- raise typer.Exit(1)
2986
+ raise typer.Exit(1) from e
2021
2987
  return
2022
2988
 
2023
2989
  # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
@@ -2114,7 +3080,7 @@ def remove(
2114
3080
  console.print("\n[bold]Available platforms:[/bold]")
2115
3081
  for p in platform_mapping.keys():
2116
3082
  console.print(f" • {p}")
2117
- raise typer.Exit(1)
3083
+ raise typer.Exit(1) from None
2118
3084
 
2119
3085
  config = platform_mapping[platform]
2120
3086
 
@@ -2122,7 +3088,7 @@ def remove(
2122
3088
  config["func"]()
2123
3089
  except Exception as e:
2124
3090
  console.print(f"[red]Removal failed: {e}[/red]")
2125
- raise typer.Exit(1)
3091
+ raise typer.Exit(1) from e
2126
3092
 
2127
3093
 
2128
3094
  @app.command()
@@ -2161,7 +3127,7 @@ def uninstall(
2161
3127
 
2162
3128
 
2163
3129
  @app.command(deprecated=True, hidden=True)
2164
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
3130
+ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
2165
3131
  """Check status of a queued operation.
2166
3132
 
2167
3133
  DEPRECATED: Use 'mcp-ticketer ticket check' instead.
@@ -2174,7 +3140,7 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
2174
3140
 
2175
3141
  if not item:
2176
3142
  console.print(f"[red]Queue item not found: {queue_id}[/red]")
2177
- raise typer.Exit(1)
3143
+ raise typer.Exit(1) from None
2178
3144
 
2179
3145
  # Display status
2180
3146
  console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
@@ -2216,7 +3182,7 @@ def mcp_serve(
2216
3182
  base_path: str | None = typer.Option(
2217
3183
  None, "--base-path", help="Base path for AITrackdown adapter"
2218
3184
  ),
2219
- ):
3185
+ ) -> None:
2220
3186
  """Start MCP server for JSON-RPC communication over stdio.
2221
3187
 
2222
3188
  This command is used by Claude Code/Desktop when connecting to the MCP server.
@@ -2236,7 +3202,7 @@ def mcp_serve(
2236
3202
  # Load configuration (respects project-specific config in cwd)
2237
3203
  config = load_config()
2238
3204
 
2239
- # Determine adapter type with priority: CLI arg > .env files > config > default
3205
+ # Determine adapter type with priority: CLI arg > config > .env files > default
2240
3206
  if adapter:
2241
3207
  # Priority 1: Command line argument
2242
3208
  adapter_type = adapter.value
@@ -2244,18 +3210,24 @@ def mcp_serve(
2244
3210
  adapters_config = config.get("adapters", {})
2245
3211
  adapter_config = adapters_config.get(adapter_type, {})
2246
3212
  else:
2247
- # Priority 2: .env files
2248
- from ..mcp.server.main import _load_env_configuration
2249
-
2250
- env_config = _load_env_configuration()
2251
- if env_config:
2252
- adapter_type = env_config["adapter_type"]
2253
- adapter_config = env_config["adapter_config"]
2254
- else:
2255
- # Priority 3: Configuration file
2256
- adapter_type = config.get("default_adapter", "aitrackdown")
3213
+ # Priority 2: Configuration file (project-specific)
3214
+ adapter_type = config.get("default_adapter")
3215
+ if adapter_type:
2257
3216
  adapters_config = config.get("adapters", {})
2258
3217
  adapter_config = adapters_config.get(adapter_type, {})
3218
+ else:
3219
+ # Priority 3: .env files (auto-detection fallback)
3220
+ from ..mcp.server.main import _load_env_configuration
3221
+
3222
+ env_config = _load_env_configuration()
3223
+ if env_config:
3224
+ adapter_type = env_config["adapter_type"]
3225
+ adapter_config = env_config["adapter_config"]
3226
+ else:
3227
+ # Priority 4: Default fallback
3228
+ adapter_type = "aitrackdown"
3229
+ adapters_config = config.get("adapters", {})
3230
+ adapter_config = adapters_config.get(adapter_type, {})
2259
3231
 
2260
3232
  # Override with command line options if provided (highest priority)
2261
3233
  if base_path and adapter_type == "aitrackdown":
@@ -2305,7 +3277,7 @@ def mcp_claude(
2305
3277
  force: bool = typer.Option(
2306
3278
  False, "--force", "-f", help="Overwrite existing configuration"
2307
3279
  ),
2308
- ):
3280
+ ) -> None:
2309
3281
  """Configure Claude Code to use mcp-ticketer MCP server.
2310
3282
 
2311
3283
  Reads configuration from .mcp-ticketer/config.json and updates
@@ -2331,7 +3303,7 @@ def mcp_claude(
2331
3303
  configure_claude_mcp(global_config=global_config, force=force)
2332
3304
  except Exception as e:
2333
3305
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2334
- raise typer.Exit(1)
3306
+ raise typer.Exit(1) from e
2335
3307
 
2336
3308
 
2337
3309
  @mcp_app.command(name="gemini")
@@ -2345,7 +3317,7 @@ def mcp_gemini(
2345
3317
  force: bool = typer.Option(
2346
3318
  False, "--force", "-f", help="Overwrite existing configuration"
2347
3319
  ),
2348
- ):
3320
+ ) -> None:
2349
3321
  """Configure Gemini CLI to use mcp-ticketer MCP server.
2350
3322
 
2351
3323
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2372,13 +3344,13 @@ def mcp_gemini(
2372
3344
  console.print(
2373
3345
  f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
2374
3346
  )
2375
- raise typer.Exit(1)
3347
+ raise typer.Exit(1) from None
2376
3348
 
2377
3349
  try:
2378
3350
  configure_gemini_mcp(scope=scope, force=force) # type: ignore
2379
3351
  except Exception as e:
2380
3352
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2381
- raise typer.Exit(1)
3353
+ raise typer.Exit(1) from e
2382
3354
 
2383
3355
 
2384
3356
  @mcp_app.command(name="codex")
@@ -2386,7 +3358,7 @@ def mcp_codex(
2386
3358
  force: bool = typer.Option(
2387
3359
  False, "--force", "-f", help="Overwrite existing configuration"
2388
3360
  ),
2389
- ):
3361
+ ) -> None:
2390
3362
  """Configure Codex CLI to use mcp-ticketer MCP server.
2391
3363
 
2392
3364
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2410,7 +3382,7 @@ def mcp_codex(
2410
3382
  configure_codex_mcp(force=force)
2411
3383
  except Exception as e:
2412
3384
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2413
- raise typer.Exit(1)
3385
+ raise typer.Exit(1) from e
2414
3386
 
2415
3387
 
2416
3388
  @mcp_app.command(name="auggie")
@@ -2418,7 +3390,7 @@ def mcp_auggie(
2418
3390
  force: bool = typer.Option(
2419
3391
  False, "--force", "-f", help="Overwrite existing configuration"
2420
3392
  ),
2421
- ):
3393
+ ) -> None:
2422
3394
  """Configure Auggie CLI to use mcp-ticketer MCP server.
2423
3395
 
2424
3396
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2442,11 +3414,11 @@ def mcp_auggie(
2442
3414
  configure_auggie_mcp(force=force)
2443
3415
  except Exception as e:
2444
3416
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2445
- raise typer.Exit(1)
3417
+ raise typer.Exit(1) from e
2446
3418
 
2447
3419
 
2448
3420
  @mcp_app.command(name="status")
2449
- def mcp_status():
3421
+ def mcp_status() -> None:
2450
3422
  """Check MCP server status.
2451
3423
 
2452
3424
  Shows whether the MCP server is configured and running for various platforms.
@@ -2544,7 +3516,7 @@ def mcp_status():
2544
3516
 
2545
3517
 
2546
3518
  @mcp_app.command(name="stop")
2547
- def mcp_stop():
3519
+ def mcp_stop() -> None:
2548
3520
  """Stop MCP server (placeholder - MCP runs on-demand via stdio).
2549
3521
 
2550
3522
  Note: The MCP server runs on-demand when AI clients connect via stdio.
@@ -2568,8 +3540,8 @@ def mcp_stop():
2568
3540
  app.add_typer(mcp_app, name="mcp")
2569
3541
 
2570
3542
 
2571
- def main():
2572
- """Main entry point."""
3543
+ def main() -> None:
3544
+ """Execute the main CLI application entry point."""
2573
3545
  app()
2574
3546
 
2575
3547