mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,794 @@
1
+ """Setup command for mcp-ticketer - smart initialization with platform detection."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+ # Mapping of adapter types to their required dependencies
14
+ ADAPTER_DEPENDENCIES = {
15
+ "linear": {"package": "gql[httpx]", "extras": "linear"},
16
+ "jira": {"package": "jira", "extras": "jira"},
17
+ "github": {"package": "PyGithub", "extras": "github"},
18
+ "aitrackdown": None, # No extra dependencies
19
+ }
20
+
21
+
22
+ def _check_package_installed(adapter_type: str) -> bool:
23
+ """Check if adapter-specific package is installed.
24
+
25
+ Args:
26
+ adapter_type: Type of adapter (linear, jira, github)
27
+
28
+ Returns:
29
+ True if package is installed, False otherwise
30
+
31
+ """
32
+ try:
33
+ # Try to import the package
34
+ if adapter_type == "linear":
35
+ import gql # noqa: F401
36
+ elif adapter_type == "jira":
37
+ import jira # noqa: F401
38
+ elif adapter_type == "github":
39
+ import github # noqa: F401
40
+ return True
41
+ except ImportError:
42
+ return False
43
+
44
+
45
+ def _check_and_install_adapter_dependencies(
46
+ adapter_type: str, console: Console
47
+ ) -> bool:
48
+ """Check if adapter-specific dependencies are installed and offer to install.
49
+
50
+ Args:
51
+ adapter_type: Type of adapter (linear, jira, github, aitrackdown)
52
+ console: Rich console for output
53
+
54
+ Returns:
55
+ True if dependencies are satisfied (installed or not needed), False if failed
56
+
57
+ """
58
+ # Check if adapter needs extra dependencies
59
+ dependency_info = ADAPTER_DEPENDENCIES.get(adapter_type)
60
+
61
+ if dependency_info is None:
62
+ # No extra dependencies needed (e.g., aitrackdown)
63
+ console.print(
64
+ f"[green]✓[/green] No extra dependencies required for {adapter_type}\n"
65
+ )
66
+ return True
67
+
68
+ # Check if the required package is already installed
69
+ if _check_package_installed(adapter_type):
70
+ console.print(
71
+ f"[green]✓[/green] {adapter_type.capitalize()} dependencies already installed\n"
72
+ )
73
+ return True
74
+
75
+ # Dependencies not installed
76
+ console.print(
77
+ f"[yellow]⚠[/yellow] {adapter_type.capitalize()} adapter requires additional dependencies\n"
78
+ )
79
+ console.print(f"[dim]Required package: {dependency_info['package']}[/dim]\n")
80
+
81
+ # Prompt user to install
82
+ try:
83
+ if not typer.confirm("Install dependencies now?", default=True):
84
+ console.print(
85
+ "\n[yellow]Skipping installation. Install manually with:[/yellow]"
86
+ )
87
+ console.print(
88
+ f"[cyan] pip install mcp-ticketer[{dependency_info['extras']}][/cyan]\n"
89
+ )
90
+ return True # User declined, but we continue
91
+
92
+ except typer.Abort:
93
+ console.print("\n[yellow]Installation cancelled[/yellow]\n")
94
+ return True
95
+
96
+ # Install dependencies
97
+ console.print(f"[cyan]Installing {adapter_type} dependencies...[/cyan]\n")
98
+
99
+ try:
100
+ # Run pip install with the extras
101
+ subprocess.check_call(
102
+ [
103
+ sys.executable,
104
+ "-m",
105
+ "pip",
106
+ "install",
107
+ f"mcp-ticketer[{dependency_info['extras']}]",
108
+ ],
109
+ stdout=subprocess.DEVNULL,
110
+ stderr=subprocess.PIPE,
111
+ )
112
+
113
+ console.print(
114
+ f"[green]✓[/green] Successfully installed {adapter_type} dependencies\n"
115
+ )
116
+ return True
117
+
118
+ except subprocess.CalledProcessError as e:
119
+ console.print(
120
+ f"[red]✗[/red] Failed to install dependencies: {e.stderr.decode() if e.stderr else 'Unknown error'}\n"
121
+ )
122
+ console.print("[yellow]Please install manually with:[/yellow]")
123
+ console.print(
124
+ f"[cyan] pip install mcp-ticketer[{dependency_info['extras']}][/cyan]\n"
125
+ )
126
+ return True # Continue even if installation failed
127
+
128
+
129
+ def _prompt_for_adapter_selection(console: Console) -> str:
130
+ """Interactive prompt for adapter selection.
131
+
132
+ Args:
133
+ console: Rich console for output
134
+
135
+ Returns:
136
+ Selected adapter type
137
+
138
+ """
139
+ console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
140
+ console.print("Choose which ticket system you want to connect to:\n")
141
+
142
+ # Define adapter options with descriptions
143
+ adapters = [
144
+ {
145
+ "name": "linear",
146
+ "title": "Linear",
147
+ "description": "Modern project management (linear.app)",
148
+ "requirements": "API key and team ID",
149
+ },
150
+ {
151
+ "name": "github",
152
+ "title": "GitHub Issues",
153
+ "description": "GitHub repository issues",
154
+ "requirements": "Personal access token, owner, and repo",
155
+ },
156
+ {
157
+ "name": "jira",
158
+ "title": "JIRA",
159
+ "description": "Atlassian JIRA project management",
160
+ "requirements": "Server URL, email, and API token",
161
+ },
162
+ {
163
+ "name": "aitrackdown",
164
+ "title": "Local Files (AITrackdown)",
165
+ "description": "Store tickets in local files (no external service)",
166
+ "requirements": "None - works offline",
167
+ },
168
+ ]
169
+
170
+ # Display options
171
+ for i, adapter in enumerate(adapters, 1):
172
+ console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
173
+ console.print(f" {adapter['description']}")
174
+ console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
175
+
176
+ # Get user selection
177
+ while True:
178
+ try:
179
+ choice = typer.prompt("Select adapter (1-4)", type=int, default=1)
180
+ if 1 <= choice <= len(adapters):
181
+ selected_adapter = adapters[choice - 1]
182
+ console.print(
183
+ f"\n[green]✓ Selected: {selected_adapter['title']}[/green]"
184
+ )
185
+ return selected_adapter["name"]
186
+ else:
187
+ console.print(
188
+ f"[red]Please enter a number between 1 and {len(adapters)}[/red]"
189
+ )
190
+ except (ValueError, typer.Abort):
191
+ console.print("[yellow]Setup cancelled.[/yellow]")
192
+ raise typer.Exit(0) from None
193
+
194
+
195
+ def setup(
196
+ project_path: str | None = typer.Option(
197
+ None, "--path", help="Project path (default: current directory)"
198
+ ),
199
+ skip_platforms: bool = typer.Option(
200
+ False,
201
+ "--skip-platforms",
202
+ help="Skip platform installation (only initialize adapter)",
203
+ ),
204
+ force_reinit: bool = typer.Option(
205
+ False,
206
+ "--force-reinit",
207
+ help="Force re-initialization even if config exists",
208
+ ),
209
+ ) -> None:
210
+ """Smart setup command - combines init + platform installation.
211
+
212
+ This command intelligently detects your current setup state and only
213
+ performs necessary configuration. It's the recommended way to get started.
214
+
215
+ Detection & Smart Actions:
216
+ - First run: Full setup (init + platform installation)
217
+ - Existing config: Skip init, offer platform installation
218
+ - Detects changes: Offers to update configurations
219
+ - Respects existing: Won't overwrite without confirmation
220
+
221
+ Examples:
222
+ # Smart setup (recommended for first-time setup)
223
+ mcp-ticketer setup
224
+
225
+ # Setup for different project
226
+ mcp-ticketer setup --path /path/to/project
227
+
228
+ # Re-initialize configuration
229
+ mcp-ticketer setup --force-reinit
230
+
231
+ # Only init adapter, skip platform installation
232
+ mcp-ticketer setup --skip-platforms
233
+
234
+ Note: For advanced configuration, use 'init' and 'install' separately.
235
+
236
+ """
237
+ from .platform_detection import PlatformDetector
238
+
239
+ proj_path = Path(project_path) if project_path else Path.cwd()
240
+ config_path = proj_path / ".mcp-ticketer" / "config.json"
241
+
242
+ console.print("[bold cyan]🚀 MCP Ticketer Smart Setup[/bold cyan]\n")
243
+
244
+ # Step 1: Detect existing configuration
245
+ config_exists = config_path.exists()
246
+ config_valid = False
247
+ current_adapter = None
248
+
249
+ if config_exists and not force_reinit:
250
+ try:
251
+ with open(config_path) as f:
252
+ config = json.load(f)
253
+ current_adapter = config.get("default_adapter")
254
+ config_valid = bool(current_adapter and config.get("adapters"))
255
+ except (json.JSONDecodeError, OSError):
256
+ config_valid = False
257
+
258
+ if config_valid:
259
+ console.print("[green]✓[/green] Configuration detected")
260
+ console.print(f"[dim] Adapter: {current_adapter}[/dim]")
261
+ console.print(f"[dim] Location: {config_path}[/dim]\n")
262
+
263
+ # Offer to reconfigure
264
+ if not typer.confirm(
265
+ "Configuration already exists. Keep existing settings?", default=True
266
+ ):
267
+ console.print("[cyan]Re-initializing configuration...[/cyan]\n")
268
+ force_reinit = True
269
+ config_valid = False
270
+ else:
271
+ if config_exists:
272
+ console.print(
273
+ "[yellow]⚠[/yellow] Configuration file exists but is invalid\n"
274
+ )
275
+ else:
276
+ console.print("[yellow]⚠[/yellow] No configuration found\n")
277
+
278
+ # Step 2: Initialize adapter configuration if needed
279
+ if not config_valid or force_reinit:
280
+ console.print("[bold]Step 1/2: Adapter Configuration[/bold]\n")
281
+
282
+ # Run init command non-interactively through function call
283
+ # We'll use the discover and prompt flow from init
284
+ from ..core.env_discovery import discover_config
285
+ from .init_command import _init_adapter_internal
286
+
287
+ discovered = discover_config(proj_path)
288
+ adapter_type = None
289
+
290
+ # Try auto-discovery
291
+ if discovered and discovered.adapters:
292
+ primary = discovered.get_primary_adapter()
293
+ if primary:
294
+ adapter_type = primary.adapter_type
295
+ console.print(f"[green]✓ Auto-detected {adapter_type} adapter[/green]")
296
+ console.print(f"[dim] Source: {primary.found_in}[/dim]")
297
+ console.print(f"[dim] Confidence: {primary.confidence:.0%}[/dim]\n")
298
+
299
+ if not typer.confirm(
300
+ f"Use detected {adapter_type} adapter?", default=True
301
+ ):
302
+ adapter_type = None
303
+
304
+ # If no adapter detected, prompt for selection
305
+ if not adapter_type:
306
+ adapter_type = _prompt_for_adapter_selection(console)
307
+
308
+ # Now run the full init with the selected adapter
309
+ console.print(f"\n[cyan]Initializing {adapter_type} adapter...[/cyan]\n")
310
+
311
+ # Call internal init function programmatically (NOT the CLI command)
312
+ # Note: Only pass required parameters - all optional params should be None
313
+ # to avoid passing OptionInfo objects which are not JSON serializable
314
+ success = _init_adapter_internal(
315
+ adapter=adapter_type,
316
+ project_path=str(proj_path),
317
+ global_config=False,
318
+ base_path=None,
319
+ api_key=None,
320
+ team_id=None,
321
+ jira_server=None,
322
+ jira_email=None,
323
+ jira_project=None,
324
+ github_url=None,
325
+ github_token=None,
326
+ )
327
+
328
+ if not success:
329
+ console.print("[red]Failed to initialize adapter configuration[/red]")
330
+ raise typer.Exit(1) from None
331
+
332
+ # Check and install adapter-specific dependencies
333
+ _check_and_install_adapter_dependencies(adapter_type, console)
334
+
335
+ # Update existing MCP configurations with new credentials
336
+ _update_mcp_json_credentials(proj_path, console)
337
+
338
+ console.print("\n[green]✓ Adapter configuration complete[/green]\n")
339
+ else:
340
+ console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
341
+
342
+ # Even though adapter is configured, prompt for default values
343
+ # This handles the case where credentials exist but defaults were never set
344
+ _prompt_and_update_default_values(config_path, current_adapter, console)
345
+
346
+ # Update existing MCP configurations with credentials
347
+ _update_mcp_json_credentials(proj_path, console)
348
+
349
+ # Step 3: Platform installation
350
+ if skip_platforms:
351
+ console.print(
352
+ "[yellow]⚠[/yellow] Skipping platform installation (--skip-platforms)\n"
353
+ )
354
+ _show_setup_complete_message(console, proj_path)
355
+ return
356
+
357
+ console.print("[bold]Step 2/2: Platform Installation[/bold]\n")
358
+
359
+ # Detect available platforms
360
+ detector = PlatformDetector()
361
+ detected = detector.detect_all(project_path=proj_path, exclude_desktop=True)
362
+
363
+ if not detected:
364
+ console.print("[yellow]No AI platforms detected on this system.[/yellow]")
365
+ console.print(
366
+ "\n[dim]Supported platforms: Claude Code, Claude Desktop, Gemini, Codex, Auggie[/dim]"
367
+ )
368
+ console.print(
369
+ "[dim]Install these platforms to use them with mcp-ticketer.[/dim]\n"
370
+ )
371
+ _show_setup_complete_message(console, proj_path)
372
+ return
373
+
374
+ # Filter to only installed platforms
375
+ installed = [p for p in detected if p.is_installed]
376
+
377
+ if not installed:
378
+ console.print(
379
+ "[yellow]AI platforms detected but have configuration issues.[/yellow]"
380
+ )
381
+ console.print(
382
+ "\n[dim]Run 'mcp-ticketer install --auto-detect' for details.[/dim]\n"
383
+ )
384
+ _show_setup_complete_message(console, proj_path)
385
+ return
386
+
387
+ # Show detected platforms
388
+ console.print(f"[green]✓[/green] Detected {len(installed)} platform(s):\n")
389
+ for plat in installed:
390
+ console.print(f" • {plat.display_name} ({plat.scope})")
391
+
392
+ console.print()
393
+
394
+ # Check if mcp-ticketer is already configured for these platforms
395
+ already_configured = _check_existing_platform_configs(installed, proj_path)
396
+
397
+ if already_configured:
398
+ console.print(
399
+ f"[green]✓[/green] mcp-ticketer already configured for {len(already_configured)} platform(s)\n"
400
+ )
401
+ for plat_name in already_configured:
402
+ console.print(f" • {plat_name}")
403
+ console.print()
404
+
405
+ if not typer.confirm("Update platform configurations anyway?", default=False):
406
+ console.print("[yellow]Skipping platform installation[/yellow]\n")
407
+ _show_setup_complete_message(console, proj_path)
408
+ return
409
+
410
+ # Offer to install for all or select specific
411
+ console.print("[bold]Platform Installation Options:[/bold]")
412
+ console.print("1. Install for all detected platforms")
413
+ console.print("2. Select specific platform")
414
+ console.print("3. Skip platform installation")
415
+
416
+ try:
417
+ choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
418
+ except typer.Abort:
419
+ console.print("[yellow]Setup cancelled[/yellow]")
420
+ raise typer.Exit(0) from None
421
+
422
+ if choice == 3:
423
+ console.print("[yellow]Skipping platform installation[/yellow]\n")
424
+ _show_setup_complete_message(console, proj_path)
425
+ return
426
+
427
+ # Import configuration functions
428
+ from .auggie_configure import configure_auggie_mcp
429
+ from .codex_configure import configure_codex_mcp
430
+ from .gemini_configure import configure_gemini_mcp
431
+ from .mcp_configure import configure_claude_mcp
432
+
433
+ platform_mapping = {
434
+ "claude-code": lambda: configure_claude_mcp(global_config=False, force=True),
435
+ "claude-desktop": lambda: configure_claude_mcp(global_config=True, force=True),
436
+ "auggie": lambda: configure_auggie_mcp(force=True),
437
+ "gemini": lambda: configure_gemini_mcp(scope="project", force=True),
438
+ "codex": lambda: configure_codex_mcp(force=True),
439
+ }
440
+
441
+ platforms_to_install = []
442
+
443
+ if choice == 1:
444
+ # Install for all
445
+ platforms_to_install = installed
446
+ elif choice == 2:
447
+ # Select specific platform
448
+ console.print("\n[bold]Select platform:[/bold]")
449
+ for idx, plat in enumerate(installed, 1):
450
+ console.print(f" {idx}. {plat.display_name} ({plat.scope})")
451
+
452
+ try:
453
+ plat_choice = typer.prompt("\nSelect platform number", type=int)
454
+ if 1 <= plat_choice <= len(installed):
455
+ platforms_to_install = [installed[plat_choice - 1]]
456
+ else:
457
+ console.print("[red]Invalid selection[/red]")
458
+ raise typer.Exit(1) from None
459
+ except typer.Abort:
460
+ console.print("[yellow]Setup cancelled[/yellow]")
461
+ raise typer.Exit(0) from None
462
+
463
+ # Install for selected platforms
464
+ console.print()
465
+ success_count = 0
466
+ failed = []
467
+
468
+ for plat in platforms_to_install:
469
+ config_func = platform_mapping.get(plat.name)
470
+ if not config_func:
471
+ console.print(f"[yellow]⚠[/yellow] No installer for {plat.display_name}")
472
+ continue
473
+
474
+ try:
475
+ console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
476
+ config_func()
477
+ console.print(f"[green]✓[/green] {plat.display_name} configured\n")
478
+ success_count += 1
479
+ except Exception as e:
480
+ console.print(
481
+ f"[red]✗[/red] Failed to configure {plat.display_name}: {e}\n"
482
+ )
483
+ failed.append(plat.display_name)
484
+
485
+ # Summary
486
+ console.print(
487
+ f"[bold]Platform Installation:[/bold] {success_count}/{len(platforms_to_install)} succeeded"
488
+ )
489
+ if failed:
490
+ console.print(f"[red]Failed:[/red] {', '.join(failed)}")
491
+
492
+ console.print()
493
+ _show_setup_complete_message(console, proj_path)
494
+
495
+
496
+ def _prompt_and_update_default_values(
497
+ config_path: Path, adapter_type: str, console: Console
498
+ ) -> None:
499
+ """Prompt user for default values and update configuration.
500
+
501
+ This function handles the case where adapter credentials exist but
502
+ default values (default_user, default_epic, default_project, default_tags)
503
+ need to be set or updated.
504
+
505
+ Args:
506
+ config_path: Path to the configuration file (.mcp-ticketer/config.json)
507
+ adapter_type: Type of adapter (linear, jira, github, aitrackdown)
508
+ console: Rich console for output
509
+
510
+ Raises:
511
+ typer.Exit: If configuration cannot be loaded or updated
512
+
513
+ """
514
+ from .configure import prompt_default_values
515
+
516
+ try:
517
+ # Load current config to get existing default values
518
+ with open(config_path) as f:
519
+ existing_config = json.load(f)
520
+
521
+ existing_defaults = {
522
+ "default_user": existing_config.get("default_user"),
523
+ "default_epic": existing_config.get("default_epic"),
524
+ "default_project": existing_config.get("default_project"),
525
+ "default_tags": existing_config.get("default_tags"),
526
+ }
527
+
528
+ # Prompt for default values
529
+ console.print("[bold]Configure Default Values[/bold] (for ticket creation)\n")
530
+ default_values = prompt_default_values(
531
+ adapter_type=adapter_type, existing_values=existing_defaults
532
+ )
533
+
534
+ # Update config with new default values
535
+ if default_values:
536
+ existing_config.update(default_values)
537
+ with open(config_path, "w") as f:
538
+ json.dump(existing_config, f, indent=2)
539
+ console.print("\n[green]✓ Default values updated[/green]\n")
540
+ else:
541
+ console.print("\n[dim]No default values set[/dim]\n")
542
+
543
+ except json.JSONDecodeError as e:
544
+ console.print(f"[red]✗ Invalid JSON in configuration file: {e}[/red]\n")
545
+ console.print(
546
+ "[yellow]Please fix the configuration file manually or run 'mcp-ticketer init --force'[/yellow]\n"
547
+ )
548
+ except OSError as e:
549
+ console.print(f"[red]✗ Could not read/write configuration file: {e}[/red]\n")
550
+ console.print("[yellow]Please check file permissions and try again[/yellow]\n")
551
+ except Exception as e:
552
+ console.print(f"[red]✗ Unexpected error updating default values: {e}[/red]\n")
553
+ console.print(
554
+ "[yellow]Configuration may be incomplete. Run 'mcp-ticketer doctor' to verify[/yellow]\n"
555
+ )
556
+
557
+
558
+ def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[str]:
559
+ """Check if mcp-ticketer is already configured for given platforms.
560
+
561
+ Args:
562
+ platforms: List of DetectedPlatform objects
563
+ proj_path: Project path
564
+
565
+ Returns:
566
+ List of platform display names that are already configured
567
+
568
+ """
569
+ configured = []
570
+
571
+ for plat in platforms:
572
+ try:
573
+ if plat.name == "claude-code":
574
+ # Check both new and old locations
575
+ new_config = Path.home() / ".config" / "claude" / "mcp.json"
576
+ old_config = Path.home() / ".claude.json"
577
+
578
+ is_configured = False
579
+
580
+ # Check new global location (flat structure)
581
+ if new_config.exists():
582
+ with open(new_config) as f:
583
+ config = json.load(f)
584
+ if "mcp-ticketer" in config.get("mcpServers", {}):
585
+ is_configured = True
586
+
587
+ # Check old location (nested structure)
588
+ if not is_configured and old_config.exists():
589
+ with open(old_config) as f:
590
+ config = json.load(f)
591
+ projects = config.get("projects", {})
592
+ proj_key = str(proj_path)
593
+ if proj_key in projects:
594
+ mcp_servers = projects[proj_key].get("mcpServers", {})
595
+ if "mcp-ticketer" in mcp_servers:
596
+ is_configured = True
597
+
598
+ if is_configured:
599
+ configured.append(plat.display_name)
600
+
601
+ elif plat.name == "claude-desktop":
602
+ if plat.config_path.exists():
603
+ with open(plat.config_path) as f:
604
+ config = json.load(f)
605
+ if "mcp-ticketer" in config.get("mcpServers", {}):
606
+ configured.append(plat.display_name)
607
+
608
+ elif plat.name in ["auggie", "codex", "gemini"]:
609
+ if plat.config_path.exists():
610
+ # Check if mcp-ticketer is configured
611
+ # Implementation depends on each platform's config format
612
+ # For now, just check if config exists (simplified)
613
+ pass
614
+
615
+ except (json.JSONDecodeError, OSError):
616
+ pass
617
+
618
+ return configured
619
+
620
+
621
+ def _update_mcp_json_credentials(proj_path: Path, console: Console) -> None:
622
+ """Update .mcp.json with adapter credentials if mcp-ticketer is already configured.
623
+
624
+ This function updates the existing MCP configuration with the latest credentials
625
+ from the project's .mcp-ticketer/config.json file. It also ensures .mcp.json is
626
+ added to .gitignore to prevent credential leaks.
627
+
628
+ Additionally, it updates the official @modelcontextprotocol/server-github MCP server
629
+ if found, since it also requires GITHUB_PERSONAL_ACCESS_TOKEN.
630
+
631
+ Args:
632
+ proj_path: Project path
633
+ console: Rich console for output
634
+
635
+ """
636
+ # Check multiple .mcp.json locations
637
+ new_mcp_json_path = Path.home() / ".config" / "claude" / "mcp.json"
638
+ old_mcp_json_path = Path.home() / ".claude.json"
639
+ legacy_mcp_json_path = proj_path / ".claude" / "mcp.local.json"
640
+ project_mcp_json_path = proj_path / ".mcp.json"
641
+
642
+ mcp_json_paths = [
643
+ (new_mcp_json_path, True), # (path, is_global_mcp_config)
644
+ (old_mcp_json_path, False),
645
+ (legacy_mcp_json_path, False),
646
+ (project_mcp_json_path, False),
647
+ ]
648
+
649
+ # Also check parent directories for .mcp.json (Claude Code inheritance)
650
+ # This handles cases like /Users/masa/Projects/.mcp.json
651
+ current = proj_path.parent
652
+ home = Path.home()
653
+ checked_parents: set[Path] = set()
654
+ while current != home and current != current.parent:
655
+ parent_mcp = current / ".mcp.json"
656
+ if parent_mcp not in checked_parents and parent_mcp.exists():
657
+ mcp_json_paths.append((parent_mcp, False))
658
+ checked_parents.add(parent_mcp)
659
+ current = current.parent
660
+
661
+ # Import the helper function to get adapter credentials
662
+ from .mcp_configure import _get_adapter_env_vars
663
+
664
+ env_vars = _get_adapter_env_vars()
665
+
666
+ if not env_vars:
667
+ return
668
+
669
+ updated_count = 0
670
+
671
+ for mcp_json_path, is_global_mcp_config in mcp_json_paths:
672
+ if not mcp_json_path.exists():
673
+ continue
674
+
675
+ try:
676
+ with open(mcp_json_path) as f:
677
+ mcp_config = json.load(f)
678
+
679
+ # Check if mcp-ticketer is configured
680
+ mcp_servers = None
681
+ if is_global_mcp_config:
682
+ # Global mcp.json uses flat structure
683
+ mcp_servers = mcp_config.get("mcpServers", {})
684
+ else:
685
+ # Old structure uses projects
686
+ projects = mcp_config.get("projects", {})
687
+ project_key = str(proj_path.resolve())
688
+ if project_key in projects:
689
+ mcp_servers = projects[project_key].get("mcpServers", {})
690
+ else:
691
+ # Try flat structure for backward compatibility
692
+ mcp_servers = mcp_config.get("mcpServers", {})
693
+
694
+ if mcp_servers is None:
695
+ continue
696
+
697
+ config_updated = False
698
+
699
+ # Update the mcp-ticketer server env vars if configured
700
+ if "mcp-ticketer" in mcp_servers:
701
+ current_env = mcp_servers["mcp-ticketer"].get("env", {})
702
+ current_env.update(env_vars)
703
+ mcp_servers["mcp-ticketer"]["env"] = current_env
704
+ config_updated = True
705
+
706
+ # Also update official @modelcontextprotocol/server-github if present
707
+ # This server uses GITHUB_PERSONAL_ACCESS_TOKEN instead of GITHUB_TOKEN
708
+ if "github" in mcp_servers and "GITHUB_TOKEN" in env_vars:
709
+ github_server = mcp_servers["github"]
710
+ # Check if it's the official GitHub MCP server (uses npx or server-github)
711
+ cmd = github_server.get("command", "")
712
+ args = github_server.get("args", [])
713
+ is_official_github = (
714
+ cmd == "npx"
715
+ and any("server-github" in str(arg) for arg in args)
716
+ ) or "server-github" in cmd
717
+
718
+ if is_official_github:
719
+ current_env = github_server.get("env", {})
720
+ # The official server uses GITHUB_PERSONAL_ACCESS_TOKEN
721
+ current_env["GITHUB_PERSONAL_ACCESS_TOKEN"] = env_vars["GITHUB_TOKEN"]
722
+ github_server["env"] = current_env
723
+ config_updated = True
724
+ console.print(
725
+ f"[dim] Also updated official GitHub MCP server in {mcp_json_path}[/dim]"
726
+ )
727
+
728
+ if not config_updated:
729
+ continue
730
+
731
+ # Save updated config
732
+ with open(mcp_json_path, "w") as f:
733
+ json.dump(mcp_config, f, indent=2)
734
+
735
+ updated_count += 1
736
+
737
+ except (json.JSONDecodeError, OSError) as e:
738
+ console.print(
739
+ f"[yellow]Warning: Could not update {mcp_json_path.name}: {e}[/yellow]"
740
+ )
741
+
742
+ if updated_count > 0:
743
+ console.print(
744
+ f"[green]✓[/green] Updated MCP configuration with adapter credentials ({updated_count} file(s))"
745
+ )
746
+
747
+ # Ensure .mcp.json files are in .gitignore (only for project-local files)
748
+ gitignore_path = proj_path / ".gitignore"
749
+ patterns_to_add = [".claude/", ".mcp.json"]
750
+
751
+ if gitignore_path.exists():
752
+ content = gitignore_path.read_text()
753
+ patterns_added = []
754
+
755
+ for pattern in patterns_to_add:
756
+ if pattern not in content:
757
+ patterns_added.append(pattern)
758
+
759
+ if patterns_added:
760
+ with open(gitignore_path, "a") as f:
761
+ f.write("\n# MCP configuration (contains tokens)\n")
762
+ for pattern in patterns_added:
763
+ f.write(f"{pattern}\n")
764
+
765
+ console.print(
766
+ f"[dim]✓ Added {', '.join(patterns_added)} to .gitignore[/dim]"
767
+ )
768
+
769
+
770
+ def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
771
+ """Show setup complete message with next steps.
772
+
773
+ Args:
774
+ console: Rich console for output
775
+ proj_path: Project path
776
+
777
+ """
778
+ console.print("[bold green]🎉 Setup Complete![/bold green]\n")
779
+
780
+ console.print("[bold]Quick Start:[/bold]")
781
+ console.print("1. Create a test ticket:")
782
+ console.print(" [cyan]mcp-ticketer create 'My first ticket'[/cyan]\n")
783
+
784
+ console.print("2. List tickets:")
785
+ console.print(" [cyan]mcp-ticketer list[/cyan]\n")
786
+
787
+ console.print("[bold]Useful Commands:[/bold]")
788
+ console.print(" [cyan]mcp-ticketer doctor[/cyan] - Validate configuration")
789
+ console.print(" [cyan]mcp-ticketer install <platform>[/cyan] - Add more platforms")
790
+ console.print(" [cyan]mcp-ticketer --help[/cyan] - See all commands\n")
791
+
792
+ console.print(
793
+ f"[dim]Configuration: {proj_path / '.mcp-ticketer' / 'config.json'}[/dim]"
794
+ )