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