mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (109) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +796 -46
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -2,63 +2,43 @@
2
2
 
3
3
  import json
4
4
  import os
5
- import shutil
6
5
  import sys
7
6
  from pathlib import Path
8
- from typing import Optional
9
7
 
10
8
  from rich.console import Console
11
9
 
10
+ from .python_detection import get_mcp_ticketer_python
11
+
12
12
  console = Console()
13
13
 
14
14
 
15
- def find_mcp_ticketer_binary() -> str:
16
- """Find the mcp-ticketer binary path.
15
+ def load_env_file(env_path: Path) -> dict[str, str]:
16
+ """Load environment variables from .env file.
17
17
 
18
- Returns:
19
- Path to mcp-ticketer binary
18
+ Args:
19
+ env_path: Path to .env file
20
20
 
21
- Raises:
22
- FileNotFoundError: If binary not found
21
+ Returns:
22
+ Dict of environment variable key-value pairs
23
23
 
24
24
  """
25
- # Check if running from development environment
26
- import mcp_ticketer
27
-
28
- package_path = Path(mcp_ticketer.__file__).parent.parent.parent
29
-
30
- # Check for virtual environment bin
31
- possible_paths = [
32
- # Development paths
33
- package_path / "venv" / "bin" / "mcp-ticketer",
34
- package_path / ".venv" / "bin" / "mcp-ticketer",
35
- package_path / "test_venv" / "bin" / "mcp-ticketer",
36
- # System installation
37
- Path.home() / ".local" / "bin" / "mcp-ticketer",
38
- # pipx installation
39
- Path.home()
40
- / ".local"
41
- / "pipx"
42
- / "venvs"
43
- / "mcp-ticketer"
44
- / "bin"
45
- / "mcp-ticketer",
46
- ]
47
-
48
- # Check PATH
49
- which_result = shutil.which("mcp-ticketer")
50
- if which_result:
51
- return which_result
52
-
53
- # Check possible paths
54
- for path in possible_paths:
55
- if path.exists():
56
- return str(path.resolve())
57
-
58
- raise FileNotFoundError(
59
- "Could not find mcp-ticketer binary. Please ensure mcp-ticketer is installed.\n"
60
- "Install with: pip install mcp-ticketer"
61
- )
25
+ env_vars: dict[str, str] = {}
26
+ if not env_path.exists():
27
+ return env_vars
28
+
29
+ with open(env_path) as f:
30
+ for line in f:
31
+ line = line.strip()
32
+ # Skip comments and empty lines
33
+ if not line or line.startswith("#"):
34
+ continue
35
+
36
+ # Parse KEY=VALUE format
37
+ if "=" in line:
38
+ key, value = line.split("=", 1)
39
+ env_vars[key.strip()] = value.strip()
40
+
41
+ return env_vars
62
42
 
63
43
 
64
44
  def load_project_config() -> dict:
@@ -127,28 +107,74 @@ def find_claude_mcp_config(global_config: bool = False) -> Path:
127
107
  Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
128
108
  )
129
109
  else:
130
- # Project-level configuration
131
- config_path = Path.cwd() / ".mcp" / "config.json"
110
+ # Claude Code configuration - check both locations
111
+ # Priority 1: New global location ~/.config/claude/mcp.json
112
+ new_config_path = Path.home() / ".config" / "claude" / "mcp.json"
113
+ if new_config_path.exists():
114
+ return new_config_path
115
+
116
+ # Priority 2: Legacy project-specific location ~/.claude.json
117
+ config_path = Path.home() / ".claude.json"
132
118
 
133
119
  return config_path
134
120
 
135
121
 
136
- def load_claude_mcp_config(config_path: Path) -> dict:
122
+ def load_claude_mcp_config(config_path: Path, is_claude_code: bool = False) -> dict:
137
123
  """Load existing Claude MCP configuration or return empty structure.
138
124
 
139
125
  Args:
140
126
  config_path: Path to MCP config file
127
+ is_claude_code: If True, return Claude Code structure with projects
141
128
 
142
129
  Returns:
143
130
  MCP configuration dict
144
131
 
145
132
  """
146
- if config_path.exists():
147
- with open(config_path) as f:
148
- return json.load(f)
133
+ # Detect if this is the new global config location
134
+ is_global_mcp_config = str(config_path).endswith(".config/claude/mcp.json")
149
135
 
150
- # Return empty structure
151
- return {"mcpServers": {}}
136
+ if config_path.exists():
137
+ try:
138
+ with open(config_path) as f:
139
+ content = f.read().strip()
140
+ if not content:
141
+ # Empty file, return default structure based on location
142
+ if is_global_mcp_config:
143
+ return {"mcpServers": {}} # Flat structure
144
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
145
+
146
+ config = json.loads(content)
147
+
148
+ # Auto-detect structure format based on content
149
+ if "projects" in config:
150
+ # This is the old nested project structure
151
+ return config
152
+ elif "mcpServers" in config:
153
+ # This is flat mcpServers structure
154
+ return config
155
+ else:
156
+ # Empty or unknown structure, return default
157
+ if is_global_mcp_config:
158
+ return {"mcpServers": {}}
159
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
160
+
161
+ except json.JSONDecodeError as e:
162
+ console.print(
163
+ f"[yellow]⚠ Warning: Invalid JSON in {config_path}, creating new config[/yellow]"
164
+ )
165
+ console.print(f"[dim]Error: {e}[/dim]")
166
+ # Return default structure on parse error
167
+ if is_global_mcp_config:
168
+ return {"mcpServers": {}}
169
+ return {"projects": {}} if is_claude_code else {"mcpServers": {}}
170
+
171
+ # Return empty structure based on config type and location
172
+ if is_global_mcp_config:
173
+ return {"mcpServers": {}} # New location always uses flat structure
174
+ if is_claude_code:
175
+ return {"projects": {}}
176
+ else:
177
+ return {"mcpServers": {}}
152
178
 
153
179
 
154
180
  def save_claude_mcp_config(config_path: Path, config: dict) -> None:
@@ -168,44 +194,102 @@ def save_claude_mcp_config(config_path: Path, config: dict) -> None:
168
194
 
169
195
 
170
196
  def create_mcp_server_config(
171
- binary_path: str, project_config: dict, cwd: Optional[str] = None
197
+ python_path: str,
198
+ project_config: dict,
199
+ project_path: str | None = None,
200
+ is_global_config: bool = False,
172
201
  ) -> dict:
173
202
  """Create MCP server configuration for mcp-ticketer.
174
203
 
204
+ Uses the CLI command (mcp-ticketer mcp) which implements proper
205
+ Content-Length framing via FastMCP SDK, required for modern MCP clients.
206
+
175
207
  Args:
176
- binary_path: Path to mcp-ticketer binary
208
+ python_path: Path to Python executable in mcp-ticketer venv
177
209
  project_config: Project configuration from .mcp-ticketer/config.json
178
- cwd: Working directory for server (optional)
210
+ project_path: Project directory path (optional)
211
+ is_global_config: If True, create config for global location (no project path in args)
179
212
 
180
213
  Returns:
181
- MCP server configuration dict
214
+ MCP server configuration dict matching Claude Code stdio pattern
182
215
 
183
216
  """
217
+ # IMPORTANT: Use CLI command, NOT Python module invocation
218
+ # The CLI uses FastMCP SDK which implements proper Content-Length framing
219
+ # Legacy python -m mcp_ticketer.mcp.server uses line-delimited JSON (incompatible)
220
+
221
+ # Get mcp-ticketer CLI path from Python path
222
+ # If python_path is /path/to/venv/bin/python, CLI is /path/to/venv/bin/mcp-ticketer
223
+ python_dir = Path(python_path).parent
224
+ cli_path = str(python_dir / "mcp-ticketer")
225
+
226
+ # Build CLI arguments
227
+ args = ["mcp"]
228
+
229
+ # Add project path if provided and not global config
230
+ if project_path and not is_global_config:
231
+ args.extend(["--path", project_path])
232
+
233
+ # REQUIRED: Add "type": "stdio" for Claude Code compatibility
184
234
  config = {
185
- "command": binary_path,
186
- "args": ["serve"], # Use 'serve' command to start MCP server
235
+ "type": "stdio",
236
+ "command": cli_path,
237
+ "args": args,
187
238
  }
188
239
 
189
- # Add working directory if provided
190
- if cwd:
191
- config["cwd"] = cwd
240
+ # NOTE: The CLI command loads configuration from .mcp-ticketer/config.json
241
+ # Environment variables below are optional fallbacks for backward compatibility
242
+ # The FastMCP SDK server will automatically load config from the project directory
192
243
 
193
- # Add environment variables based on adapter
194
244
  adapter = project_config.get("default_adapter", "aitrackdown")
195
245
  adapters_config = project_config.get("adapters", {})
196
246
  adapter_config = adapters_config.get(adapter, {})
197
247
 
198
248
  env_vars = {}
199
249
 
200
- # Add adapter-specific environment variables
250
+ # Add PYTHONPATH for project context (only for project-specific configs)
251
+ if project_path and not is_global_config:
252
+ env_vars["PYTHONPATH"] = project_path
253
+
254
+ # Add MCP_TICKETER_ADAPTER to identify which adapter to use (optional fallback)
255
+ env_vars["MCP_TICKETER_ADAPTER"] = adapter
256
+
257
+ # Load environment variables from .env.local if it exists
258
+ if project_path:
259
+ env_file_path = Path(project_path) / ".env.local"
260
+ env_file_vars = load_env_file(env_file_path)
261
+
262
+ # Add relevant adapter-specific vars from .env.local
263
+ adapter_env_keys = {
264
+ "linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
265
+ "github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
266
+ "jira": [
267
+ "JIRA_ACCESS_USER",
268
+ "JIRA_ACCESS_TOKEN",
269
+ "JIRA_ORGANIZATION_ID",
270
+ "JIRA_URL",
271
+ "JIRA_EMAIL",
272
+ "JIRA_API_TOKEN",
273
+ ],
274
+ "aitrackdown": [], # No specific env vars needed
275
+ }
276
+
277
+ # Include adapter-specific env vars from .env.local
278
+ for key in adapter_env_keys.get(adapter, []):
279
+ if key in env_file_vars:
280
+ env_vars[key] = env_file_vars[key]
281
+
282
+ # Fallback: Add adapter-specific environment variables from project config
201
283
  if adapter == "linear" and "api_key" in adapter_config:
202
- env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
284
+ if "LINEAR_API_KEY" not in env_vars:
285
+ env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
203
286
  elif adapter == "github" and "token" in adapter_config:
204
- env_vars["GITHUB_TOKEN"] = adapter_config["token"]
287
+ if "GITHUB_TOKEN" not in env_vars:
288
+ env_vars["GITHUB_TOKEN"] = adapter_config["token"]
205
289
  elif adapter == "jira":
206
- if "api_token" in adapter_config:
290
+ if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
207
291
  env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
208
- if "email" in adapter_config:
292
+ if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
209
293
  env_vars["JIRA_EMAIL"] = adapter_config["email"]
210
294
 
211
295
  if env_vars:
@@ -214,6 +298,182 @@ def create_mcp_server_config(
214
298
  return config
215
299
 
216
300
 
301
+ def detect_legacy_claude_config(
302
+ config_path: Path, is_claude_code: bool = True, project_path: str | None = None
303
+ ) -> tuple[bool, dict | None]:
304
+ """Detect if existing Claude config uses legacy Python module invocation.
305
+
306
+ Args:
307
+ ----
308
+ config_path: Path to Claude configuration file
309
+ is_claude_code: Whether this is Claude Code (project-level) or Claude Desktop (global)
310
+ project_path: Project path for Claude Code configs
311
+
312
+ Returns:
313
+ -------
314
+ Tuple of (is_legacy, server_config):
315
+ - is_legacy: True if config uses 'python -m mcp_ticketer.mcp.server'
316
+ - server_config: The legacy server config dict, or None if not legacy
317
+
318
+ """
319
+ if not config_path.exists():
320
+ return False, None
321
+
322
+ try:
323
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
324
+ except Exception:
325
+ return False, None
326
+
327
+ # For Claude Code, check project-specific config
328
+ if is_claude_code and project_path:
329
+ projects = mcp_config.get("projects", {})
330
+ project_config = projects.get(project_path, {})
331
+ mcp_servers = project_config.get("mcpServers", {})
332
+ else:
333
+ # For Claude Desktop, check global config
334
+ mcp_servers = mcp_config.get("mcpServers", {})
335
+
336
+ if "mcp-ticketer" in mcp_servers:
337
+ server_config = mcp_servers["mcp-ticketer"]
338
+ args = server_config.get("args", [])
339
+
340
+ # Check for legacy pattern: ["-m", "mcp_ticketer.mcp.server", ...]
341
+ if len(args) >= 2 and args[0] == "-m" and "mcp_ticketer.mcp.server" in args[1]:
342
+ return True, server_config
343
+
344
+ return False, None
345
+
346
+
347
+ def remove_claude_mcp(global_config: bool = False, dry_run: bool = False) -> None:
348
+ """Remove mcp-ticketer from Claude Code/Desktop configuration.
349
+
350
+ Args:
351
+ global_config: Remove from Claude Desktop instead of project-level
352
+ dry_run: Show what would be removed without making changes
353
+
354
+ """
355
+ # Step 1: Find Claude MCP config location
356
+ config_type = "Claude Desktop" if global_config else "Claude Code"
357
+ console.print(f"[cyan]🔍 Removing {config_type} MCP configuration...[/cyan]")
358
+
359
+ # Get absolute project path for Claude Code
360
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
361
+
362
+ # Check both locations for Claude Code
363
+ config_paths_to_check = []
364
+ if not global_config:
365
+ # Check both new and old locations
366
+ new_config = Path.home() / ".config" / "claude" / "mcp.json"
367
+ old_config = Path.home() / ".claude.json"
368
+ legacy_config = Path.cwd() / ".claude" / "mcp.local.json"
369
+
370
+ if new_config.exists():
371
+ config_paths_to_check.append(
372
+ (new_config, True)
373
+ ) # True = is_global_mcp_config
374
+ if old_config.exists():
375
+ config_paths_to_check.append((old_config, False))
376
+ if legacy_config.exists():
377
+ config_paths_to_check.append((legacy_config, False))
378
+ else:
379
+ mcp_config_path = find_claude_mcp_config(global_config)
380
+ if mcp_config_path.exists():
381
+ config_paths_to_check.append((mcp_config_path, False))
382
+
383
+ if not config_paths_to_check:
384
+ console.print("[yellow]⚠ No configuration files found[/yellow]")
385
+ console.print("[dim]mcp-ticketer is not configured for this platform[/dim]")
386
+ return
387
+
388
+ # Step 2-7: Process each config file
389
+ removed_count = 0
390
+ for config_path, is_global_mcp_config in config_paths_to_check:
391
+ console.print(f"[dim]Checking: {config_path}[/dim]")
392
+
393
+ # Load existing MCP configuration
394
+ is_claude_code = not global_config
395
+ mcp_config = load_claude_mcp_config(config_path, is_claude_code=is_claude_code)
396
+
397
+ # Check if mcp-ticketer is configured
398
+ is_configured = False
399
+ if is_global_mcp_config:
400
+ # Global mcp.json uses flat structure
401
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
402
+ elif is_claude_code:
403
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
404
+ if absolute_project_path:
405
+ projects = mcp_config.get("projects", {})
406
+ project_config_entry = projects.get(absolute_project_path, {})
407
+ is_configured = "mcp-ticketer" in project_config_entry.get(
408
+ "mcpServers", {}
409
+ )
410
+ else:
411
+ # Check flat structure for backward compatibility
412
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
413
+ else:
414
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
415
+ is_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
416
+
417
+ if not is_configured:
418
+ continue
419
+
420
+ # Show what would be removed (dry run)
421
+ if dry_run:
422
+ console.print(f"\n[cyan]DRY RUN - Would remove from: {config_path}[/cyan]")
423
+ console.print(" Server name: mcp-ticketer")
424
+ if absolute_project_path and not is_global_mcp_config:
425
+ console.print(f" Project: {absolute_project_path}")
426
+ continue
427
+
428
+ # Remove mcp-ticketer from configuration
429
+ if is_global_mcp_config:
430
+ # Global mcp.json uses flat structure
431
+ del mcp_config["mcpServers"]["mcp-ticketer"]
432
+ elif is_claude_code and absolute_project_path and "projects" in mcp_config:
433
+ # Remove from Claude Code nested structure
434
+ del mcp_config["projects"][absolute_project_path]["mcpServers"][
435
+ "mcp-ticketer"
436
+ ]
437
+
438
+ # Clean up empty structures
439
+ if not mcp_config["projects"][absolute_project_path]["mcpServers"]:
440
+ del mcp_config["projects"][absolute_project_path]["mcpServers"]
441
+ if not mcp_config["projects"][absolute_project_path]:
442
+ del mcp_config["projects"][absolute_project_path]
443
+ else:
444
+ # Remove from flat structure (legacy or Claude Desktop)
445
+ if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
446
+ del mcp_config["mcpServers"]["mcp-ticketer"]
447
+
448
+ # Save updated configuration
449
+ try:
450
+ save_claude_mcp_config(config_path, mcp_config)
451
+ console.print(f"[green]✓ Removed from: {config_path}[/green]")
452
+ removed_count += 1
453
+ except Exception as e:
454
+ console.print(f"[red]✗ Failed to update {config_path}:[/red] {e}")
455
+
456
+ if dry_run:
457
+ return
458
+
459
+ if removed_count > 0:
460
+ console.print("\n[green]✓ Successfully removed mcp-ticketer[/green]")
461
+ console.print(f"[dim]Updated {removed_count} configuration file(s)[/dim]")
462
+
463
+ # Next steps
464
+ console.print("\n[bold cyan]Next Steps:[/bold cyan]")
465
+ if global_config:
466
+ console.print("1. Restart Claude Desktop")
467
+ console.print("2. mcp-ticketer will no longer be available in MCP menu")
468
+ else:
469
+ console.print("1. Restart Claude Code")
470
+ console.print("2. mcp-ticketer will no longer be available in this project")
471
+ else:
472
+ console.print(
473
+ "\n[yellow]⚠ mcp-ticketer was not found in any configuration[/yellow]"
474
+ )
475
+
476
+
217
477
  def configure_claude_mcp(global_config: bool = False, force: bool = False) -> None:
218
478
  """Configure Claude Code to use mcp-ticketer.
219
479
 
@@ -222,18 +482,31 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
222
482
  force: Overwrite existing configuration
223
483
 
224
484
  Raises:
225
- FileNotFoundError: If binary or project config not found
485
+ FileNotFoundError: If Python executable or project config not found
226
486
  ValueError: If configuration is invalid
227
487
 
228
488
  """
229
- # Step 1: Find mcp-ticketer binary
230
- console.print("[cyan]🔍 Finding mcp-ticketer binary...[/cyan]")
489
+ # Determine project path for venv detection
490
+ project_path = Path.cwd() if not global_config else None
491
+
492
+ # Step 1: Find Python executable (project-specific if available)
493
+ console.print("[cyan]🔍 Finding mcp-ticketer Python executable...[/cyan]")
231
494
  try:
232
- binary_path = find_mcp_ticketer_binary()
233
- console.print(f"[green]✓[/green] Found: {binary_path}")
234
- except FileNotFoundError as e:
235
- console.print(f"[red]✗[/red] {e}")
236
- raise
495
+ python_path = get_mcp_ticketer_python(project_path=project_path)
496
+ console.print(f"[green]✓[/green] Found: {python_path}")
497
+
498
+ # Show if using project venv or fallback
499
+ if project_path and str(project_path / ".venv") in python_path:
500
+ console.print("[dim]Using project-specific venv[/dim]")
501
+ else:
502
+ console.print("[dim]Using pipx/system Python[/dim]")
503
+ except Exception as e:
504
+ console.print(f"[red]✗[/red] Could not find Python executable: {e}")
505
+ raise FileNotFoundError(
506
+ "Could not find mcp-ticketer Python executable. "
507
+ "Please ensure mcp-ticketer is installed.\n"
508
+ "Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
509
+ ) from e
237
510
 
238
511
  # Step 2: Load project configuration
239
512
  console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
@@ -246,17 +519,67 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
246
519
  raise
247
520
 
248
521
  # Step 3: Find Claude MCP config location
249
- config_type = "Claude Desktop" if global_config else "project-level"
522
+ config_type = "Claude Desktop" if global_config else "Claude Code"
250
523
  console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
251
524
 
252
525
  mcp_config_path = find_claude_mcp_config(global_config)
253
- console.print(f"[dim]Config location: {mcp_config_path}[/dim]")
526
+ console.print(f"[dim]Primary config: {mcp_config_path}[/dim]")
527
+
528
+ # Get absolute project path for Claude Code
529
+ absolute_project_path = str(Path.cwd().resolve()) if not global_config else None
254
530
 
255
531
  # Step 4: Load existing MCP configuration
256
- mcp_config = load_claude_mcp_config(mcp_config_path)
532
+ is_claude_code = not global_config
533
+ mcp_config = load_claude_mcp_config(mcp_config_path, is_claude_code=is_claude_code)
534
+
535
+ # Detect if using new global config location
536
+ is_global_mcp_config = str(mcp_config_path).endswith(".config/claude/mcp.json")
537
+
538
+ # Step 4.5: Check for legacy configuration (DETECTION & MIGRATION)
539
+ is_legacy, legacy_config = detect_legacy_claude_config(
540
+ mcp_config_path,
541
+ is_claude_code=is_claude_code,
542
+ project_path=absolute_project_path,
543
+ )
544
+ if is_legacy:
545
+ console.print("\n[yellow]⚠ LEGACY CONFIGURATION DETECTED[/yellow]")
546
+ console.print(
547
+ "[yellow]Your current configuration uses the legacy line-delimited JSON server:[/yellow]"
548
+ )
549
+ console.print(f"[dim] Command: {legacy_config.get('command')}[/dim]")
550
+ console.print(f"[dim] Args: {legacy_config.get('args')}[/dim]")
551
+ console.print(
552
+ f"\n[red]This legacy server is incompatible with modern MCP clients ({config_type}).[/red]"
553
+ )
554
+ console.print(
555
+ "[red]The legacy server uses line-delimited JSON instead of Content-Length framing.[/red]"
556
+ )
557
+ console.print(
558
+ "\n[cyan]✨ Automatically migrating to modern FastMCP-based server...[/cyan]"
559
+ )
560
+ force = True # Auto-enable force mode for migration
257
561
 
258
562
  # Step 5: Check if mcp-ticketer already configured
259
- if "mcp-ticketer" in mcp_config.get("mcpServers", {}):
563
+ already_configured = False
564
+ if is_global_mcp_config:
565
+ # New global config uses flat structure
566
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
567
+ elif is_claude_code:
568
+ # Check Claude Code structure: .projects[path].mcpServers["mcp-ticketer"]
569
+ if absolute_project_path and "projects" in mcp_config:
570
+ projects = mcp_config.get("projects", {})
571
+ project_config_entry = projects.get(absolute_project_path, {})
572
+ already_configured = "mcp-ticketer" in project_config_entry.get(
573
+ "mcpServers", {}
574
+ )
575
+ elif "mcpServers" in mcp_config:
576
+ # Check flat structure for backward compatibility
577
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
578
+ else:
579
+ # Check Claude Desktop structure: .mcpServers["mcp-ticketer"]
580
+ already_configured = "mcp-ticketer" in mcp_config.get("mcpServers", {})
581
+
582
+ if already_configured:
260
583
  if not force:
261
584
  console.print("[yellow]⚠ mcp-ticketer is already configured[/yellow]")
262
585
  console.print("[dim]Use --force to overwrite existing configuration[/dim]")
@@ -265,16 +588,61 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
265
588
  console.print("[yellow]⚠ Overwriting existing configuration[/yellow]")
266
589
 
267
590
  # Step 6: Create mcp-ticketer server config
268
- cwd = str(Path.cwd()) if not global_config else None
269
591
  server_config = create_mcp_server_config(
270
- binary_path=binary_path, project_config=project_config, cwd=cwd
592
+ python_path=python_path,
593
+ project_config=project_config,
594
+ project_path=absolute_project_path,
595
+ is_global_config=is_global_mcp_config,
271
596
  )
272
597
 
273
- # Step 7: Update MCP configuration
274
- if "mcpServers" not in mcp_config:
275
- mcp_config["mcpServers"] = {}
276
-
277
- mcp_config["mcpServers"]["mcp-ticketer"] = server_config
598
+ # Step 7: Update MCP configuration based on platform
599
+ if is_global_mcp_config:
600
+ # New global location: ~/.config/claude/mcp.json uses flat structure
601
+ if "mcpServers" not in mcp_config:
602
+ mcp_config["mcpServers"] = {}
603
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
604
+ elif is_claude_code:
605
+ # Claude Code: Write to ~/.claude.json with project-specific path
606
+ if absolute_project_path:
607
+ # Ensure projects structure exists
608
+ if "projects" not in mcp_config:
609
+ mcp_config["projects"] = {}
610
+
611
+ # Ensure project entry exists
612
+ if absolute_project_path not in mcp_config["projects"]:
613
+ mcp_config["projects"][absolute_project_path] = {}
614
+
615
+ # Ensure mcpServers for this project exists
616
+ if "mcpServers" not in mcp_config["projects"][absolute_project_path]:
617
+ mcp_config["projects"][absolute_project_path]["mcpServers"] = {}
618
+
619
+ # Add mcp-ticketer configuration
620
+ mcp_config["projects"][absolute_project_path]["mcpServers"][
621
+ "mcp-ticketer"
622
+ ] = server_config
623
+
624
+ # Also write to backward-compatible location for older Claude Code versions
625
+ legacy_config_path = Path.cwd() / ".claude" / "mcp.local.json"
626
+ console.print(f"[dim]Legacy config: {legacy_config_path}[/dim]")
627
+
628
+ try:
629
+ legacy_config = load_claude_mcp_config(
630
+ legacy_config_path, is_claude_code=False
631
+ )
632
+ if "mcpServers" not in legacy_config:
633
+ legacy_config["mcpServers"] = {}
634
+ legacy_config["mcpServers"]["mcp-ticketer"] = server_config
635
+ save_claude_mcp_config(legacy_config_path, legacy_config)
636
+ console.print("[dim]✓ Backward-compatible config also written[/dim]")
637
+ except Exception as e:
638
+ console.print(
639
+ f"[dim]⚠ Could not write legacy config (non-fatal): {e}[/dim]"
640
+ )
641
+ else:
642
+ # Claude Desktop: Write to platform-specific config
643
+ if "mcpServers" not in mcp_config:
644
+ mcp_config["mcpServers"] = {}
645
+ mcp_config["mcpServers"]["mcp-ticketer"] = server_config
278
646
 
279
647
  # Step 8: Save configuration
280
648
  try:
@@ -286,14 +654,30 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
286
654
  console.print("\n[bold]Configuration Details:[/bold]")
287
655
  console.print(" Server name: mcp-ticketer")
288
656
  console.print(f" Adapter: {adapter}")
289
- console.print(f" Binary: {binary_path}")
290
- if cwd:
291
- console.print(f" Working directory: {cwd}")
657
+ console.print(f" Python: {python_path}")
658
+ console.print(f" Command: {server_config.get('command')}")
659
+ console.print(f" Args: {server_config.get('args')}")
660
+ console.print(" Protocol: Content-Length framing (FastMCP SDK)")
661
+ if absolute_project_path:
662
+ console.print(f" Project path: {absolute_project_path}")
292
663
  if "env" in server_config:
293
664
  console.print(
294
665
  f" Environment variables: {list(server_config['env'].keys())}"
295
666
  )
296
667
 
668
+ # Migration success message (if legacy config was detected)
669
+ if is_legacy:
670
+ console.print("\n[green]✅ Migration Complete![/green]")
671
+ console.print(
672
+ "[green]Your configuration has been upgraded from legacy line-delimited JSON[/green]"
673
+ )
674
+ console.print(
675
+ "[green]to modern Content-Length framing (FastMCP SDK).[/green]"
676
+ )
677
+ console.print(
678
+ f"\n[cyan]This fixes MCP connection issues with {config_type}.[/cyan]"
679
+ )
680
+
297
681
  # Next steps
298
682
  console.print("\n[bold cyan]Next Steps:[/bold cyan]")
299
683
  if global_config: