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