mcp-ticketer 0.4.1__py3-none-any.whl → 0.4.3__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 (56) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +243 -11
  4. mcp_ticketer/adapters/github.py +15 -14
  5. mcp_ticketer/adapters/hybrid.py +11 -11
  6. mcp_ticketer/adapters/jira.py +22 -25
  7. mcp_ticketer/adapters/linear/adapter.py +9 -21
  8. mcp_ticketer/adapters/linear/client.py +2 -1
  9. mcp_ticketer/adapters/linear/mappers.py +2 -1
  10. mcp_ticketer/cache/memory.py +6 -5
  11. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  12. mcp_ticketer/cli/auggie_configure.py +66 -0
  13. mcp_ticketer/cli/codex_configure.py +70 -2
  14. mcp_ticketer/cli/configure.py +7 -14
  15. mcp_ticketer/cli/diagnostics.py +2 -2
  16. mcp_ticketer/cli/discover.py +6 -11
  17. mcp_ticketer/cli/gemini_configure.py +68 -2
  18. mcp_ticketer/cli/linear_commands.py +6 -7
  19. mcp_ticketer/cli/main.py +341 -203
  20. mcp_ticketer/cli/mcp_configure.py +61 -2
  21. mcp_ticketer/cli/ticket_commands.py +27 -30
  22. mcp_ticketer/cli/utils.py +23 -22
  23. mcp_ticketer/core/__init__.py +3 -1
  24. mcp_ticketer/core/adapter.py +82 -13
  25. mcp_ticketer/core/config.py +27 -29
  26. mcp_ticketer/core/env_discovery.py +10 -10
  27. mcp_ticketer/core/env_loader.py +8 -8
  28. mcp_ticketer/core/http_client.py +16 -16
  29. mcp_ticketer/core/mappers.py +10 -10
  30. mcp_ticketer/core/models.py +50 -20
  31. mcp_ticketer/core/project_config.py +40 -34
  32. mcp_ticketer/core/registry.py +2 -2
  33. mcp_ticketer/mcp/dto.py +32 -32
  34. mcp_ticketer/mcp/response_builder.py +2 -2
  35. mcp_ticketer/mcp/server.py +17 -37
  36. mcp_ticketer/mcp/server_sdk.py +93 -0
  37. mcp_ticketer/mcp/tools/__init__.py +36 -0
  38. mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
  39. mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
  40. mcp_ticketer/mcp/tools/comment_tools.py +90 -0
  41. mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
  42. mcp_ticketer/mcp/tools/pr_tools.py +154 -0
  43. mcp_ticketer/mcp/tools/search_tools.py +206 -0
  44. mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
  45. mcp_ticketer/queue/health_monitor.py +4 -4
  46. mcp_ticketer/queue/manager.py +2 -2
  47. mcp_ticketer/queue/queue.py +16 -16
  48. mcp_ticketer/queue/ticket_registry.py +7 -7
  49. mcp_ticketer/queue/worker.py +2 -2
  50. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
  51. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  52. mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
  53. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  54. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  55. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  56. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,7 +5,6 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Optional
9
8
 
10
9
  import typer
11
10
  from dotenv import load_dotenv
@@ -21,7 +20,8 @@ from ..core.models import Comment, SearchQuery
21
20
  from ..queue import Queue, QueueStatus, WorkerManager
22
21
  from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
23
22
  from ..queue.ticket_registry import TicketRegistry
24
- from .configure import configure_wizard, set_adapter_config, show_current_config
23
+ from .configure import (configure_wizard, set_adapter_config,
24
+ show_current_config)
25
25
  from .diagnostics import run_diagnostics
26
26
  from .discover import app as discover_app
27
27
  from .migrate_config import migrate_config_command
@@ -84,7 +84,7 @@ class AdapterType(str, Enum):
84
84
  GITHUB = "github"
85
85
 
86
86
 
87
- def load_config(project_dir: Optional[Path] = None) -> dict:
87
+ def load_config(project_dir: Path | None = None) -> dict:
88
88
  """Load configuration from project-local config file ONLY.
89
89
 
90
90
  SECURITY: This method ONLY reads from the current project directory
@@ -146,7 +146,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
146
146
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
147
147
 
148
148
 
149
- def _discover_from_env_files() -> Optional[str]:
149
+ def _discover_from_env_files() -> str | None:
150
150
  """Discover adapter configuration from .env or .env.local files.
151
151
 
152
152
  Returns:
@@ -270,7 +270,7 @@ def merge_config(updates: dict) -> dict:
270
270
 
271
271
 
272
272
  def get_adapter(
273
- override_adapter: Optional[str] = None, override_config: Optional[dict] = None
273
+ override_adapter: str | None = None, override_config: dict | None = None
274
274
  ):
275
275
  """Get configured adapter instance.
276
276
 
@@ -387,13 +387,13 @@ def _prompt_for_adapter_selection(console: Console) -> str:
387
387
 
388
388
  @app.command()
389
389
  def setup(
390
- adapter: Optional[str] = typer.Option(
390
+ adapter: str | None = typer.Option(
391
391
  None,
392
392
  "--adapter",
393
393
  "-a",
394
394
  help="Adapter type to use (interactive prompt if not specified)",
395
395
  ),
396
- project_path: Optional[str] = typer.Option(
396
+ project_path: str | None = typer.Option(
397
397
  None, "--path", help="Project path (default: current directory)"
398
398
  ),
399
399
  global_config: bool = typer.Option(
@@ -402,36 +402,36 @@ def setup(
402
402
  "-g",
403
403
  help="Save to global config instead of project-specific",
404
404
  ),
405
- base_path: Optional[str] = typer.Option(
405
+ base_path: str | None = typer.Option(
406
406
  None,
407
407
  "--base-path",
408
408
  "-p",
409
409
  help="Base path for ticket storage (AITrackdown only)",
410
410
  ),
411
- api_key: Optional[str] = typer.Option(
411
+ api_key: str | None = typer.Option(
412
412
  None, "--api-key", help="API key for Linear or API token for JIRA"
413
413
  ),
414
- team_id: Optional[str] = typer.Option(
414
+ team_id: str | None = typer.Option(
415
415
  None, "--team-id", help="Linear team ID (required for Linear adapter)"
416
416
  ),
417
- jira_server: Optional[str] = typer.Option(
417
+ jira_server: str | None = typer.Option(
418
418
  None,
419
419
  "--jira-server",
420
420
  help="JIRA server URL (e.g., https://company.atlassian.net)",
421
421
  ),
422
- jira_email: Optional[str] = typer.Option(
422
+ jira_email: str | None = typer.Option(
423
423
  None, "--jira-email", help="JIRA user email for authentication"
424
424
  ),
425
- jira_project: Optional[str] = typer.Option(
425
+ jira_project: str | None = typer.Option(
426
426
  None, "--jira-project", help="Default JIRA project key"
427
427
  ),
428
- github_owner: Optional[str] = typer.Option(
428
+ github_owner: str | None = typer.Option(
429
429
  None, "--github-owner", help="GitHub repository owner"
430
430
  ),
431
- github_repo: Optional[str] = typer.Option(
431
+ github_repo: str | None = typer.Option(
432
432
  None, "--github-repo", help="GitHub repository name"
433
433
  ),
434
- github_token: Optional[str] = typer.Option(
434
+ github_token: str | None = typer.Option(
435
435
  None, "--github-token", help="GitHub Personal Access Token"
436
436
  ),
437
437
  ) -> None:
@@ -471,13 +471,13 @@ def setup(
471
471
 
472
472
  @app.command()
473
473
  def init(
474
- adapter: Optional[str] = typer.Option(
474
+ adapter: str | None = typer.Option(
475
475
  None,
476
476
  "--adapter",
477
477
  "-a",
478
478
  help="Adapter type to use (interactive prompt if not specified)",
479
479
  ),
480
- project_path: Optional[str] = typer.Option(
480
+ project_path: str | None = typer.Option(
481
481
  None, "--path", help="Project path (default: current directory)"
482
482
  ),
483
483
  global_config: bool = typer.Option(
@@ -486,40 +486,40 @@ def init(
486
486
  "-g",
487
487
  help="Save to global config instead of project-specific",
488
488
  ),
489
- base_path: Optional[str] = typer.Option(
489
+ base_path: str | None = typer.Option(
490
490
  None,
491
491
  "--base-path",
492
492
  "-p",
493
493
  help="Base path for ticket storage (AITrackdown only)",
494
494
  ),
495
- api_key: Optional[str] = typer.Option(
495
+ api_key: str | None = typer.Option(
496
496
  None, "--api-key", help="API key for Linear or API token for JIRA"
497
497
  ),
498
- team_id: Optional[str] = typer.Option(
498
+ team_id: str | None = typer.Option(
499
499
  None, "--team-id", help="Linear team ID (required for Linear adapter)"
500
500
  ),
501
- jira_server: Optional[str] = typer.Option(
501
+ jira_server: str | None = typer.Option(
502
502
  None,
503
503
  "--jira-server",
504
504
  help="JIRA server URL (e.g., https://company.atlassian.net)",
505
505
  ),
506
- jira_email: Optional[str] = typer.Option(
506
+ jira_email: str | None = typer.Option(
507
507
  None, "--jira-email", help="JIRA user email for authentication"
508
508
  ),
509
- jira_project: Optional[str] = typer.Option(
509
+ jira_project: str | None = typer.Option(
510
510
  None, "--jira-project", help="Default JIRA project key"
511
511
  ),
512
- github_owner: Optional[str] = typer.Option(
512
+ github_owner: str | None = typer.Option(
513
513
  None, "--github-owner", help="GitHub repository owner"
514
514
  ),
515
- github_repo: Optional[str] = typer.Option(
515
+ github_repo: str | None = typer.Option(
516
516
  None, "--github-repo", help="GitHub repository name"
517
517
  ),
518
- github_token: Optional[str] = typer.Option(
518
+ github_token: str | None = typer.Option(
519
519
  None, "--github-token", help="GitHub Personal Access Token"
520
520
  ),
521
521
  ) -> None:
522
- """Initialize mcp-ticketer for the current project.
522
+ """Initialize mcp-ticketer for the current project (synonymous with 'install' and 'setup').
523
523
 
524
524
  This command sets up MCP Ticketer configuration with interactive prompts
525
525
  to guide you through the process. It auto-detects adapter configuration
@@ -528,11 +528,13 @@ def init(
528
528
  Creates .mcp-ticketer/config.json in the current directory with
529
529
  auto-detected or specified adapter configuration.
530
530
 
531
- Note: 'setup' and 'install' are synonyms for this command.
531
+ Note: 'init', 'install', and 'setup' are all synonyms - use whichever feels natural.
532
532
 
533
533
  Examples:
534
- # Interactive setup (same as 'setup' and 'install')
534
+ # Interactive setup (all three commands are identical)
535
535
  mcp-ticketer init
536
+ mcp-ticketer install
537
+ mcp-ticketer setup
536
538
 
537
539
  # Force specific adapter
538
540
  mcp-ticketer init --adapter linear
@@ -669,20 +671,29 @@ def init(
669
671
  if linear_api_key:
670
672
  linear_config["api_key"] = linear_api_key
671
673
 
672
- # Team ID
674
+ # Team ID or Team Key
675
+ # Try environment variables first
676
+ linear_team_key = os.getenv("LINEAR_TEAM_KEY")
673
677
  linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
674
- if not linear_team_id and not discovered:
675
- console.print("\nYou need your Linear team ID.")
676
- console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
677
678
 
678
- linear_team_id = typer.prompt("Enter your Linear team ID")
679
+ if not linear_team_key and not linear_team_id and not discovered:
680
+ console.print("\n[bold]Linear Team Configuration[/bold]")
681
+ console.print("Enter your team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
682
+ console.print("[dim]Find it in: Linear Settings → Teams → Your Team → Key field[/dim]\n")
683
+
684
+ linear_team_key = typer.prompt("Team key")
679
685
 
686
+ # Save whichever was provided
687
+ if linear_team_key:
688
+ linear_config["team_key"] = linear_team_key
680
689
  if linear_team_id:
681
690
  linear_config["team_id"] = linear_team_id
682
691
 
683
- if not linear_config.get("api_key") or not linear_config.get("team_id"):
692
+ if not linear_config.get("api_key") or (
693
+ not linear_config.get("team_id") and not linear_config.get("team_key")
694
+ ):
684
695
  console.print(
685
- "[red]Error:[/red] Linear requires both API key and team ID"
696
+ "[red]Error:[/red] Linear requires both API key and team ID/key"
686
697
  )
687
698
  console.print(
688
699
  "Run 'mcp-ticketer init --adapter linear' with proper credentials"
@@ -891,98 +902,20 @@ def _show_next_steps(
891
902
  console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
892
903
 
893
904
 
894
- # Keep the old install command as deprecated alias to init
895
- @app.command(deprecated=True, hidden=True)
896
- def install(
897
- adapter: Optional[str] = typer.Option(
898
- None,
899
- "--adapter",
900
- "-a",
901
- help="Adapter type to use (auto-detected from .env if not specified)",
902
- ),
903
- project_path: Optional[str] = typer.Option(
904
- None, "--path", help="Project path (default: current directory)"
905
- ),
906
- global_config: bool = typer.Option(
907
- False,
908
- "--global",
909
- "-g",
910
- help="Save to global config instead of project-specific",
911
- ),
912
- base_path: Optional[str] = typer.Option(
913
- None,
914
- "--base-path",
915
- "-p",
916
- help="Base path for ticket storage (AITrackdown only)",
917
- ),
918
- api_key: Optional[str] = typer.Option(
919
- None, "--api-key", help="API key for Linear or API token for JIRA"
920
- ),
921
- team_id: Optional[str] = typer.Option(
922
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
923
- ),
924
- jira_server: Optional[str] = typer.Option(
925
- None,
926
- "--jira-server",
927
- help="JIRA server URL (e.g., https://company.atlassian.net)",
928
- ),
929
- jira_email: Optional[str] = typer.Option(
930
- None, "--jira-email", help="JIRA user email for authentication"
931
- ),
932
- jira_project: Optional[str] = typer.Option(
933
- None, "--jira-project", help="Default JIRA project key"
934
- ),
935
- github_owner: Optional[str] = typer.Option(
936
- None, "--github-owner", help="GitHub repository owner"
937
- ),
938
- github_repo: Optional[str] = typer.Option(
939
- None, "--github-repo", help="GitHub repository name"
940
- ),
941
- github_token: Optional[str] = typer.Option(
942
- None, "--github-token", help="GitHub Personal Access Token"
943
- ),
944
- ) -> None:
945
- """DEPRECATED: Use 'mcp-ticketer init' instead.
946
-
947
- This command is deprecated. Use 'mcp-ticketer init' for project initialization.
948
-
949
- """
950
- console.print(
951
- "[yellow]⚠️ 'install' is deprecated. Use 'mcp-ticketer init' instead.[/yellow]\n"
952
- )
953
- # Call init with all parameters
954
- init(
955
- adapter=adapter,
956
- project_path=project_path,
957
- global_config=global_config,
958
- base_path=base_path,
959
- api_key=api_key,
960
- team_id=team_id,
961
- jira_server=jira_server,
962
- jira_email=jira_email,
963
- jira_project=jira_project,
964
- github_owner=github_owner,
965
- github_repo=github_repo,
966
- github_token=github_token,
967
- )
968
-
969
-
970
905
  @app.command("set")
971
906
  def set_config(
972
- adapter: Optional[AdapterType] = typer.Option(
907
+ adapter: AdapterType | None = typer.Option(
973
908
  None, "--adapter", "-a", help="Set default adapter"
974
909
  ),
975
- team_key: Optional[str] = typer.Option(
910
+ team_key: str | None = typer.Option(
976
911
  None, "--team-key", help="Linear team key (e.g., BTA)"
977
912
  ),
978
- team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
979
- owner: Optional[str] = typer.Option(
980
- None, "--owner", help="GitHub repository owner"
981
- ),
982
- repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
983
- server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
984
- project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
985
- base_path: Optional[str] = typer.Option(
913
+ team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
914
+ owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
915
+ repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
916
+ server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
917
+ project: str | None = typer.Option(None, "--project", help="JIRA project key"),
918
+ base_path: str | None = typer.Option(
986
919
  None, "--base-path", help="AITrackdown base path"
987
920
  ),
988
921
  ) -> None:
@@ -1072,16 +1005,12 @@ def set_config(
1072
1005
  @app.command("configure")
1073
1006
  def configure_command(
1074
1007
  show: bool = typer.Option(False, "--show", help="Show current configuration"),
1075
- adapter: Optional[str] = typer.Option(
1008
+ adapter: str | None = typer.Option(
1076
1009
  None, "--adapter", help="Set default adapter type"
1077
1010
  ),
1078
- api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
1079
- project_id: Optional[str] = typer.Option(
1080
- None, "--project-id", help="Set project ID"
1081
- ),
1082
- team_id: Optional[str] = typer.Option(
1083
- None, "--team-id", help="Set team ID (Linear)"
1084
- ),
1011
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
1012
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
1013
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
1085
1014
  global_scope: bool = typer.Option(
1086
1015
  False,
1087
1016
  "--global",
@@ -1257,29 +1186,29 @@ def old_queue_health_command(
1257
1186
  @app.command(deprecated=True, hidden=True)
1258
1187
  def create(
1259
1188
  title: str = typer.Argument(..., help="Ticket title"),
1260
- description: Optional[str] = typer.Option(
1189
+ description: str | None = typer.Option(
1261
1190
  None, "--description", "-d", help="Ticket description"
1262
1191
  ),
1263
1192
  priority: Priority = typer.Option(
1264
1193
  Priority.MEDIUM, "--priority", "-p", help="Priority level"
1265
1194
  ),
1266
- tags: Optional[list[str]] = typer.Option(
1195
+ tags: list[str] | None = typer.Option(
1267
1196
  None, "--tag", "-t", help="Tags (can be specified multiple times)"
1268
1197
  ),
1269
- assignee: Optional[str] = typer.Option(
1198
+ assignee: str | None = typer.Option(
1270
1199
  None, "--assignee", "-a", help="Assignee username"
1271
1200
  ),
1272
- project: Optional[str] = typer.Option(
1201
+ project: str | None = typer.Option(
1273
1202
  None,
1274
1203
  "--project",
1275
1204
  help="Parent project/epic ID (synonym for --epic)",
1276
1205
  ),
1277
- epic: Optional[str] = typer.Option(
1206
+ epic: str | None = typer.Option(
1278
1207
  None,
1279
1208
  "--epic",
1280
1209
  help="Parent epic/project ID (synonym for --project)",
1281
1210
  ),
1282
- adapter: Optional[AdapterType] = typer.Option(
1211
+ adapter: AdapterType | None = typer.Option(
1283
1212
  None, "--adapter", help="Override default adapter"
1284
1213
  ),
1285
1214
  ) -> None:
@@ -1488,14 +1417,14 @@ def create(
1488
1417
 
1489
1418
  @app.command("list", deprecated=True, hidden=True)
1490
1419
  def list_tickets(
1491
- state: Optional[TicketState] = typer.Option(
1420
+ state: TicketState | None = typer.Option(
1492
1421
  None, "--state", "-s", help="Filter by state"
1493
1422
  ),
1494
- priority: Optional[Priority] = typer.Option(
1423
+ priority: Priority | None = typer.Option(
1495
1424
  None, "--priority", "-p", help="Filter by priority"
1496
1425
  ),
1497
1426
  limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1498
- adapter: Optional[AdapterType] = typer.Option(
1427
+ adapter: AdapterType | None = typer.Option(
1499
1428
  None, "--adapter", help="Override default adapter"
1500
1429
  ),
1501
1430
  ) -> None:
@@ -1551,7 +1480,7 @@ def list_tickets(
1551
1480
  def show(
1552
1481
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1553
1482
  comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1554
- adapter: Optional[AdapterType] = typer.Option(
1483
+ adapter: AdapterType | None = typer.Option(
1555
1484
  None, "--adapter", help="Override default adapter"
1556
1485
  ),
1557
1486
  ) -> None:
@@ -1607,7 +1536,7 @@ def show(
1607
1536
  def comment(
1608
1537
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1609
1538
  content: str = typer.Argument(..., help="Comment content"),
1610
- adapter: Optional[AdapterType] = typer.Option(
1539
+ adapter: AdapterType | None = typer.Option(
1611
1540
  None, "--adapter", help="Override default adapter"
1612
1541
  ),
1613
1542
  ) -> None:
@@ -1648,17 +1577,15 @@ def comment(
1648
1577
  @app.command(deprecated=True, hidden=True)
1649
1578
  def update(
1650
1579
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1651
- title: Optional[str] = typer.Option(None, "--title", help="New title"),
1652
- description: Optional[str] = typer.Option(
1580
+ title: str | None = typer.Option(None, "--title", help="New title"),
1581
+ description: str | None = typer.Option(
1653
1582
  None, "--description", "-d", help="New description"
1654
1583
  ),
1655
- priority: Optional[Priority] = typer.Option(
1584
+ priority: Priority | None = typer.Option(
1656
1585
  None, "--priority", "-p", help="New priority"
1657
1586
  ),
1658
- assignee: Optional[str] = typer.Option(
1659
- None, "--assignee", "-a", help="New assignee"
1660
- ),
1661
- adapter: Optional[AdapterType] = typer.Option(
1587
+ assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
1588
+ adapter: AdapterType | None = typer.Option(
1662
1589
  None, "--adapter", help="Override default adapter"
1663
1590
  ),
1664
1591
  ) -> None:
@@ -1718,13 +1645,13 @@ def update(
1718
1645
  @app.command(deprecated=True, hidden=True)
1719
1646
  def transition(
1720
1647
  ticket_id: str = typer.Argument(..., help="Ticket ID"),
1721
- state_positional: Optional[TicketState] = typer.Argument(
1648
+ state_positional: TicketState | None = typer.Argument(
1722
1649
  None, help="Target state (positional - deprecated, use --state instead)"
1723
1650
  ),
1724
- state: Optional[TicketState] = typer.Option(
1651
+ state: TicketState | None = typer.Option(
1725
1652
  None, "--state", "-s", help="Target state (recommended)"
1726
1653
  ),
1727
- adapter: Optional[AdapterType] = typer.Option(
1654
+ adapter: AdapterType | None = typer.Option(
1728
1655
  None, "--adapter", help="Override default adapter"
1729
1656
  ),
1730
1657
  ) -> None:
@@ -1789,12 +1716,12 @@ def transition(
1789
1716
 
1790
1717
  @app.command(deprecated=True, hidden=True)
1791
1718
  def search(
1792
- query: Optional[str] = typer.Argument(None, help="Search query"),
1793
- state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
1794
- priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
1795
- assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
1719
+ query: str | None = typer.Argument(None, help="Search query"),
1720
+ state: TicketState | None = typer.Option(None, "--state", "-s"),
1721
+ priority: Priority | None = typer.Option(None, "--priority", "-p"),
1722
+ assignee: str | None = typer.Option(None, "--assignee", "-a"),
1796
1723
  limit: int = typer.Option(10, "--limit", "-l"),
1797
- adapter: Optional[AdapterType] = typer.Option(
1724
+ adapter: AdapterType | None = typer.Option(
1798
1725
  None, "--adapter", help="Override default adapter"
1799
1726
  ),
1800
1727
  ) -> None:
@@ -1852,7 +1779,7 @@ app.add_typer(discover_app, name="discover")
1852
1779
  # Add diagnostics command
1853
1780
  @app.command("diagnose")
1854
1781
  def diagnose_command(
1855
- output_file: Optional[str] = typer.Option(
1782
+ output_file: str | None = typer.Option(
1856
1783
  None, "--output", "-o", help="Save full report to file"
1857
1784
  ),
1858
1785
  json_output: bool = typer.Option(
@@ -1899,7 +1826,7 @@ def diagnose_command(
1899
1826
 
1900
1827
  @app.command("doctor")
1901
1828
  def doctor_alias(
1902
- output_file: Optional[str] = typer.Option(
1829
+ output_file: str | None = typer.Option(
1903
1830
  None, "--output", "-o", help="Save full report to file"
1904
1831
  ),
1905
1832
  json_output: bool = typer.Option(
@@ -1941,50 +1868,229 @@ mcp_app = typer.Typer(
1941
1868
  add_completion=False,
1942
1869
  )
1943
1870
 
1944
- # Create install command group (like kuzu-memory)
1945
- install_app = typer.Typer(
1946
- name="install",
1947
- help="Manage AI system integrations",
1948
- add_completion=False,
1949
- )
1950
-
1951
1871
 
1952
- @install_app.command(name="add")
1953
- def install_add(
1954
- platform: str = typer.Argument(
1955
- ..., help="Platform to install (claude-code, claude-desktop, etc.)"
1872
+ @app.command()
1873
+ def install(
1874
+ adapter: str | None = typer.Option(
1875
+ None,
1876
+ "--adapter",
1877
+ "-a",
1878
+ help="Adapter type to use (interactive prompt if not specified)",
1879
+ ),
1880
+ project_path: str | None = typer.Option(
1881
+ None, "--path", help="Project path (default: current directory)"
1956
1882
  ),
1957
- project_path: Optional[str] = typer.Option(
1958
- None, "--project", help="Project directory"
1883
+ global_config: bool = typer.Option(
1884
+ False,
1885
+ "--global",
1886
+ "-g",
1887
+ help="Save to global config instead of project-specific",
1888
+ ),
1889
+ base_path: str | None = typer.Option(
1890
+ None,
1891
+ "--base-path",
1892
+ "-p",
1893
+ help="Base path for ticket storage (AITrackdown only)",
1894
+ ),
1895
+ api_key: str | None = typer.Option(
1896
+ None, "--api-key", help="API key for Linear or API token for JIRA"
1897
+ ),
1898
+ team_id: str | None = typer.Option(
1899
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
1900
+ ),
1901
+ jira_server: str | None = typer.Option(
1902
+ None,
1903
+ "--jira-server",
1904
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
1905
+ ),
1906
+ jira_email: str | None = typer.Option(
1907
+ None, "--jira-email", help="JIRA user email for authentication"
1908
+ ),
1909
+ jira_project: str | None = typer.Option(
1910
+ None, "--jira-project", help="Default JIRA project key"
1911
+ ),
1912
+ github_owner: str | None = typer.Option(
1913
+ None, "--github-owner", help="GitHub repository owner"
1914
+ ),
1915
+ github_repo: str | None = typer.Option(
1916
+ None, "--github-repo", help="GitHub repository name"
1917
+ ),
1918
+ github_token: str | None = typer.Option(
1919
+ None, "--github-token", help="GitHub Personal Access Token"
1920
+ ),
1921
+ platform: str | None = typer.Option(
1922
+ None,
1923
+ "--platform",
1924
+ help="Platform to configure MCP for (claude-code, claude-desktop, auggie, gemini, codex)",
1925
+ ),
1926
+ dry_run: bool = typer.Option(
1927
+ False,
1928
+ "--dry-run",
1929
+ help="Show what would be done without making changes (for platform installation)",
1930
+ ),
1931
+ ) -> None:
1932
+ """Install and initialize mcp-ticketer (synonymous with 'init' and 'setup').
1933
+
1934
+ Without arguments, runs interactive setup wizard to configure your ticket adapter.
1935
+ With --platform, installs MCP configuration for AI platforms.
1936
+
1937
+ This command serves two purposes:
1938
+ 1. Adapter initialization (same as 'init' and 'setup')
1939
+ 2. Platform MCP configuration (when using --platform flag)
1940
+
1941
+ Examples:
1942
+ # Interactive setup (same as 'init' and 'setup')
1943
+ mcp-ticketer install
1944
+
1945
+ # Setup with specific adapter
1946
+ mcp-ticketer install --adapter linear
1947
+
1948
+ # Install MCP for Claude Code
1949
+ mcp-ticketer install --platform claude-code
1950
+
1951
+ # Install MCP for Claude Desktop
1952
+ mcp-ticketer install --platform claude-desktop
1953
+
1954
+ """
1955
+ # If platform flag is provided, handle MCP platform installation
1956
+ if platform is not None:
1957
+ # Import configuration functions
1958
+ from .auggie_configure import configure_auggie_mcp
1959
+ from .codex_configure import configure_codex_mcp
1960
+ from .gemini_configure import configure_gemini_mcp
1961
+ from .mcp_configure import configure_claude_mcp
1962
+
1963
+ # Map platform names to configuration functions
1964
+ platform_mapping = {
1965
+ "claude-code": {
1966
+ "func": lambda: configure_claude_mcp(global_config=False, force=True),
1967
+ "name": "Claude Code",
1968
+ },
1969
+ "claude-desktop": {
1970
+ "func": lambda: configure_claude_mcp(global_config=True, force=True),
1971
+ "name": "Claude Desktop",
1972
+ },
1973
+ "auggie": {
1974
+ "func": lambda: configure_auggie_mcp(force=True),
1975
+ "name": "Auggie",
1976
+ },
1977
+ "gemini": {
1978
+ "func": lambda: configure_gemini_mcp(scope="project", force=True),
1979
+ "name": "Gemini CLI",
1980
+ },
1981
+ "codex": {
1982
+ "func": lambda: configure_codex_mcp(force=True),
1983
+ "name": "Codex",
1984
+ },
1985
+ }
1986
+
1987
+ if platform not in platform_mapping:
1988
+ console.print(f"[red]Unknown platform: {platform}[/red]")
1989
+ console.print("\n[bold]Available platforms:[/bold]")
1990
+ for p in platform_mapping.keys():
1991
+ console.print(f" • {p}")
1992
+ raise typer.Exit(1)
1993
+
1994
+ config = platform_mapping[platform]
1995
+
1996
+ if dry_run:
1997
+ console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
1998
+ return
1999
+
2000
+ try:
2001
+ config["func"]()
2002
+ except Exception as e:
2003
+ console.print(f"[red]Installation failed: {e}[/red]")
2004
+ raise typer.Exit(1)
2005
+ return
2006
+
2007
+ # Otherwise, delegate to init for adapter initialization
2008
+ # This makes 'install' and 'init' synonymous when called without --platform
2009
+ init(
2010
+ adapter=adapter,
2011
+ project_path=project_path,
2012
+ global_config=global_config,
2013
+ base_path=base_path,
2014
+ api_key=api_key,
2015
+ team_id=team_id,
2016
+ jira_server=jira_server,
2017
+ jira_email=jira_email,
2018
+ jira_project=jira_project,
2019
+ github_owner=github_owner,
2020
+ github_repo=github_repo,
2021
+ github_token=github_token,
2022
+ )
2023
+
2024
+
2025
+ @app.command()
2026
+ def remove(
2027
+ platform: str | None = typer.Argument(
2028
+ None,
2029
+ help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
1959
2030
  ),
1960
2031
  dry_run: bool = typer.Option(
1961
2032
  False, "--dry-run", help="Show what would be done without making changes"
1962
2033
  ),
1963
- verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"),
1964
2034
  ) -> None:
1965
- """Install mcp-ticketer integration for an AI platform.
2035
+ """Remove mcp-ticketer from AI platforms.
1966
2036
 
1967
- Each platform gets the right configuration automatically:
1968
- - claude-code: Project-level MCP server
1969
- - claude-desktop: Global MCP server
2037
+ Without arguments, shows help and available platforms.
2038
+ With a platform argument, removes MCP configuration for that platform.
1970
2039
 
1971
2040
  Examples:
1972
- # Install for Claude Code (project-level)
1973
- mcp-ticketer install add claude-code
2041
+ # Remove from Claude Code (project-level)
2042
+ mcp-ticketer remove claude-code
2043
+
2044
+ # Remove from Claude Desktop (global)
2045
+ mcp-ticketer remove claude-desktop
1974
2046
 
1975
- # Install for Claude Desktop (global)
1976
- mcp-ticketer install add claude-desktop
2047
+ # Remove from Auggie
2048
+ mcp-ticketer remove auggie
1977
2049
 
1978
2050
  # Dry run to preview changes
1979
- mcp-ticketer install add claude-code --dry-run
2051
+ mcp-ticketer remove claude-code --dry-run
1980
2052
 
1981
2053
  """
1982
- from .mcp_configure import configure_claude_mcp
2054
+ # If no platform specified, show help message
2055
+ if platform is None:
2056
+ console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
2057
+ console.print("Usage: mcp-ticketer remove <platform>\n")
2058
+ console.print("[bold]Available platforms:[/bold]")
2059
+ console.print(" • claude-code - Claude Code (project-level)")
2060
+ console.print(" • claude-desktop - Claude Desktop (global)")
2061
+ console.print(" • auggie - Auggie (global)")
2062
+ console.print(" • gemini - Gemini CLI (project-level by default)")
2063
+ console.print(" • codex - Codex (global)")
2064
+ return
2065
+
2066
+ # Import removal functions
2067
+ from .auggie_configure import remove_auggie_mcp
2068
+ from .codex_configure import remove_codex_mcp
2069
+ from .gemini_configure import remove_gemini_mcp
2070
+ from .mcp_configure import remove_claude_mcp
1983
2071
 
1984
- # Map platform names to configuration
2072
+ # Map platform names to removal functions
1985
2073
  platform_mapping = {
1986
- "claude-code": {"global": False, "name": "Claude Code"},
1987
- "claude-desktop": {"global": True, "name": "Claude Desktop"},
2074
+ "claude-code": {
2075
+ "func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
2076
+ "name": "Claude Code",
2077
+ },
2078
+ "claude-desktop": {
2079
+ "func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
2080
+ "name": "Claude Desktop",
2081
+ },
2082
+ "auggie": {
2083
+ "func": lambda: remove_auggie_mcp(dry_run=dry_run),
2084
+ "name": "Auggie",
2085
+ },
2086
+ "gemini": {
2087
+ "func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
2088
+ "name": "Gemini CLI",
2089
+ },
2090
+ "codex": {
2091
+ "func": lambda: remove_codex_mcp(dry_run=dry_run),
2092
+ "name": "Codex",
2093
+ },
1988
2094
  }
1989
2095
 
1990
2096
  if platform not in platform_mapping:
@@ -1996,17 +2102,48 @@ def install_add(
1996
2102
 
1997
2103
  config = platform_mapping[platform]
1998
2104
 
1999
- if dry_run:
2000
- console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
2001
- return
2002
-
2003
2105
  try:
2004
- configure_claude_mcp(global_config=config["global"], force=True)
2106
+ config["func"]()
2005
2107
  except Exception as e:
2006
- console.print(f"[red]Installation failed: {e}[/red]")
2108
+ console.print(f"[red]Removal failed: {e}[/red]")
2007
2109
  raise typer.Exit(1)
2008
2110
 
2009
2111
 
2112
+ @app.command()
2113
+ def uninstall(
2114
+ platform: str | None = typer.Argument(
2115
+ None,
2116
+ help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
2117
+ ),
2118
+ dry_run: bool = typer.Option(
2119
+ False, "--dry-run", help="Show what would be done without making changes"
2120
+ ),
2121
+ ) -> None:
2122
+ """Uninstall mcp-ticketer from AI platforms (alias for remove).
2123
+
2124
+ This is an alias for the 'remove' command.
2125
+
2126
+ Without arguments, shows help and available platforms.
2127
+ With a platform argument, removes MCP configuration for that platform.
2128
+
2129
+ Examples:
2130
+ # Uninstall from Claude Code (project-level)
2131
+ mcp-ticketer uninstall claude-code
2132
+
2133
+ # Uninstall from Claude Desktop (global)
2134
+ mcp-ticketer uninstall claude-desktop
2135
+
2136
+ # Uninstall from Auggie
2137
+ mcp-ticketer uninstall auggie
2138
+
2139
+ # Dry run to preview changes
2140
+ mcp-ticketer uninstall claude-code --dry-run
2141
+
2142
+ """
2143
+ # Call the remove command with the same parameters
2144
+ remove(platform=platform, dry_run=dry_run)
2145
+
2146
+
2010
2147
  @app.command(deprecated=True, hidden=True)
2011
2148
  def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
2012
2149
  """Check status of a queued operation.
@@ -2057,10 +2194,10 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
2057
2194
 
2058
2195
  @mcp_app.command(name="serve")
2059
2196
  def mcp_serve(
2060
- adapter: Optional[AdapterType] = typer.Option(
2197
+ adapter: AdapterType | None = typer.Option(
2061
2198
  None, "--adapter", "-a", help="Override default adapter type"
2062
2199
  ),
2063
- base_path: Optional[str] = typer.Option(
2200
+ base_path: str | None = typer.Option(
2064
2201
  None, "--base-path", help="Base path for AITrackdown adapter"
2065
2202
  ),
2066
2203
  ):
@@ -2077,7 +2214,8 @@ def mcp_serve(
2077
2214
  2. Global: ~/.mcp-ticketer/config.json
2078
2215
  3. Default: aitrackdown adapter with .aitrackdown base path
2079
2216
  """
2080
- from ..mcp.server import MCPTicketServer
2217
+ from ..mcp.server_sdk import configure_adapter
2218
+ from ..mcp.server_sdk import main as sdk_main
2081
2219
 
2082
2220
  # Load configuration (respects project-specific config in cwd)
2083
2221
  config = load_config()
@@ -2118,21 +2256,22 @@ def mcp_serve(
2118
2256
  if sys.stderr.isatty():
2119
2257
  # Only print if stderr is a terminal (not redirected)
2120
2258
  console.file = sys.stderr
2121
- console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
2259
+ console.print(
2260
+ f"[green]Starting MCP SDK server[/green] with {adapter_type} adapter"
2261
+ )
2122
2262
  console.print(
2123
2263
  "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
2124
2264
  )
2125
2265
 
2126
- # Create and run server
2266
+ # Configure adapter and run SDK server
2127
2267
  try:
2128
- server = MCPTicketServer(adapter_type, adapter_config)
2129
- asyncio.run(server.run())
2268
+ configure_adapter(adapter_type, adapter_config)
2269
+ sdk_main()
2130
2270
  except KeyboardInterrupt:
2131
- # Also send this to stderr
2271
+ # Send this to stderr
2132
2272
  if sys.stderr.isatty():
2133
2273
  console.print("\n[yellow]Server stopped by user[/yellow]")
2134
- if "server" in locals():
2135
- asyncio.run(server.stop())
2274
+ sys.exit(0)
2136
2275
  except Exception as e:
2137
2276
  # Log error to stderr
2138
2277
  sys.stderr.write(f"MCP server error: {e}\n")
@@ -2292,7 +2431,6 @@ def mcp_auggie(
2292
2431
 
2293
2432
  # Add command groups to main app (must be after all subcommands are defined)
2294
2433
  app.add_typer(mcp_app, name="mcp")
2295
- app.add_typer(install_app, name="install")
2296
2434
 
2297
2435
 
2298
2436
  def main():