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
@@ -3,63 +3,351 @@
3
3
  import json
4
4
  import os
5
5
  import shutil
6
+ import subprocess
6
7
  import sys
7
8
  from pathlib import Path
8
- from typing import Optional
9
9
 
10
10
  from rich.console import Console
11
11
 
12
+ from .python_detection import get_mcp_ticketer_python
13
+
12
14
  console = Console()
13
15
 
14
16
 
15
- def find_mcp_ticketer_binary() -> str:
16
- """Find the mcp-ticketer binary path.
17
+ def is_mcp_ticketer_in_path() -> bool:
18
+ """Check if mcp-ticketer command is accessible via PATH.
19
+
20
+ This is critical for native Claude CLI mode, which writes bare
21
+ command names like "mcp-ticketer" instead of full paths.
22
+
23
+ Returns:
24
+ True if mcp-ticketer can be found in PATH, False otherwise.
25
+
26
+ Examples:
27
+ >>> # pipx with PATH configured
28
+ >>> is_mcp_ticketer_in_path()
29
+ True
30
+
31
+ >>> # pipx without PATH configured
32
+ >>> is_mcp_ticketer_in_path()
33
+ False
34
+
35
+ """
36
+ result = shutil.which("mcp-ticketer") is not None
37
+ if result:
38
+ console.print("[dim]✓ mcp-ticketer found in PATH[/dim]", highlight=False)
39
+ else:
40
+ console.print(
41
+ "[dim]⚠ mcp-ticketer not in PATH (will use legacy JSON mode)[/dim]",
42
+ highlight=False,
43
+ )
44
+ return result
45
+
46
+
47
+ def is_claude_cli_available() -> bool:
48
+ """Check if Claude CLI is available in PATH.
17
49
 
18
50
  Returns:
19
- Path to mcp-ticketer binary
51
+ True if 'claude' command is available, False otherwise
52
+
53
+ """
54
+ try:
55
+ result = subprocess.run(
56
+ ["claude", "--version"],
57
+ capture_output=True,
58
+ text=True,
59
+ timeout=5,
60
+ )
61
+ return result.returncode == 0
62
+ except (subprocess.SubprocessError, FileNotFoundError, OSError):
63
+ return False
64
+
65
+
66
+ def build_claude_mcp_command(
67
+ project_config: dict,
68
+ project_path: str | None = None,
69
+ global_config: bool = False,
70
+ ) -> list[str]:
71
+ """Build 'claude mcp add' command arguments.
72
+
73
+ Args:
74
+ project_config: Project configuration dict
75
+ project_path: Path to project (for --path arg)
76
+ global_config: If True, use --scope user (global), else --scope local
77
+
78
+ Returns:
79
+ List of command arguments for subprocess
80
+
81
+ """
82
+ cmd = ["claude", "mcp", "add"]
83
+
84
+ # Scope: user (global) or local (project)
85
+ scope = "user" if global_config else "local"
86
+ cmd.extend(["--scope", scope])
87
+
88
+ # Transport: always stdio
89
+ cmd.extend(["--transport", "stdio"])
90
+
91
+ # Server name - MUST come before -e flags per Claude CLI syntax:
92
+ # claude mcp add [options] <name> -e KEY=val... -- <command> [args...]
93
+ cmd.append("mcp-ticketer")
94
+
95
+ # Environment variables (credentials) - MUST come after server name
96
+ adapters = project_config.get("adapters", {})
97
+
98
+ # Linear adapter
99
+ if "linear" in adapters:
100
+ linear_config = adapters["linear"]
101
+ if "api_key" in linear_config:
102
+ cmd.extend(["-e", f"LINEAR_API_KEY={linear_config['api_key']}"])
103
+ if "team_id" in linear_config:
104
+ cmd.extend(["-e", f"LINEAR_TEAM_ID={linear_config['team_id']}"])
105
+ if "team_key" in linear_config:
106
+ cmd.extend(["-e", f"LINEAR_TEAM_KEY={linear_config['team_key']}"])
107
+
108
+ # GitHub adapter
109
+ if "github" in adapters:
110
+ github_config = adapters["github"]
111
+ if "token" in github_config:
112
+ cmd.extend(["-e", f"GITHUB_TOKEN={github_config['token']}"])
113
+ if "owner" in github_config:
114
+ cmd.extend(["-e", f"GITHUB_OWNER={github_config['owner']}"])
115
+ if "repo" in github_config:
116
+ cmd.extend(["-e", f"GITHUB_REPO={github_config['repo']}"])
117
+
118
+ # JIRA adapter
119
+ if "jira" in adapters:
120
+ jira_config = adapters["jira"]
121
+ if "api_token" in jira_config:
122
+ cmd.extend(["-e", f"JIRA_API_TOKEN={jira_config['api_token']}"])
123
+ if "email" in jira_config:
124
+ cmd.extend(["-e", f"JIRA_EMAIL={jira_config['email']}"])
125
+ if "url" in jira_config:
126
+ cmd.extend(["-e", f"JIRA_URL={jira_config['url']}"])
127
+
128
+ # Add default adapter
129
+ default_adapter = project_config.get("default_adapter", "aitrackdown")
130
+ cmd.extend(["-e", f"MCP_TICKETER_ADAPTER={default_adapter}"])
131
+
132
+ # Command separator
133
+ cmd.append("--")
134
+
135
+ # Server command and args
136
+ cmd.extend(["mcp-ticketer", "mcp"])
137
+
138
+ # Project path (for local scope)
139
+ if project_path and not global_config:
140
+ cmd.extend(["--path", project_path])
141
+
142
+ return cmd
143
+
144
+
145
+ def configure_claude_mcp_native(
146
+ project_config: dict,
147
+ project_path: str | None = None,
148
+ global_config: bool = False,
149
+ force: bool = False,
150
+ ) -> None:
151
+ """Configure Claude Code using native 'claude mcp add' command.
152
+
153
+ This method is preferred when both Claude CLI and mcp-ticketer
154
+ are available in PATH. It provides better integration and
155
+ automatic updates.
156
+
157
+ Args:
158
+ project_config: Project configuration dict
159
+ project_path: Path to project directory
160
+ global_config: If True, install globally (--scope user)
161
+ force: If True, force reinstallation by removing existing config first
20
162
 
21
163
  Raises:
22
- FileNotFoundError: If binary not found
164
+ RuntimeError: If claude mcp add command fails
165
+ subprocess.TimeoutExpired: If command times out
23
166
 
24
167
  """
25
- # Check if running from development environment
26
- import mcp_ticketer
27
-
28
- package_path = Path(mcp_ticketer.__file__).parent.parent.parent
29
-
30
- # Check for virtual environment bin
31
- possible_paths = [
32
- # Development paths
33
- package_path / "venv" / "bin" / "mcp-ticketer",
34
- package_path / ".venv" / "bin" / "mcp-ticketer",
35
- package_path / "test_venv" / "bin" / "mcp-ticketer",
36
- # System installation
37
- Path.home() / ".local" / "bin" / "mcp-ticketer",
38
- # pipx installation
39
- Path.home()
40
- / ".local"
41
- / "pipx"
42
- / "venvs"
43
- / "mcp-ticketer"
44
- / "bin"
45
- / "mcp-ticketer",
46
- ]
47
-
48
- # Check PATH
49
- which_result = shutil.which("mcp-ticketer")
50
- if which_result:
51
- return which_result
52
-
53
- # Check possible paths
54
- for path in possible_paths:
55
- if path.exists():
56
- return str(path.resolve())
57
-
58
- raise FileNotFoundError(
59
- "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
60
- "Install with: pip install mcp-ticketer"
168
+ console.print("[cyan]⚙️ Configuring MCP via native Claude CLI[/cyan]")
169
+ console.print("[dim]Command will be: mcp-ticketer (resolved from PATH)[/dim]")
170
+
171
+ # Auto-remove before re-adding when force=True
172
+ if force:
173
+ console.print("[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]")
174
+ try:
175
+ removal_success = remove_claude_mcp_native(
176
+ global_config=global_config, dry_run=False
177
+ )
178
+ if removal_success:
179
+ console.print("[green]✓[/green] Existing configuration removed")
180
+ else:
181
+ console.print(
182
+ "[yellow]⚠[/yellow] Could not remove existing configuration"
183
+ )
184
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
185
+ except Exception as e:
186
+ console.print(f"[yellow]⚠[/yellow] Removal error: {e}")
187
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
188
+
189
+ console.print() # Blank line for visual separation
190
+
191
+ # Build command
192
+ cmd = build_claude_mcp_command(
193
+ project_config=project_config,
194
+ project_path=project_path,
195
+ global_config=global_config,
61
196
  )
62
197
 
198
+ # Show command to user (mask sensitive values)
199
+ masked_cmd = []
200
+ for i, arg in enumerate(cmd):
201
+ if arg.startswith("-e=") or (i > 0 and cmd[i - 1] == "-e"):
202
+ # Mask environment variable values
203
+ if "=" in arg:
204
+ key, _ = arg.split("=", 1)
205
+ masked_cmd.append(f"{key}=***")
206
+ else:
207
+ masked_cmd.append(arg)
208
+ else:
209
+ masked_cmd.append(arg)
210
+
211
+ console.print(f"[cyan]Executing:[/cyan] {' '.join(masked_cmd)}")
212
+
213
+ try:
214
+ # Execute native command
215
+ result = subprocess.run(
216
+ cmd,
217
+ capture_output=True,
218
+ text=True,
219
+ timeout=30,
220
+ )
221
+
222
+ if result.returncode == 0:
223
+ scope_label = (
224
+ "globally" if global_config else f"for project: {project_path}"
225
+ )
226
+ console.print(f"[green]✓[/green] Claude Code configured {scope_label}")
227
+ console.print("[dim]Restart Claude Code to load the MCP server[/dim]")
228
+
229
+ # Show adapter information
230
+ adapter = project_config.get("default_adapter", "aitrackdown")
231
+ console.print("\n[bold]Configuration Details:[/bold]")
232
+ console.print(" Server name: mcp-ticketer")
233
+ console.print(f" Adapter: {adapter}")
234
+ console.print(" Protocol: Content-Length framing (FastMCP SDK)")
235
+ if project_path and not global_config:
236
+ console.print(f" Project path: {project_path}")
237
+
238
+ # Next steps
239
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
240
+ if global_config:
241
+ console.print("1. Restart Claude Desktop")
242
+ console.print("2. Open a conversation")
243
+ else:
244
+ console.print("1. Restart Claude Code")
245
+ console.print("2. Open this project in Claude Code")
246
+ console.print("3. mcp-ticketer tools will be available in the MCP menu")
247
+ else:
248
+ console.print("[red]✗[/red] Failed to configure Claude Code")
249
+ console.print(f"[red]Error:[/red] {result.stderr}")
250
+ raise RuntimeError(f"claude mcp add failed: {result.stderr}")
251
+
252
+ except subprocess.TimeoutExpired:
253
+ console.print("[red]✗[/red] Claude CLI command timed out")
254
+ raise
255
+ except Exception as e:
256
+ console.print(f"[red]✗[/red] Error executing Claude CLI: {e}")
257
+ raise
258
+
259
+
260
+ def _get_adapter_env_vars() -> dict[str, str]:
261
+ """Get environment variables for the configured adapter from project config.
262
+
263
+ Reads credentials from .mcp-ticketer/config.json and returns them as
264
+ environment variables suitable for MCP server configuration.
265
+
266
+ Returns:
267
+ Dict of environment variables with adapter credentials
268
+
269
+ Example:
270
+ >>> env_vars = _get_adapter_env_vars()
271
+ >>> env_vars
272
+ {
273
+ 'MCP_TICKETER_ADAPTER': 'github',
274
+ 'GITHUB_TOKEN': 'ghp_...',
275
+ 'GITHUB_OWNER': 'username',
276
+ 'GITHUB_REPO': 'repo-name'
277
+ }
278
+
279
+ """
280
+ config_path = Path.cwd() / ".mcp-ticketer" / "config.json"
281
+ if not config_path.exists():
282
+ return {}
283
+
284
+ try:
285
+ with open(config_path) as f:
286
+ config = json.load(f)
287
+
288
+ adapter_type = config.get("default_adapter", "aitrackdown")
289
+ adapters = config.get("adapters", {})
290
+ adapter_config = adapters.get(adapter_type, {})
291
+
292
+ env_vars = {"MCP_TICKETER_ADAPTER": adapter_type}
293
+
294
+ if adapter_type == "github":
295
+ if token := adapter_config.get("token"):
296
+ env_vars["GITHUB_TOKEN"] = token
297
+ if owner := adapter_config.get("owner"):
298
+ env_vars["GITHUB_OWNER"] = owner
299
+ if repo := adapter_config.get("repo"):
300
+ env_vars["GITHUB_REPO"] = repo
301
+ elif adapter_type == "linear":
302
+ if api_key := adapter_config.get("api_key"):
303
+ env_vars["LINEAR_API_KEY"] = api_key
304
+ if team_key := adapter_config.get("team_key"):
305
+ env_vars["LINEAR_TEAM_KEY"] = team_key
306
+ elif team_id := adapter_config.get("team_id"):
307
+ env_vars["LINEAR_TEAM_ID"] = team_id
308
+ elif adapter_type == "jira":
309
+ if api_token := adapter_config.get("api_token"):
310
+ env_vars["JIRA_API_TOKEN"] = api_token
311
+ if server := adapter_config.get("server"):
312
+ env_vars["JIRA_SERVER"] = server
313
+ if email := adapter_config.get("email"):
314
+ env_vars["JIRA_EMAIL"] = email
315
+ if project := adapter_config.get("project_key"):
316
+ env_vars["JIRA_PROJECT_KEY"] = project
317
+
318
+ return env_vars
319
+ except (json.JSONDecodeError, OSError):
320
+ return {}
321
+
322
+
323
+ def load_env_file(env_path: Path) -> dict[str, str]:
324
+ """Load environment variables from .env file.
325
+
326
+ Args:
327
+ env_path: Path to .env file
328
+
329
+ Returns:
330
+ Dict of environment variable key-value pairs
331
+
332
+ """
333
+ env_vars: dict[str, str] = {}
334
+ if not env_path.exists():
335
+ return env_vars
336
+
337
+ with open(env_path) as f:
338
+ for line in f:
339
+ line = line.strip()
340
+ # Skip comments and empty lines
341
+ if not line or line.startswith("#"):
342
+ continue
343
+
344
+ # Parse KEY=VALUE format
345
+ if "=" in line:
346
+ key, value = line.split("=", 1)
347
+ env_vars[key.strip()] = value.strip()
348
+
349
+ return env_vars
350
+
63
351
 
64
352
  def load_project_config() -> dict:
65
353
  """Load mcp-ticketer project configuration.
@@ -127,28 +415,74 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
127
415
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
128
416
  )
129
417
  else:
130
- # Project-level configuration
131
- config_path = Path.cwd() / ".mcp" / "config.json"
418
+ # Claude Code configuration - check both locations
419
+ # Priority 1: New global location ~/.config/claude/mcp.json
420
+ new_config_path = Path.home() / ".config" / "claude" / "mcp.json"
421
+ if new_config_path.exists():
422
+ return new_config_path
423
+
424
+ # Priority 2: Legacy project-specific location ~/.claude.json
425
+ config_path = Path.home() / ".claude.json"
132
426
 
133
427
  return config_path
134
428
 
135
429
 
136
- def load_claude_mcp_config(config_path: Path) -> dict:
430
+ def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> dict:
137
431
  """Load existing Claude MCP configuration or return empty structure.
138
432
 
139
433
  Args:
140
434
  config_path: Path to MCP config file
435
+ is_claude_code: If True, return Claude Code structure with projects
141
436
 
142
437
  Returns:
143
438
  MCP configuration dict
144
439
 
145
440
  """
146
- if config_path.exists():
147
- with open(config_path) as f:
148
- return json.load(f)
441
+ # Detect if this is the new global config location
442
+ is_global_mcp_config = str(config_path).endswith(".config/claude/mcp.json")
149
443
 
150
- # Return empty structure
151
- return {"mcpServers": {}}
444
+ if config_path.exists():
445
+ try:
446
+ with open(config_path) as f:
447
+ content = f.read().strip()
448
+ if not content:
449
+ # Empty file, return default structure based on location
450
+ if is_global_mcp_config:
451
+ return {"mcpServers": {}} # Flat structure
452
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
453
+
454
+ config = json.loads(content)
455
+
456
+ # Auto-detect structure format based on content
457
+ if "projects" in config:
458
+ # This is the old nested project structure
459
+ return config
460
+ elif "mcpServers" in config:
461
+ # This is flat mcpServers structure
462
+ return config
463
+ else:
464
+ # Empty or unknown structure, return default
465
+ if is_global_mcp_config:
466
+ return {"mcpServers": {}}
467
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
468
+
469
+ except json.JSONDecodeError as e:
470
+ console.print(
471
+ f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
472
+ )
473
+ console.print(f"[dim]Error: {e}[/dim]")
474
+ # Return default structure on parse error
475
+ if is_global_mcp_config:
476
+ return {"mcpServers": {}}
477
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
478
+
479
+ # Return empty structure based on config type and location
480
+ if is_global_mcp_config:
481
+ return {"mcpServers": {}} # New location always uses flat structure
482
+ if is_claude_code:
483
+ return {"projects": {}}
484
+ else:
485
+ return {"mcpServers": {}}
152
486
 
153
487
 
154
488
  def save_claude_mcp_config(config_path: Path, config: dict) -> None:
@@ -168,45 +502,89 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
168
502
 
169
503
 
170
504
  def create_mcp_server_config(
171
- binary_path: str, project_config: dict, cwd: Optional[str] = None
505
+ python_path: str,
506
+ project_config: dict,
507
+ project_path: str | None = None,
508
+ is_global_config: bool = False,
172
509
  ) -> dict:
173
510
  """Create MCP server configuration for mcp-ticketer.
174
511
 
512
+ Uses the CLI command (mcp-ticketer mcp) which implements proper
513
+ Content-Length framing via FastMCP SDK, required for modern MCP clients.
514
+
175
515
  Args:
176
- binary_path: Path to mcp-ticketer binary
516
+ python_path: Path to Python executable in mcp-ticketer venv
177
517
  project_config: Project configuration from .mcp-ticketer/config.json
178
- cwd: Working directory for server (optional)
518
+ project_path: Project directory path (optional)
519
+ is_global_config: If True, create config for global location (no project path in args)
179
520
 
180
521
  Returns:
181
- MCP server configuration dict
522
+ MCP server configuration dict matching Claude Code stdio pattern
182
523
 
183
524
  """
525
+ # IMPORTANT: Use CLI command, NOT Python module invocation
526
+ # The CLI uses FastMCP SDK which implements proper Content-Length framing
527
+ # Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
528
+
529
+ # Get mcp-ticketer CLI path from Python path
530
+ # If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
531
+ python_dir = Path(python_path).parent
532
+ cli_path = str(python_dir / "mcp-ticketer")
533
+
534
+ # Build CLI arguments
535
+ args = ["mcp"]
536
+
537
+ # Add project path if provided and not global config
538
+ if project_path and not is_global_config:
539
+ args.extend(["--path", project_path])
540
+
541
+ # REQUIRED: Add "type": "stdio" for Claude Code compatibility
184
542
  config = {
185
- "command": binary_path,
186
- "args": ["serve"], # Use 'serve' command to start MCP server
543
+ "type": "stdio",
544
+ "command": cli_path,
545
+ "args": args,
187
546
  }
188
547
 
189
- # Add working directory if provided
190
- if cwd:
191
- config["cwd"] = cwd
192
-
193
- # Add environment variables based on adapter
194
- adapter = project_config.get("default_adapter", "aitrackdown")
195
- adapters_config = project_config.get("adapters", {})
196
- adapter_config = adapters_config.get(adapter, {})
548
+ # NOTE: The CLI command loads configuration from .mcp-ticketer/config.json
549
+ # Environment variables below are optional fallbacks for backward compatibility
550
+ # The FastMCP SDK server will automatically load config from the project directory
197
551
 
198
552
  env_vars = {}
199
553
 
200
- # Add adapter-specific environment variables
201
- if adapter == "linear" and "api_key" in adapter_config:
202
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
203
- elif adapter == "github" and "token" in adapter_config:
204
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
205
- elif adapter == "jira":
206
- if "api_token" in adapter_config:
207
- env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
208
- if "email" in adapter_config:
209
- env_vars["JIRA_EMAIL"] = adapter_config["email"]
554
+ # Add PYTHONPATH for project context (only for project-specific configs)
555
+ if project_path and not is_global_config:
556
+ env_vars["PYTHONPATH"] = project_path
557
+
558
+ # Get adapter credentials from project config
559
+ # This is the primary source for MCP server environment variables
560
+ adapter_env_vars = _get_adapter_env_vars()
561
+ env_vars.update(adapter_env_vars)
562
+
563
+ # Load environment variables from .env.local if it exists (as override)
564
+ if project_path:
565
+ env_file_path = Path(project_path) / ".env.local"
566
+ env_file_vars = load_env_file(env_file_path)
567
+
568
+ # Add relevant adapter-specific vars from .env.local (overrides config.json)
569
+ adapter_env_keys = {
570
+ "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
571
+ "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
572
+ "jira": [
573
+ "JIRA_ACCESS_USER",
574
+ "JIRA_ACCESS_TOKEN",
575
+ "JIRA_ORGANIZATION_ID",
576
+ "JIRA_URL",
577
+ "JIRA_EMAIL",
578
+ "JIRA_API_TOKEN",
579
+ ],
580
+ "aitrackdown": [], # No specific env vars needed
581
+ }
582
+
583
+ adapter = env_vars.get("MCP_TICKETER_ADAPTER", "aitrackdown")
584
+ # Include adapter-specific env vars from .env.local (overrides config.json)
585
+ for key in adapter_env_keys.get(adapter, []):
586
+ if key in env_file_vars:
587
+ env_vars[key] = env_file_vars[key]
210
588
 
211
589
  if env_vars:
212
590
  config["env"] = env_vars
@@ -214,29 +592,339 @@ def create_mcp_server_config(
214
592
  return config
215
593
 
216
594
 
595
+ def detect_legacy_claude_config(
596
+ config_path: Path, is_claude_code: bool = True, project_path: str | None = None
597
+ ) -> tuple[bool, dict | None]:
598
+ """Detect if existing Claude config uses legacy Python module invocation.
599
+
600
+ Args:
601
+ ----
602
+ config_path: Path to Claude configuration file
603
+ is_claude_code: Whether this is Claude Code (project-level) or Claude Desktop (global)
604
+ project_path: Project path for Claude Code configs
605
+
606
+ Returns:
607
+ -------
608
+ Tuple of (is_legacy, server_config):
609
+ - is_legacy: True if config uses 'python -m mcp_ticketer.mcp.server'
610
+ - server_config: The legacy server config dict, or None if not legacy
611
+
612
+ """
613
+ if not config_path.exists():
614
+ return False, None
615
+
616
+ try:
617
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
618
+ except Exception:
619
+ return False, None
620
+
621
+ # For Claude Code, check project-specific config
622
+ if is_claude_code and project_path:
623
+ projects = mcp_config.get("projects", {})
624
+ project_config = projects.get(project_path, {})
625
+ mcp_servers = project_config.get("mcpServers", {})
626
+ else:
627
+ # For Claude Desktop, check global config
628
+ mcp_servers = mcp_config.get("mcpServers", {})
629
+
630
+ if "mcp-ticketer" in mcp_servers:
631
+ server_config = mcp_servers["mcp-ticketer"]
632
+ args = server_config.get("args", [])
633
+
634
+ # Check for legacy pattern: ["-m", "mcp_ticketer.mcp.server", ...]
635
+ if len(args) >= 2 and args[0] == "-m" and "mcp_ticketer.mcp.server" in args[1]:
636
+ return True, server_config
637
+
638
+ return False, None
639
+
640
+
641
+ def remove_claude_mcp_native(
642
+ global_config: bool = False,
643
+ dry_run: bool = False,
644
+ ) -> bool:
645
+ """Remove mcp-ticketer using native 'claude mcp remove' command.
646
+
647
+ This function attempts to use the Claude CLI's native remove command
648
+ first, falling back to JSON manipulation if the native command fails.
649
+
650
+ Args:
651
+ global_config: If True, remove from Claude Desktop (--scope user)
652
+ If False, remove from Claude Code (--scope local)
653
+ dry_run: If True, only show what would be removed without making changes
654
+
655
+ Returns:
656
+ bool: True if removal was successful, False if failed or skipped
657
+
658
+ Raises:
659
+ Does not raise exceptions - all errors are caught and handled gracefully
660
+ with fallback to JSON manipulation
661
+
662
+ Example:
663
+ >>> # Remove from local Claude Code configuration
664
+ >>> remove_claude_mcp_native(global_config=False, dry_run=False)
665
+ True
666
+
667
+ >>> # Preview removal without making changes
668
+ >>> remove_claude_mcp_native(global_config=False, dry_run=True)
669
+ True
670
+
671
+ Notes:
672
+ - Automatically falls back to remove_claude_mcp_json() if native fails
673
+ - Designed to be non-blocking for auto-remove scenarios
674
+ - Uses --scope flag for backward compatibility with Claude CLI
675
+
676
+ """
677
+ scope = "user" if global_config else "local"
678
+ cmd = ["claude", "mcp", "remove", "--scope", scope, "mcp-ticketer"]
679
+
680
+ config_type = "Claude Desktop" if global_config else "Claude Code"
681
+
682
+ if dry_run:
683
+ console.print(f"[cyan]DRY RUN - Would execute:[/cyan] {' '.join(cmd)}")
684
+ console.print(f"[dim]Target: {config_type}[/dim]")
685
+ return True
686
+
687
+ try:
688
+ # Execute native remove command
689
+ result = subprocess.run(
690
+ cmd,
691
+ capture_output=True,
692
+ text=True,
693
+ timeout=30,
694
+ )
695
+
696
+ if result.returncode == 0:
697
+ console.print("[green]✓[/green] Removed mcp-ticketer via native CLI")
698
+ console.print(f"[dim]Target: {config_type}[/dim]")
699
+ return True
700
+ else:
701
+ # Native command failed, fallback to JSON
702
+ console.print(
703
+ f"[yellow]⚠[/yellow] Native remove failed: {result.stderr.strip()}"
704
+ )
705
+ console.print(
706
+ "[yellow]Falling back to JSON configuration removal...[/yellow]"
707
+ )
708
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
709
+
710
+ except subprocess.TimeoutExpired:
711
+ console.print("[yellow]⚠[/yellow] Native remove command timed out")
712
+ console.print("[yellow]Falling back to JSON configuration removal...[/yellow]")
713
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
714
+
715
+ except Exception as e:
716
+ console.print(f"[yellow]⚠[/yellow] Error executing native remove: {e}")
717
+ console.print("[yellow]Falling back to JSON configuration removal...[/yellow]")
718
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
719
+
720
+
721
+ def remove_claude_mcp_json(global_config: bool = False, dry_run: bool = False) -> bool:
722
+ """Remove mcp-ticketer from Claude Code/Desktop configuration using JSON.
723
+
724
+ This is a fallback method when native 'claude mcp remove' is unavailable
725
+ or fails. It directly manipulates the JSON configuration files.
726
+
727
+ Args:
728
+ global_config: Remove from Claude Desktop instead of project-level
729
+ dry_run: Show what would be removed without making changes
730
+
731
+ Returns:
732
+ bool: True if removal was successful (or files not found),
733
+ False if an error occurred during JSON manipulation
734
+
735
+ Notes:
736
+ - Handles multiple config file locations (new, old, legacy)
737
+ - Supports both flat and nested configuration structures
738
+ - Cleans up empty structures after removal
739
+ - Provides detailed logging of actions taken
740
+
741
+ """
742
+ # Step 1: Find Claude MCP config location
743
+ config_type = "Claude Desktop" if global_config else "Claude Code"
744
+ console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
745
+
746
+ # Get absolute project path for Claude Code
747
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
748
+
749
+ # Check both locations for Claude Code
750
+ config_paths_to_check = []
751
+ if not global_config:
752
+ # Check both new and old locations
753
+ new_config = Path.home() / ".config" / "claude" / "mcp.json"
754
+ old_config = Path.home() / ".claude.json"
755
+ legacy_config = Path.cwd() / ".claude" / "mcp.local.json"
756
+
757
+ if new_config.exists():
758
+ config_paths_to_check.append(
759
+ (new_config, True)
760
+ ) # True = is_global_mcp_config
761
+ if old_config.exists():
762
+ config_paths_to_check.append((old_config, False))
763
+ if legacy_config.exists():
764
+ config_paths_to_check.append((legacy_config, False))
765
+ else:
766
+ mcp_config_path = find_claude_mcp_config(global_config)
767
+ if mcp_config_path.exists():
768
+ config_paths_to_check.append((mcp_config_path, False))
769
+
770
+ if not config_paths_to_check:
771
+ console.print("[yellow]⚠ No configuration files found[/yellow]")
772
+ console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
773
+ return
774
+
775
+ # Step 2-7: Process each config file
776
+ removed_count = 0
777
+ for config_path, is_global_mcp_config in config_paths_to_check:
778
+ console.print(f"[dim]Checking: {config_path}[/dim]")
779
+
780
+ # Load existing MCP configuration
781
+ is_claude_code = not global_config
782
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
783
+
784
+ # Check if mcp-ticketer is configured
785
+ is_configured = False
786
+ if is_global_mcp_config:
787
+ # Global mcp.json uses flat structure
788
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
789
+ elif is_claude_code:
790
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
791
+ if absolute_project_path:
792
+ projects = mcp_config.get("projects", {})
793
+ project_config_entry = projects.get(absolute_project_path, {})
794
+ is_configured = "mcp-ticketer" in project_config_entry.get(
795
+ "mcpServers", {}
796
+ )
797
+ else:
798
+ # Check flat structure for backward compatibility
799
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
800
+ else:
801
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
802
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
803
+
804
+ if not is_configured:
805
+ continue
806
+
807
+ # Show what would be removed (dry run)
808
+ if dry_run:
809
+ console.print(f"\n[cyan]DRY RUN - Would remove from: {config_path}[/cyan]")
810
+ console.print(" Server name: mcp-ticketer")
811
+ if absolute_project_path and not is_global_mcp_config:
812
+ console.print(f" Project: {absolute_project_path}")
813
+ continue
814
+
815
+ # Remove mcp-ticketer from configuration
816
+ if is_global_mcp_config:
817
+ # Global mcp.json uses flat structure
818
+ del mcp_config["mcpServers"]["mcp-ticketer"]
819
+ elif is_claude_code and absolute_project_path and "projects" in mcp_config:
820
+ # Remove from Claude Code nested structure
821
+ del mcp_config["projects"][absolute_project_path]["mcpServers"][
822
+ "mcp-ticketer"
823
+ ]
824
+
825
+ # Clean up empty structures
826
+ if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
827
+ del mcp_config["projects"][absolute_project_path]["mcpServers"]
828
+ if not mcp_config["projects"][absolute_project_path]:
829
+ del mcp_config["projects"][absolute_project_path]
830
+ else:
831
+ # Remove from flat structure (legacy or Claude Desktop)
832
+ if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
833
+ del mcp_config["mcpServers"]["mcp-ticketer"]
834
+
835
+ # Save updated configuration
836
+ try:
837
+ save_claude_mcp_config(config_path, mcp_config)
838
+ console.print(f"[green]✓ Removed from: {config_path}[/green]")
839
+ removed_count += 1
840
+ except Exception as e:
841
+ console.print(f"[red]✗ Failed to update {config_path}:[/red] {e}")
842
+
843
+ if dry_run:
844
+ return
845
+
846
+ if removed_count > 0:
847
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
848
+ console.print(f"[dim]Updated {removed_count} configuration file(s)[/dim]")
849
+
850
+ # Next steps
851
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
852
+ if global_config:
853
+ console.print("1. Restart Claude Desktop")
854
+ console.print("2. mcp-ticketer will no longer be available in MCP menu")
855
+ else:
856
+ console.print("1. Restart Claude Code")
857
+ console.print("2. mcp-ticketer will no longer be available in this project")
858
+ else:
859
+ console.print(
860
+ "\n[yellow]⚠ mcp-ticketer was not found in any configuration[/yellow]"
861
+ )
862
+
863
+ # Return True even if not found (successful removal)
864
+ return True
865
+
866
+
867
+ def remove_claude_mcp(
868
+ global_config: bool = False,
869
+ dry_run: bool = False,
870
+ ) -> bool:
871
+ """Remove mcp-ticketer from Claude Code/Desktop configuration.
872
+
873
+ Automatically detects if Claude CLI is available and uses the native
874
+ 'claude mcp remove' command if possible, falling back to JSON configuration
875
+ manipulation when necessary.
876
+
877
+ Args:
878
+ global_config: Remove from Claude Desktop instead of project-level
879
+ dry_run: Show what would be removed without making changes
880
+
881
+ Returns:
882
+ bool: True if removal was successful, False if failed
883
+
884
+ Example:
885
+ >>> # Remove from Claude Code (project-level)
886
+ >>> remove_claude_mcp(global_config=False)
887
+ True
888
+
889
+ >>> # Remove from Claude Desktop (global)
890
+ >>> remove_claude_mcp(global_config=True)
891
+ True
892
+
893
+ Notes:
894
+ - Uses native CLI when available for better reliability
895
+ - Automatically falls back to JSON manipulation if needed
896
+ - Safe to call even if mcp-ticketer is not configured
897
+
898
+ """
899
+ # Check for native CLI availability
900
+ if is_claude_cli_available():
901
+ console.print("[green]✓[/green] Claude CLI found - using native remove command")
902
+ return remove_claude_mcp_native(global_config=global_config, dry_run=dry_run)
903
+
904
+ # Fall back to JSON manipulation
905
+ console.print(
906
+ "[yellow]⚠[/yellow] Claude CLI not found - using JSON configuration removal"
907
+ )
908
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
909
+
910
+
217
911
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
218
912
  """Configure Claude Code to use mcp-ticketer.
219
913
 
914
+ Automatically detects if Claude CLI is available and uses native
915
+ 'claude mcp add' command if possible, falling back to JSON configuration.
916
+
220
917
  Args:
221
918
  global_config: Configure Claude Desktop instead of project-level
222
919
  force: Overwrite existing configuration
223
920
 
224
921
  Raises:
225
- FileNotFoundError: If binary or project config not found
922
+ FileNotFoundError: If Python executable or project config not found
226
923
  ValueError: If configuration is invalid
227
924
 
228
925
  """
229
- # Step 1: Find mcp-ticketer binary
230
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
231
- try:
232
- binary_path = find_mcp_ticketer_binary()
233
- console.print(f"[green]✓[/green] Found: {binary_path}")
234
- except FileNotFoundError as e:
235
- console.print(f"[red]✗[/red] {e}")
236
- raise
237
-
238
- # Step 2: Load project configuration
239
- console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
926
+ # Load project configuration early (needed for both native and JSON methods)
927
+ console.print("[cyan]📖 Reading project configuration...[/cyan]")
240
928
  try:
241
929
  project_config = load_project_config()
242
930
  adapter = project_config.get("default_adapter", "aitrackdown")
@@ -245,18 +933,166 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
245
933
  console.print(f"[red]✗[/red] {e}")
246
934
  raise
247
935
 
936
+ # Check for native CLI availability AND PATH configuration
937
+ console.print("\n[cyan]🔍 Checking for Claude CLI...[/cyan]")
938
+
939
+ # Native CLI requires both claude command AND mcp-ticketer in PATH
940
+ claude_cli_available = is_claude_cli_available()
941
+ mcp_ticketer_in_path = is_mcp_ticketer_in_path()
942
+
943
+ use_native_cli = claude_cli_available and mcp_ticketer_in_path
944
+
945
+ if use_native_cli:
946
+ console.print("[green]✓[/green] Claude CLI found - using native command")
947
+ console.print(
948
+ "[dim]This provides better integration and automatic updates[/dim]"
949
+ )
950
+
951
+ # Get absolute project path for local scope
952
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
953
+
954
+ return configure_claude_mcp_native(
955
+ project_config=project_config,
956
+ project_path=absolute_project_path,
957
+ global_config=global_config,
958
+ force=force,
959
+ )
960
+
961
+ # Fall back to reliable JSON manipulation with full paths
962
+ if claude_cli_available and not mcp_ticketer_in_path:
963
+ console.print(
964
+ "[yellow]⚠[/yellow] mcp-ticketer not found in PATH - using legacy JSON mode"
965
+ )
966
+ console.print(
967
+ "[dim]Native CLI writes bare command names that fail when not in PATH[/dim]"
968
+ )
969
+ console.print(
970
+ "[dim]To enable native CLI, add pipx bin directory to your PATH:[/dim]"
971
+ )
972
+ console.print('[dim] export PATH="$HOME/.local/bin:$PATH"[/dim]')
973
+ elif not claude_cli_available:
974
+ console.print(
975
+ "[yellow]⚠[/yellow] Claude CLI not found - using legacy JSON configuration"
976
+ )
977
+ console.print(
978
+ "[dim]For better experience, install Claude CLI: https://docs.claude.ai/cli[/dim]"
979
+ )
980
+
981
+ # Auto-remove before re-adding when force=True
982
+ if force:
983
+ console.print(
984
+ "\n[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]"
985
+ )
986
+ try:
987
+ removal_success = remove_claude_mcp_json(
988
+ global_config=global_config, dry_run=False
989
+ )
990
+ if removal_success:
991
+ console.print("[green]✓[/green] Existing configuration removed")
992
+ else:
993
+ console.print(
994
+ "[yellow]⚠[/yellow] Could not remove existing configuration"
995
+ )
996
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
997
+ except Exception as e:
998
+ console.print(f"[yellow]⚠[/yellow] Removal error: {e}")
999
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
1000
+
1001
+ console.print() # Blank line for visual separation
1002
+
1003
+ # Show that we're using legacy JSON mode with full paths
1004
+ console.print("\n[cyan]⚙️ Configuring MCP via legacy JSON mode[/cyan]")
1005
+ console.print("[dim]This mode uses full paths for reliable operation[/dim]")
1006
+
1007
+ # Determine project path for venv detection
1008
+ project_path = Path.cwd() if not global_config else None
1009
+
1010
+ # Step 1: Find Python executable (project-specific if available)
1011
+ console.print("\n[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
1012
+ try:
1013
+ python_path = get_mcp_ticketer_python(project_path=project_path)
1014
+ console.print(f"[green]✓[/green] Found: {python_path}")
1015
+
1016
+ # Show if using project venv or fallback
1017
+ if project_path and str(project_path / ".venv") in python_path:
1018
+ console.print("[dim]Using project-specific venv[/dim]")
1019
+ else:
1020
+ console.print("[dim]Using pipx/system Python[/dim]")
1021
+
1022
+ # Derive CLI path from Python path
1023
+ python_dir = Path(python_path).parent
1024
+ cli_path = str(python_dir / "mcp-ticketer")
1025
+ console.print(f"[dim]CLI command will be: {cli_path}[/dim]")
1026
+ except Exception as e:
1027
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
1028
+ raise FileNotFoundError(
1029
+ "Could not find mcp-ticketer Python executable. "
1030
+ "Please ensure mcp-ticketer is installed.\n"
1031
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
1032
+ ) from e
1033
+
248
1034
  # Step 3: Find Claude MCP config location
249
- config_type = "Claude Desktop" if global_config else "project-level"
1035
+ config_type = "Claude Desktop" if global_config else "Claude Code"
250
1036
  console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
251
1037
 
252
1038
  mcp_config_path = find_claude_mcp_config(global_config)
253
- console.print(f"[dim]Config location: {mcp_config_path}[/dim]")
1039
+ console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
1040
+
1041
+ # Get absolute project path for Claude Code
1042
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
254
1043
 
255
1044
  # Step 4: Load existing MCP configuration
256
- mcp_config = load_claude_mcp_config(mcp_config_path)
1045
+ is_claude_code = not global_config
1046
+ mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
1047
+
1048
+ # Detect if using new global config location
1049
+ is_global_mcp_config = str(mcp_config_path).endswith(".config/claude/mcp.json")
1050
+
1051
+ # Step 4.5: Check for legacy configuration (DETECTION & MIGRATION)
1052
+ is_legacy, legacy_config = detect_legacy_claude_config(
1053
+ mcp_config_path,
1054
+ is_claude_code=is_claude_code,
1055
+ project_path=absolute_project_path,
1056
+ )
1057
+ if is_legacy:
1058
+ console.print("\n[yellow]⚠ LEGACY CONFIGURATION DETECTED[/yellow]")
1059
+ console.print(
1060
+ "[yellow]Your current configuration uses the legacy line-delimited JSON server:[/yellow]"
1061
+ )
1062
+ console.print(f"[dim] Command: {legacy_config.get('command')}[/dim]")
1063
+ console.print(f"[dim] Args: {legacy_config.get('args')}[/dim]")
1064
+ console.print(
1065
+ f"\n[red]This legacy server is incompatible with modern MCP clients ({config_type}).[/red]"
1066
+ )
1067
+ console.print(
1068
+ "[red]The legacy server uses line-delimited JSON instead of Content-Length framing.[/red]"
1069
+ )
1070
+ console.print(
1071
+ "\n[cyan]✨ Automatically migrating to modern FastMCP-based server...[/cyan]"
1072
+ )
1073
+ force = True # Auto-enable force mode for migration
257
1074
 
258
1075
  # Step 5: Check if mcp-ticketer already configured
259
- if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
1076
+ already_configured = False
1077
+ if is_global_mcp_config:
1078
+ # New global config uses flat structure
1079
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
1080
+ elif is_claude_code:
1081
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
1082
+ if absolute_project_path and "projects" in mcp_config:
1083
+ projects = mcp_config.get("projects", {})
1084
+ project_config_entry = projects.get(absolute_project_path, {})
1085
+ already_configured = "mcp-ticketer" in project_config_entry.get(
1086
+ "mcpServers", {}
1087
+ )
1088
+ elif "mcpServers" in mcp_config:
1089
+ # Check flat structure for backward compatibility
1090
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
1091
+ else:
1092
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
1093
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
1094
+
1095
+ if already_configured:
260
1096
  if not force:
261
1097
  console.print("[yellow]⚠ mcp-ticketer is already configured[/yellow]")
262
1098
  console.print("[dim]Use --force to overwrite existing configuration[/dim]")
@@ -265,16 +1101,61 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
265
1101
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
266
1102
 
267
1103
  # Step 6: Create mcp-ticketer server config
268
- cwd = str(Path.cwd()) if not global_config else None
269
1104
  server_config = create_mcp_server_config(
270
- binary_path=binary_path, project_config=project_config, cwd=cwd
1105
+ python_path=python_path,
1106
+ project_config=project_config,
1107
+ project_path=absolute_project_path,
1108
+ is_global_config=is_global_mcp_config,
271
1109
  )
272
1110
 
273
- # Step 7: Update MCP configuration
274
- if "mcpServers" not in mcp_config:
275
- mcp_config["mcpServers"] = {}
276
-
277
- mcp_config["mcpServers"]["mcp-ticketer"] = server_config
1111
+ # Step 7: Update MCP configuration based on platform
1112
+ if is_global_mcp_config:
1113
+ # New global location: ~/.config/claude/mcp.json uses flat structure
1114
+ if "mcpServers" not in mcp_config:
1115
+ mcp_config["mcpServers"] = {}
1116
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
1117
+ elif is_claude_code:
1118
+ # Claude Code: Write to ~/.claude.json with project-specific path
1119
+ if absolute_project_path:
1120
+ # Ensure projects structure exists
1121
+ if "projects" not in mcp_config:
1122
+ mcp_config["projects"] = {}
1123
+
1124
+ # Ensure project entry exists
1125
+ if absolute_project_path not in mcp_config["projects"]:
1126
+ mcp_config["projects"][absolute_project_path] = {}
1127
+
1128
+ # Ensure mcpServers for this project exists
1129
+ if "mcpServers" not in mcp_config["projects"][absolute_project_path]:
1130
+ mcp_config["projects"][absolute_project_path]["mcpServers"] = {}
1131
+
1132
+ # Add mcp-ticketer configuration
1133
+ mcp_config["projects"][absolute_project_path]["mcpServers"][
1134
+ "mcp-ticketer"
1135
+ ] = server_config
1136
+
1137
+ # Also write to backward-compatible location for older Claude Code versions
1138
+ legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
1139
+ console.print(f"[dim]Legacy config: {legacy_config_path}[/dim]")
1140
+
1141
+ try:
1142
+ legacy_config = load_claude_mcp_config(
1143
+ legacy_config_path, is_claude_code=False
1144
+ )
1145
+ if "mcpServers" not in legacy_config:
1146
+ legacy_config["mcpServers"] = {}
1147
+ legacy_config["mcpServers"]["mcp-ticketer"] = server_config
1148
+ save_claude_mcp_config(legacy_config_path, legacy_config)
1149
+ console.print("[dim]✓ Backward-compatible config also written[/dim]")
1150
+ except Exception as e:
1151
+ console.print(
1152
+ f"[dim]⚠ Could not write legacy config (non-fatal): {e}[/dim]"
1153
+ )
1154
+ else:
1155
+ # Claude Desktop: Write to platform-specific config
1156
+ if "mcpServers" not in mcp_config:
1157
+ mcp_config["mcpServers"] = {}
1158
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
278
1159
 
279
1160
  # Step 8: Save configuration
280
1161
  try:
@@ -286,12 +1167,44 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
286
1167
  console.print("\n[bold]Configuration Details:[/bold]")
287
1168
  console.print(" Server name: mcp-ticketer")
288
1169
  console.print(f" Adapter: {adapter}")
289
- console.print(f" Binary: {binary_path}")
290
- if cwd:
291
- console.print(f" Working directory: {cwd}")
1170
+ console.print(f" Python: {python_path}")
1171
+ console.print(f" Command: {server_config.get('command')}")
1172
+ console.print(f" Args: {server_config.get('args')}")
1173
+ console.print(" Protocol: Content-Length framing (FastMCP SDK)")
1174
+ if absolute_project_path:
1175
+ console.print(f" Project path: {absolute_project_path}")
292
1176
  if "env" in server_config:
1177
+ env_keys = list(server_config["env"].keys())
1178
+ console.print(f" Environment variables: {env_keys}")
1179
+
1180
+ # Security warning about credentials
1181
+ sensitive_keys = [
1182
+ k
1183
+ for k in env_keys
1184
+ if "TOKEN" in k or "KEY" in k or "PASSWORD" in k
1185
+ ]
1186
+ if sensitive_keys:
1187
+ console.print(
1188
+ "\n[yellow]⚠️ Security Notice:[/yellow] Configuration contains credentials"
1189
+ )
1190
+ console.print(
1191
+ f"[yellow] Location: {mcp_config_path}[/yellow]"
1192
+ )
1193
+ console.print(
1194
+ "[yellow] Make sure this file is excluded from version control[/yellow]"
1195
+ )
1196
+
1197
+ # Migration success message (if legacy config was detected)
1198
+ if is_legacy:
1199
+ console.print("\n[green]✅ Migration Complete![/green]")
1200
+ console.print(
1201
+ "[green]Your configuration has been upgraded from legacy line-delimited JSON[/green]"
1202
+ )
1203
+ console.print(
1204
+ "[green]to modern Content-Length framing (FastMCP SDK).[/green]"
1205
+ )
293
1206
  console.print(
294
- f" Environment variables: {list(server_config['env'].keys())}"
1207
+ f"\n[cyan]This fixes MCP connection issues with {config_type}.[/cyan]"
295
1208
  )
296
1209
 
297
1210
  # Next steps