mcp-ticketer 0.12.0__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.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
|
|
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
|
|
350
|
+
return {"projects": {}} if is_claude_code else {"mcpServers": {}}
|
|
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": {}}
|
|
133
365
|
return {"projects": {}} if is_claude_code else {"mcpServers": {}}
|
|
134
|
-
|
|
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,
|
|
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
|
|
181
|
-
|
|
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
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
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":
|
|
442
|
+
"command": cli_path,
|
|
191
443
|
"args": args,
|
|
192
444
|
}
|
|
193
445
|
|
|
194
|
-
#
|
|
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
|
|
253
|
-
|
|
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
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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}")
|
|
@@ -385,16 +915,6 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
385
915
|
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
386
916
|
) from e
|
|
387
917
|
|
|
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
918
|
# Step 3: Find Claude MCP config location
|
|
399
919
|
config_type = "Claude Desktop" if global_config else "Claude Code"
|
|
400
920
|
console.print(f"\n[cyan]🔧 Configuring {config_type} MCP...[/cyan]")
|
|
@@ -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
|
|
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
|
|
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:
|
|
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:
|