mcp-ticketer 0.3.5__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 (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  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 +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,7 +5,7 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Optional
8
+ from typing import Any
9
9
 
10
10
  import typer
11
11
  from dotenv import load_dotenv
@@ -24,9 +24,11 @@ from ..queue.ticket_registry import TicketRegistry
24
24
  from .configure import configure_wizard, set_adapter_config, show_current_config
25
25
  from .diagnostics import run_diagnostics
26
26
  from .discover import app as discover_app
27
- from .linear_commands import app as linear_app
27
+ from .instruction_commands import app as instruction_app
28
28
  from .migrate_config import migrate_config_command
29
+ from .platform_commands import app as platform_app
29
30
  from .queue_commands import app as queue_app
31
+ from .ticket_commands import app as ticket_app
30
32
 
31
33
  # Load environment variables from .env files
32
34
  # Priority: .env.local (highest) > .env (base)
@@ -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
 
@@ -83,7 +85,7 @@ class AdapterType(str, Enum):
83
85
  GITHUB = "github"
84
86
 
85
87
 
86
- def load_config(project_dir: Optional[Path] = None) -> dict:
88
+ def load_config(project_dir: Path | None = None) -> dict:
87
89
  """Load configuration from project-local config file ONLY.
88
90
 
89
91
  SECURITY: This method ONLY reads from the current project directory
@@ -145,7 +147,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
145
147
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
146
148
 
147
149
 
148
- def _discover_from_env_files() -> Optional[str]:
150
+ def _discover_from_env_files() -> str | None:
149
151
  """Discover adapter configuration from .env or .env.local files.
150
152
 
151
153
  Returns:
@@ -269,8 +271,8 @@ def merge_config(updates: dict) -> dict:
269
271
 
270
272
 
271
273
  def get_adapter(
272
- override_adapter: Optional[str] = None, override_config: Optional[dict] = None
273
- ):
274
+ override_adapter: str | None = None, override_config: dict | None = None
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,172 +793,442 @@ 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: Optional[str] = typer.Option(
390
- None,
391
- "--adapter",
392
- "-a",
393
- help="Adapter type to use (interactive prompt if not specified)",
394
- ),
395
- project_path: Optional[str] = typer.Option(
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",
403
- ),
404
- base_path: Optional[str] = typer.Option(
405
- None,
406
- "--base-path",
407
- "-p",
408
- help="Base path for ticket storage (AITrackdown only)",
409
- ),
410
- api_key: Optional[str] = typer.Option(
411
- None, "--api-key", help="API key for Linear or API token for JIRA"
412
- ),
413
- team_id: Optional[str] = typer.Option(
414
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
415
- ),
416
- jira_server: Optional[str] = typer.Option(
417
- None,
418
- "--jira-server",
419
- help="JIRA server URL (e.g., https://company.atlassian.net)",
420
- ),
421
- jira_email: Optional[str] = typer.Option(
422
- None, "--jira-email", help="JIRA user email for authentication"
423
- ),
424
- jira_project: Optional[str] = typer.Option(
425
- None, "--jira-project", help="Default JIRA project key"
426
- ),
427
- github_owner: Optional[str] = typer.Option(
428
- None, "--github-owner", help="GitHub repository owner"
429
- ),
430
- github_repo: Optional[str] = typer.Option(
431
- None, "--github-repo", help="GitHub repository name"
806
+ "--skip-platforms",
807
+ help="Skip platform installation (only initialize adapter)",
432
808
  ),
433
- github_token: Optional[str] = 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
 
453
- """
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,
468
- )
833
+ # Re-initialize configuration
834
+ mcp-ticketer setup --force-reinit
469
835
 
836
+ # Only init adapter, skip platform installation
837
+ mcp-ticketer setup --skip-platforms
470
838
 
471
- @app.command()
472
- def init(
473
- adapter: Optional[str] = typer.Option(
474
- None,
475
- "--adapter",
476
- "-a",
477
- help="Adapter type to use (interactive prompt if not specified)",
478
- ),
479
- project_path: Optional[str] = typer.Option(
480
- None, "--path", help="Project path (default: current directory)"
481
- ),
482
- global_config: bool = typer.Option(
483
- False,
484
- "--global",
485
- "-g",
486
- help="Save to global config instead of project-specific",
487
- ),
488
- base_path: Optional[str] = typer.Option(
489
- None,
490
- "--base-path",
491
- "-p",
492
- help="Base path for ticket storage (AITrackdown only)",
493
- ),
494
- api_key: Optional[str] = typer.Option(
495
- None, "--api-key", help="API key for Linear or API token for JIRA"
496
- ),
497
- team_id: Optional[str] = typer.Option(
498
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
499
- ),
500
- jira_server: Optional[str] = typer.Option(
501
- None,
502
- "--jira-server",
503
- help="JIRA server URL (e.g., https://company.atlassian.net)",
504
- ),
505
- jira_email: Optional[str] = typer.Option(
506
- None, "--jira-email", help="JIRA user email for authentication"
507
- ),
508
- jira_project: Optional[str] = typer.Option(
509
- None, "--jira-project", help="Default JIRA project key"
510
- ),
511
- github_owner: Optional[str] = typer.Option(
512
- None, "--github-owner", help="GitHub repository owner"
513
- ),
514
- github_repo: Optional[str] = typer.Option(
515
- None, "--github-repo", help="GitHub repository name"
516
- ),
517
- github_token: Optional[str] = typer.Option(
518
- None, "--github-token", help="GitHub Personal Access Token"
519
- ),
520
- ) -> None:
521
- """Initialize mcp-ticketer for the current project.
839
+ Note: For advanced configuration, use 'init' and 'install' separately.
522
840
 
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.
841
+ """
842
+ from .platform_detection import PlatformDetector
526
843
 
527
- Creates .mcp-ticketer/config.json in the current directory with
528
- auto-detected or specified adapter configuration.
844
+ proj_path = Path(project_path) if project_path else Path.cwd()
845
+ config_path = proj_path / ".mcp-ticketer" / "config.json"
529
846
 
530
- Note: 'setup' and 'install' are synonyms for this command.
847
+ console.print("[bold cyan]🚀 MCP Ticketer Smart Setup[/bold cyan]\n")
531
848
 
532
- Examples:
533
- # Interactive setup (same as 'setup' and 'install')
534
- mcp-ticketer init
849
+ # Step 1: Detect existing configuration
850
+ config_exists = config_path.exists()
851
+ config_valid = False
852
+ current_adapter = None
535
853
 
536
- # Force specific adapter
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]"
1144
+ )
1145
+
1146
+
1147
+ @app.command()
1148
+ def init(
1149
+ adapter: str | None = typer.Option(
1150
+ None,
1151
+ "--adapter",
1152
+ "-a",
1153
+ help="Adapter type to use (interactive prompt if not specified)",
1154
+ ),
1155
+ project_path: str | None = typer.Option(
1156
+ None, "--path", help="Project path (default: current directory)"
1157
+ ),
1158
+ global_config: bool = typer.Option(
1159
+ False,
1160
+ "--global",
1161
+ "-g",
1162
+ help="Save to global config instead of project-specific",
1163
+ ),
1164
+ base_path: str | None = typer.Option(
1165
+ None,
1166
+ "--base-path",
1167
+ "-p",
1168
+ help="Base path for ticket storage (AITrackdown only)",
1169
+ ),
1170
+ api_key: str | None = typer.Option(
1171
+ None, "--api-key", help="API key for Linear or API token for JIRA"
1172
+ ),
1173
+ team_id: str | None = typer.Option(
1174
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
1175
+ ),
1176
+ jira_server: str | None = typer.Option(
1177
+ None,
1178
+ "--jira-server",
1179
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
1180
+ ),
1181
+ jira_email: str | None = typer.Option(
1182
+ None, "--jira-email", help="JIRA user email for authentication"
1183
+ ),
1184
+ jira_project: str | None = typer.Option(
1185
+ None, "--jira-project", help="Default JIRA project key"
1186
+ ),
1187
+ github_owner: str | None = typer.Option(
1188
+ None, "--github-owner", help="GitHub repository owner"
1189
+ ),
1190
+ github_repo: str | None = typer.Option(
1191
+ None, "--github-repo", help="GitHub repository name"
1192
+ ),
1193
+ github_token: str | None = typer.Option(
1194
+ None, "--github-token", help="GitHub Personal Access Token"
1195
+ ),
1196
+ ) -> None:
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.
1202
+
1203
+ Creates .mcp-ticketer/config.json in the current directory.
1204
+
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.
1208
+
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'
1214
+
1215
+ Examples:
1216
+ # For first-time setup, use 'setup' instead (recommended)
1217
+ mcp-ticketer setup
1218
+
1219
+ # Initialize adapter only (advanced usage)
1220
+ mcp-ticketer init
1221
+
1222
+ # Force specific adapter
537
1223
  mcp-ticketer init --adapter linear
538
1224
 
539
1225
  # Initialize for different project
540
1226
  mcp-ticketer init --path /path/to/project
541
1227
 
542
- # Save globally (not recommended)
543
- mcp-ticketer init --global
544
-
545
1228
  """
546
1229
  from pathlib import Path
547
1230
 
548
1231
  from ..core.env_discovery import discover_config
549
- from ..core.project_config import ConfigResolver
550
1232
 
551
1233
  # Determine project path
552
1234
  proj_path = Path(project_path) if project_path else Path.cwd()
@@ -561,7 +1243,7 @@ def init(
561
1243
  default=False,
562
1244
  ):
563
1245
  console.print("[yellow]Initialization cancelled.[/yellow]")
564
- raise typer.Exit(0)
1246
+ raise typer.Exit(0) from None
565
1247
 
566
1248
  # 1. Try auto-discovery if no adapter specified
567
1249
  discovered = None
@@ -573,7 +1255,7 @@ def init(
573
1255
  )
574
1256
 
575
1257
  # First try our improved .env configuration loader
576
- from ..mcp.server import _load_env_configuration
1258
+ from ..mcp.server.main import _load_env_configuration
577
1259
 
578
1260
  env_config = _load_env_configuration()
579
1261
 
@@ -650,11 +1332,9 @@ def init(
650
1332
  elif adapter_type == "linear":
651
1333
  # If not auto-discovered, build from CLI params or prompt
652
1334
  if adapter_type not in config["adapters"]:
653
- linear_config = {}
654
-
655
1335
  # API Key
656
1336
  linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
657
- if not linear_api_key and not discovered:
1337
+ if not linear_api_key:
658
1338
  console.print("\n[bold]Linear Configuration[/bold]")
659
1339
  console.print("You need a Linear API key to connect to Linear.")
660
1340
  console.print(
@@ -665,30 +1345,81 @@ def init(
665
1345
  "Enter your Linear API key", hide_input=True
666
1346
  )
667
1347
 
668
- if linear_api_key:
669
- linear_config["api_key"] = linear_api_key
670
-
671
- # Team ID
1348
+ # Team ID or Team Key or Team URL
1349
+ # Try environment variables first
1350
+ linear_team_key = os.getenv("LINEAR_TEAM_KEY")
672
1351
  linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
673
- if not linear_team_id and not discovered:
674
- console.print("\nYou need your Linear team ID.")
675
- console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
676
-
677
- linear_team_id = typer.prompt("Enter your Linear team ID")
678
-
679
- if linear_team_id:
680
- linear_config["team_id"] = linear_team_id
681
1352
 
682
- if not linear_config.get("api_key") or not linear_config.get("team_id"):
1353
+ if not linear_team_key and not linear_team_id:
1354
+ console.print("\n[bold]Linear Team Configuration[/bold]")
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)")
683
1361
  console.print(
684
- "[red]Error:[/red] Linear requires both API key and team ID"
1362
+ "[dim]Find team URL or key in: Linear Your Team Team Issues Page[/dim]\n"
685
1363
  )
1364
+
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:
686
1406
  console.print(
687
- "Run 'mcp-ticketer init --adapter linear' with proper credentials"
1407
+ "[red]Error:[/red] Linear requires either team ID or team key"
688
1408
  )
689
- raise typer.Exit(1)
1409
+ raise typer.Exit(1) from None
1410
+
1411
+ # Build configuration
1412
+ linear_config = {
1413
+ "api_key": linear_api_key,
1414
+ "type": "linear",
1415
+ }
1416
+
1417
+ # Save whichever was provided
1418
+ if linear_team_key:
1419
+ linear_config["team_key"] = linear_team_key
1420
+ if linear_team_id:
1421
+ linear_config["team_id"] = linear_team_id
690
1422
 
691
- linear_config["type"] = "linear"
692
1423
  config["adapters"]["linear"] = linear_config
693
1424
 
694
1425
  elif adapter_type == "jira":
@@ -700,7 +1431,7 @@ def init(
700
1431
  project = jira_project or os.getenv("JIRA_PROJECT_KEY")
701
1432
 
702
1433
  # Interactive prompts for missing values
703
- if not server and not discovered:
1434
+ if not server:
704
1435
  console.print("\n[bold]JIRA Configuration[/bold]")
705
1436
  console.print("Enter your JIRA server details.\n")
706
1437
 
@@ -708,10 +1439,10 @@ def init(
708
1439
  "JIRA server URL (e.g., https://company.atlassian.net)"
709
1440
  )
710
1441
 
711
- if not email and not discovered:
1442
+ if not email:
712
1443
  email = typer.prompt("Your JIRA email address")
713
1444
 
714
- if not token and not discovered:
1445
+ if not token:
715
1446
  console.print("\nYou need a JIRA API token.")
716
1447
  console.print(
717
1448
  "[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
@@ -719,7 +1450,7 @@ def init(
719
1450
 
720
1451
  token = typer.prompt("Enter your JIRA API token", hide_input=True)
721
1452
 
722
- if not project and not discovered:
1453
+ if not project:
723
1454
  project = typer.prompt(
724
1455
  "Default JIRA project key (optional, press Enter to skip)",
725
1456
  default="",
@@ -729,15 +1460,15 @@ def init(
729
1460
  # Validate required fields
730
1461
  if not server:
731
1462
  console.print("[red]Error:[/red] JIRA server URL is required")
732
- raise typer.Exit(1)
1463
+ raise typer.Exit(1) from None
733
1464
 
734
1465
  if not email:
735
1466
  console.print("[red]Error:[/red] JIRA email is required")
736
- raise typer.Exit(1)
1467
+ raise typer.Exit(1) from None
737
1468
 
738
1469
  if not token:
739
1470
  console.print("[red]Error:[/red] JIRA API token is required")
740
- raise typer.Exit(1)
1471
+ raise typer.Exit(1) from None
741
1472
 
742
1473
  jira_config = {
743
1474
  "server": server,
@@ -759,7 +1490,7 @@ def init(
759
1490
  token = github_token or os.getenv("GITHUB_TOKEN")
760
1491
 
761
1492
  # Interactive prompts for missing values
762
- if not owner and not discovered:
1493
+ if not owner:
763
1494
  console.print("\n[bold]GitHub Configuration[/bold]")
764
1495
  console.print("Enter your GitHub repository details.\n")
765
1496
 
@@ -767,10 +1498,10 @@ def init(
767
1498
  "GitHub repository owner (username or organization)"
768
1499
  )
769
1500
 
770
- if not repo and not discovered:
1501
+ if not repo:
771
1502
  repo = typer.prompt("GitHub repository name")
772
1503
 
773
- if not token and not discovered:
1504
+ if not token:
774
1505
  console.print("\nYou need a GitHub Personal Access Token.")
775
1506
  console.print(
776
1507
  "[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
@@ -786,17 +1517,17 @@ def init(
786
1517
  # Validate required fields
787
1518
  if not owner:
788
1519
  console.print("[red]Error:[/red] GitHub repository owner is required")
789
- raise typer.Exit(1)
1520
+ raise typer.Exit(1) from None
790
1521
 
791
1522
  if not repo:
792
1523
  console.print("[red]Error:[/red] GitHub repository name is required")
793
- raise typer.Exit(1)
1524
+ raise typer.Exit(1) from None
794
1525
 
795
1526
  if not token:
796
1527
  console.print(
797
1528
  "[red]Error:[/red] GitHub Personal Access Token is required"
798
1529
  )
799
- raise typer.Exit(1)
1530
+ raise typer.Exit(1) from None
800
1531
 
801
1532
  config["adapters"]["github"] = {
802
1533
  "owner": owner,
@@ -805,42 +1536,46 @@ def init(
805
1536
  "type": "github",
806
1537
  }
807
1538
 
808
- # 5. Save to appropriate location
809
- if global_config:
810
- # Save to ~/.mcp-ticketer/config.json
811
- resolver = ConfigResolver(project_path=proj_path)
812
- config_file_path = resolver.GLOBAL_CONFIG_PATH
813
- 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)
1543
+
1544
+ with open(config_file_path, "w") as f:
1545
+ json.dump(config, f, indent=2)
814
1546
 
815
- with open(config_file_path, "w") as f:
816
- json.dump(config, f, indent=2)
1547
+ if global_config:
1548
+ console.print(
1549
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
1550
+ )
817
1551
 
818
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
819
- 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]")
820
1563
  else:
821
- # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
822
- config_file_path = proj_path / ".mcp-ticketer" / "config.json"
823
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
824
-
825
- with open(config_file_path, "w") as f:
826
- json.dump(config, f, indent=2)
827
-
828
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
829
- console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
830
-
831
- # Add .mcp-ticketer to .gitignore if not already there
832
- gitignore_path = proj_path / ".gitignore"
833
- if gitignore_path.exists():
834
- gitignore_content = gitignore_path.read_text()
835
- if ".mcp-ticketer" not in gitignore_content:
836
- with open(gitignore_path, "a") as f:
837
- f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
838
- console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
839
- else:
840
- # Create .gitignore if it doesn't exist
841
- with open(gitignore_path, "w") as f:
842
- f.write("# MCP Ticketer\n.mcp-ticketer/\n")
843
- 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
844
1579
 
845
1580
  # Show next steps
846
1581
  _show_next_steps(console, adapter_type, config_file_path)
@@ -861,16 +1596,13 @@ def _show_next_steps(
861
1596
  console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
862
1597
 
863
1598
  console.print("[bold]Next Steps:[/bold]")
864
- console.print("1. [cyan]Test your configuration:[/cyan]")
865
- console.print(" mcp-ticketer diagnose")
866
- console.print("\n2. [cyan]Create a test ticket:[/cyan]")
1599
+ console.print("1. [cyan]Create a test ticket:[/cyan]")
867
1600
  console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
868
1601
 
869
1602
  if adapter_type != "aitrackdown":
870
1603
  console.print(
871
- 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]"
872
1605
  )
873
-
874
1606
  if adapter_type == "linear":
875
1607
  console.print(" Check your Linear workspace for the new ticket")
876
1608
  elif adapter_type == "github":
@@ -878,121 +1610,36 @@ def _show_next_steps(
878
1610
  elif adapter_type == "jira":
879
1611
  console.print(" Check your JIRA project for the new ticket")
880
1612
  else:
881
- console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
1613
+ console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
882
1614
  console.print(" ls .aitrackdown/")
883
1615
 
884
- console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
885
- console.print(" mcp-ticketer mcp claude # For Claude Code")
886
- console.print(" mcp-ticketer mcp auggie # For Auggie")
887
- console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
1616
+ console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
1617
+ console.print(" mcp-ticketer install claude-code # For Claude Code")
1618
+ console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
1619
+ console.print(" mcp-ticketer install auggie # For Auggie")
1620
+ console.print(" mcp-ticketer install gemini # For Gemini CLI")
888
1621
 
889
1622
  console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
890
- console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
891
-
892
-
893
- @app.command()
894
- def install(
895
- adapter: Optional[str] = typer.Option(
896
- None,
897
- "--adapter",
898
- "-a",
899
- help="Adapter type to use (auto-detected from .env if not specified)",
900
- ),
901
- project_path: Optional[str] = typer.Option(
902
- None, "--path", help="Project path (default: current directory)"
903
- ),
904
- global_config: bool = typer.Option(
905
- False,
906
- "--global",
907
- "-g",
908
- help="Save to global config instead of project-specific",
909
- ),
910
- base_path: Optional[str] = typer.Option(
911
- None,
912
- "--base-path",
913
- "-p",
914
- help="Base path for ticket storage (AITrackdown only)",
915
- ),
916
- api_key: Optional[str] = typer.Option(
917
- None, "--api-key", help="API key for Linear or API token for JIRA"
918
- ),
919
- team_id: Optional[str] = typer.Option(
920
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
921
- ),
922
- jira_server: Optional[str] = typer.Option(
923
- None,
924
- "--jira-server",
925
- help="JIRA server URL (e.g., https://company.atlassian.net)",
926
- ),
927
- jira_email: Optional[str] = typer.Option(
928
- None, "--jira-email", help="JIRA user email for authentication"
929
- ),
930
- jira_project: Optional[str] = typer.Option(
931
- None, "--jira-project", help="Default JIRA project key"
932
- ),
933
- github_owner: Optional[str] = typer.Option(
934
- None, "--github-owner", help="GitHub repository owner"
935
- ),
936
- github_repo: Optional[str] = typer.Option(
937
- None, "--github-repo", help="GitHub repository name"
938
- ),
939
- github_token: Optional[str] = typer.Option(
940
- None, "--github-token", help="GitHub Personal Access Token"
941
- ),
942
- ) -> None:
943
- """Initialize mcp-ticketer for the current project (alias for init).
944
-
945
- This command is synonymous with 'init' and 'setup' - all three provide
946
- identical functionality with interactive prompts to guide you through
947
- configuring MCP Ticketer for your preferred ticket management system.
948
-
949
- Examples:
950
- # Interactive setup (same as 'init' and 'setup')
951
- mcp-ticketer install
952
-
953
- # Force specific adapter
954
- mcp-ticketer install --adapter linear
955
-
956
- # Initialize for different project
957
- mcp-ticketer install --path /path/to/project
958
-
959
- # Save globally (not recommended)
960
- mcp-ticketer install --global
961
-
962
- """
963
- # Call init with all parameters
964
- init(
965
- adapter=adapter,
966
- project_path=project_path,
967
- global_config=global_config,
968
- base_path=base_path,
969
- api_key=api_key,
970
- team_id=team_id,
971
- jira_server=jira_server,
972
- jira_email=jira_email,
973
- jira_project=jira_project,
974
- github_owner=github_owner,
975
- github_repo=github_repo,
976
- github_token=github_token,
1623
+ console.print(
1624
+ "[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
977
1625
  )
1626
+ console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
978
1627
 
979
1628
 
980
1629
  @app.command("set")
981
1630
  def set_config(
982
- adapter: Optional[AdapterType] = typer.Option(
1631
+ adapter: AdapterType | None = typer.Option(
983
1632
  None, "--adapter", "-a", help="Set default adapter"
984
1633
  ),
985
- team_key: Optional[str] = typer.Option(
1634
+ team_key: str | None = typer.Option(
986
1635
  None, "--team-key", help="Linear team key (e.g., BTA)"
987
1636
  ),
988
- team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
989
- owner: Optional[str] = typer.Option(
990
- None, "--owner", help="GitHub repository owner"
991
- ),
992
- repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
993
- server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
994
- project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
995
- base_path: Optional[str] = typer.Option(
1637
+ team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
1638
+ owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
1639
+ repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
1640
+ server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
1641
+ project: str | None = typer.Option(None, "--project", help="JIRA project key"),
1642
+ base_path: str | None = typer.Option(
996
1643
  None, "--base-path", help="AITrackdown base path"
997
1644
  ),
998
1645
  ) -> None:
@@ -1082,16 +1729,12 @@ def set_config(
1082
1729
  @app.command("configure")
1083
1730
  def configure_command(
1084
1731
  show: bool = typer.Option(False, "--show", help="Show current configuration"),
1085
- adapter: Optional[str] = typer.Option(
1732
+ adapter: str | None = typer.Option(
1086
1733
  None, "--adapter", help="Set default adapter type"
1087
1734
  ),
1088
- api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
1089
- project_id: Optional[str] = typer.Option(
1090
- None, "--project-id", help="Set project ID"
1091
- ),
1092
- team_id: Optional[str] = typer.Option(
1093
- None, "--team-id", help="Set team ID (Linear)"
1094
- ),
1735
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
1736
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
1737
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
1095
1738
  global_scope: bool = typer.Option(
1096
1739
  False,
1097
1740
  "--global",
@@ -1142,9 +1785,16 @@ def migrate_config(
1142
1785
  migrate_config_command(dry_run=dry_run)
1143
1786
 
1144
1787
 
1145
- @app.command("status")
1146
- def status_command():
1147
- """Show queue and worker status."""
1788
+ @app.command("queue-status", deprecated=True, hidden=True)
1789
+ def old_queue_status_command() -> None:
1790
+ """Show queue and worker status.
1791
+
1792
+ DEPRECATED: Use 'mcp-ticketer queue status' instead.
1793
+ """
1794
+ console.print(
1795
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n"
1796
+ )
1797
+
1148
1798
  queue = Queue()
1149
1799
  manager = WorkerManager()
1150
1800
 
@@ -1169,12 +1819,12 @@ def status_command():
1169
1819
  console.print("\n[red]○ Worker is not running[/red]")
1170
1820
  if pending > 0:
1171
1821
  console.print(
1172
- "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
1822
+ "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
1173
1823
  )
1174
1824
 
1175
1825
 
1176
- @app.command()
1177
- def health(
1826
+ @app.command("queue-health", deprecated=True, hidden=True)
1827
+ def old_queue_health_command(
1178
1828
  auto_repair: bool = typer.Option(
1179
1829
  False, "--auto-repair", help="Attempt automatic repair of issues"
1180
1830
  ),
@@ -1182,7 +1832,13 @@ def health(
1182
1832
  False, "--verbose", "-v", help="Show detailed health information"
1183
1833
  ),
1184
1834
  ) -> None:
1185
- """Check queue system health and detect issues immediately."""
1835
+ """Check queue system health and detect issues immediately.
1836
+
1837
+ DEPRECATED: Use 'mcp-ticketer queue health' instead.
1838
+ """
1839
+ console.print(
1840
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n"
1841
+ )
1186
1842
  health_monitor = QueueHealthMonitor()
1187
1843
  health = health_monitor.check_health()
1188
1844
 
@@ -1246,41 +1902,48 @@ def health(
1246
1902
 
1247
1903
  # Exit with appropriate code
1248
1904
  if health["status"] == HealthStatus.CRITICAL:
1249
- raise typer.Exit(1)
1905
+ raise typer.Exit(1) from None
1250
1906
  elif health["status"] == HealthStatus.WARNING:
1251
- raise typer.Exit(2)
1907
+ raise typer.Exit(2) from None
1252
1908
 
1253
1909
 
1254
- @app.command()
1910
+ @app.command(deprecated=True, hidden=True)
1255
1911
  def create(
1256
1912
  title: str = typer.Argument(..., help="Ticket title"),
1257
- description: Optional[str] = typer.Option(
1913
+ description: str | None = typer.Option(
1258
1914
  None, "--description", "-d", help="Ticket description"
1259
1915
  ),
1260
1916
  priority: Priority = typer.Option(
1261
1917
  Priority.MEDIUM, "--priority", "-p", help="Priority level"
1262
1918
  ),
1263
- tags: Optional[list[str]] = typer.Option(
1919
+ tags: list[str] | None = typer.Option(
1264
1920
  None, "--tag", "-t", help="Tags (can be specified multiple times)"
1265
1921
  ),
1266
- assignee: Optional[str] = typer.Option(
1922
+ assignee: str | None = typer.Option(
1267
1923
  None, "--assignee", "-a", help="Assignee username"
1268
1924
  ),
1269
- project: Optional[str] = typer.Option(
1925
+ project: str | None = typer.Option(
1270
1926
  None,
1271
1927
  "--project",
1272
1928
  help="Parent project/epic ID (synonym for --epic)",
1273
1929
  ),
1274
- epic: Optional[str] = typer.Option(
1930
+ epic: str | None = typer.Option(
1275
1931
  None,
1276
1932
  "--epic",
1277
1933
  help="Parent epic/project ID (synonym for --project)",
1278
1934
  ),
1279
- adapter: Optional[AdapterType] = typer.Option(
1935
+ adapter: AdapterType | None = typer.Option(
1280
1936
  None, "--adapter", help="Override default adapter"
1281
1937
  ),
1282
1938
  ) -> None:
1283
- """Create a new ticket with comprehensive health checks."""
1939
+ """Create a new ticket with comprehensive health checks.
1940
+
1941
+ DEPRECATED: Use 'mcp-ticketer ticket create' instead.
1942
+ """
1943
+ console.print(
1944
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n"
1945
+ )
1946
+
1284
1947
  # IMMEDIATE HEALTH CHECK - Critical for reliability
1285
1948
  health_monitor = QueueHealthMonitor()
1286
1949
  health = health_monitor.check_health()
@@ -1309,7 +1972,7 @@ def create(
1309
1972
  console.print(
1310
1973
  "[red]Cannot safely create ticket. Please check system status.[/red]"
1311
1974
  )
1312
- raise typer.Exit(1)
1975
+ raise typer.Exit(1) from None
1313
1976
  else:
1314
1977
  console.print(
1315
1978
  "[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
@@ -1318,7 +1981,7 @@ def create(
1318
1981
  console.print(
1319
1982
  "[red]❌ No repair actions available. Manual intervention required.[/red]"
1320
1983
  )
1321
- raise typer.Exit(1)
1984
+ raise typer.Exit(1) from None
1322
1985
 
1323
1986
  elif health["status"] == HealthStatus.WARNING:
1324
1987
  console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
@@ -1476,22 +2139,28 @@ def create(
1476
2139
  )
1477
2140
 
1478
2141
 
1479
- @app.command("list")
2142
+ @app.command("list", deprecated=True, hidden=True)
1480
2143
  def list_tickets(
1481
- state: Optional[TicketState] = typer.Option(
2144
+ state: TicketState | None = typer.Option(
1482
2145
  None, "--state", "-s", help="Filter by state"
1483
2146
  ),
1484
- priority: Optional[Priority] = typer.Option(
2147
+ priority: Priority | None = typer.Option(
1485
2148
  None, "--priority", "-p", help="Filter by priority"
1486
2149
  ),
1487
2150
  limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1488
- adapter: Optional[AdapterType] = typer.Option(
2151
+ adapter: AdapterType | None = typer.Option(
1489
2152
  None, "--adapter", help="Override default adapter"
1490
2153
  ),
1491
2154
  ) -> None:
1492
- """List tickets with optional filters."""
2155
+ """List tickets with optional filters.
1493
2156
 
1494
- async def _list():
2157
+ DEPRECATED: Use 'mcp-ticketer ticket list' instead.
2158
+ """
2159
+ console.print(
2160
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
2161
+ )
2162
+
2163
+ async def _list() -> None:
1495
2164
  adapter_instance = get_adapter(
1496
2165
  override_adapter=adapter.value if adapter else None
1497
2166
  )
@@ -1531,17 +2200,23 @@ def list_tickets(
1531
2200
  console.print(table)
1532
2201
 
1533
2202
 
1534
- @app.command()
2203
+ @app.command(deprecated=True, hidden=True)
1535
2204
  def show(
1536
2205
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1537
2206
  comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1538
- adapter: Optional[AdapterType] = typer.Option(
2207
+ adapter: AdapterType | None = typer.Option(
1539
2208
  None, "--adapter", help="Override default adapter"
1540
2209
  ),
1541
2210
  ) -> None:
1542
- """Show detailed ticket information."""
2211
+ """Show detailed ticket information.
1543
2212
 
1544
- async def _show():
2213
+ DEPRECATED: Use 'mcp-ticketer ticket show' instead.
2214
+ """
2215
+ console.print(
2216
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
2217
+ )
2218
+
2219
+ async def _show() -> None:
1545
2220
  adapter_instance = get_adapter(
1546
2221
  override_adapter=adapter.value if adapter else None
1547
2222
  )
@@ -1555,7 +2230,7 @@ def show(
1555
2230
 
1556
2231
  if not ticket:
1557
2232
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
1558
- raise typer.Exit(1)
2233
+ raise typer.Exit(1) from None
1559
2234
 
1560
2235
  # Display ticket details
1561
2236
  console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
@@ -1581,17 +2256,23 @@ def show(
1581
2256
  console.print(comment.content)
1582
2257
 
1583
2258
 
1584
- @app.command()
2259
+ @app.command(deprecated=True, hidden=True)
1585
2260
  def comment(
1586
2261
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1587
2262
  content: str = typer.Argument(..., help="Comment content"),
1588
- adapter: Optional[AdapterType] = typer.Option(
2263
+ adapter: AdapterType | None = typer.Option(
1589
2264
  None, "--adapter", help="Override default adapter"
1590
2265
  ),
1591
2266
  ) -> None:
1592
- """Add a comment to a ticket."""
2267
+ """Add a comment to a ticket.
2268
+
2269
+ DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
2270
+ """
2271
+ console.print(
2272
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
2273
+ )
1593
2274
 
1594
- async def _comment():
2275
+ async def _comment() -> None:
1595
2276
  adapter_instance = get_adapter(
1596
2277
  override_adapter=adapter.value if adapter else None
1597
2278
  )
@@ -1614,27 +2295,31 @@ def comment(
1614
2295
  console.print(f"Content: {content}")
1615
2296
  except Exception as e:
1616
2297
  console.print(f"[red]✗[/red] Failed to add comment: {e}")
1617
- raise typer.Exit(1)
2298
+ raise typer.Exit(1) from e
1618
2299
 
1619
2300
 
1620
- @app.command()
2301
+ @app.command(deprecated=True, hidden=True)
1621
2302
  def update(
1622
2303
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1623
- title: Optional[str] = typer.Option(None, "--title", help="New title"),
1624
- description: Optional[str] = typer.Option(
2304
+ title: str | None = typer.Option(None, "--title", help="New title"),
2305
+ description: str | None = typer.Option(
1625
2306
  None, "--description", "-d", help="New description"
1626
2307
  ),
1627
- priority: Optional[Priority] = typer.Option(
2308
+ priority: Priority | None = typer.Option(
1628
2309
  None, "--priority", "-p", help="New priority"
1629
2310
  ),
1630
- assignee: Optional[str] = typer.Option(
1631
- None, "--assignee", "-a", help="New assignee"
1632
- ),
1633
- adapter: Optional[AdapterType] = typer.Option(
2311
+ assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
2312
+ adapter: AdapterType | None = typer.Option(
1634
2313
  None, "--adapter", help="Override default adapter"
1635
2314
  ),
1636
2315
  ) -> None:
1637
- """Update ticket fields."""
2316
+ """Update ticket fields.
2317
+
2318
+ DEPRECATED: Use 'mcp-ticketer ticket update' instead.
2319
+ """
2320
+ console.print(
2321
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n"
2322
+ )
1638
2323
  updates = {}
1639
2324
  if title:
1640
2325
  updates["title"] = title
@@ -1649,7 +2334,7 @@ def update(
1649
2334
 
1650
2335
  if not updates:
1651
2336
  console.print("[yellow]No updates specified[/yellow]")
1652
- raise typer.Exit(1)
2337
+ raise typer.Exit(1) from None
1653
2338
 
1654
2339
  # Get the adapter name
1655
2340
  config = load_config()
@@ -1681,30 +2366,36 @@ def update(
1681
2366
  console.print("[dim]Worker started to process request[/dim]")
1682
2367
 
1683
2368
 
1684
- @app.command()
2369
+ @app.command(deprecated=True, hidden=True)
1685
2370
  def transition(
1686
2371
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1687
- state_positional: Optional[TicketState] = typer.Argument(
2372
+ state_positional: TicketState | None = typer.Argument(
1688
2373
  None, help="Target state (positional - deprecated, use --state instead)"
1689
2374
  ),
1690
- state: Optional[TicketState] = typer.Option(
2375
+ state: TicketState | None = typer.Option(
1691
2376
  None, "--state", "-s", help="Target state (recommended)"
1692
2377
  ),
1693
- adapter: Optional[AdapterType] = typer.Option(
2378
+ adapter: AdapterType | None = typer.Option(
1694
2379
  None, "--adapter", help="Override default adapter"
1695
2380
  ),
1696
2381
  ) -> None:
1697
2382
  """Change ticket state with validation.
1698
2383
 
2384
+ DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
2385
+
1699
2386
  Examples:
1700
2387
  # Recommended syntax with flag:
1701
- mcp-ticketer transition BTA-215 --state done
1702
- mcp-ticketer transition BTA-215 -s in_progress
2388
+ mcp-ticketer ticket transition BTA-215 --state done
2389
+ mcp-ticketer ticket transition BTA-215 -s in_progress
1703
2390
 
1704
2391
  # Legacy positional syntax (still supported):
1705
- mcp-ticketer transition BTA-215 done
2392
+ mcp-ticketer ticket transition BTA-215 done
1706
2393
 
1707
2394
  """
2395
+ console.print(
2396
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n"
2397
+ )
2398
+
1708
2399
  # Determine which state to use (prefer flag over positional)
1709
2400
  target_state = state if state is not None else state_positional
1710
2401
 
@@ -1715,7 +2406,7 @@ def transition(
1715
2406
  " - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
1716
2407
  " - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
1717
2408
  )
1718
- raise typer.Exit(1)
2409
+ raise typer.Exit(1) from None
1719
2410
 
1720
2411
  # Get the adapter name
1721
2412
  config = load_config()
@@ -1747,20 +2438,26 @@ def transition(
1747
2438
  console.print("[dim]Worker started to process request[/dim]")
1748
2439
 
1749
2440
 
1750
- @app.command()
2441
+ @app.command(deprecated=True, hidden=True)
1751
2442
  def search(
1752
- query: Optional[str] = typer.Argument(None, help="Search query"),
1753
- state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
1754
- priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
1755
- assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
2443
+ query: str | None = typer.Argument(None, help="Search query"),
2444
+ state: TicketState | None = typer.Option(None, "--state", "-s"),
2445
+ priority: Priority | None = typer.Option(None, "--priority", "-p"),
2446
+ assignee: str | None = typer.Option(None, "--assignee", "-a"),
1756
2447
  limit: int = typer.Option(10, "--limit", "-l"),
1757
- adapter: Optional[AdapterType] = typer.Option(
2448
+ adapter: AdapterType | None = typer.Option(
1758
2449
  None, "--adapter", help="Override default adapter"
1759
2450
  ),
1760
2451
  ) -> None:
1761
- """Search tickets with advanced query."""
2452
+ """Search tickets with advanced query.
1762
2453
 
1763
- async def _search():
2454
+ DEPRECATED: Use 'mcp-ticketer ticket search' instead.
2455
+ """
2456
+ console.print(
2457
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
2458
+ )
2459
+
2460
+ async def _search() -> None:
1764
2461
  adapter_instance = get_adapter(
1765
2462
  override_adapter=adapter.value if adapter else None
1766
2463
  )
@@ -1790,17 +2487,26 @@ def search(
1790
2487
  console.print()
1791
2488
 
1792
2489
 
2490
+ # Add ticket command group to main app
2491
+ app.add_typer(ticket_app, name="ticket")
2492
+
2493
+ # Add platform command group to main app
2494
+ app.add_typer(platform_app, name="platform")
2495
+
1793
2496
  # Add queue command to main app
1794
2497
  app.add_typer(queue_app, name="queue")
1795
2498
 
1796
2499
  # Add discover command to main app
1797
2500
  app.add_typer(discover_app, name="discover")
1798
2501
 
2502
+ # Add instructions command to main app
2503
+ app.add_typer(instruction_app, name="instructions")
2504
+
1799
2505
 
1800
2506
  # Add diagnostics command
1801
- @app.command()
1802
- def diagnose(
1803
- output_file: Optional[str] = typer.Option(
2507
+ @app.command("doctor")
2508
+ def doctor_command(
2509
+ output_file: str | None = typer.Option(
1804
2510
  None, "--output", "-o", help="Save full report to file"
1805
2511
  ),
1806
2512
  json_output: bool = typer.Option(
@@ -1810,7 +2516,7 @@ def diagnose(
1810
2516
  False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1811
2517
  ),
1812
2518
  ) -> None:
1813
- """Run comprehensive system diagnostics and health check."""
2519
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
1814
2520
  if simple:
1815
2521
  from .simple_health import simple_diagnose
1816
2522
 
@@ -1826,7 +2532,7 @@ def diagnose(
1826
2532
 
1827
2533
  console.print("\n" + json.dumps(report, indent=2))
1828
2534
  if report["issues"]:
1829
- raise typer.Exit(1)
2535
+ raise typer.Exit(1) from None
1830
2536
  else:
1831
2537
  try:
1832
2538
  asyncio.run(
@@ -1842,17 +2548,44 @@ def diagnose(
1842
2548
 
1843
2549
  report = simple_diagnose()
1844
2550
  if report["issues"]:
1845
- raise typer.Exit(1)
2551
+ raise typer.Exit(1) from None
1846
2552
 
1847
2553
 
1848
- @app.command()
1849
- def health() -> None:
1850
- """Quick health check - shows system status summary."""
2554
+ @app.command("diagnose", hidden=True)
2555
+ def diagnose_alias(
2556
+ output_file: str | None = typer.Option(
2557
+ None, "--output", "-o", help="Save full report to file"
2558
+ ),
2559
+ json_output: bool = typer.Option(
2560
+ False, "--json", help="Output report in JSON format"
2561
+ ),
2562
+ simple: bool = typer.Option(
2563
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
2564
+ ),
2565
+ ) -> None:
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)
2569
+
2570
+
2571
+ @app.command("status")
2572
+ def status_command() -> None:
2573
+ """Quick health check - shows system status summary (alias: health)."""
2574
+ from .simple_health import simple_health_check
2575
+
2576
+ result = simple_health_check()
2577
+ if result != 0:
2578
+ raise typer.Exit(result) from None
2579
+
2580
+
2581
+ @app.command("health")
2582
+ def health_alias() -> None:
2583
+ """Quick health check - shows system status summary (alias for status)."""
1851
2584
  from .simple_health import simple_health_check
1852
2585
 
1853
2586
  result = simple_health_check()
1854
2587
  if result != 0:
1855
- raise typer.Exit(result)
2588
+ raise typer.Exit(result) from None
1856
2589
 
1857
2590
 
1858
2591
  # Create MCP configuration command group
@@ -1860,18 +2593,554 @@ mcp_app = typer.Typer(
1860
2593
  name="mcp",
1861
2594
  help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
1862
2595
  add_completion=False,
2596
+ invoke_without_command=True,
1863
2597
  )
1864
2598
 
1865
2599
 
2600
+ @mcp_app.callback()
2601
+ def mcp_callback(
2602
+ ctx: typer.Context,
2603
+ project_path: str | None = typer.Option(
2604
+ None, "--path", "-p", help="Project directory path (default: current directory)"
2605
+ ),
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
+ """
2617
+ if ctx.invoked_subcommand is None:
2618
+ # No subcommand provided, run the serve command
2619
+ # Change to project directory if provided
2620
+ if project_path:
2621
+ import os
2622
+
2623
+ os.chdir(project_path)
2624
+ # Invoke the serve command through context
2625
+ ctx.invoke(mcp_serve, adapter=None, base_path=None)
2626
+
2627
+
1866
2628
  @app.command()
1867
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1868
- """Check status of a queued operation."""
2629
+ def install(
2630
+ platform: str | None = typer.Argument(
2631
+ None,
2632
+ help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
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
+ ),
2645
+ adapter: str | None = typer.Option(
2646
+ None,
2647
+ "--adapter",
2648
+ "-a",
2649
+ help="Adapter type to use (interactive prompt if not specified)",
2650
+ ),
2651
+ project_path: str | None = typer.Option(
2652
+ None, "--path", help="Project path (default: current directory)"
2653
+ ),
2654
+ global_config: bool = typer.Option(
2655
+ False,
2656
+ "--global",
2657
+ "-g",
2658
+ help="Save to global config instead of project-specific",
2659
+ ),
2660
+ base_path: str | None = typer.Option(
2661
+ None,
2662
+ "--base-path",
2663
+ "-p",
2664
+ help="Base path for ticket storage (AITrackdown only)",
2665
+ ),
2666
+ api_key: str | None = typer.Option(
2667
+ None, "--api-key", help="API key for Linear or API token for JIRA"
2668
+ ),
2669
+ team_id: str | None = typer.Option(
2670
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
2671
+ ),
2672
+ jira_server: str | None = typer.Option(
2673
+ None,
2674
+ "--jira-server",
2675
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
2676
+ ),
2677
+ jira_email: str | None = typer.Option(
2678
+ None, "--jira-email", help="JIRA user email for authentication"
2679
+ ),
2680
+ jira_project: str | None = typer.Option(
2681
+ None, "--jira-project", help="Default JIRA project key"
2682
+ ),
2683
+ github_owner: str | None = typer.Option(
2684
+ None, "--github-owner", help="GitHub repository owner"
2685
+ ),
2686
+ github_repo: str | None = typer.Option(
2687
+ None, "--github-repo", help="GitHub repository name"
2688
+ ),
2689
+ github_token: str | None = typer.Option(
2690
+ None, "--github-token", help="GitHub Personal Access Token"
2691
+ ),
2692
+ dry_run: bool = typer.Option(
2693
+ False,
2694
+ "--dry-run",
2695
+ help="Show what would be done without making changes (for platform installation)",
2696
+ ),
2697
+ ) -> None:
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.
2706
+
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
2713
+
2714
+ # Install for all detected platforms
2715
+ mcp-ticketer install --all
2716
+
2717
+ # Install for specific platform
2718
+ mcp-ticketer install claude-code # Claude Code (project-level)
2719
+ mcp-ticketer install claude-desktop # Claude Desktop (global)
2720
+ mcp-ticketer install gemini # Gemini CLI
2721
+ mcp-ticketer install codex # Codex
2722
+ mcp-ticketer install auggie # Auggie
2723
+
2724
+ Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
2725
+ mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
2726
+
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
+
2903
+ # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
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
+
2939
+ # Import configuration functions
2940
+ from .auggie_configure import configure_auggie_mcp
2941
+ from .codex_configure import configure_codex_mcp
2942
+ from .gemini_configure import configure_gemini_mcp
2943
+ from .mcp_configure import configure_claude_mcp
2944
+
2945
+ # Map platform names to configuration functions
2946
+ platform_mapping = {
2947
+ "claude-code": {
2948
+ "func": lambda: configure_claude_mcp(global_config=False, force=True),
2949
+ "name": "Claude Code",
2950
+ },
2951
+ "claude-desktop": {
2952
+ "func": lambda: configure_claude_mcp(global_config=True, force=True),
2953
+ "name": "Claude Desktop",
2954
+ },
2955
+ "auggie": {
2956
+ "func": lambda: configure_auggie_mcp(force=True),
2957
+ "name": "Auggie",
2958
+ },
2959
+ "gemini": {
2960
+ "func": lambda: configure_gemini_mcp(scope="project", force=True),
2961
+ "name": "Gemini CLI",
2962
+ },
2963
+ "codex": {
2964
+ "func": lambda: configure_codex_mcp(force=True),
2965
+ "name": "Codex",
2966
+ },
2967
+ }
2968
+
2969
+ if platform not in platform_mapping:
2970
+ console.print(f"[red]Unknown platform: {platform}[/red]")
2971
+ console.print("\n[bold]Available platforms:[/bold]")
2972
+ for p in platform_mapping.keys():
2973
+ console.print(f" • {p}")
2974
+ raise typer.Exit(1) from None
2975
+
2976
+ config = platform_mapping[platform]
2977
+
2978
+ if dry_run:
2979
+ console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
2980
+ return
2981
+
2982
+ try:
2983
+ config["func"]()
2984
+ except Exception as e:
2985
+ console.print(f"[red]Installation failed: {e}[/red]")
2986
+ raise typer.Exit(1) from e
2987
+ return
2988
+
2989
+ # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
2990
+ # This makes 'install' and 'init' synonymous when called without platform argument
2991
+ init(
2992
+ adapter=adapter,
2993
+ project_path=project_path,
2994
+ global_config=global_config,
2995
+ base_path=base_path,
2996
+ api_key=api_key,
2997
+ team_id=team_id,
2998
+ jira_server=jira_server,
2999
+ jira_email=jira_email,
3000
+ jira_project=jira_project,
3001
+ github_owner=github_owner,
3002
+ github_repo=github_repo,
3003
+ github_token=github_token,
3004
+ )
3005
+
3006
+
3007
+ @app.command()
3008
+ def remove(
3009
+ platform: str | None = typer.Argument(
3010
+ None,
3011
+ help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
3012
+ ),
3013
+ dry_run: bool = typer.Option(
3014
+ False, "--dry-run", help="Show what would be done without making changes"
3015
+ ),
3016
+ ) -> None:
3017
+ """Remove mcp-ticketer from AI platforms.
3018
+
3019
+ Without arguments, shows help and available platforms.
3020
+ With a platform argument, removes MCP configuration for that platform.
3021
+
3022
+ Examples:
3023
+ # Remove from Claude Code (project-level)
3024
+ mcp-ticketer remove claude-code
3025
+
3026
+ # Remove from Claude Desktop (global)
3027
+ mcp-ticketer remove claude-desktop
3028
+
3029
+ # Remove from Auggie
3030
+ mcp-ticketer remove auggie
3031
+
3032
+ # Dry run to preview changes
3033
+ mcp-ticketer remove claude-code --dry-run
3034
+
3035
+ """
3036
+ # If no platform specified, show help message
3037
+ if platform is None:
3038
+ console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
3039
+ console.print("Usage: mcp-ticketer remove <platform>\n")
3040
+ console.print("[bold]Available platforms:[/bold]")
3041
+ console.print(" • claude-code - Claude Code (project-level)")
3042
+ console.print(" • claude-desktop - Claude Desktop (global)")
3043
+ console.print(" • auggie - Auggie (global)")
3044
+ console.print(" • gemini - Gemini CLI (project-level by default)")
3045
+ console.print(" • codex - Codex (global)")
3046
+ return
3047
+
3048
+ # Import removal functions
3049
+ from .auggie_configure import remove_auggie_mcp
3050
+ from .codex_configure import remove_codex_mcp
3051
+ from .gemini_configure import remove_gemini_mcp
3052
+ from .mcp_configure import remove_claude_mcp
3053
+
3054
+ # Map platform names to removal functions
3055
+ platform_mapping = {
3056
+ "claude-code": {
3057
+ "func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
3058
+ "name": "Claude Code",
3059
+ },
3060
+ "claude-desktop": {
3061
+ "func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
3062
+ "name": "Claude Desktop",
3063
+ },
3064
+ "auggie": {
3065
+ "func": lambda: remove_auggie_mcp(dry_run=dry_run),
3066
+ "name": "Auggie",
3067
+ },
3068
+ "gemini": {
3069
+ "func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
3070
+ "name": "Gemini CLI",
3071
+ },
3072
+ "codex": {
3073
+ "func": lambda: remove_codex_mcp(dry_run=dry_run),
3074
+ "name": "Codex",
3075
+ },
3076
+ }
3077
+
3078
+ if platform not in platform_mapping:
3079
+ console.print(f"[red]Unknown platform: {platform}[/red]")
3080
+ console.print("\n[bold]Available platforms:[/bold]")
3081
+ for p in platform_mapping.keys():
3082
+ console.print(f" • {p}")
3083
+ raise typer.Exit(1) from None
3084
+
3085
+ config = platform_mapping[platform]
3086
+
3087
+ try:
3088
+ config["func"]()
3089
+ except Exception as e:
3090
+ console.print(f"[red]Removal failed: {e}[/red]")
3091
+ raise typer.Exit(1) from e
3092
+
3093
+
3094
+ @app.command()
3095
+ def uninstall(
3096
+ platform: str | None = typer.Argument(
3097
+ None,
3098
+ help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
3099
+ ),
3100
+ dry_run: bool = typer.Option(
3101
+ False, "--dry-run", help="Show what would be done without making changes"
3102
+ ),
3103
+ ) -> None:
3104
+ """Uninstall mcp-ticketer from AI platforms (alias for remove).
3105
+
3106
+ This is an alias for the 'remove' command.
3107
+
3108
+ Without arguments, shows help and available platforms.
3109
+ With a platform argument, removes MCP configuration for that platform.
3110
+
3111
+ Examples:
3112
+ # Uninstall from Claude Code (project-level)
3113
+ mcp-ticketer uninstall claude-code
3114
+
3115
+ # Uninstall from Claude Desktop (global)
3116
+ mcp-ticketer uninstall claude-desktop
3117
+
3118
+ # Uninstall from Auggie
3119
+ mcp-ticketer uninstall auggie
3120
+
3121
+ # Dry run to preview changes
3122
+ mcp-ticketer uninstall claude-code --dry-run
3123
+
3124
+ """
3125
+ # Call the remove command with the same parameters
3126
+ remove(platform=platform, dry_run=dry_run)
3127
+
3128
+
3129
+ @app.command(deprecated=True, hidden=True)
3130
+ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
3131
+ """Check status of a queued operation.
3132
+
3133
+ DEPRECATED: Use 'mcp-ticketer ticket check' instead.
3134
+ """
3135
+ console.print(
3136
+ "[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket check' instead.[/yellow]\n"
3137
+ )
1869
3138
  queue = Queue()
1870
3139
  item = queue.get_item(queue_id)
1871
3140
 
1872
3141
  if not item:
1873
3142
  console.print(f"[red]Queue item not found: {queue_id}[/red]")
1874
- raise typer.Exit(1)
3143
+ raise typer.Exit(1) from None
1875
3144
 
1876
3145
  # Display status
1877
3146
  console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
@@ -1905,19 +3174,19 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1905
3174
  console.print(f"\nRetry Count: {item.retry_count}")
1906
3175
 
1907
3176
 
1908
- @app.command()
1909
- def serve(
1910
- adapter: Optional[AdapterType] = typer.Option(
3177
+ @mcp_app.command(name="serve")
3178
+ def mcp_serve(
3179
+ adapter: AdapterType | None = typer.Option(
1911
3180
  None, "--adapter", "-a", help="Override default adapter type"
1912
3181
  ),
1913
- base_path: Optional[str] = typer.Option(
3182
+ base_path: str | None = typer.Option(
1914
3183
  None, "--base-path", help="Base path for AITrackdown adapter"
1915
3184
  ),
1916
- ):
3185
+ ) -> None:
1917
3186
  """Start MCP server for JSON-RPC communication over stdio.
1918
3187
 
1919
3188
  This command is used by Claude Code/Desktop when connecting to the MCP server.
1920
- You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
3189
+ You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
1921
3190
 
1922
3191
  Configuration Resolution:
1923
3192
  - When MCP server starts, it uses the current working directory (cwd)
@@ -1927,12 +3196,13 @@ def serve(
1927
3196
  2. Global: ~/.mcp-ticketer/config.json
1928
3197
  3. Default: aitrackdown adapter with .aitrackdown base path
1929
3198
  """
1930
- from ..mcp.server import MCPTicketServer
3199
+ from ..mcp.server.server_sdk import configure_adapter
3200
+ from ..mcp.server.server_sdk import main as sdk_main
1931
3201
 
1932
3202
  # Load configuration (respects project-specific config in cwd)
1933
3203
  config = load_config()
1934
3204
 
1935
- # Determine adapter type with priority: CLI arg > .env files > config > default
3205
+ # Determine adapter type with priority: CLI arg > config > .env files > default
1936
3206
  if adapter:
1937
3207
  # Priority 1: Command line argument
1938
3208
  adapter_type = adapter.value
@@ -1940,18 +3210,24 @@ def serve(
1940
3210
  adapters_config = config.get("adapters", {})
1941
3211
  adapter_config = adapters_config.get(adapter_type, {})
1942
3212
  else:
1943
- # Priority 2: .env files
1944
- from ..mcp.server import _load_env_configuration
1945
-
1946
- env_config = _load_env_configuration()
1947
- if env_config:
1948
- adapter_type = env_config["adapter_type"]
1949
- adapter_config = env_config["adapter_config"]
1950
- else:
1951
- # Priority 3: Configuration file
1952
- 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:
1953
3216
  adapters_config = config.get("adapters", {})
1954
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, {})
1955
3231
 
1956
3232
  # Override with command line options if provided (highest priority)
1957
3233
  if base_path and adapter_type == "aitrackdown":
@@ -1968,21 +3244,22 @@ def serve(
1968
3244
  if sys.stderr.isatty():
1969
3245
  # Only print if stderr is a terminal (not redirected)
1970
3246
  console.file = sys.stderr
1971
- console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
3247
+ console.print(
3248
+ f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
3249
+ )
1972
3250
  console.print(
1973
3251
  "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
1974
3252
  )
1975
3253
 
1976
- # Create and run server
3254
+ # Configure adapter and run SDK server
1977
3255
  try:
1978
- server = MCPTicketServer(adapter_type, adapter_config)
1979
- asyncio.run(server.run())
3256
+ configure_adapter(adapter_type, adapter_config)
3257
+ sdk_main()
1980
3258
  except KeyboardInterrupt:
1981
- # Also send this to stderr
3259
+ # Send this to stderr
1982
3260
  if sys.stderr.isatty():
1983
3261
  console.print("\n[yellow]Server stopped by user[/yellow]")
1984
- if "server" in locals():
1985
- asyncio.run(server.stop())
3262
+ sys.exit(0)
1986
3263
  except Exception as e:
1987
3264
  # Log error to stderr
1988
3265
  sys.stderr.write(f"MCP server error: {e}\n")
@@ -2000,7 +3277,7 @@ def mcp_claude(
2000
3277
  force: bool = typer.Option(
2001
3278
  False, "--force", "-f", help="Overwrite existing configuration"
2002
3279
  ),
2003
- ):
3280
+ ) -> None:
2004
3281
  """Configure Claude Code to use mcp-ticketer MCP server.
2005
3282
 
2006
3283
  Reads configuration from .mcp-ticketer/config.json and updates
@@ -2026,7 +3303,7 @@ def mcp_claude(
2026
3303
  configure_claude_mcp(global_config=global_config, force=force)
2027
3304
  except Exception as e:
2028
3305
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2029
- raise typer.Exit(1)
3306
+ raise typer.Exit(1) from e
2030
3307
 
2031
3308
 
2032
3309
  @mcp_app.command(name="gemini")
@@ -2040,7 +3317,7 @@ def mcp_gemini(
2040
3317
  force: bool = typer.Option(
2041
3318
  False, "--force", "-f", help="Overwrite existing configuration"
2042
3319
  ),
2043
- ):
3320
+ ) -> None:
2044
3321
  """Configure Gemini CLI to use mcp-ticketer MCP server.
2045
3322
 
2046
3323
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2067,13 +3344,13 @@ def mcp_gemini(
2067
3344
  console.print(
2068
3345
  f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
2069
3346
  )
2070
- raise typer.Exit(1)
3347
+ raise typer.Exit(1) from None
2071
3348
 
2072
3349
  try:
2073
3350
  configure_gemini_mcp(scope=scope, force=force) # type: ignore
2074
3351
  except Exception as e:
2075
3352
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2076
- raise typer.Exit(1)
3353
+ raise typer.Exit(1) from e
2077
3354
 
2078
3355
 
2079
3356
  @mcp_app.command(name="codex")
@@ -2081,7 +3358,7 @@ def mcp_codex(
2081
3358
  force: bool = typer.Option(
2082
3359
  False, "--force", "-f", help="Overwrite existing configuration"
2083
3360
  ),
2084
- ):
3361
+ ) -> None:
2085
3362
  """Configure Codex CLI to use mcp-ticketer MCP server.
2086
3363
 
2087
3364
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2105,7 +3382,7 @@ def mcp_codex(
2105
3382
  configure_codex_mcp(force=force)
2106
3383
  except Exception as e:
2107
3384
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2108
- raise typer.Exit(1)
3385
+ raise typer.Exit(1) from e
2109
3386
 
2110
3387
 
2111
3388
  @mcp_app.command(name="auggie")
@@ -2113,7 +3390,7 @@ def mcp_auggie(
2113
3390
  force: bool = typer.Option(
2114
3391
  False, "--force", "-f", help="Overwrite existing configuration"
2115
3392
  ),
2116
- ):
3393
+ ) -> None:
2117
3394
  """Configure Auggie CLI to use mcp-ticketer MCP server.
2118
3395
 
2119
3396
  Reads configuration from .mcp-ticketer/config.json and creates
@@ -2137,16 +3414,134 @@ def mcp_auggie(
2137
3414
  configure_auggie_mcp(force=force)
2138
3415
  except Exception as e:
2139
3416
  console.print(f"[red]✗ Configuration failed:[/red] {e}")
2140
- raise typer.Exit(1)
3417
+ raise typer.Exit(1) from e
3418
+
3419
+
3420
+ @mcp_app.command(name="status")
3421
+ def mcp_status() -> None:
3422
+ """Check MCP server status.
3423
+
3424
+ Shows whether the MCP server is configured and running for various platforms.
3425
+
3426
+ Examples:
3427
+ mcp-ticketer mcp status
3428
+
3429
+ """
3430
+ import json
3431
+ from pathlib import Path
3432
+
3433
+ console.print("[bold]MCP Server Status[/bold]\n")
3434
+
3435
+ # Check project-level configuration
3436
+ project_config = Path.cwd() / ".mcp-ticketer" / "config.json"
3437
+ if project_config.exists():
3438
+ console.print(f"[green]✓[/green] Project config found: {project_config}")
3439
+ try:
3440
+ with open(project_config) as f:
3441
+ config = json.load(f)
3442
+ adapter = config.get("default_adapter", "aitrackdown")
3443
+ console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
3444
+ except Exception as e:
3445
+ console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
3446
+ else:
3447
+ console.print("[yellow]○[/yellow] No project config found")
3448
+
3449
+ # Check Claude Code configuration
3450
+ claude_code_config = Path.cwd() / ".mcp" / "config.json"
3451
+ if claude_code_config.exists():
3452
+ console.print(
3453
+ f"\n[green]✓[/green] Claude Code configured: {claude_code_config}"
3454
+ )
3455
+ else:
3456
+ console.print("\n[yellow]○[/yellow] Claude Code not configured")
3457
+
3458
+ # Check Claude Desktop configuration
3459
+ claude_desktop_config = (
3460
+ Path.home()
3461
+ / "Library"
3462
+ / "Application Support"
3463
+ / "Claude"
3464
+ / "claude_desktop_config.json"
3465
+ )
3466
+ if claude_desktop_config.exists():
3467
+ try:
3468
+ with open(claude_desktop_config) as f:
3469
+ config = json.load(f)
3470
+ if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
3471
+ console.print(
3472
+ f"[green]✓[/green] Claude Desktop configured: {claude_desktop_config}"
3473
+ )
3474
+ else:
3475
+ console.print(
3476
+ "[yellow]○[/yellow] Claude Desktop config exists but mcp-ticketer not found"
3477
+ )
3478
+ except Exception:
3479
+ console.print(
3480
+ "[yellow]○[/yellow] Claude Desktop config exists but could not be read"
3481
+ )
3482
+ else:
3483
+ console.print("[yellow]○[/yellow] Claude Desktop not configured")
3484
+
3485
+ # Check Gemini configuration
3486
+ gemini_project_config = Path.cwd() / ".gemini" / "settings.json"
3487
+ gemini_user_config = Path.home() / ".gemini" / "settings.json"
3488
+ if gemini_project_config.exists():
3489
+ console.print(
3490
+ f"\n[green]✓[/green] Gemini (project) configured: {gemini_project_config}"
3491
+ )
3492
+ elif gemini_user_config.exists():
3493
+ console.print(
3494
+ f"\n[green]✓[/green] Gemini (user) configured: {gemini_user_config}"
3495
+ )
3496
+ else:
3497
+ console.print("\n[yellow]○[/yellow] Gemini not configured")
3498
+
3499
+ # Check Codex configuration
3500
+ codex_config = Path.home() / ".codex" / "config.toml"
3501
+ if codex_config.exists():
3502
+ console.print(f"[green]✓[/green] Codex configured: {codex_config}")
3503
+ else:
3504
+ console.print("[yellow]○[/yellow] Codex not configured")
3505
+
3506
+ # Check Auggie configuration
3507
+ auggie_config = Path.home() / ".augment" / "settings.json"
3508
+ if auggie_config.exists():
3509
+ console.print(f"[green]✓[/green] Auggie configured: {auggie_config}")
3510
+ else:
3511
+ console.print("[yellow]○[/yellow] Auggie not configured")
3512
+
3513
+ console.print(
3514
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
3515
+ )
3516
+
3517
+
3518
+ @mcp_app.command(name="stop")
3519
+ def mcp_stop() -> None:
3520
+ """Stop MCP server (placeholder - MCP runs on-demand via stdio).
3521
+
3522
+ Note: The MCP server runs on-demand when AI clients connect via stdio.
3523
+ It doesn't run as a persistent background service, so there's nothing to stop.
3524
+ This command is provided for consistency but has no effect.
3525
+
3526
+ Examples:
3527
+ mcp-ticketer mcp stop
3528
+
3529
+ """
3530
+ console.print(
3531
+ "[yellow]ℹ[/yellow] MCP server runs on-demand via stdio (not as a background service)"
3532
+ )
3533
+ console.print("There is no persistent server process to stop.")
3534
+ console.print(
3535
+ "\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
3536
+ )
2141
3537
 
2142
3538
 
2143
3539
  # Add command groups to main app (must be after all subcommands are defined)
2144
- app.add_typer(linear_app, name="linear")
2145
3540
  app.add_typer(mcp_app, name="mcp")
2146
3541
 
2147
3542
 
2148
- def main():
2149
- """Main entry point."""
3543
+ def main() -> None:
3544
+ """Execute the main CLI application entry point."""
2150
3545
  app()
2151
3546
 
2152
3547