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