mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,513 @@
1
+ """Platform installation commands for mcp-ticketer MCP server.
2
+
3
+ This module provides commands for installing and removing mcp-ticketer
4
+ MCP server configuration for various AI platforms (Claude, Gemini, Codex, Auggie).
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ # Import for typing compatibility
14
+ from .init_command import init
15
+ from .platform_detection import PlatformDetector, get_platform_by_name
16
+
17
+ console = Console()
18
+
19
+
20
+ def install(
21
+ platform: str | None = typer.Argument(
22
+ None,
23
+ help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
24
+ ),
25
+ auto_detect: bool = typer.Option(
26
+ False,
27
+ "--auto-detect",
28
+ "-d",
29
+ help="Auto-detect and show all installed AI platforms",
30
+ ),
31
+ install_all: bool = typer.Option(
32
+ False,
33
+ "--all",
34
+ help="Install for all detected platforms",
35
+ ),
36
+ adapter: str | None = typer.Option(
37
+ None,
38
+ "--adapter",
39
+ "-a",
40
+ help="Adapter type to use (interactive prompt if not specified)",
41
+ ),
42
+ project_path: str | None = typer.Option(
43
+ None, "--path", help="Project path (default: current directory)"
44
+ ),
45
+ global_config: bool = typer.Option(
46
+ False,
47
+ "--global",
48
+ "-g",
49
+ help="Save to global config instead of project-specific",
50
+ ),
51
+ base_path: str | None = typer.Option(
52
+ None,
53
+ "--base-path",
54
+ "-p",
55
+ help="Base path for ticket storage (AITrackdown only)",
56
+ ),
57
+ api_key: str | None = typer.Option(
58
+ None, "--api-key", help="API key for Linear or API token for JIRA"
59
+ ),
60
+ team_id: str | None = typer.Option(
61
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
62
+ ),
63
+ jira_server: str | None = typer.Option(
64
+ None,
65
+ "--jira-server",
66
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
67
+ ),
68
+ jira_email: str | None = typer.Option(
69
+ None, "--jira-email", help="JIRA user email for authentication"
70
+ ),
71
+ jira_project: str | None = typer.Option(
72
+ None, "--jira-project", help="Default JIRA project key"
73
+ ),
74
+ github_owner: str | None = typer.Option(
75
+ None, "--github-owner", help="GitHub repository owner"
76
+ ),
77
+ github_repo: str | None = typer.Option(
78
+ None, "--github-repo", help="GitHub repository name"
79
+ ),
80
+ github_token: str | None = typer.Option(
81
+ None, "--github-token", help="GitHub Personal Access Token"
82
+ ),
83
+ dry_run: bool = typer.Option(
84
+ False,
85
+ "--dry-run",
86
+ help="Show what would be done without making changes (for platform installation)",
87
+ ),
88
+ ) -> None:
89
+ """Install MCP server configuration for AI platforms.
90
+
91
+ This command configures mcp-ticketer as an MCP server for various AI
92
+ platforms. It updates platform-specific configuration files to enable
93
+ mcp-ticketer integration.
94
+
95
+ RECOMMENDED: Use 'mcp-ticketer setup' for first-time setup, which
96
+ handles both adapter configuration and platform installation together.
97
+
98
+ Platform Installation:
99
+ # Auto-detect and prompt for platform selection
100
+ mcp-ticketer install
101
+
102
+ # Show all detected platforms
103
+ mcp-ticketer install --auto-detect
104
+
105
+ # Install for all detected platforms
106
+ mcp-ticketer install --all
107
+
108
+ # Install for specific platform
109
+ mcp-ticketer install claude-code # Claude Code (project-level)
110
+ mcp-ticketer install claude-desktop # Claude Desktop (global)
111
+ mcp-ticketer install gemini # Gemini CLI
112
+ mcp-ticketer install codex # Codex
113
+ mcp-ticketer install auggie # Auggie
114
+
115
+ Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
116
+ mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
117
+
118
+ """
119
+ detector = PlatformDetector()
120
+
121
+ # Handle auto-detect flag (just show detected platforms and exit)
122
+ if auto_detect:
123
+ detected = detector.detect_all(
124
+ project_path=Path(project_path) if project_path else Path.cwd()
125
+ )
126
+
127
+ if not detected:
128
+ console.print("[yellow]No AI platforms detected.[/yellow]")
129
+ console.print("\n[bold]Supported platforms:[/bold]")
130
+ console.print(" • Claude Code - Project-level configuration")
131
+ console.print(" • Claude Desktop - Global GUI application")
132
+ console.print(" • Auggie - CLI tool with global config")
133
+ console.print(" • Codex - CLI tool with global config")
134
+ console.print(" • Gemini - CLI tool with project/global config")
135
+ console.print(
136
+ "\n[dim]Install these platforms to use them with mcp-ticketer.[/dim]"
137
+ )
138
+ return
139
+
140
+ console.print("[bold]Detected AI platforms:[/bold]\n")
141
+ table = Table(show_header=True, header_style="bold cyan")
142
+ table.add_column("Platform", style="green")
143
+ table.add_column("Status", style="yellow")
144
+ table.add_column("Scope", style="blue")
145
+ table.add_column("Config Path", style="dim")
146
+
147
+ for plat in detected:
148
+ status = "✓ Installed" if plat.is_installed else "⚠ Config Issue"
149
+ table.add_row(plat.display_name, status, plat.scope, str(plat.config_path))
150
+
151
+ console.print(table)
152
+ console.print(
153
+ "\n[dim]Run 'mcp-ticketer install <platform>' to configure a specific platform[/dim]"
154
+ )
155
+ console.print(
156
+ "[dim]Run 'mcp-ticketer install --all' to configure all detected platforms[/dim]"
157
+ )
158
+ return
159
+
160
+ # Handle --all flag (install for all detected platforms)
161
+ if install_all:
162
+ detected = detector.detect_all(
163
+ project_path=Path(project_path) if project_path else Path.cwd()
164
+ )
165
+
166
+ if not detected:
167
+ console.print("[yellow]No AI platforms detected.[/yellow]")
168
+ console.print(
169
+ "Run 'mcp-ticketer install --auto-detect' to see supported platforms."
170
+ )
171
+ return
172
+
173
+ # Handle dry-run mode - show what would be installed without actually installing
174
+ if dry_run:
175
+ console.print(
176
+ "\n[yellow]DRY RUN - The following platforms would be configured:[/yellow]\n"
177
+ )
178
+
179
+ installable_count = 0
180
+ for plat in detected:
181
+ if plat.is_installed:
182
+ console.print(f" ✓ {plat.display_name} ({plat.scope})")
183
+ installable_count += 1
184
+ else:
185
+ console.print(
186
+ f" ⚠ {plat.display_name} ({plat.scope}) - would be skipped (configuration issue)"
187
+ )
188
+
189
+ console.print(
190
+ f"\n[dim]Would configure {installable_count} platform(s)[/dim]"
191
+ )
192
+ return
193
+
194
+ console.print(
195
+ f"[bold]Installing for {len(detected)} detected platform(s)...[/bold]\n"
196
+ )
197
+
198
+ # Import configuration functions
199
+ from .auggie_configure import configure_auggie_mcp
200
+ from .codex_configure import configure_codex_mcp
201
+ from .gemini_configure import configure_gemini_mcp
202
+ from .mcp_configure import configure_claude_mcp
203
+
204
+ # Map platform names to configuration functions
205
+ platform_mapping = {
206
+ "claude-code": lambda: configure_claude_mcp(
207
+ global_config=False, force=True
208
+ ),
209
+ "claude-desktop": lambda: configure_claude_mcp(
210
+ global_config=True, force=True
211
+ ),
212
+ "auggie": lambda: configure_auggie_mcp(force=True),
213
+ "gemini": lambda: configure_gemini_mcp(scope="project", force=True),
214
+ "codex": lambda: configure_codex_mcp(force=True),
215
+ }
216
+
217
+ success_count = 0
218
+ failed = []
219
+
220
+ for plat in detected:
221
+ if not plat.is_installed:
222
+ console.print(
223
+ f"[yellow]⚠[/yellow] Skipping {plat.display_name} (configuration issue)"
224
+ )
225
+ continue
226
+
227
+ config_func = platform_mapping.get(plat.name)
228
+ if not config_func:
229
+ console.print(
230
+ f"[yellow]⚠[/yellow] No installer for {plat.display_name}"
231
+ )
232
+ continue
233
+
234
+ try:
235
+ console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
236
+ config_func()
237
+ success_count += 1
238
+ except Exception as e:
239
+ console.print(
240
+ f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
241
+ )
242
+ failed.append(plat.display_name)
243
+
244
+ console.print(
245
+ f"\n[bold]Installation complete:[/bold] {success_count} succeeded"
246
+ )
247
+ if failed:
248
+ console.print(f"[red]Failed:[/red] {', '.join(failed)}")
249
+ return
250
+
251
+ # If no platform argument and no adapter flag, auto-detect and prompt
252
+ if platform is None and adapter is None:
253
+ detected = detector.detect_all(
254
+ project_path=Path(project_path) if project_path else Path.cwd()
255
+ )
256
+
257
+ # Filter to only installed platforms
258
+ installed = [p for p in detected if p.is_installed]
259
+
260
+ if not installed:
261
+ console.print("[yellow]No AI platforms detected.[/yellow]")
262
+ console.print("\n[bold]To see supported platforms:[/bold]")
263
+ console.print(" mcp-ticketer install --auto-detect")
264
+ console.print("\n[bold]Or run legacy adapter setup:[/bold]")
265
+ console.print(" mcp-ticketer install --adapter <adapter-type>")
266
+ return
267
+
268
+ # Show detected platforms and prompt for selection
269
+ console.print("[bold]Detected AI platforms:[/bold]\n")
270
+ for idx, plat in enumerate(installed, 1):
271
+ console.print(f" {idx}. {plat.display_name} ({plat.scope})")
272
+
273
+ console.print(
274
+ "\n[dim]Enter the number of the platform to configure, or 'q' to quit:[/dim]"
275
+ )
276
+ choice = typer.prompt("Select platform")
277
+
278
+ if choice.lower() == "q":
279
+ console.print("Installation cancelled.")
280
+ return
281
+
282
+ try:
283
+ idx = int(choice) - 1
284
+ if idx < 0 or idx >= len(installed):
285
+ console.print("[red]Invalid selection.[/red]")
286
+ raise typer.Exit(1) from None
287
+ platform = installed[idx].name
288
+ except ValueError as e:
289
+ console.print("[red]Invalid input. Please enter a number.[/red]")
290
+ raise typer.Exit(1) from e
291
+
292
+ # If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
293
+ if platform is not None:
294
+ # Validate that the platform is actually installed
295
+ platform_info = get_platform_by_name(
296
+ platform, project_path=Path(project_path) if project_path else Path.cwd()
297
+ )
298
+
299
+ if platform_info and not platform_info.is_installed:
300
+ console.print(
301
+ f"[yellow]⚠[/yellow] {platform_info.display_name} was detected but has a configuration issue."
302
+ )
303
+ console.print(f"[dim]Config path: {platform_info.config_path}[/dim]\n")
304
+
305
+ proceed = typer.confirm(
306
+ "Do you want to proceed with installation anyway?", default=False
307
+ )
308
+ if not proceed:
309
+ console.print("Installation cancelled.")
310
+ return
311
+
312
+ elif not platform_info:
313
+ # Platform not detected at all - warn but allow proceeding
314
+ console.print(
315
+ f"[yellow]⚠[/yellow] Platform '{platform}' not detected on this system."
316
+ )
317
+ console.print(
318
+ "[dim]Run 'mcp-ticketer install --auto-detect' to see detected platforms.[/dim]\n"
319
+ )
320
+
321
+ proceed = typer.confirm(
322
+ "Do you want to proceed with installation anyway?", default=False
323
+ )
324
+ if not proceed:
325
+ console.print("Installation cancelled.")
326
+ return
327
+
328
+ # Import configuration functions
329
+ from .auggie_configure import configure_auggie_mcp
330
+ from .codex_configure import configure_codex_mcp
331
+ from .gemini_configure import configure_gemini_mcp
332
+ from .mcp_configure import configure_claude_mcp
333
+
334
+ # Map platform names to configuration functions
335
+ platform_mapping = {
336
+ "claude-code": {
337
+ "func": lambda: configure_claude_mcp(global_config=False, force=True),
338
+ "name": "Claude Code",
339
+ },
340
+ "claude-desktop": {
341
+ "func": lambda: configure_claude_mcp(global_config=True, force=True),
342
+ "name": "Claude Desktop",
343
+ },
344
+ "auggie": {
345
+ "func": lambda: configure_auggie_mcp(force=True),
346
+ "name": "Auggie",
347
+ },
348
+ "gemini": {
349
+ "func": lambda: configure_gemini_mcp(scope="project", force=True),
350
+ "name": "Gemini CLI",
351
+ },
352
+ "codex": {
353
+ "func": lambda: configure_codex_mcp(force=True),
354
+ "name": "Codex",
355
+ },
356
+ }
357
+
358
+ if platform not in platform_mapping:
359
+ console.print(f"[red]Unknown platform: {platform}[/red]")
360
+ console.print("\n[bold]Available platforms:[/bold]")
361
+ for p in platform_mapping.keys():
362
+ console.print(f" • {p}")
363
+ raise typer.Exit(1) from None
364
+
365
+ config = platform_mapping[platform]
366
+
367
+ if dry_run:
368
+ console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
369
+ return
370
+
371
+ try:
372
+ config["func"]()
373
+ except Exception as e:
374
+ console.print(f"[red]Installation failed: {e}[/red]")
375
+ raise typer.Exit(1) from e
376
+ return
377
+
378
+ # Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
379
+ # This makes 'install' and 'init' synonymous when called without platform argument
380
+ init(
381
+ adapter=adapter,
382
+ project_path=project_path,
383
+ global_config=global_config,
384
+ base_path=base_path,
385
+ api_key=api_key,
386
+ team_id=team_id,
387
+ jira_server=jira_server,
388
+ jira_email=jira_email,
389
+ jira_project=jira_project,
390
+ github_owner=github_owner,
391
+ github_repo=github_repo,
392
+ github_token=github_token,
393
+ )
394
+
395
+
396
+ def remove(
397
+ platform: str | None = typer.Argument(
398
+ None,
399
+ help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
400
+ ),
401
+ dry_run: bool = typer.Option(
402
+ False, "--dry-run", help="Show what would be done without making changes"
403
+ ),
404
+ ) -> None:
405
+ """Remove mcp-ticketer from AI platforms.
406
+
407
+ Without arguments, shows help and available platforms.
408
+ With a platform argument, removes MCP configuration for that platform.
409
+
410
+ Examples:
411
+ # Remove from Claude Code (project-level)
412
+ mcp-ticketer remove claude-code
413
+
414
+ # Remove from Claude Desktop (global)
415
+ mcp-ticketer remove claude-desktop
416
+
417
+ # Remove from Auggie
418
+ mcp-ticketer remove auggie
419
+
420
+ # Dry run to preview changes
421
+ mcp-ticketer remove claude-code --dry-run
422
+
423
+ """
424
+ # If no platform specified, show help message
425
+ if platform is None:
426
+ console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
427
+ console.print("Usage: mcp-ticketer remove <platform>\n")
428
+ console.print("[bold]Available platforms:[/bold]")
429
+ console.print(" • claude-code - Claude Code (project-level)")
430
+ console.print(" • claude-desktop - Claude Desktop (global)")
431
+ console.print(" • auggie - Auggie (global)")
432
+ console.print(" • gemini - Gemini CLI (project-level by default)")
433
+ console.print(" • codex - Codex (global)")
434
+ return
435
+
436
+ # Import removal functions
437
+ from .auggie_configure import remove_auggie_mcp
438
+ from .codex_configure import remove_codex_mcp
439
+ from .gemini_configure import remove_gemini_mcp
440
+ from .mcp_configure import remove_claude_mcp
441
+
442
+ # Map platform names to removal functions
443
+ platform_mapping = {
444
+ "claude-code": {
445
+ "func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
446
+ "name": "Claude Code",
447
+ },
448
+ "claude-desktop": {
449
+ "func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
450
+ "name": "Claude Desktop",
451
+ },
452
+ "auggie": {
453
+ "func": lambda: remove_auggie_mcp(dry_run=dry_run),
454
+ "name": "Auggie",
455
+ },
456
+ "gemini": {
457
+ "func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
458
+ "name": "Gemini CLI",
459
+ },
460
+ "codex": {
461
+ "func": lambda: remove_codex_mcp(dry_run=dry_run),
462
+ "name": "Codex",
463
+ },
464
+ }
465
+
466
+ if platform not in platform_mapping:
467
+ console.print(f"[red]Unknown platform: {platform}[/red]")
468
+ console.print("\n[bold]Available platforms:[/bold]")
469
+ for p in platform_mapping.keys():
470
+ console.print(f" • {p}")
471
+ raise typer.Exit(1) from None
472
+
473
+ config = platform_mapping[platform]
474
+
475
+ try:
476
+ config["func"]()
477
+ except Exception as e:
478
+ console.print(f"[red]Removal failed: {e}[/red]")
479
+ raise typer.Exit(1) from e
480
+
481
+
482
+ def uninstall(
483
+ platform: str | None = typer.Argument(
484
+ None,
485
+ help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
486
+ ),
487
+ dry_run: bool = typer.Option(
488
+ False, "--dry-run", help="Show what would be done without making changes"
489
+ ),
490
+ ) -> None:
491
+ """Uninstall mcp-ticketer from AI platforms (alias for remove).
492
+
493
+ This is an alias for the 'remove' command.
494
+
495
+ Without arguments, shows help and available platforms.
496
+ With a platform argument, removes MCP configuration for that platform.
497
+
498
+ Examples:
499
+ # Uninstall from Claude Code (project-level)
500
+ mcp-ticketer uninstall claude-code
501
+
502
+ # Uninstall from Claude Desktop (global)
503
+ mcp-ticketer uninstall claude-desktop
504
+
505
+ # Uninstall from Auggie
506
+ mcp-ticketer uninstall auggie
507
+
508
+ # Dry run to preview changes
509
+ mcp-ticketer uninstall claude-code --dry-run
510
+
511
+ """
512
+ # Call the remove command with the same parameters
513
+ remove(platform=platform, dry_run=dry_run)
@@ -0,0 +1,126 @@
1
+ """Reliable Python executable detection for mcp-ticketer.
2
+
3
+ This module provides reliable detection of the Python executable for mcp-ticketer
4
+ across different installation methods (pipx, pip, uv, direct venv).
5
+
6
+ The module follows the proven pattern from mcp-vector-search:
7
+ - Detect venv Python path reliably
8
+ - Use `python -m mcp_ticketer.mcp.server` instead of binary paths
9
+ - Support multiple installation methods transparently
10
+ """
11
+
12
+ import os
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def get_mcp_ticketer_python(project_path: Path | None = None) -> str:
19
+ """Get the correct Python executable for mcp-ticketer MCP server.
20
+
21
+ This function follows the mcp-vector-search pattern of using project-specific
22
+ venv Python for proper project isolation and dependency management.
23
+
24
+ Detection priority:
25
+ 1. Project-local venv (.venv/bin/python) if project_path provided
26
+ 2. Current Python executable if in pipx venv
27
+ 3. Python from mcp-ticketer binary shebang
28
+ 4. Current Python executable (fallback)
29
+
30
+ Args:
31
+ project_path: Optional project directory path to check for local venv
32
+
33
+ Returns:
34
+ Path to Python executable
35
+
36
+ Examples:
37
+ >>> # With project venv
38
+ >>> python_path = get_mcp_ticketer_python(Path("/home/user/my-project"))
39
+ >>> # Returns: "/home/user/my-project/.venv/bin/python"
40
+
41
+ >>> # Without project path (fallback to pipx)
42
+ >>> python_path = get_mcp_ticketer_python()
43
+ >>> # Returns: "/Users/user/.local/pipx/venvs/mcp-ticketer/bin/python"
44
+
45
+ """
46
+ # Priority 1: Check for project-local venv
47
+ if project_path:
48
+ project_venv_python = project_path / ".venv" / "bin" / "python"
49
+ if project_venv_python.exists():
50
+ return str(project_venv_python)
51
+
52
+ current_executable = sys.executable
53
+
54
+ # Priority 2: Check if we're in a pipx venv
55
+ if "/pipx/venvs/" in current_executable:
56
+ return current_executable
57
+
58
+ # Priority 3: Check mcp-ticketer binary shebang
59
+ mcp_ticketer_path = shutil.which("mcp-ticketer")
60
+ if mcp_ticketer_path:
61
+ try:
62
+ with open(mcp_ticketer_path) as f:
63
+ first_line = f.readline().strip()
64
+ if first_line.startswith("#!") and "python" in first_line:
65
+ python_path = first_line[2:].strip()
66
+ if os.path.exists(python_path):
67
+ return python_path
68
+ except OSError:
69
+ pass
70
+
71
+ # Priority 4: Fallback to current Python
72
+ return current_executable
73
+
74
+
75
+ def get_mcp_server_command(project_path: str | None = None) -> tuple[str, list[str]]:
76
+ """Get the complete command to run the MCP server.
77
+
78
+ Args:
79
+ project_path: Optional project path to pass as argument and check for venv
80
+
81
+ Returns:
82
+ Tuple of (python_executable, args_list)
83
+ Example: ("/path/to/python", ["-m", "mcp_ticketer.mcp.server", "/project/path"])
84
+
85
+ Examples:
86
+ >>> python, args = get_mcp_server_command("/home/user/project")
87
+ >>> # python: "/home/user/project/.venv/bin/python" (if .venv exists)
88
+ >>> # args: ["-m", "mcp_ticketer.mcp.server", "/home/user/project"]
89
+
90
+ """
91
+ # Convert project_path to Path object for venv detection
92
+ project_path_obj = Path(project_path) if project_path else None
93
+ python_path = get_mcp_ticketer_python(project_path=project_path_obj)
94
+ args = ["-m", "mcp_ticketer.mcp.server"]
95
+
96
+ if project_path:
97
+ args.append(str(project_path))
98
+
99
+ return python_path, args
100
+
101
+
102
+ def validate_python_executable(python_path: str) -> bool:
103
+ """Validate that a Python executable can import mcp_ticketer.
104
+
105
+ Args:
106
+ python_path: Path to Python executable to validate
107
+
108
+ Returns:
109
+ True if Python can import mcp_ticketer, False otherwise
110
+
111
+ Examples:
112
+ >>> is_valid = validate_python_executable("/usr/bin/python3")
113
+ >>> # Returns: False (system Python doesn't have mcp_ticketer)
114
+
115
+ """
116
+ try:
117
+ import subprocess
118
+
119
+ result = subprocess.run(
120
+ [python_path, "-c", "import mcp_ticketer.mcp.server"],
121
+ capture_output=True,
122
+ timeout=5,
123
+ )
124
+ return result.returncode == 0
125
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
126
+ return False