mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (129) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/aitrackdown.py +507 -6
  5. mcp_ticketer/adapters/asana/adapter.py +229 -0
  6. mcp_ticketer/adapters/asana/mappers.py +14 -0
  7. mcp_ticketer/adapters/github/__init__.py +26 -0
  8. mcp_ticketer/adapters/github/adapter.py +3229 -0
  9. mcp_ticketer/adapters/github/client.py +335 -0
  10. mcp_ticketer/adapters/github/mappers.py +797 -0
  11. mcp_ticketer/adapters/github/queries.py +692 -0
  12. mcp_ticketer/adapters/github/types.py +460 -0
  13. mcp_ticketer/adapters/hybrid.py +47 -5
  14. mcp_ticketer/adapters/jira/__init__.py +35 -0
  15. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  16. mcp_ticketer/adapters/jira/client.py +271 -0
  17. mcp_ticketer/adapters/jira/mappers.py +246 -0
  18. mcp_ticketer/adapters/jira/queries.py +216 -0
  19. mcp_ticketer/adapters/jira/types.py +304 -0
  20. mcp_ticketer/adapters/linear/adapter.py +2730 -139
  21. mcp_ticketer/adapters/linear/client.py +175 -3
  22. mcp_ticketer/adapters/linear/mappers.py +203 -8
  23. mcp_ticketer/adapters/linear/queries.py +280 -3
  24. mcp_ticketer/adapters/linear/types.py +120 -4
  25. mcp_ticketer/analysis/__init__.py +56 -0
  26. mcp_ticketer/analysis/dependency_graph.py +255 -0
  27. mcp_ticketer/analysis/health_assessment.py +304 -0
  28. mcp_ticketer/analysis/orphaned.py +218 -0
  29. mcp_ticketer/analysis/project_status.py +594 -0
  30. mcp_ticketer/analysis/similarity.py +224 -0
  31. mcp_ticketer/analysis/staleness.py +266 -0
  32. mcp_ticketer/automation/__init__.py +11 -0
  33. mcp_ticketer/automation/project_updates.py +378 -0
  34. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  35. mcp_ticketer/cli/auggie_configure.py +17 -5
  36. mcp_ticketer/cli/codex_configure.py +97 -61
  37. mcp_ticketer/cli/configure.py +1288 -105
  38. mcp_ticketer/cli/cursor_configure.py +314 -0
  39. mcp_ticketer/cli/diagnostics.py +13 -12
  40. mcp_ticketer/cli/discover.py +5 -0
  41. mcp_ticketer/cli/gemini_configure.py +17 -5
  42. mcp_ticketer/cli/init_command.py +880 -0
  43. mcp_ticketer/cli/install_mcp_server.py +418 -0
  44. mcp_ticketer/cli/instruction_commands.py +6 -0
  45. mcp_ticketer/cli/main.py +267 -3175
  46. mcp_ticketer/cli/mcp_configure.py +821 -119
  47. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  48. mcp_ticketer/cli/platform_detection.py +77 -12
  49. mcp_ticketer/cli/platform_installer.py +545 -0
  50. mcp_ticketer/cli/project_update_commands.py +350 -0
  51. mcp_ticketer/cli/setup_command.py +795 -0
  52. mcp_ticketer/cli/simple_health.py +12 -10
  53. mcp_ticketer/cli/ticket_commands.py +705 -103
  54. mcp_ticketer/cli/utils.py +113 -0
  55. mcp_ticketer/core/__init__.py +56 -6
  56. mcp_ticketer/core/adapter.py +533 -2
  57. mcp_ticketer/core/config.py +21 -21
  58. mcp_ticketer/core/exceptions.py +7 -1
  59. mcp_ticketer/core/label_manager.py +732 -0
  60. mcp_ticketer/core/mappers.py +31 -19
  61. mcp_ticketer/core/milestone_manager.py +252 -0
  62. mcp_ticketer/core/models.py +480 -0
  63. mcp_ticketer/core/onepassword_secrets.py +1 -1
  64. mcp_ticketer/core/priority_matcher.py +463 -0
  65. mcp_ticketer/core/project_config.py +132 -14
  66. mcp_ticketer/core/project_utils.py +281 -0
  67. mcp_ticketer/core/project_validator.py +376 -0
  68. mcp_ticketer/core/session_state.py +176 -0
  69. mcp_ticketer/core/state_matcher.py +625 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/mcp/server/__main__.py +2 -1
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/main.py +106 -25
  75. mcp_ticketer/mcp/server/routing.py +723 -0
  76. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  77. mcp_ticketer/mcp/server/tools/__init__.py +33 -11
  78. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  79. mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
  80. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  81. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  82. mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
  83. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  84. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  85. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  86. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  87. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  88. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  89. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  90. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  91. mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
  92. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  93. mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
  94. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  95. mcp_ticketer/queue/queue.py +68 -0
  96. mcp_ticketer/queue/worker.py +1 -1
  97. mcp_ticketer/utils/__init__.py +5 -0
  98. mcp_ticketer/utils/token_utils.py +246 -0
  99. mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
  100. mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
  101. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  102. py_mcp_installer/examples/phase3_demo.py +178 -0
  103. py_mcp_installer/scripts/manage_version.py +54 -0
  104. py_mcp_installer/setup.py +6 -0
  105. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  106. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  107. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  108. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  109. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  110. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  111. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  112. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  113. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  114. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  115. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  116. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  117. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  118. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  119. py_mcp_installer/tests/__init__.py +0 -0
  120. py_mcp_installer/tests/platforms/__init__.py +0 -0
  121. py_mcp_installer/tests/test_platform_detector.py +17 -0
  122. mcp_ticketer/adapters/github.py +0 -1574
  123. mcp_ticketer/adapters/jira.py +0 -1258
  124. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  125. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  126. mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
  127. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  128. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  129. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,8 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import shutil
6
+ import subprocess
5
7
  import sys
6
8
  from pathlib import Path
7
9
 
@@ -12,6 +14,312 @@ from .python_detection import get_mcp_ticketer_python
12
14
  console = Console()
13
15
 
14
16
 
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.
49
+
50
+ Returns:
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
162
+
163
+ Raises:
164
+ RuntimeError: If claude mcp add command fails
165
+ subprocess.TimeoutExpired: If command times out
166
+
167
+ """
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,
196
+ )
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
+
15
323
  def load_env_file(env_path: Path) -> dict[str, str]:
16
324
  """Load environment variables from .env file.
17
325
 
@@ -22,7 +330,7 @@ def load_env_file(env_path: Path) -> dict[str, str]:
22
330
  Dict of environment variable key-value pairs
23
331
 
24
332
  """
25
- env_vars = {}
333
+ env_vars: dict[str, str] = {}
26
334
  if not env_path.exists():
27
335
  return env_vars
28
336
 
@@ -107,7 +415,13 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
107
415
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
108
416
  )
109
417
  else:
110
- # Claude Code configuration (project-specific)
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
111
425
  config_path = Path.home() / ".claude.json"
112
426
 
113
427
  return config_path
@@ -124,23 +438,47 @@ def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> d
124
438
  MCP configuration dict
125
439
 
126
440
  """
441
+ # Detect if this is the new global config location
442
+ is_global_mcp_config = str(config_path).endswith(".config/claude/mcp.json")
443
+
127
444
  if config_path.exists():
128
445
  try:
129
446
  with open(config_path) as f:
130
447
  content = f.read().strip()
131
448
  if not content:
132
- # Empty file, return default structure
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": {}}
133
467
  return {"projects": {}} if is_claude_code else {"mcpServers": {}}
134
- return json.loads(content)
468
+
135
469
  except json.JSONDecodeError as e:
136
470
  console.print(
137
471
  f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
138
472
  )
139
473
  console.print(f"[dim]Error: {e}[/dim]")
140
474
  # Return default structure on parse error
475
+ if is_global_mcp_config:
476
+ return {"mcpServers": {}}
141
477
  return {"projects": {}} if is_claude_code else {"mcpServers": {}}
142
478
 
143
- # Return empty structure based on config type
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
144
482
  if is_claude_code:
145
483
  return {"projects": {}}
146
484
  else:
@@ -164,53 +502,70 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
164
502
 
165
503
 
166
504
  def create_mcp_server_config(
167
- python_path: str, project_config: dict, project_path: str | None = None
505
+ python_path: str,
506
+ project_config: dict,
507
+ project_path: str | None = None,
508
+ is_global_config: bool = False,
168
509
  ) -> dict:
169
510
  """Create MCP server configuration for mcp-ticketer.
170
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
+
171
515
  Args:
172
516
  python_path: Path to Python executable in mcp-ticketer venv
173
517
  project_config: Project configuration from .mcp-ticketer/config.json
174
518
  project_path: Project directory path (optional)
519
+ is_global_config: If True, create config for global location (no project path in args)
175
520
 
176
521
  Returns:
177
522
  MCP server configuration dict matching Claude Code stdio pattern
178
523
 
179
524
  """
180
- # Use Python module invocation pattern (works regardless of where package is installed)
181
- args = ["-m", "mcp_ticketer.mcp.server"]
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)
182
528
 
183
- # Add project path if provided
184
- if project_path:
185
- args.append(project_path)
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])
186
540
 
187
541
  # REQUIRED: Add "type": "stdio" for Claude Code compatibility
188
542
  config = {
189
543
  "type": "stdio",
190
- "command": python_path,
544
+ "command": cli_path,
191
545
  "args": args,
192
546
  }
193
547
 
194
- # Add environment variables based on adapter
195
- adapter = project_config.get("default_adapter", "aitrackdown")
196
- adapters_config = project_config.get("adapters", {})
197
- 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
198
551
 
199
552
  env_vars = {}
200
553
 
201
- # Add PYTHONPATH for project context
202
- if project_path:
554
+ # Add PYTHONPATH for project context (only for project-specific configs)
555
+ if project_path and not is_global_config:
203
556
  env_vars["PYTHONPATH"] = project_path
204
557
 
205
- # Add MCP_TICKETER_ADAPTER to identify which adapter to use
206
- env_vars["MCP_TICKETER_ADAPTER"] = adapter
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)
207
562
 
208
- # Load environment variables from .env.local if it exists
563
+ # Load environment variables from .env.local if it exists (as override)
209
564
  if project_path:
210
565
  env_file_path = Path(project_path) / ".env.local"
211
566
  env_file_vars = load_env_file(env_file_path)
212
567
 
213
- # Add relevant adapter-specific vars from .env.local
568
+ # Add relevant adapter-specific vars from .env.local (overrides config.json)
214
569
  adapter_env_keys = {
215
570
  "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
216
571
  "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
@@ -225,117 +580,272 @@ def create_mcp_server_config(
225
580
  "aitrackdown": [], # No specific env vars needed
226
581
  }
227
582
 
228
- # Include adapter-specific env vars from .env.local
583
+ adapter = env_vars.get("MCP_TICKETER_ADAPTER", "aitrackdown")
584
+ # Include adapter-specific env vars from .env.local (overrides config.json)
229
585
  for key in adapter_env_keys.get(adapter, []):
230
586
  if key in env_file_vars:
231
587
  env_vars[key] = env_file_vars[key]
232
588
 
233
- # Fallback: Add adapter-specific environment variables from project config
234
- if adapter == "linear" and "api_key" in adapter_config:
235
- if "LINEAR_API_KEY" not in env_vars:
236
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
237
- elif adapter == "github" and "token" in adapter_config:
238
- if "GITHUB_TOKEN" not in env_vars:
239
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
240
- elif adapter == "jira":
241
- if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
242
- env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
243
- if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
244
- env_vars["JIRA_EMAIL"] = adapter_config["email"]
245
-
246
589
  if env_vars:
247
590
  config["env"] = env_vars
248
591
 
249
592
  return config
250
593
 
251
594
 
252
- def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
253
- """Remove mcp-ticketer from Claude Code/Desktop configuration.
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.
254
726
 
255
727
  Args:
256
728
  global_config: Remove from Claude Desktop instead of project-level
257
729
  dry_run: Show what would be removed without making changes
258
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
+
259
741
  """
260
742
  # Step 1: Find Claude MCP config location
261
743
  config_type = "Claude Desktop" if global_config else "Claude Code"
262
744
  console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
263
745
 
264
- mcp_config_path = find_claude_mcp_config(global_config)
265
- console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
266
-
267
746
  # Get absolute project path for Claude Code
268
747
  absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
269
748
 
270
- # Step 2: Check if config file exists
271
- if not mcp_config_path.exists():
272
- console.print(f"[yellow]⚠ No configuration found at {mcp_config_path}[/yellow]")
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]")
273
772
  console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
274
773
  return
275
774
 
276
- # Step 3: Load existing MCP configuration
277
- is_claude_code = not global_config
278
- mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
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
+ ]
279
824
 
280
- # Step 4: Check if mcp-ticketer is configured
281
- is_configured = False
282
- if is_claude_code:
283
- # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
284
- if absolute_project_path:
285
- projects = mcp_config.get("projects", {})
286
- project_config_entry = projects.get(absolute_project_path, {})
287
- is_configured = "mcp-ticketer" in project_config_entry.get("mcpServers", {})
288
- else:
289
- # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
290
- is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
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"]
291
834
 
292
- if not is_configured:
293
- console.print("[yellow]⚠ mcp-ticketer is not configured[/yellow]")
294
- console.print(f"[dim]No mcp-ticketer entry found in {mcp_config_path}[/dim]")
295
- return
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}")
296
842
 
297
- # Step 5: Show what would be removed (dry run or actual removal)
298
843
  if dry_run:
299
- console.print("\n[cyan]DRY RUN - Would remove:[/cyan]")
300
- console.print(" Server name: mcp-ticketer")
301
- console.print(f" From: {mcp_config_path}")
302
- if absolute_project_path:
303
- console.print(f" Project: {absolute_project_path}")
304
844
  return
305
845
 
306
- # Step 6: Remove mcp-ticketer from configuration
307
- if is_claude_code and absolute_project_path:
308
- # Remove from Claude Code structure
309
- del mcp_config["projects"][absolute_project_path]["mcpServers"]["mcp-ticketer"]
310
-
311
- # Clean up empty structures
312
- if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
313
- del mcp_config["projects"][absolute_project_path]["mcpServers"]
314
- if not mcp_config["projects"][absolute_project_path]:
315
- del mcp_config["projects"][absolute_project_path]
316
-
317
- # Also remove from legacy location if it exists
318
- legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
319
- if legacy_config_path.exists():
320
- try:
321
- legacy_config = load_claude_mcp_config(
322
- legacy_config_path, is_claude_code=False
323
- )
324
- if "mcp-ticketer" in legacy_config.get("mcpServers", {}):
325
- del legacy_config["mcpServers"]["mcp-ticketer"]
326
- save_claude_mcp_config(legacy_config_path, legacy_config)
327
- console.print("[dim]✓ Removed from legacy config as well[/dim]")
328
- except Exception as e:
329
- console.print(f"[dim]⚠ Could not remove from legacy config: {e}[/dim]")
330
- else:
331
- # Remove from Claude Desktop structure
332
- del mcp_config["mcpServers"]["mcp-ticketer"]
333
-
334
- # Step 7: Save updated configuration
335
- try:
336
- save_claude_mcp_config(mcp_config_path, mcp_config)
846
+ if removed_count > 0:
337
847
  console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
338
- console.print(f"[dim]Configuration updated: {mcp_config_path}[/dim]")
848
+ console.print(f"[dim]Updated {removed_count} configuration file(s)[/dim]")
339
849
 
340
850
  # Next steps
341
851
  console.print("\n[bold cyan]Next Steps:[/bold cyan]")
@@ -345,15 +855,65 @@ def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> Non
345
855
  else:
346
856
  console.print("1. Restart Claude Code")
347
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
+ )
348
862
 
349
- except Exception as e:
350
- console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
351
- raise
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)
352
909
 
353
910
 
354
911
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
355
912
  """Configure Claude Code to use mcp-ticketer.
356
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
+
357
917
  Args:
358
918
  global_config: Configure Claude Desktop instead of project-level
359
919
  force: Overwrite existing configuration
@@ -363,11 +923,92 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
363
923
  ValueError: If configuration is invalid
364
924
 
365
925
  """
926
+ # Load project configuration early (needed for both native and JSON methods)
927
+ console.print("[cyan]📖 Reading project configuration...[/cyan]")
928
+ try:
929
+ project_config = load_project_config()
930
+ adapter = project_config.get("default_adapter", "aitrackdown")
931
+ console.print(f"[green]✓[/green] Adapter: {adapter}")
932
+ except (FileNotFoundError, ValueError) as e:
933
+ console.print(f"[red]✗[/red] {e}")
934
+ raise
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
+
366
1007
  # Determine project path for venv detection
367
1008
  project_path = Path.cwd() if not global_config else None
368
1009
 
369
1010
  # Step 1: Find Python executable (project-specific if available)
370
- console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
1011
+ console.print("\n[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
371
1012
  try:
372
1013
  python_path = get_mcp_ticketer_python(project_path=project_path)
373
1014
  console.print(f"[green]✓[/green] Found: {python_path}")
@@ -377,6 +1018,11 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
377
1018
  console.print("[dim]Using project-specific venv[/dim]")
378
1019
  else:
379
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]")
380
1026
  except Exception as e:
381
1027
  console.print(f"[red]✗[/red] Could not find Python executable: {e}")
382
1028
  raise FileNotFoundError(
@@ -385,16 +1031,6 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
385
1031
  "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
386
1032
  ) from e
387
1033
 
388
- # Step 2: Load project configuration
389
- console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
390
- try:
391
- project_config = load_project_config()
392
- adapter = project_config.get("default_adapter", "aitrackdown")
393
- console.print(f"[green]✓[/green] Adapter: {adapter}")
394
- except (FileNotFoundError, ValueError) as e:
395
- console.print(f"[red]✗[/red] {e}")
396
- raise
397
-
398
1034
  # Step 3: Find Claude MCP config location
399
1035
  config_type = "Claude Desktop" if global_config else "Claude Code"
400
1036
  console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
@@ -409,16 +1045,49 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
409
1045
  is_claude_code = not global_config
410
1046
  mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
411
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
1074
+
412
1075
  # Step 5: Check if mcp-ticketer already configured
413
1076
  already_configured = False
414
- if is_claude_code:
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:
415
1081
  # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
416
- if absolute_project_path:
1082
+ if absolute_project_path and "projects" in mcp_config:
417
1083
  projects = mcp_config.get("projects", {})
418
1084
  project_config_entry = projects.get(absolute_project_path, {})
419
1085
  already_configured = "mcp-ticketer" in project_config_entry.get(
420
1086
  "mcpServers", {}
421
1087
  )
1088
+ elif "mcpServers" in mcp_config:
1089
+ # Check flat structure for backward compatibility
1090
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
422
1091
  else:
423
1092
  # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
424
1093
  already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
@@ -436,10 +1105,16 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
436
1105
  python_path=python_path,
437
1106
  project_config=project_config,
438
1107
  project_path=absolute_project_path,
1108
+ is_global_config=is_global_mcp_config,
439
1109
  )
440
1110
 
441
1111
  # Step 7: Update MCP configuration based on platform
442
- if is_claude_code:
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:
443
1118
  # Claude Code: Write to ~/.claude.json with project-specific path
444
1119
  if absolute_project_path:
445
1120
  # Ensure projects structure exists
@@ -493,12 +1168,39 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
493
1168
  console.print(" Server name: mcp-ticketer")
494
1169
  console.print(f" Adapter: {adapter}")
495
1170
  console.print(f" Python: {python_path}")
496
- console.print(" Command: python -m mcp_ticketer.mcp.server")
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)")
497
1174
  if absolute_project_path:
498
1175
  console.print(f" Project path: {absolute_project_path}")
499
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 for k in env_keys if "TOKEN" in k or "KEY" in k or "PASSWORD" in k
1183
+ ]
1184
+ if sensitive_keys:
1185
+ console.print(
1186
+ "\n[yellow]⚠️ Security Notice:[/yellow] Configuration contains credentials"
1187
+ )
1188
+ console.print(f"[yellow] Location: {mcp_config_path}[/yellow]")
1189
+ console.print(
1190
+ "[yellow] Make sure this file is excluded from version control[/yellow]"
1191
+ )
1192
+
1193
+ # Migration success message (if legacy config was detected)
1194
+ if is_legacy:
1195
+ console.print("\n[green]✅ Migration Complete![/green]")
1196
+ console.print(
1197
+ "[green]Your configuration has been upgraded from legacy line-delimited JSON[/green]"
1198
+ )
1199
+ console.print(
1200
+ "[green]to modern Content-Length framing (FastMCP SDK).[/green]"
1201
+ )
500
1202
  console.print(
501
- f" Environment variables: {list(server_config['env'].keys())}"
1203
+ f"\n[cyan]This fixes MCP connection issues with {config_type}.[/cyan]"
502
1204
  )
503
1205
 
504
1206
  # Next steps