mcp-ticketer 2.0.1__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/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +122 -0
- mcp_ticketer/adapters/asana/adapter.py +121 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
- 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/jira/__init__.py +35 -0
- mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
- 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 +1000 -92
- mcp_ticketer/adapters/linear/client.py +91 -1
- mcp_ticketer/adapters/linear/mappers.py +107 -0
- mcp_ticketer/adapters/linear/queries.py +112 -2
- mcp_ticketer/adapters/linear/types.py +50 -10
- mcp_ticketer/cli/configure.py +524 -89
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/main.py +10 -0
- mcp_ticketer/cli/mcp_configure.py +177 -49
- mcp_ticketer/cli/platform_installer.py +9 -0
- mcp_ticketer/cli/setup_command.py +157 -1
- mcp_ticketer/cli/ticket_commands.py +443 -81
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +28 -0
- mcp_ticketer/core/adapter.py +367 -1
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +345 -0
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +6 -1
- mcp_ticketer/core/state_matcher.py +36 -3
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/routing.py +68 -0
- mcp_ticketer/mcp/server/tools/__init__.py +7 -4
- mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
- mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
- mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
- mcp_ticketer/queue/queue.py +68 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
- 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-2.0.1.dist-info/top_level.txt +0 -1
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -13,6 +14,36 @@ from .python_detection import get_mcp_ticketer_python
|
|
|
13
14
|
console = Console()
|
|
14
15
|
|
|
15
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
|
+
|
|
16
47
|
def is_claude_cli_available() -> bool:
|
|
17
48
|
"""Check if Claude CLI is available in PATH.
|
|
18
49
|
|
|
@@ -57,45 +88,46 @@ def build_claude_mcp_command(
|
|
|
57
88
|
# Transport: always stdio
|
|
58
89
|
cmd.extend(["--transport", "stdio"])
|
|
59
90
|
|
|
60
|
-
#
|
|
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
|
|
61
96
|
adapters = project_config.get("adapters", {})
|
|
62
97
|
|
|
63
98
|
# Linear adapter
|
|
64
99
|
if "linear" in adapters:
|
|
65
100
|
linear_config = adapters["linear"]
|
|
66
101
|
if "api_key" in linear_config:
|
|
67
|
-
cmd.extend(["
|
|
102
|
+
cmd.extend(["-e", f"LINEAR_API_KEY={linear_config['api_key']}"])
|
|
68
103
|
if "team_id" in linear_config:
|
|
69
|
-
cmd.extend(["
|
|
104
|
+
cmd.extend(["-e", f"LINEAR_TEAM_ID={linear_config['team_id']}"])
|
|
70
105
|
if "team_key" in linear_config:
|
|
71
|
-
cmd.extend(["
|
|
106
|
+
cmd.extend(["-e", f"LINEAR_TEAM_KEY={linear_config['team_key']}"])
|
|
72
107
|
|
|
73
108
|
# GitHub adapter
|
|
74
109
|
if "github" in adapters:
|
|
75
110
|
github_config = adapters["github"]
|
|
76
111
|
if "token" in github_config:
|
|
77
|
-
cmd.extend(["
|
|
112
|
+
cmd.extend(["-e", f"GITHUB_TOKEN={github_config['token']}"])
|
|
78
113
|
if "owner" in github_config:
|
|
79
|
-
cmd.extend(["
|
|
114
|
+
cmd.extend(["-e", f"GITHUB_OWNER={github_config['owner']}"])
|
|
80
115
|
if "repo" in github_config:
|
|
81
|
-
cmd.extend(["
|
|
116
|
+
cmd.extend(["-e", f"GITHUB_REPO={github_config['repo']}"])
|
|
82
117
|
|
|
83
118
|
# JIRA adapter
|
|
84
119
|
if "jira" in adapters:
|
|
85
120
|
jira_config = adapters["jira"]
|
|
86
121
|
if "api_token" in jira_config:
|
|
87
|
-
cmd.extend(["
|
|
122
|
+
cmd.extend(["-e", f"JIRA_API_TOKEN={jira_config['api_token']}"])
|
|
88
123
|
if "email" in jira_config:
|
|
89
|
-
cmd.extend(["
|
|
124
|
+
cmd.extend(["-e", f"JIRA_EMAIL={jira_config['email']}"])
|
|
90
125
|
if "url" in jira_config:
|
|
91
|
-
cmd.extend(["
|
|
126
|
+
cmd.extend(["-e", f"JIRA_URL={jira_config['url']}"])
|
|
92
127
|
|
|
93
128
|
# Add default adapter
|
|
94
129
|
default_adapter = project_config.get("default_adapter", "aitrackdown")
|
|
95
|
-
cmd.extend(["
|
|
96
|
-
|
|
97
|
-
# Server label
|
|
98
|
-
cmd.append("mcp-ticketer")
|
|
130
|
+
cmd.extend(["-e", f"MCP_TICKETER_ADAPTER={default_adapter}"])
|
|
99
131
|
|
|
100
132
|
# Command separator
|
|
101
133
|
cmd.append("--")
|
|
@@ -118,6 +150,10 @@ def configure_claude_mcp_native(
|
|
|
118
150
|
) -> None:
|
|
119
151
|
"""Configure Claude Code using native 'claude mcp add' command.
|
|
120
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
|
+
|
|
121
157
|
Args:
|
|
122
158
|
project_config: Project configuration dict
|
|
123
159
|
project_path: Path to project directory
|
|
@@ -129,6 +165,9 @@ def configure_claude_mcp_native(
|
|
|
129
165
|
subprocess.TimeoutExpired: If command times out
|
|
130
166
|
|
|
131
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
|
+
|
|
132
171
|
# Auto-remove before re-adding when force=True
|
|
133
172
|
if force:
|
|
134
173
|
console.print("[cyan]🗑️ Force mode: Removing existing configuration...[/cyan]")
|
|
@@ -159,7 +198,7 @@ def configure_claude_mcp_native(
|
|
|
159
198
|
# Show command to user (mask sensitive values)
|
|
160
199
|
masked_cmd = []
|
|
161
200
|
for i, arg in enumerate(cmd):
|
|
162
|
-
if arg.startswith("
|
|
201
|
+
if arg.startswith("-e=") or (i > 0 and cmd[i - 1] == "-e"):
|
|
163
202
|
# Mask environment variable values
|
|
164
203
|
if "=" in arg:
|
|
165
204
|
key, _ = arg.split("=", 1)
|
|
@@ -218,6 +257,69 @@ def configure_claude_mcp_native(
|
|
|
218
257
|
raise
|
|
219
258
|
|
|
220
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
|
+
|
|
221
323
|
def load_env_file(env_path: Path) -> dict[str, str]:
|
|
222
324
|
"""Load environment variables from .env file.
|
|
223
325
|
|
|
@@ -447,25 +549,23 @@ def create_mcp_server_config(
|
|
|
447
549
|
# Environment variables below are optional fallbacks for backward compatibility
|
|
448
550
|
# The FastMCP SDK server will automatically load config from the project directory
|
|
449
551
|
|
|
450
|
-
adapter = project_config.get("default_adapter", "aitrackdown")
|
|
451
|
-
adapters_config = project_config.get("adapters", {})
|
|
452
|
-
adapter_config = adapters_config.get(adapter, {})
|
|
453
|
-
|
|
454
552
|
env_vars = {}
|
|
455
553
|
|
|
456
554
|
# Add PYTHONPATH for project context (only for project-specific configs)
|
|
457
555
|
if project_path and not is_global_config:
|
|
458
556
|
env_vars["PYTHONPATH"] = project_path
|
|
459
557
|
|
|
460
|
-
#
|
|
461
|
-
|
|
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)
|
|
462
562
|
|
|
463
|
-
# Load environment variables from .env.local if it exists
|
|
563
|
+
# Load environment variables from .env.local if it exists (as override)
|
|
464
564
|
if project_path:
|
|
465
565
|
env_file_path = Path(project_path) / ".env.local"
|
|
466
566
|
env_file_vars = load_env_file(env_file_path)
|
|
467
567
|
|
|
468
|
-
# Add relevant adapter-specific vars from .env.local
|
|
568
|
+
# Add relevant adapter-specific vars from .env.local (overrides config.json)
|
|
469
569
|
adapter_env_keys = {
|
|
470
570
|
"linear": ["LINEAR_API_KEY", "LINEAR_TEAM_ID", "LINEAR_TEAM_KEY"],
|
|
471
571
|
"github": ["GITHUB_TOKEN", "GITHUB_OWNER", "GITHUB_REPO"],
|
|
@@ -480,24 +580,12 @@ def create_mcp_server_config(
|
|
|
480
580
|
"aitrackdown": [], # No specific env vars needed
|
|
481
581
|
}
|
|
482
582
|
|
|
483
|
-
|
|
583
|
+
adapter = env_vars.get("MCP_TICKETER_ADAPTER", "aitrackdown")
|
|
584
|
+
# Include adapter-specific env vars from .env.local (overrides config.json)
|
|
484
585
|
for key in adapter_env_keys.get(adapter, []):
|
|
485
586
|
if key in env_file_vars:
|
|
486
587
|
env_vars[key] = env_file_vars[key]
|
|
487
588
|
|
|
488
|
-
# Fallback: Add adapter-specific environment variables from project config
|
|
489
|
-
if adapter == "linear" and "api_key" in adapter_config:
|
|
490
|
-
if "LINEAR_API_KEY" not in env_vars:
|
|
491
|
-
env_vars["LINEAR_API_KEY"] = adapter_config["api_key"]
|
|
492
|
-
elif adapter == "github" and "token" in adapter_config:
|
|
493
|
-
if "GITHUB_TOKEN" not in env_vars:
|
|
494
|
-
env_vars["GITHUB_TOKEN"] = adapter_config["token"]
|
|
495
|
-
elif adapter == "jira":
|
|
496
|
-
if "api_token" in adapter_config and "JIRA_API_TOKEN" not in env_vars:
|
|
497
|
-
env_vars["JIRA_API_TOKEN"] = adapter_config["api_token"]
|
|
498
|
-
if "email" in adapter_config and "JIRA_EMAIL" not in env_vars:
|
|
499
|
-
env_vars["JIRA_EMAIL"] = adapter_config["email"]
|
|
500
|
-
|
|
501
589
|
if env_vars:
|
|
502
590
|
config["env"] = env_vars
|
|
503
591
|
|
|
@@ -845,9 +933,16 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
845
933
|
console.print(f"[red]✗[/red] {e}")
|
|
846
934
|
raise
|
|
847
935
|
|
|
848
|
-
# Check for native CLI availability
|
|
936
|
+
# Check for native CLI availability AND PATH configuration
|
|
849
937
|
console.print("\n[cyan]🔍 Checking for Claude CLI...[/cyan]")
|
|
850
|
-
|
|
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:
|
|
851
946
|
console.print("[green]✓[/green] Claude CLI found - using native command")
|
|
852
947
|
console.print(
|
|
853
948
|
"[dim]This provides better integration and automatic updates[/dim]"
|
|
@@ -863,13 +958,25 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
863
958
|
force=force,
|
|
864
959
|
)
|
|
865
960
|
|
|
866
|
-
# Fall back to JSON manipulation
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
+
)
|
|
873
980
|
|
|
874
981
|
# Auto-remove before re-adding when force=True
|
|
875
982
|
if force:
|
|
@@ -893,6 +1000,10 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
893
1000
|
|
|
894
1001
|
console.print() # Blank line for visual separation
|
|
895
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
|
+
|
|
896
1007
|
# Determine project path for venv detection
|
|
897
1008
|
project_path = Path.cwd() if not global_config else None
|
|
898
1009
|
|
|
@@ -907,6 +1018,11 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
907
1018
|
console.print("[dim]Using project-specific venv[/dim]")
|
|
908
1019
|
else:
|
|
909
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]")
|
|
910
1026
|
except Exception as e:
|
|
911
1027
|
console.print(f"[red]✗[/red] Could not find Python executable: {e}")
|
|
912
1028
|
raise FileNotFoundError(
|
|
@@ -1058,9 +1174,21 @@ def configure_claude_mcp(global_config: bool = False, force: bool = False) -> No
|
|
|
1058
1174
|
if absolute_project_path:
|
|
1059
1175
|
console.print(f" Project path: {absolute_project_path}")
|
|
1060
1176
|
if "env" in server_config:
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
+
)
|
|
1064
1192
|
|
|
1065
1193
|
# Migration success message (if legacy config was detected)
|
|
1066
1194
|
if is_legacy:
|
|
@@ -13,6 +13,7 @@ from rich.table import Table
|
|
|
13
13
|
# Import for typing compatibility
|
|
14
14
|
from .init_command import init
|
|
15
15
|
from .platform_detection import PlatformDetector, get_platform_by_name
|
|
16
|
+
from .setup_command import _update_mcp_json_credentials
|
|
16
17
|
|
|
17
18
|
console = Console()
|
|
18
19
|
|
|
@@ -248,6 +249,10 @@ def install(
|
|
|
248
249
|
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
249
250
|
config_func()
|
|
250
251
|
success_count += 1
|
|
252
|
+
|
|
253
|
+
# Update credentials in parent .mcp.json for Claude platforms
|
|
254
|
+
if plat.name in ("claude-code", "claude-desktop"):
|
|
255
|
+
_update_mcp_json_credentials(Path.cwd(), console)
|
|
251
256
|
except Exception as e:
|
|
252
257
|
console.print(
|
|
253
258
|
f"[red]✗[/red] Failed to install for {plat.display_name}: {e}"
|
|
@@ -388,6 +393,10 @@ def install(
|
|
|
388
393
|
|
|
389
394
|
try:
|
|
390
395
|
config["func"]()
|
|
396
|
+
|
|
397
|
+
# Update credentials in parent .mcp.json for Claude platforms
|
|
398
|
+
if platform in ("claude-code", "claude-desktop"):
|
|
399
|
+
_update_mcp_json_credentials(Path.cwd(), console)
|
|
391
400
|
except Exception as e:
|
|
392
401
|
console.print(f"[red]Installation failed: {e}[/red]")
|
|
393
402
|
raise typer.Exit(1) from e
|
|
@@ -332,6 +332,9 @@ def setup(
|
|
|
332
332
|
# Check and install adapter-specific dependencies
|
|
333
333
|
_check_and_install_adapter_dependencies(adapter_type, console)
|
|
334
334
|
|
|
335
|
+
# Update existing MCP configurations with new credentials
|
|
336
|
+
_update_mcp_json_credentials(proj_path, console)
|
|
337
|
+
|
|
335
338
|
console.print("\n[green]✓ Adapter configuration complete[/green]\n")
|
|
336
339
|
else:
|
|
337
340
|
console.print("[green]✓ Step 1/2: Adapter already configured[/green]\n")
|
|
@@ -340,6 +343,9 @@ def setup(
|
|
|
340
343
|
# This handles the case where credentials exist but defaults were never set
|
|
341
344
|
_prompt_and_update_default_values(config_path, current_adapter, console)
|
|
342
345
|
|
|
346
|
+
# Update existing MCP configurations with credentials
|
|
347
|
+
_update_mcp_json_credentials(proj_path, console)
|
|
348
|
+
|
|
343
349
|
# Step 3: Platform installation
|
|
344
350
|
if skip_platforms:
|
|
345
351
|
console.print(
|
|
@@ -352,7 +358,7 @@ def setup(
|
|
|
352
358
|
|
|
353
359
|
# Detect available platforms
|
|
354
360
|
detector = PlatformDetector()
|
|
355
|
-
detected = detector.detect_all(project_path=proj_path)
|
|
361
|
+
detected = detector.detect_all(project_path=proj_path, exclude_desktop=True)
|
|
356
362
|
|
|
357
363
|
if not detected:
|
|
358
364
|
console.print("[yellow]No AI platforms detected on this system.[/yellow]")
|
|
@@ -612,6 +618,156 @@ def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[s
|
|
|
612
618
|
return configured
|
|
613
619
|
|
|
614
620
|
|
|
621
|
+
def _update_mcp_json_credentials(proj_path: Path, console: Console) -> None:
|
|
622
|
+
"""Update .mcp.json with adapter credentials if mcp-ticketer is already configured.
|
|
623
|
+
|
|
624
|
+
This function updates the existing MCP configuration with the latest credentials
|
|
625
|
+
from the project's .mcp-ticketer/config.json file. It also ensures .mcp.json is
|
|
626
|
+
added to .gitignore to prevent credential leaks.
|
|
627
|
+
|
|
628
|
+
Additionally, it updates the official @modelcontextprotocol/server-github MCP server
|
|
629
|
+
if found, since it also requires GITHUB_PERSONAL_ACCESS_TOKEN.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
proj_path: Project path
|
|
633
|
+
console: Rich console for output
|
|
634
|
+
|
|
635
|
+
"""
|
|
636
|
+
# Check multiple .mcp.json locations
|
|
637
|
+
new_mcp_json_path = Path.home() / ".config" / "claude" / "mcp.json"
|
|
638
|
+
old_mcp_json_path = Path.home() / ".claude.json"
|
|
639
|
+
legacy_mcp_json_path = proj_path / ".claude" / "mcp.local.json"
|
|
640
|
+
project_mcp_json_path = proj_path / ".mcp.json"
|
|
641
|
+
|
|
642
|
+
mcp_json_paths = [
|
|
643
|
+
(new_mcp_json_path, True), # (path, is_global_mcp_config)
|
|
644
|
+
(old_mcp_json_path, False),
|
|
645
|
+
(legacy_mcp_json_path, False),
|
|
646
|
+
(project_mcp_json_path, False),
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
# Also check parent directories for .mcp.json (Claude Code inheritance)
|
|
650
|
+
# This handles cases like /Users/masa/Projects/.mcp.json
|
|
651
|
+
current = proj_path.parent
|
|
652
|
+
home = Path.home()
|
|
653
|
+
checked_parents: set[Path] = set()
|
|
654
|
+
while current != home and current != current.parent:
|
|
655
|
+
parent_mcp = current / ".mcp.json"
|
|
656
|
+
if parent_mcp not in checked_parents and parent_mcp.exists():
|
|
657
|
+
mcp_json_paths.append((parent_mcp, False))
|
|
658
|
+
checked_parents.add(parent_mcp)
|
|
659
|
+
current = current.parent
|
|
660
|
+
|
|
661
|
+
# Import the helper function to get adapter credentials
|
|
662
|
+
from .mcp_configure import _get_adapter_env_vars
|
|
663
|
+
|
|
664
|
+
env_vars = _get_adapter_env_vars()
|
|
665
|
+
|
|
666
|
+
if not env_vars:
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
updated_count = 0
|
|
670
|
+
|
|
671
|
+
for mcp_json_path, is_global_mcp_config in mcp_json_paths:
|
|
672
|
+
if not mcp_json_path.exists():
|
|
673
|
+
continue
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
with open(mcp_json_path) as f:
|
|
677
|
+
mcp_config = json.load(f)
|
|
678
|
+
|
|
679
|
+
# Check if mcp-ticketer is configured
|
|
680
|
+
mcp_servers = None
|
|
681
|
+
if is_global_mcp_config:
|
|
682
|
+
# Global mcp.json uses flat structure
|
|
683
|
+
mcp_servers = mcp_config.get("mcpServers", {})
|
|
684
|
+
else:
|
|
685
|
+
# Old structure uses projects
|
|
686
|
+
projects = mcp_config.get("projects", {})
|
|
687
|
+
project_key = str(proj_path.resolve())
|
|
688
|
+
if project_key in projects:
|
|
689
|
+
mcp_servers = projects[project_key].get("mcpServers", {})
|
|
690
|
+
else:
|
|
691
|
+
# Try flat structure for backward compatibility
|
|
692
|
+
mcp_servers = mcp_config.get("mcpServers", {})
|
|
693
|
+
|
|
694
|
+
if mcp_servers is None:
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
config_updated = False
|
|
698
|
+
|
|
699
|
+
# Update the mcp-ticketer server env vars if configured
|
|
700
|
+
if "mcp-ticketer" in mcp_servers:
|
|
701
|
+
current_env = mcp_servers["mcp-ticketer"].get("env", {})
|
|
702
|
+
current_env.update(env_vars)
|
|
703
|
+
mcp_servers["mcp-ticketer"]["env"] = current_env
|
|
704
|
+
config_updated = True
|
|
705
|
+
|
|
706
|
+
# Also update official @modelcontextprotocol/server-github if present
|
|
707
|
+
# This server uses GITHUB_PERSONAL_ACCESS_TOKEN instead of GITHUB_TOKEN
|
|
708
|
+
if "github" in mcp_servers and "GITHUB_TOKEN" in env_vars:
|
|
709
|
+
github_server = mcp_servers["github"]
|
|
710
|
+
# Check if it's the official GitHub MCP server (uses npx or server-github)
|
|
711
|
+
cmd = github_server.get("command", "")
|
|
712
|
+
args = github_server.get("args", [])
|
|
713
|
+
is_official_github = (
|
|
714
|
+
cmd == "npx" and any("server-github" in str(arg) for arg in args)
|
|
715
|
+
) or "server-github" in cmd
|
|
716
|
+
|
|
717
|
+
if is_official_github:
|
|
718
|
+
current_env = github_server.get("env", {})
|
|
719
|
+
# The official server uses GITHUB_PERSONAL_ACCESS_TOKEN
|
|
720
|
+
current_env["GITHUB_PERSONAL_ACCESS_TOKEN"] = env_vars[
|
|
721
|
+
"GITHUB_TOKEN"
|
|
722
|
+
]
|
|
723
|
+
github_server["env"] = current_env
|
|
724
|
+
config_updated = True
|
|
725
|
+
console.print(
|
|
726
|
+
f"[dim] Also updated official GitHub MCP server in {mcp_json_path}[/dim]"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
if not config_updated:
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
# Save updated config
|
|
733
|
+
with open(mcp_json_path, "w") as f:
|
|
734
|
+
json.dump(mcp_config, f, indent=2)
|
|
735
|
+
|
|
736
|
+
updated_count += 1
|
|
737
|
+
|
|
738
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
739
|
+
console.print(
|
|
740
|
+
f"[yellow]Warning: Could not update {mcp_json_path.name}: {e}[/yellow]"
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if updated_count > 0:
|
|
744
|
+
console.print(
|
|
745
|
+
f"[green]✓[/green] Updated MCP configuration with adapter credentials ({updated_count} file(s))"
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Ensure .mcp.json files are in .gitignore (only for project-local files)
|
|
749
|
+
gitignore_path = proj_path / ".gitignore"
|
|
750
|
+
patterns_to_add = [".claude/", ".mcp.json"]
|
|
751
|
+
|
|
752
|
+
if gitignore_path.exists():
|
|
753
|
+
content = gitignore_path.read_text()
|
|
754
|
+
patterns_added = []
|
|
755
|
+
|
|
756
|
+
for pattern in patterns_to_add:
|
|
757
|
+
if pattern not in content:
|
|
758
|
+
patterns_added.append(pattern)
|
|
759
|
+
|
|
760
|
+
if patterns_added:
|
|
761
|
+
with open(gitignore_path, "a") as f:
|
|
762
|
+
f.write("\n# MCP configuration (contains tokens)\n")
|
|
763
|
+
for pattern in patterns_added:
|
|
764
|
+
f.write(f"{pattern}\n")
|
|
765
|
+
|
|
766
|
+
console.print(
|
|
767
|
+
f"[dim]✓ Added {', '.join(patterns_added)} to .gitignore[/dim]"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
|
|
615
771
|
def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
|
|
616
772
|
"""Show setup complete message with next steps.
|
|
617
773
|
|