mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import subprocess
5
6
  import sys
6
7
  from pathlib import Path
7
8
 
@@ -12,6 +13,211 @@ from .python_detection import get_mcp_ticketer_python
12
13
  console = Console()
13
14
 
14
15
 
16
+ def is_claude_cli_available() -> bool:
17
+ """Check if Claude CLI is available in PATH.
18
+
19
+ Returns:
20
+ True if 'claude' command is available, False otherwise
21
+
22
+ """
23
+ try:
24
+ result = subprocess.run(
25
+ ["claude", "--version"],
26
+ capture_output=True,
27
+ text=True,
28
+ timeout=5,
29
+ )
30
+ return result.returncode == 0
31
+ except (subprocess.SubprocessError, FileNotFoundError, OSError):
32
+ return False
33
+
34
+
35
+ def build_claude_mcp_command(
36
+ project_config: dict,
37
+ project_path: str | None = None,
38
+ global_config: bool = False,
39
+ ) -> list[str]:
40
+ """Build 'claude mcp add' command arguments.
41
+
42
+ Args:
43
+ project_config: Project configuration dict
44
+ project_path: Path to project (for --path arg)
45
+ global_config: If True, use --scope user (global), else --scope local
46
+
47
+ Returns:
48
+ List of command arguments for subprocess
49
+
50
+ """
51
+ cmd = ["claude", "mcp", "add"]
52
+
53
+ # Scope: user (global) or local (project)
54
+ scope = "user" if global_config else "local"
55
+ cmd.extend(["--scope", scope])
56
+
57
+ # Transport: always stdio
58
+ cmd.extend(["--transport", "stdio"])
59
+
60
+ # Environment variables (credentials)
61
+ adapters = project_config.get("adapters", {})
62
+
63
+ # Linear adapter
64
+ if "linear" in adapters:
65
+ linear_config = adapters["linear"]
66
+ if "api_key" in linear_config:
67
+ cmd.extend(["--env", f"LINEAR_API_KEY={linear_config['api_key']}"])
68
+ if "team_id" in linear_config:
69
+ cmd.extend(["--env", f"LINEAR_TEAM_ID={linear_config['team_id']}"])
70
+ if "team_key" in linear_config:
71
+ cmd.extend(["--env", f"LINEAR_TEAM_KEY={linear_config['team_key']}"])
72
+
73
+ # GitHub adapter
74
+ if "github" in adapters:
75
+ github_config = adapters["github"]
76
+ if "token" in github_config:
77
+ cmd.extend(["--env", f"GITHUB_TOKEN={github_config['token']}"])
78
+ if "owner" in github_config:
79
+ cmd.extend(["--env", f"GITHUB_OWNER={github_config['owner']}"])
80
+ if "repo" in github_config:
81
+ cmd.extend(["--env", f"GITHUB_REPO={github_config['repo']}"])
82
+
83
+ # JIRA adapter
84
+ if "jira" in adapters:
85
+ jira_config = adapters["jira"]
86
+ if "api_token" in jira_config:
87
+ cmd.extend(["--env", f"JIRA_API_TOKEN={jira_config['api_token']}"])
88
+ if "email" in jira_config:
89
+ cmd.extend(["--env", f"JIRA_EMAIL={jira_config['email']}"])
90
+ if "url" in jira_config:
91
+ cmd.extend(["--env", f"JIRA_URL={jira_config['url']}"])
92
+
93
+ # Add default adapter
94
+ default_adapter = project_config.get("default_adapter", "aitrackdown")
95
+ cmd.extend(["--env", f"MCP_TICKETER_ADAPTER={default_adapter}"])
96
+
97
+ # Server label
98
+ cmd.append("mcp-ticketer")
99
+
100
+ # Command separator
101
+ cmd.append("--")
102
+
103
+ # Server command and args
104
+ cmd.extend(["mcp-ticketer", "mcp"])
105
+
106
+ # Project path (for local scope)
107
+ if project_path and not global_config:
108
+ cmd.extend(["--path", project_path])
109
+
110
+ return cmd
111
+
112
+
113
+ def configure_claude_mcp_native(
114
+ project_config: dict,
115
+ project_path: str | None = None,
116
+ global_config: bool = False,
117
+ force: bool = False,
118
+ ) -> None:
119
+ """Configure Claude Code using native 'claude mcp add' command.
120
+
121
+ Args:
122
+ project_config: Project configuration dict
123
+ project_path: Path to project directory
124
+ global_config: If True, install globally (--scope user)
125
+ force: If True, force reinstallation by removing existing config first
126
+
127
+ Raises:
128
+ RuntimeError: If claude mcp add command fails
129
+ subprocess.TimeoutExpired: If command times out
130
+
131
+ """
132
+ # Auto-remove before re-adding when force=True
133
+ if force:
134
+ console.print("[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]")
135
+ try:
136
+ removal_success = remove_claude_mcp_native(
137
+ global_config=global_config, dry_run=False
138
+ )
139
+ if removal_success:
140
+ console.print("[green]✓[/green] Existing configuration removed")
141
+ else:
142
+ console.print(
143
+ "[yellow]⚠[/yellow] Could not remove existing configuration"
144
+ )
145
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
146
+ except Exception as e:
147
+ console.print(f"[yellow]⚠[/yellow] Removal error: {e}")
148
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
149
+
150
+ console.print() # Blank line for visual separation
151
+
152
+ # Build command
153
+ cmd = build_claude_mcp_command(
154
+ project_config=project_config,
155
+ project_path=project_path,
156
+ global_config=global_config,
157
+ )
158
+
159
+ # Show command to user (mask sensitive values)
160
+ masked_cmd = []
161
+ for i, arg in enumerate(cmd):
162
+ if arg.startswith("--env=") or (i > 0 and cmd[i - 1] == "--env"):
163
+ # Mask environment variable values
164
+ if "=" in arg:
165
+ key, _ = arg.split("=", 1)
166
+ masked_cmd.append(f"{key}=***")
167
+ else:
168
+ masked_cmd.append(arg)
169
+ else:
170
+ masked_cmd.append(arg)
171
+
172
+ console.print(f"[cyan]Executing:[/cyan] {' '.join(masked_cmd)}")
173
+
174
+ try:
175
+ # Execute native command
176
+ result = subprocess.run(
177
+ cmd,
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=30,
181
+ )
182
+
183
+ if result.returncode == 0:
184
+ scope_label = (
185
+ "globally" if global_config else f"for project: {project_path}"
186
+ )
187
+ console.print(f"[green]✓[/green] Claude Code configured {scope_label}")
188
+ console.print("[dim]Restart Claude Code to load the MCP server[/dim]")
189
+
190
+ # Show adapter information
191
+ adapter = project_config.get("default_adapter", "aitrackdown")
192
+ console.print("\n[bold]Configuration Details:[/bold]")
193
+ console.print(" Server name: mcp-ticketer")
194
+ console.print(f" Adapter: {adapter}")
195
+ console.print(" Protocol: Content-Length framing (FastMCP SDK)")
196
+ if project_path and not global_config:
197
+ console.print(f" Project path: {project_path}")
198
+
199
+ # Next steps
200
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
201
+ if global_config:
202
+ console.print("1. Restart Claude Desktop")
203
+ console.print("2. Open a conversation")
204
+ else:
205
+ console.print("1. Restart Claude Code")
206
+ console.print("2. Open this project in Claude Code")
207
+ console.print("3. mcp-ticketer tools will be available in the MCP menu")
208
+ else:
209
+ console.print("[red]✗[/red] Failed to configure Claude Code")
210
+ console.print(f"[red]Error:[/red] {result.stderr}")
211
+ raise RuntimeError(f"claude mcp add failed: {result.stderr}")
212
+
213
+ except subprocess.TimeoutExpired:
214
+ console.print("[red]✗[/red] Claude CLI command timed out")
215
+ raise
216
+ except Exception as e:
217
+ console.print(f"[red]✗[/red] Error executing Claude CLI: {e}")
218
+ raise
219
+
220
+
15
221
  def load_env_file(env_path: Path) -> dict[str, str]:
16
222
  """Load environment variables from .env file.
17
223
 
@@ -22,7 +228,7 @@ def load_env_file(env_path: Path) -> dict[str, str]:
22
228
  Dict of environment variable key-value pairs
23
229
 
24
230
  """
25
- env_vars = {}
231
+ env_vars: dict[str, str] = {}
26
232
  if not env_path.exists():
27
233
  return env_vars
28
234
 
@@ -107,7 +313,13 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
107
313
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
108
314
  )
109
315
  else:
110
- # Claude Code configuration (project-specific)
316
+ # Claude Code configuration - check both locations
317
+ # Priority 1: New global location ~/.config/claude/mcp.json
318
+ new_config_path = Path.home() / ".config" / "claude" / "mcp.json"
319
+ if new_config_path.exists():
320
+ return new_config_path
321
+
322
+ # Priority 2: Legacy project-specific location ~/.claude.json
111
323
  config_path = Path.home() / ".claude.json"
112
324
 
113
325
  return config_path
@@ -124,23 +336,47 @@ def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> d
124
336
  MCP configuration dict
125
337
 
126
338
  """
339
+ # Detect if this is the new global config location
340
+ is_global_mcp_config = str(config_path).endswith(".config/claude/mcp.json")
341
+
127
342
  if config_path.exists():
128
343
  try:
129
344
  with open(config_path) as f:
130
345
  content = f.read().strip()
131
346
  if not content:
132
- # Empty file, return default structure
347
+ # Empty file, return default structure based on location
348
+ if is_global_mcp_config:
349
+ return {"mcpServers": {}} # Flat structure
133
350
  return {"projects": {}} if is_claude_code else {"mcpServers": {}}
134
- return json.loads(content)
351
+
352
+ config = json.loads(content)
353
+
354
+ # Auto-detect structure format based on content
355
+ if "projects" in config:
356
+ # This is the old nested project structure
357
+ return config
358
+ elif "mcpServers" in config:
359
+ # This is flat mcpServers structure
360
+ return config
361
+ else:
362
+ # Empty or unknown structure, return default
363
+ if is_global_mcp_config:
364
+ return {"mcpServers": {}}
365
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
366
+
135
367
  except json.JSONDecodeError as e:
136
368
  console.print(
137
369
  f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
138
370
  )
139
371
  console.print(f"[dim]Error: {e}[/dim]")
140
372
  # Return default structure on parse error
373
+ if is_global_mcp_config:
374
+ return {"mcpServers": {}}
141
375
  return {"projects": {}} if is_claude_code else {"mcpServers": {}}
142
376
 
143
- # Return empty structure based on config type
377
+ # Return empty structure based on config type and location
378
+ if is_global_mcp_config:
379
+ return {"mcpServers": {}} # New location always uses flat structure
144
380
  if is_claude_code:
145
381
  return {"projects": {}}
146
382
  else:
@@ -164,45 +400,64 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
164
400
 
165
401
 
166
402
  def create_mcp_server_config(
167
- python_path: str, project_config: dict, project_path: str | None = None
403
+ python_path: str,
404
+ project_config: dict,
405
+ project_path: str | None = None,
406
+ is_global_config: bool = False,
168
407
  ) -> dict:
169
408
  """Create MCP server configuration for mcp-ticketer.
170
409
 
410
+ Uses the CLI command (mcp-ticketer mcp) which implements proper
411
+ Content-Length framing via FastMCP SDK, required for modern MCP clients.
412
+
171
413
  Args:
172
414
  python_path: Path to Python executable in mcp-ticketer venv
173
415
  project_config: Project configuration from .mcp-ticketer/config.json
174
416
  project_path: Project directory path (optional)
417
+ is_global_config: If True, create config for global location (no project path in args)
175
418
 
176
419
  Returns:
177
420
  MCP server configuration dict matching Claude Code stdio pattern
178
421
 
179
422
  """
180
- # Use Python module invocation pattern (works regardless of where package is installed)
181
- args = ["-m", "mcp_ticketer.mcp.server"]
423
+ # IMPORTANT: Use CLI command, NOT Python module invocation
424
+ # The CLI uses FastMCP SDK which implements proper Content-Length framing
425
+ # Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
182
426
 
183
- # Add project path if provided
184
- if project_path:
185
- args.append(project_path)
427
+ # Get mcp-ticketer CLI path from Python path
428
+ # If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
429
+ python_dir = Path(python_path).parent
430
+ cli_path = str(python_dir / "mcp-ticketer")
431
+
432
+ # Build CLI arguments
433
+ args = ["mcp"]
434
+
435
+ # Add project path if provided and not global config
436
+ if project_path and not is_global_config:
437
+ args.extend(["--path", project_path])
186
438
 
187
439
  # REQUIRED: Add "type": "stdio" for Claude Code compatibility
188
440
  config = {
189
441
  "type": "stdio",
190
- "command": python_path,
442
+ "command": cli_path,
191
443
  "args": args,
192
444
  }
193
445
 
194
- # Add environment variables based on adapter
446
+ # NOTE: The CLI command loads configuration from .mcp-ticketer/config.json
447
+ # Environment variables below are optional fallbacks for backward compatibility
448
+ # The FastMCP SDK server will automatically load config from the project directory
449
+
195
450
  adapter = project_config.get("default_adapter", "aitrackdown")
196
451
  adapters_config = project_config.get("adapters", {})
197
452
  adapter_config = adapters_config.get(adapter, {})
198
453
 
199
454
  env_vars = {}
200
455
 
201
- # Add PYTHONPATH for project context
202
- if project_path:
456
+ # Add PYTHONPATH for project context (only for project-specific configs)
457
+ if project_path and not is_global_config:
203
458
  env_vars["PYTHONPATH"] = project_path
204
459
 
205
- # Add MCP_TICKETER_ADAPTER to identify which adapter to use
460
+ # Add MCP_TICKETER_ADAPTER to identify which adapter to use (optional fallback)
206
461
  env_vars["MCP_TICKETER_ADAPTER"] = adapter
207
462
 
208
463
  # Load environment variables from .env.local if it exists
@@ -249,93 +504,260 @@ def create_mcp_server_config(
249
504
  return config
250
505
 
251
506
 
252
- def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
253
- """Remove mcp-ticketer from Claude Code/Desktop configuration.
507
+ def detect_legacy_claude_config(
508
+ config_path: Path, is_claude_code: bool = True, project_path: str | None = None
509
+ ) -> tuple[bool, dict | None]:
510
+ """Detect if existing Claude config uses legacy Python module invocation.
511
+
512
+ Args:
513
+ ----
514
+ config_path: Path to Claude configuration file
515
+ is_claude_code: Whether this is Claude Code (project-level) or Claude Desktop (global)
516
+ project_path: Project path for Claude Code configs
517
+
518
+ Returns:
519
+ -------
520
+ Tuple of (is_legacy, server_config):
521
+ - is_legacy: True if config uses 'python -m mcp_ticketer.mcp.server'
522
+ - server_config: The legacy server config dict, or None if not legacy
523
+
524
+ """
525
+ if not config_path.exists():
526
+ return False, None
527
+
528
+ try:
529
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
530
+ except Exception:
531
+ return False, None
532
+
533
+ # For Claude Code, check project-specific config
534
+ if is_claude_code and project_path:
535
+ projects = mcp_config.get("projects", {})
536
+ project_config = projects.get(project_path, {})
537
+ mcp_servers = project_config.get("mcpServers", {})
538
+ else:
539
+ # For Claude Desktop, check global config
540
+ mcp_servers = mcp_config.get("mcpServers", {})
541
+
542
+ if "mcp-ticketer" in mcp_servers:
543
+ server_config = mcp_servers["mcp-ticketer"]
544
+ args = server_config.get("args", [])
545
+
546
+ # Check for legacy pattern: ["-m", "mcp_ticketer.mcp.server", ...]
547
+ if len(args) >= 2 and args[0] == "-m" and "mcp_ticketer.mcp.server" in args[1]:
548
+ return True, server_config
549
+
550
+ return False, None
551
+
552
+
553
+ def remove_claude_mcp_native(
554
+ global_config: bool = False,
555
+ dry_run: bool = False,
556
+ ) -> bool:
557
+ """Remove mcp-ticketer using native 'claude mcp remove' command.
558
+
559
+ This function attempts to use the Claude CLI's native remove command
560
+ first, falling back to JSON manipulation if the native command fails.
561
+
562
+ Args:
563
+ global_config: If True, remove from Claude Desktop (--scope user)
564
+ If False, remove from Claude Code (--scope local)
565
+ dry_run: If True, only show what would be removed without making changes
566
+
567
+ Returns:
568
+ bool: True if removal was successful, False if failed or skipped
569
+
570
+ Raises:
571
+ Does not raise exceptions - all errors are caught and handled gracefully
572
+ with fallback to JSON manipulation
573
+
574
+ Example:
575
+ >>> # Remove from local Claude Code configuration
576
+ >>> remove_claude_mcp_native(global_config=False, dry_run=False)
577
+ True
578
+
579
+ >>> # Preview removal without making changes
580
+ >>> remove_claude_mcp_native(global_config=False, dry_run=True)
581
+ True
582
+
583
+ Notes:
584
+ - Automatically falls back to remove_claude_mcp_json() if native fails
585
+ - Designed to be non-blocking for auto-remove scenarios
586
+ - Uses --scope flag for backward compatibility with Claude CLI
587
+
588
+ """
589
+ scope = "user" if global_config else "local"
590
+ cmd = ["claude", "mcp", "remove", "--scope", scope, "mcp-ticketer"]
591
+
592
+ config_type = "Claude Desktop" if global_config else "Claude Code"
593
+
594
+ if dry_run:
595
+ console.print(f"[cyan]DRY RUN - Would execute:[/cyan] {' '.join(cmd)}")
596
+ console.print(f"[dim]Target: {config_type}[/dim]")
597
+ return True
598
+
599
+ try:
600
+ # Execute native remove command
601
+ result = subprocess.run(
602
+ cmd,
603
+ capture_output=True,
604
+ text=True,
605
+ timeout=30,
606
+ )
607
+
608
+ if result.returncode == 0:
609
+ console.print("[green]✓[/green] Removed mcp-ticketer via native CLI")
610
+ console.print(f"[dim]Target: {config_type}[/dim]")
611
+ return True
612
+ else:
613
+ # Native command failed, fallback to JSON
614
+ console.print(
615
+ f"[yellow]⚠[/yellow] Native remove failed: {result.stderr.strip()}"
616
+ )
617
+ console.print(
618
+ "[yellow]Falling back to JSON configuration removal...[/yellow]"
619
+ )
620
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
621
+
622
+ except subprocess.TimeoutExpired:
623
+ console.print("[yellow]⚠[/yellow] Native remove command timed out")
624
+ console.print("[yellow]Falling back to JSON configuration removal...[/yellow]")
625
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
626
+
627
+ except Exception as e:
628
+ console.print(f"[yellow]⚠[/yellow] Error executing native remove: {e}")
629
+ console.print("[yellow]Falling back to JSON configuration removal...[/yellow]")
630
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
631
+
632
+
633
+ def remove_claude_mcp_json(global_config: bool = False, dry_run: bool = False) -> bool:
634
+ """Remove mcp-ticketer from Claude Code/Desktop configuration using JSON.
635
+
636
+ This is a fallback method when native 'claude mcp remove' is unavailable
637
+ or fails. It directly manipulates the JSON configuration files.
254
638
 
255
639
  Args:
256
640
  global_config: Remove from Claude Desktop instead of project-level
257
641
  dry_run: Show what would be removed without making changes
258
642
 
643
+ Returns:
644
+ bool: True if removal was successful (or files not found),
645
+ False if an error occurred during JSON manipulation
646
+
647
+ Notes:
648
+ - Handles multiple config file locations (new, old, legacy)
649
+ - Supports both flat and nested configuration structures
650
+ - Cleans up empty structures after removal
651
+ - Provides detailed logging of actions taken
652
+
259
653
  """
260
654
  # Step 1: Find Claude MCP config location
261
655
  config_type = "Claude Desktop" if global_config else "Claude Code"
262
656
  console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
263
657
 
264
- mcp_config_path = find_claude_mcp_config(global_config)
265
- console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
266
-
267
658
  # Get absolute project path for Claude Code
268
659
  absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
269
660
 
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]")
661
+ # Check both locations for Claude Code
662
+ config_paths_to_check = []
663
+ if not global_config:
664
+ # Check both new and old locations
665
+ new_config = Path.home() / ".config" / "claude" / "mcp.json"
666
+ old_config = Path.home() / ".claude.json"
667
+ legacy_config = Path.cwd() / ".claude" / "mcp.local.json"
668
+
669
+ if new_config.exists():
670
+ config_paths_to_check.append(
671
+ (new_config, True)
672
+ ) # True = is_global_mcp_config
673
+ if old_config.exists():
674
+ config_paths_to_check.append((old_config, False))
675
+ if legacy_config.exists():
676
+ config_paths_to_check.append((legacy_config, False))
677
+ else:
678
+ mcp_config_path = find_claude_mcp_config(global_config)
679
+ if mcp_config_path.exists():
680
+ config_paths_to_check.append((mcp_config_path, False))
681
+
682
+ if not config_paths_to_check:
683
+ console.print("[yellow]⚠ No configuration files found[/yellow]")
273
684
  console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
274
685
  return
275
686
 
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)
687
+ # Step 2-7: Process each config file
688
+ removed_count = 0
689
+ for config_path, is_global_mcp_config in config_paths_to_check:
690
+ console.print(f"[dim]Checking: {config_path}[/dim]")
691
+
692
+ # Load existing MCP configuration
693
+ is_claude_code = not global_config
694
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
695
+
696
+ # Check if mcp-ticketer is configured
697
+ is_configured = False
698
+ if is_global_mcp_config:
699
+ # Global mcp.json uses flat structure
700
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
701
+ elif is_claude_code:
702
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
703
+ if absolute_project_path:
704
+ projects = mcp_config.get("projects", {})
705
+ project_config_entry = projects.get(absolute_project_path, {})
706
+ is_configured = "mcp-ticketer" in project_config_entry.get(
707
+ "mcpServers", {}
708
+ )
709
+ else:
710
+ # Check flat structure for backward compatibility
711
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
712
+ else:
713
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
714
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
715
+
716
+ if not is_configured:
717
+ continue
718
+
719
+ # Show what would be removed (dry run)
720
+ if dry_run:
721
+ console.print(f"\n[cyan]DRY RUN - Would remove from: {config_path}[/cyan]")
722
+ console.print(" Server name: mcp-ticketer")
723
+ if absolute_project_path and not is_global_mcp_config:
724
+ console.print(f" Project: {absolute_project_path}")
725
+ continue
726
+
727
+ # Remove mcp-ticketer from configuration
728
+ if is_global_mcp_config:
729
+ # Global mcp.json uses flat structure
730
+ del mcp_config["mcpServers"]["mcp-ticketer"]
731
+ elif is_claude_code and absolute_project_path and "projects" in mcp_config:
732
+ # Remove from Claude Code nested structure
733
+ del mcp_config["projects"][absolute_project_path]["mcpServers"][
734
+ "mcp-ticketer"
735
+ ]
279
736
 
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", {})
737
+ # Clean up empty structures
738
+ if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
739
+ del mcp_config["projects"][absolute_project_path]["mcpServers"]
740
+ if not mcp_config["projects"][absolute_project_path]:
741
+ del mcp_config["projects"][absolute_project_path]
742
+ else:
743
+ # Remove from flat structure (legacy or Claude Desktop)
744
+ if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
745
+ del mcp_config["mcpServers"]["mcp-ticketer"]
291
746
 
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
747
+ # Save updated configuration
748
+ try:
749
+ save_claude_mcp_config(config_path, mcp_config)
750
+ console.print(f"[green]✓ Removed from: {config_path}[/green]")
751
+ removed_count += 1
752
+ except Exception as e:
753
+ console.print(f"[red]✗ Failed to update {config_path}:[/red] {e}")
296
754
 
297
- # Step 5: Show what would be removed (dry run or actual removal)
298
755
  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
756
  return
305
757
 
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)
758
+ if removed_count > 0:
337
759
  console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
338
- console.print(f"[dim]Configuration updated: {mcp_config_path}[/dim]")
760
+ console.print(f"[dim]Updated {removed_count} configuration file(s)[/dim]")
339
761
 
340
762
  # Next steps
341
763
  console.print("\n[bold cyan]Next Steps:[/bold cyan]")
@@ -345,15 +767,65 @@ def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> Non
345
767
  else:
346
768
  console.print("1. Restart Claude Code")
347
769
  console.print("2. mcp-ticketer will no longer be available in this project")
770
+ else:
771
+ console.print(
772
+ "\n[yellow]⚠ mcp-ticketer was not found in any configuration[/yellow]"
773
+ )
348
774
 
349
- except Exception as e:
350
- console.print(f"\n[red]✗ Failed to update configuration:[/red] {e}")
351
- raise
775
+ # Return True even if not found (successful removal)
776
+ return True
777
+
778
+
779
+ def remove_claude_mcp(
780
+ global_config: bool = False,
781
+ dry_run: bool = False,
782
+ ) -> bool:
783
+ """Remove mcp-ticketer from Claude Code/Desktop configuration.
784
+
785
+ Automatically detects if Claude CLI is available and uses the native
786
+ 'claude mcp remove' command if possible, falling back to JSON configuration
787
+ manipulation when necessary.
788
+
789
+ Args:
790
+ global_config: Remove from Claude Desktop instead of project-level
791
+ dry_run: Show what would be removed without making changes
792
+
793
+ Returns:
794
+ bool: True if removal was successful, False if failed
795
+
796
+ Example:
797
+ >>> # Remove from Claude Code (project-level)
798
+ >>> remove_claude_mcp(global_config=False)
799
+ True
800
+
801
+ >>> # Remove from Claude Desktop (global)
802
+ >>> remove_claude_mcp(global_config=True)
803
+ True
804
+
805
+ Notes:
806
+ - Uses native CLI when available for better reliability
807
+ - Automatically falls back to JSON manipulation if needed
808
+ - Safe to call even if mcp-ticketer is not configured
809
+
810
+ """
811
+ # Check for native CLI availability
812
+ if is_claude_cli_available():
813
+ console.print("[green]✓[/green] Claude CLI found - using native remove command")
814
+ return remove_claude_mcp_native(global_config=global_config, dry_run=dry_run)
815
+
816
+ # Fall back to JSON manipulation
817
+ console.print(
818
+ "[yellow]⚠[/yellow] Claude CLI not found - using JSON configuration removal"
819
+ )
820
+ return remove_claude_mcp_json(global_config=global_config, dry_run=dry_run)
352
821
 
353
822
 
354
823
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
355
824
  """Configure Claude Code to use mcp-ticketer.
356
825
 
826
+ Automatically detects if Claude CLI is available and uses native
827
+ 'claude mcp add' command if possible, falling back to JSON configuration.
828
+
357
829
  Args:
358
830
  global_config: Configure Claude Desktop instead of project-level
359
831
  force: Overwrite existing configuration
@@ -363,11 +835,69 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
363
835
  ValueError: If configuration is invalid
364
836
 
365
837
  """
838
+ # Load project configuration early (needed for both native and JSON methods)
839
+ console.print("[cyan]📖 Reading project configuration...[/cyan]")
840
+ try:
841
+ project_config = load_project_config()
842
+ adapter = project_config.get("default_adapter", "aitrackdown")
843
+ console.print(f"[green]✓[/green] Adapter: {adapter}")
844
+ except (FileNotFoundError, ValueError) as e:
845
+ console.print(f"[red]✗[/red] {e}")
846
+ raise
847
+
848
+ # Check for native CLI availability
849
+ console.print("\n[cyan]🔍 Checking for Claude CLI...[/cyan]")
850
+ if is_claude_cli_available():
851
+ console.print("[green]✓[/green] Claude CLI found - using native command")
852
+ console.print(
853
+ "[dim]This provides better integration and automatic updates[/dim]"
854
+ )
855
+
856
+ # Get absolute project path for local scope
857
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
858
+
859
+ return configure_claude_mcp_native(
860
+ project_config=project_config,
861
+ project_path=absolute_project_path,
862
+ global_config=global_config,
863
+ force=force,
864
+ )
865
+
866
+ # Fall back to JSON manipulation
867
+ console.print(
868
+ "[yellow]⚠[/yellow] Claude CLI not found - using legacy JSON configuration"
869
+ )
870
+ console.print(
871
+ "[dim]For better experience, install Claude CLI: https://docs.claude.ai/cli[/dim]"
872
+ )
873
+
874
+ # Auto-remove before re-adding when force=True
875
+ if force:
876
+ console.print(
877
+ "\n[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]"
878
+ )
879
+ try:
880
+ removal_success = remove_claude_mcp_json(
881
+ global_config=global_config, dry_run=False
882
+ )
883
+ if removal_success:
884
+ console.print("[green]✓[/green] Existing configuration removed")
885
+ else:
886
+ console.print(
887
+ "[yellow]⚠[/yellow] Could not remove existing configuration"
888
+ )
889
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
890
+ except Exception as e:
891
+ console.print(f"[yellow]⚠[/yellow] Removal error: {e}")
892
+ console.print("[yellow]Proceeding with installation anyway...[/yellow]")
893
+
894
+ console.print() # Blank line for visual separation
895
+
366
896
  # Determine project path for venv detection
367
897
  project_path = Path.cwd() if not global_config else None
368
898
 
369
899
  # Step 1: Find Python executable (project-specific if available)
370
- console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
900
+ console.print("\n[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
371
901
  try:
372
902
  python_path = get_mcp_ticketer_python(project_path=project_path)
373
903
  console.print(f"[green]✓[/green] Found: {python_path}")
@@ -383,17 +913,7 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
383
913
  "Could not find mcp-ticketer Python executable. "
384
914
  "Please ensure mcp-ticketer is installed.\n"
385
915
  "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
386
- )
387
-
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
916
+ ) from e
397
917
 
398
918
  # Step 3: Find Claude MCP config location
399
919
  config_type = "Claude Desktop" if global_config else "Claude Code"
@@ -409,16 +929,49 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
409
929
  is_claude_code = not global_config
410
930
  mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
411
931
 
932
+ # Detect if using new global config location
933
+ is_global_mcp_config = str(mcp_config_path).endswith(".config/claude/mcp.json")
934
+
935
+ # Step 4.5: Check for legacy configuration (DETECTION & MIGRATION)
936
+ is_legacy, legacy_config = detect_legacy_claude_config(
937
+ mcp_config_path,
938
+ is_claude_code=is_claude_code,
939
+ project_path=absolute_project_path,
940
+ )
941
+ if is_legacy:
942
+ console.print("\n[yellow]⚠ LEGACY CONFIGURATION DETECTED[/yellow]")
943
+ console.print(
944
+ "[yellow]Your current configuration uses the legacy line-delimited JSON server:[/yellow]"
945
+ )
946
+ console.print(f"[dim] Command: {legacy_config.get('command')}[/dim]")
947
+ console.print(f"[dim] Args: {legacy_config.get('args')}[/dim]")
948
+ console.print(
949
+ f"\n[red]This legacy server is incompatible with modern MCP clients ({config_type}).[/red]"
950
+ )
951
+ console.print(
952
+ "[red]The legacy server uses line-delimited JSON instead of Content-Length framing.[/red]"
953
+ )
954
+ console.print(
955
+ "\n[cyan]✨ Automatically migrating to modern FastMCP-based server...[/cyan]"
956
+ )
957
+ force = True # Auto-enable force mode for migration
958
+
412
959
  # Step 5: Check if mcp-ticketer already configured
413
960
  already_configured = False
414
- if is_claude_code:
961
+ if is_global_mcp_config:
962
+ # New global config uses flat structure
963
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
964
+ elif is_claude_code:
415
965
  # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
416
- if absolute_project_path:
966
+ if absolute_project_path and "projects" in mcp_config:
417
967
  projects = mcp_config.get("projects", {})
418
968
  project_config_entry = projects.get(absolute_project_path, {})
419
969
  already_configured = "mcp-ticketer" in project_config_entry.get(
420
970
  "mcpServers", {}
421
971
  )
972
+ elif "mcpServers" in mcp_config:
973
+ # Check flat structure for backward compatibility
974
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
422
975
  else:
423
976
  # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
424
977
  already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
@@ -436,10 +989,16 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
436
989
  python_path=python_path,
437
990
  project_config=project_config,
438
991
  project_path=absolute_project_path,
992
+ is_global_config=is_global_mcp_config,
439
993
  )
440
994
 
441
995
  # Step 7: Update MCP configuration based on platform
442
- if is_claude_code:
996
+ if is_global_mcp_config:
997
+ # New global location: ~/.config/claude/mcp.json uses flat structure
998
+ if "mcpServers" not in mcp_config:
999
+ mcp_config["mcpServers"] = {}
1000
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
1001
+ elif is_claude_code:
443
1002
  # Claude Code: Write to ~/.claude.json with project-specific path
444
1003
  if absolute_project_path:
445
1004
  # Ensure projects structure exists
@@ -493,7 +1052,9 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
493
1052
  console.print(" Server name: mcp-ticketer")
494
1053
  console.print(f" Adapter: {adapter}")
495
1054
  console.print(f" Python: {python_path}")
496
- console.print(" Command: python -m mcp_ticketer.mcp.server")
1055
+ console.print(f" Command: {server_config.get('command')}")
1056
+ console.print(f" Args: {server_config.get('args')}")
1057
+ console.print(" Protocol: Content-Length framing (FastMCP SDK)")
497
1058
  if absolute_project_path:
498
1059
  console.print(f" Project path: {absolute_project_path}")
499
1060
  if "env" in server_config:
@@ -501,6 +1062,19 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
501
1062
  f" Environment variables: {list(server_config['env'].keys())}"
502
1063
  )
503
1064
 
1065
+ # Migration success message (if legacy config was detected)
1066
+ if is_legacy:
1067
+ console.print("\n[green]✅ Migration Complete![/green]")
1068
+ console.print(
1069
+ "[green]Your configuration has been upgraded from legacy line-delimited JSON[/green]"
1070
+ )
1071
+ console.print(
1072
+ "[green]to modern Content-Length framing (FastMCP SDK).[/green]"
1073
+ )
1074
+ console.print(
1075
+ f"\n[cyan]This fixes MCP connection issues with {config_type}.[/cyan]"
1076
+ )
1077
+
504
1078
  # Next steps
505
1079
  console.print("\n[bold cyan]Next Steps:[/bold cyan]")
506
1080
  if global_config: