mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1284
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
- mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""Installation strategies for different MCP platforms.
|
|
2
|
+
|
|
3
|
+
This module provides Strategy pattern implementations for installing MCP servers
|
|
4
|
+
across different platforms using native CLIs, JSON manipulation, or TOML manipulation.
|
|
5
|
+
|
|
6
|
+
Design Philosophy:
|
|
7
|
+
- Strategy pattern for platform-specific installation
|
|
8
|
+
- Fallback mechanisms (CLI → JSON for Claude)
|
|
9
|
+
- Dry-run support for testing
|
|
10
|
+
- Atomic operations with backup/restore
|
|
11
|
+
|
|
12
|
+
Strategies:
|
|
13
|
+
- NativeCLIStrategy: Use platform CLI (claude mcp add)
|
|
14
|
+
- JSONManipulationStrategy: Direct JSON config modification
|
|
15
|
+
- TOMLManipulationStrategy: Direct TOML config modification
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import subprocess
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from .config_manager import ConfigManager
|
|
23
|
+
from .exceptions import InstallationError, ValidationError
|
|
24
|
+
from .types import (
|
|
25
|
+
ConfigFormat,
|
|
26
|
+
InstallationResult,
|
|
27
|
+
InstallMethod,
|
|
28
|
+
MCPServerConfig,
|
|
29
|
+
Platform,
|
|
30
|
+
Scope,
|
|
31
|
+
)
|
|
32
|
+
from .utils import mask_credentials, resolve_command_path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InstallationStrategy(ABC):
|
|
36
|
+
"""Abstract base class for installation strategies.
|
|
37
|
+
|
|
38
|
+
Each platform may support multiple installation strategies with
|
|
39
|
+
different priorities (e.g., CLI first, JSON fallback).
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> strategy = NativeCLIStrategy(Platform.CLAUDE_CODE, "claude")
|
|
43
|
+
>>> result = strategy.install(server, Scope.PROJECT)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def install(self, server: MCPServerConfig, scope: Scope) -> InstallationResult:
|
|
48
|
+
"""Install MCP server with this strategy.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
server: Server configuration to install
|
|
52
|
+
scope: Installation scope (project or global)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
InstallationResult with status and details
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
InstallationError: If installation fails
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def uninstall(self, name: str, scope: Scope) -> InstallationResult:
|
|
64
|
+
"""Uninstall MCP server.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: Server name to uninstall
|
|
68
|
+
scope: Installation scope
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
InstallationResult with status
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
InstallationError: If uninstall fails
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def list_servers(self, scope: Scope) -> list[MCPServerConfig]:
|
|
80
|
+
"""List installed servers.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
scope: Installation scope
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of installed server configurations
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def validate(self) -> bool:
|
|
92
|
+
"""Validate this strategy can be used.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if strategy is available, False otherwise
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> strategy = NativeCLIStrategy(Platform.CLAUDE_CODE, "claude")
|
|
99
|
+
>>> if strategy.validate():
|
|
100
|
+
... result = strategy.install(server, Scope.PROJECT)
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class NativeCLIStrategy(InstallationStrategy):
|
|
106
|
+
"""Installation via platform's native CLI.
|
|
107
|
+
|
|
108
|
+
Uses commands like:
|
|
109
|
+
- `claude mcp add {name} --command "{command}" --scope {scope}`
|
|
110
|
+
- `auggie mcp install {name}`
|
|
111
|
+
|
|
112
|
+
Falls back to JSON strategy if CLI command fails.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> strategy = NativeCLIStrategy(Platform.CLAUDE_CODE, "claude")
|
|
116
|
+
>>> if strategy.validate():
|
|
117
|
+
... result = strategy.install(server, Scope.PROJECT)
|
|
118
|
+
... else:
|
|
119
|
+
... # Fallback to JSON strategy
|
|
120
|
+
... fallback = JSONManipulationStrategy(Platform.CLAUDE_CODE, config_path)
|
|
121
|
+
... result = fallback.install(server, Scope.PROJECT)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, platform: Platform, cli_command: str) -> None:
|
|
125
|
+
"""Initialize with platform and CLI command.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
platform: Target platform
|
|
129
|
+
cli_command: CLI command name (e.g., "claude", "auggie")
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> strategy = NativeCLIStrategy(Platform.CLAUDE_CODE, "claude")
|
|
133
|
+
"""
|
|
134
|
+
self.platform = platform
|
|
135
|
+
self.cli_command = cli_command
|
|
136
|
+
|
|
137
|
+
def install(self, server: MCPServerConfig, scope: Scope) -> InstallationResult:
|
|
138
|
+
"""Install server using native CLI.
|
|
139
|
+
|
|
140
|
+
Executes CLI command to add server. Falls back to JSON strategy
|
|
141
|
+
if CLI fails.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
server: Server configuration
|
|
145
|
+
scope: Installation scope
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
InstallationResult with installation status
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> server = MCPServerConfig(
|
|
152
|
+
... name="mcp-ticketer",
|
|
153
|
+
... command="uv",
|
|
154
|
+
... args=["run", "mcp-ticketer", "mcp"]
|
|
155
|
+
... )
|
|
156
|
+
>>> result = strategy.install(server, Scope.PROJECT)
|
|
157
|
+
"""
|
|
158
|
+
# Build CLI command
|
|
159
|
+
cmd = self._build_cli_command(server, scope)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Execute CLI command
|
|
163
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
164
|
+
|
|
165
|
+
if result.returncode == 0:
|
|
166
|
+
return InstallationResult(
|
|
167
|
+
success=True,
|
|
168
|
+
platform=self.platform,
|
|
169
|
+
server_name=server.name,
|
|
170
|
+
method=InstallMethod.DIRECT,
|
|
171
|
+
message=f"Successfully installed '{server.name}' via CLI",
|
|
172
|
+
config_path=None, # CLI doesn't expose config path
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
raise InstallationError(
|
|
176
|
+
f"CLI command failed: {result.stderr}",
|
|
177
|
+
recovery_suggestion="Check CLI installation and permissions",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, InstallationError) as e:
|
|
181
|
+
# CLI failed, raise error for caller to try fallback
|
|
182
|
+
raise InstallationError(
|
|
183
|
+
f"Native CLI installation failed: {e}",
|
|
184
|
+
recovery_suggestion="Try JSON manipulation strategy as fallback",
|
|
185
|
+
) from e
|
|
186
|
+
|
|
187
|
+
def uninstall(self, name: str, scope: Scope) -> InstallationResult:
|
|
188
|
+
"""Uninstall server using native CLI.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: Server name
|
|
192
|
+
scope: Installation scope
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
InstallationResult with uninstall status
|
|
196
|
+
"""
|
|
197
|
+
cmd = self._build_cli_remove_command(name, scope)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
201
|
+
|
|
202
|
+
if result.returncode == 0:
|
|
203
|
+
return InstallationResult(
|
|
204
|
+
success=True,
|
|
205
|
+
platform=self.platform,
|
|
206
|
+
server_name=name,
|
|
207
|
+
method=InstallMethod.DIRECT,
|
|
208
|
+
message=f"Successfully uninstalled '{name}' via CLI",
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
raise InstallationError(
|
|
212
|
+
f"CLI remove failed: {result.stderr}",
|
|
213
|
+
recovery_suggestion="Check if server exists",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
217
|
+
raise InstallationError(
|
|
218
|
+
f"CLI uninstall failed: {e}",
|
|
219
|
+
recovery_suggestion="Try JSON manipulation strategy",
|
|
220
|
+
) from e
|
|
221
|
+
|
|
222
|
+
def list_servers(self, scope: Scope) -> list[MCPServerConfig]:
|
|
223
|
+
"""List servers using native CLI.
|
|
224
|
+
|
|
225
|
+
Note: Most CLIs don't provide list functionality,
|
|
226
|
+
so this falls back to JSON reading.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
scope: Installation scope
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of server configurations
|
|
233
|
+
"""
|
|
234
|
+
# Most CLIs don't support listing, would need config path
|
|
235
|
+
raise NotImplementedError("Native CLI list not supported, use JSON strategy")
|
|
236
|
+
|
|
237
|
+
def validate(self) -> bool:
|
|
238
|
+
"""Check if CLI command is available.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if CLI is in PATH, False otherwise
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> strategy = NativeCLIStrategy(Platform.CLAUDE_CODE, "claude")
|
|
245
|
+
>>> if strategy.validate():
|
|
246
|
+
... print("Claude CLI available")
|
|
247
|
+
"""
|
|
248
|
+
return resolve_command_path(self.cli_command) is not None
|
|
249
|
+
|
|
250
|
+
def _build_cli_command(self, server: MCPServerConfig, scope: Scope) -> list[str]:
|
|
251
|
+
"""Build CLI command for installation.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
server: Server configuration
|
|
255
|
+
scope: Installation scope
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Command as list of strings
|
|
259
|
+
"""
|
|
260
|
+
# Platform-specific command building
|
|
261
|
+
if self.platform in (Platform.CLAUDE_CODE, Platform.CLAUDE_DESKTOP):
|
|
262
|
+
# Claude CLI: claude mcp add [options] <name> -e KEY=val -- <cmd>
|
|
263
|
+
scope_str = "project" if scope == Scope.PROJECT else "user"
|
|
264
|
+
cmd = [
|
|
265
|
+
self.cli_command,
|
|
266
|
+
"mcp",
|
|
267
|
+
"add",
|
|
268
|
+
"--scope",
|
|
269
|
+
scope_str,
|
|
270
|
+
"--transport",
|
|
271
|
+
"stdio",
|
|
272
|
+
server.name, # Name MUST come before -e flags
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# Add env vars (MUST come after server name)
|
|
276
|
+
if server.env:
|
|
277
|
+
for key, value in server.env.items():
|
|
278
|
+
cmd.extend(["-e", f"{key}={value}"])
|
|
279
|
+
|
|
280
|
+
# Command separator and server command
|
|
281
|
+
cmd.append("--")
|
|
282
|
+
cmd.append(server.command)
|
|
283
|
+
|
|
284
|
+
# Add args after the command
|
|
285
|
+
if server.args:
|
|
286
|
+
cmd.extend(server.args)
|
|
287
|
+
|
|
288
|
+
return cmd
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
raise NotImplementedError(
|
|
292
|
+
f"CLI command building not implemented for {self.platform}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _build_cli_remove_command(self, name: str, scope: Scope) -> list[str]:
|
|
296
|
+
"""Build CLI command for removal.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
name: Server name
|
|
300
|
+
scope: Installation scope
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Command as list of strings
|
|
304
|
+
"""
|
|
305
|
+
if self.platform in (Platform.CLAUDE_CODE, Platform.CLAUDE_DESKTOP):
|
|
306
|
+
scope_str = "project" if scope == Scope.PROJECT else "user"
|
|
307
|
+
return [
|
|
308
|
+
self.cli_command,
|
|
309
|
+
"mcp",
|
|
310
|
+
"remove",
|
|
311
|
+
name,
|
|
312
|
+
"--scope",
|
|
313
|
+
scope_str,
|
|
314
|
+
]
|
|
315
|
+
else:
|
|
316
|
+
raise NotImplementedError(f"CLI remove not implemented for {self.platform}")
|
|
317
|
+
|
|
318
|
+
def _mask_command(self, cmd: list[str]) -> list[str]:
|
|
319
|
+
"""Mask sensitive values in command for logging.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
cmd: Command list
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Command with masked credentials
|
|
326
|
+
"""
|
|
327
|
+
masked = []
|
|
328
|
+
mask_next = False
|
|
329
|
+
|
|
330
|
+
for part in cmd:
|
|
331
|
+
if mask_next:
|
|
332
|
+
# Mask environment variable value
|
|
333
|
+
if "=" in part:
|
|
334
|
+
key, _ = part.split("=", 1)
|
|
335
|
+
masked_dict = mask_credentials({key: "value"})
|
|
336
|
+
masked.append(f"{key}={list(masked_dict.values())[0]}")
|
|
337
|
+
else:
|
|
338
|
+
masked.append("***")
|
|
339
|
+
mask_next = False
|
|
340
|
+
elif part == "-e":
|
|
341
|
+
masked.append(part)
|
|
342
|
+
mask_next = True
|
|
343
|
+
else:
|
|
344
|
+
masked.append(part)
|
|
345
|
+
|
|
346
|
+
return masked
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class JSONManipulationStrategy(InstallationStrategy):
|
|
350
|
+
"""Installation via direct JSON config file manipulation.
|
|
351
|
+
|
|
352
|
+
Safely modifies JSON configuration files using ConfigManager.
|
|
353
|
+
Creates backups before modifications.
|
|
354
|
+
|
|
355
|
+
Supported platforms:
|
|
356
|
+
- Claude Code (fallback from CLI)
|
|
357
|
+
- Claude Desktop (fallback from CLI)
|
|
358
|
+
- Cursor
|
|
359
|
+
- Auggie
|
|
360
|
+
- Windsurf
|
|
361
|
+
- Gemini CLI
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> strategy = JSONManipulationStrategy(
|
|
365
|
+
... Platform.CURSOR,
|
|
366
|
+
... Path.home() / ".cursor/mcp.json"
|
|
367
|
+
... )
|
|
368
|
+
>>> result = strategy.install(server, Scope.GLOBAL)
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def __init__(self, platform: Platform, config_path: Path) -> None:
|
|
372
|
+
"""Initialize with platform and config path.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
platform: Target platform
|
|
376
|
+
config_path: Path to JSON config file
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> strategy = JSONManipulationStrategy(
|
|
380
|
+
... Platform.CURSOR,
|
|
381
|
+
... Path.home() / ".cursor/mcp.json"
|
|
382
|
+
... )
|
|
383
|
+
"""
|
|
384
|
+
self.platform = platform
|
|
385
|
+
self.config_path = config_path
|
|
386
|
+
self.config_manager = ConfigManager(config_path, ConfigFormat.JSON)
|
|
387
|
+
|
|
388
|
+
def install(self, server: MCPServerConfig, scope: Scope) -> InstallationResult:
|
|
389
|
+
"""Install server by modifying JSON config.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
server: Server configuration
|
|
393
|
+
scope: Installation scope (unused for JSON, config_path determines scope)
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
InstallationResult with installation status
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
InstallationError: If installation fails
|
|
400
|
+
"""
|
|
401
|
+
try:
|
|
402
|
+
# Add server using config manager
|
|
403
|
+
self.config_manager.add_server(server)
|
|
404
|
+
|
|
405
|
+
return InstallationResult(
|
|
406
|
+
success=True,
|
|
407
|
+
platform=self.platform,
|
|
408
|
+
server_name=server.name,
|
|
409
|
+
method=InstallMethod.DIRECT,
|
|
410
|
+
message=f"Successfully installed '{server.name}' to {self.config_path}",
|
|
411
|
+
config_path=self.config_path,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
except ValidationError as e:
|
|
415
|
+
# Server already exists
|
|
416
|
+
raise InstallationError(
|
|
417
|
+
f"Server '{server.name}' already exists",
|
|
418
|
+
recovery_suggestion="Use update operation or remove existing server first",
|
|
419
|
+
) from e
|
|
420
|
+
except Exception as e:
|
|
421
|
+
raise InstallationError(
|
|
422
|
+
f"Failed to install server: {e}",
|
|
423
|
+
recovery_suggestion="Check config file permissions and syntax",
|
|
424
|
+
) from e
|
|
425
|
+
|
|
426
|
+
def uninstall(self, name: str, scope: Scope) -> InstallationResult:
|
|
427
|
+
"""Uninstall server by removing from JSON config.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
name: Server name
|
|
431
|
+
scope: Installation scope (unused)
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
InstallationResult with uninstall status
|
|
435
|
+
"""
|
|
436
|
+
try:
|
|
437
|
+
self.config_manager.remove_server(name)
|
|
438
|
+
|
|
439
|
+
return InstallationResult(
|
|
440
|
+
success=True,
|
|
441
|
+
platform=self.platform,
|
|
442
|
+
server_name=name,
|
|
443
|
+
method=InstallMethod.DIRECT,
|
|
444
|
+
message=f"Successfully uninstalled '{name}' from {self.config_path}",
|
|
445
|
+
config_path=self.config_path,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
except ValidationError as e:
|
|
449
|
+
raise InstallationError(
|
|
450
|
+
f"Server '{name}' not found",
|
|
451
|
+
recovery_suggestion="Check server name with list_servers()",
|
|
452
|
+
) from e
|
|
453
|
+
except Exception as e:
|
|
454
|
+
raise InstallationError(
|
|
455
|
+
f"Failed to uninstall server: {e}",
|
|
456
|
+
recovery_suggestion="Check config file permissions",
|
|
457
|
+
) from e
|
|
458
|
+
|
|
459
|
+
def list_servers(self, scope: Scope) -> list[MCPServerConfig]:
|
|
460
|
+
"""List servers from JSON config.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
scope: Installation scope (unused)
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
List of server configurations
|
|
467
|
+
"""
|
|
468
|
+
try:
|
|
469
|
+
return self.config_manager.list_servers()
|
|
470
|
+
except Exception as e:
|
|
471
|
+
raise InstallationError(
|
|
472
|
+
f"Failed to list servers: {e}",
|
|
473
|
+
recovery_suggestion="Check config file exists and is readable",
|
|
474
|
+
) from e
|
|
475
|
+
|
|
476
|
+
def validate(self) -> bool:
|
|
477
|
+
"""Check if JSON config exists and is valid.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if config is accessible, False otherwise
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
# Try to read config
|
|
484
|
+
self.config_manager.read()
|
|
485
|
+
return True
|
|
486
|
+
except Exception:
|
|
487
|
+
# Config doesn't exist or is invalid
|
|
488
|
+
# This is OK - we can create it
|
|
489
|
+
return True
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class TOMLManipulationStrategy(InstallationStrategy):
|
|
493
|
+
"""Installation via direct TOML config file manipulation.
|
|
494
|
+
|
|
495
|
+
Used by Codex platform which uses TOML instead of JSON.
|
|
496
|
+
|
|
497
|
+
Example:
|
|
498
|
+
>>> strategy = TOMLManipulationStrategy(
|
|
499
|
+
... Platform.CODEX,
|
|
500
|
+
... Path.home() / ".codex/config.toml"
|
|
501
|
+
... )
|
|
502
|
+
>>> result = strategy.install(server, Scope.GLOBAL)
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
def __init__(self, platform: Platform, config_path: Path) -> None:
|
|
506
|
+
"""Initialize with platform and config path.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
platform: Target platform
|
|
510
|
+
config_path: Path to TOML config file
|
|
511
|
+
|
|
512
|
+
Example:
|
|
513
|
+
>>> strategy = TOMLManipulationStrategy(
|
|
514
|
+
... Platform.CODEX,
|
|
515
|
+
... Path.home() / ".codex/config.toml"
|
|
516
|
+
... )
|
|
517
|
+
"""
|
|
518
|
+
self.platform = platform
|
|
519
|
+
self.config_path = config_path
|
|
520
|
+
self.config_manager = ConfigManager(config_path, ConfigFormat.TOML)
|
|
521
|
+
|
|
522
|
+
def install(self, server: MCPServerConfig, scope: Scope) -> InstallationResult:
|
|
523
|
+
"""Install server by modifying TOML config.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
server: Server configuration
|
|
527
|
+
scope: Installation scope (unused)
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
InstallationResult with installation status
|
|
531
|
+
"""
|
|
532
|
+
try:
|
|
533
|
+
self.config_manager.add_server(server)
|
|
534
|
+
|
|
535
|
+
return InstallationResult(
|
|
536
|
+
success=True,
|
|
537
|
+
platform=self.platform,
|
|
538
|
+
server_name=server.name,
|
|
539
|
+
method=InstallMethod.DIRECT,
|
|
540
|
+
message=f"Successfully installed '{server.name}' to {self.config_path}",
|
|
541
|
+
config_path=self.config_path,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
except ValidationError as e:
|
|
545
|
+
raise InstallationError(
|
|
546
|
+
f"Server '{server.name}' already exists",
|
|
547
|
+
recovery_suggestion="Use update operation or remove existing server first",
|
|
548
|
+
) from e
|
|
549
|
+
except Exception as e:
|
|
550
|
+
raise InstallationError(
|
|
551
|
+
f"Failed to install server: {e}",
|
|
552
|
+
recovery_suggestion="Check TOML file permissions and syntax",
|
|
553
|
+
) from e
|
|
554
|
+
|
|
555
|
+
def uninstall(self, name: str, scope: Scope) -> InstallationResult:
|
|
556
|
+
"""Uninstall server by removing from TOML config.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
name: Server name
|
|
560
|
+
scope: Installation scope (unused)
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
InstallationResult with uninstall status
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
self.config_manager.remove_server(name)
|
|
567
|
+
|
|
568
|
+
return InstallationResult(
|
|
569
|
+
success=True,
|
|
570
|
+
platform=self.platform,
|
|
571
|
+
server_name=name,
|
|
572
|
+
method=InstallMethod.DIRECT,
|
|
573
|
+
message=f"Successfully uninstalled '{name}' from {self.config_path}",
|
|
574
|
+
config_path=self.config_path,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
except ValidationError as e:
|
|
578
|
+
raise InstallationError(
|
|
579
|
+
f"Server '{name}' not found",
|
|
580
|
+
recovery_suggestion="Check server name with list_servers()",
|
|
581
|
+
) from e
|
|
582
|
+
except Exception as e:
|
|
583
|
+
raise InstallationError(
|
|
584
|
+
f"Failed to uninstall server: {e}",
|
|
585
|
+
recovery_suggestion="Check TOML file permissions",
|
|
586
|
+
) from e
|
|
587
|
+
|
|
588
|
+
def list_servers(self, scope: Scope) -> list[MCPServerConfig]:
|
|
589
|
+
"""List servers from TOML config.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
scope: Installation scope (unused)
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
List of server configurations
|
|
596
|
+
"""
|
|
597
|
+
try:
|
|
598
|
+
return self.config_manager.list_servers()
|
|
599
|
+
except Exception as e:
|
|
600
|
+
raise InstallationError(
|
|
601
|
+
f"Failed to list servers: {e}",
|
|
602
|
+
recovery_suggestion="Check TOML file exists and is readable",
|
|
603
|
+
) from e
|
|
604
|
+
|
|
605
|
+
def validate(self) -> bool:
|
|
606
|
+
"""Check if TOML config exists and is valid.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
True if config is accessible, False otherwise
|
|
610
|
+
"""
|
|
611
|
+
try:
|
|
612
|
+
self.config_manager.read()
|
|
613
|
+
return True
|
|
614
|
+
except Exception:
|
|
615
|
+
# Config doesn't exist or is invalid
|
|
616
|
+
# This is OK - we can create it
|
|
617
|
+
return True
|