mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +507 -6
- mcp_ticketer/adapters/asana/adapter.py +229 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +2730 -139
- mcp_ticketer/adapters/linear/client.py +175 -3
- mcp_ticketer/adapters/linear/mappers.py +203 -8
- mcp_ticketer/adapters/linear/queries.py +280 -3
- mcp_ticketer/adapters/linear/types.py +120 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +1288 -105
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +267 -3175
- mcp_ticketer/cli/mcp_configure.py +821 -119
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +795 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +705 -103
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +56 -6
- mcp_ticketer/core/adapter.py +533 -2
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +480 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +625 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +33 -11
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1391 -145
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +209 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/queue.py +68 -0
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.13.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1574
- mcp_ticketer/adapters/jira.py +0 -1258
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- mcp_ticketer-0.12.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""Main installer orchestrator for MCP servers.
|
|
2
|
+
|
|
3
|
+
This module provides the primary API facade for installing, managing, and
|
|
4
|
+
inspecting MCP server configurations across all supported platforms.
|
|
5
|
+
|
|
6
|
+
Design Philosophy:
|
|
7
|
+
- Simple API with smart defaults (auto-detection)
|
|
8
|
+
- Atomic operations with backup/restore
|
|
9
|
+
- Comprehensive validation and inspection
|
|
10
|
+
- Dry-run support for safe testing
|
|
11
|
+
- Platform-agnostic interface
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from py_mcp_installer import MCPInstaller
|
|
15
|
+
>>> installer = MCPInstaller.auto_detect()
|
|
16
|
+
>>> result = installer.install_server(
|
|
17
|
+
... name="mcp-ticketer",
|
|
18
|
+
... command="uv",
|
|
19
|
+
... args=["run", "mcp-ticketer", "mcp"],
|
|
20
|
+
... description="Ticket management MCP server"
|
|
21
|
+
... )
|
|
22
|
+
>>> print(result.message)
|
|
23
|
+
Successfully installed mcp-ticketer
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from .command_builder import CommandBuilder
|
|
31
|
+
from .exceptions import (
|
|
32
|
+
ConfigurationError,
|
|
33
|
+
InstallationError,
|
|
34
|
+
PlatformDetectionError,
|
|
35
|
+
PlatformNotSupportedError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
)
|
|
38
|
+
from .installation_strategy import (
|
|
39
|
+
InstallationStrategy as BaseStrategy,
|
|
40
|
+
)
|
|
41
|
+
from .installation_strategy import (
|
|
42
|
+
JSONManipulationStrategy,
|
|
43
|
+
)
|
|
44
|
+
from .mcp_inspector import InspectionReport, MCPInspector, ValidationIssue
|
|
45
|
+
from .platform_detector import PlatformDetector
|
|
46
|
+
from .platforms import ClaudeCodeStrategy, CodexStrategy, CursorStrategy
|
|
47
|
+
from .types import (
|
|
48
|
+
InstallationResult,
|
|
49
|
+
InstallMethod,
|
|
50
|
+
MCPServerConfig,
|
|
51
|
+
Platform,
|
|
52
|
+
PlatformInfo,
|
|
53
|
+
Scope,
|
|
54
|
+
)
|
|
55
|
+
from .utils import detect_install_method, resolve_command_path
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# Main Installer API
|
|
62
|
+
# ============================================================================
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MCPInstaller:
|
|
66
|
+
"""Main API facade for MCP server installation.
|
|
67
|
+
|
|
68
|
+
Provides a unified interface for installing, managing, and inspecting
|
|
69
|
+
MCP servers across all supported platforms. Automatically detects platform
|
|
70
|
+
and selects best installation method.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
platform_info: Detected platform information
|
|
74
|
+
config_path: Path to configuration file
|
|
75
|
+
dry_run: If True, preview changes without applying
|
|
76
|
+
verbose: If True, enable verbose logging
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> # Auto-detect platform and install
|
|
80
|
+
>>> installer = MCPInstaller.auto_detect()
|
|
81
|
+
>>> result = installer.install_server(
|
|
82
|
+
... name="mcp-ticketer",
|
|
83
|
+
... command="uv",
|
|
84
|
+
... args=["run", "mcp-ticketer", "mcp"]
|
|
85
|
+
... )
|
|
86
|
+
>>> if result.success:
|
|
87
|
+
... print(f"Installed to {result.config_path}")
|
|
88
|
+
|
|
89
|
+
>>> # Inspect existing installation
|
|
90
|
+
>>> report = installer.inspect_installation()
|
|
91
|
+
>>> print(report.summary())
|
|
92
|
+
|
|
93
|
+
>>> # List all servers
|
|
94
|
+
>>> servers = installer.list_servers()
|
|
95
|
+
>>> for server in servers:
|
|
96
|
+
... print(f"- {server.name}: {server.command}")
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
platform: Platform | None = None,
|
|
102
|
+
dry_run: bool = False,
|
|
103
|
+
verbose: bool = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Initialize installer.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
platform: Force specific platform (None = auto-detect)
|
|
109
|
+
dry_run: Preview changes without applying
|
|
110
|
+
verbose: Enable verbose logging
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
PlatformDetectionError: If platform cannot be detected
|
|
114
|
+
PlatformNotSupportedError: If platform is not supported
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> # Auto-detect platform
|
|
118
|
+
>>> installer = MCPInstaller()
|
|
119
|
+
|
|
120
|
+
>>> # Force specific platform
|
|
121
|
+
>>> installer = MCPInstaller(platform=Platform.CLAUDE_CODE)
|
|
122
|
+
|
|
123
|
+
>>> # Dry-run mode (safe testing)
|
|
124
|
+
>>> installer = MCPInstaller(dry_run=True, verbose=True)
|
|
125
|
+
"""
|
|
126
|
+
self.dry_run = dry_run
|
|
127
|
+
self.verbose = verbose
|
|
128
|
+
|
|
129
|
+
# Configure logging
|
|
130
|
+
if verbose:
|
|
131
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
132
|
+
logger.setLevel(logging.DEBUG)
|
|
133
|
+
else:
|
|
134
|
+
logger.setLevel(logging.INFO)
|
|
135
|
+
|
|
136
|
+
# Detect or use provided platform
|
|
137
|
+
if platform:
|
|
138
|
+
# For forced platform, we still need to detect but validate it matches
|
|
139
|
+
detector = PlatformDetector()
|
|
140
|
+
detected_info = detector.detect()
|
|
141
|
+
if detected_info.platform != platform:
|
|
142
|
+
raise PlatformNotSupportedError(
|
|
143
|
+
platform.value,
|
|
144
|
+
[p.value for p in Platform if p != Platform.UNKNOWN],
|
|
145
|
+
)
|
|
146
|
+
self._platform_info = detected_info
|
|
147
|
+
else:
|
|
148
|
+
self._platform_info = self._detect_platform()
|
|
149
|
+
|
|
150
|
+
logger.info(
|
|
151
|
+
f"Initialized for {self._platform_info.platform.value} "
|
|
152
|
+
f"(confidence: {self._platform_info.confidence:.2f})"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Initialize components
|
|
156
|
+
self._command_builder = CommandBuilder(self._platform_info.platform)
|
|
157
|
+
self._inspector = MCPInspector(self._platform_info)
|
|
158
|
+
|
|
159
|
+
# Select installation strategy
|
|
160
|
+
self._strategy = self._select_strategy()
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def auto_detect(cls, **kwargs: Any) -> "MCPInstaller":
|
|
164
|
+
"""Create installer with auto-detected platform.
|
|
165
|
+
|
|
166
|
+
This is the recommended way to create an installer instance.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
**kwargs: Additional arguments passed to __init__
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Configured MCPInstaller instance
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> installer = MCPInstaller.auto_detect()
|
|
176
|
+
>>> print(f"Detected: {installer.platform_info.platform.value}")
|
|
177
|
+
"""
|
|
178
|
+
return cls(platform=None, **kwargs)
|
|
179
|
+
|
|
180
|
+
def install_server(
|
|
181
|
+
self,
|
|
182
|
+
name: str,
|
|
183
|
+
command: str,
|
|
184
|
+
args: list[str] | None = None,
|
|
185
|
+
env: dict[str, str] | None = None,
|
|
186
|
+
description: str = "",
|
|
187
|
+
scope: Scope = Scope.PROJECT,
|
|
188
|
+
method: InstallMethod | None = None,
|
|
189
|
+
) -> InstallationResult:
|
|
190
|
+
"""Install MCP server.
|
|
191
|
+
|
|
192
|
+
Auto-detects best installation method if not specified. Creates backup
|
|
193
|
+
of existing config before making changes.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
name: Unique server identifier (e.g., "mcp-ticketer")
|
|
197
|
+
command: Executable command (e.g., "uv", "/usr/bin/python")
|
|
198
|
+
args: Command arguments (e.g., ["run", "mcp-ticketer", "mcp"])
|
|
199
|
+
env: Environment variables (e.g., {"API_KEY": "..."})
|
|
200
|
+
description: Human-readable description
|
|
201
|
+
scope: Installation scope (PROJECT or GLOBAL)
|
|
202
|
+
method: Installation method (auto-detect if None)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
InstallationResult with success status and details
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValidationError: If server configuration is invalid
|
|
209
|
+
InstallationError: If installation fails
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> # Simple installation with auto-detection
|
|
213
|
+
>>> result = installer.install_server(
|
|
214
|
+
... name="mcp-ticketer",
|
|
215
|
+
... command="uv",
|
|
216
|
+
... args=["run", "mcp-ticketer", "mcp"],
|
|
217
|
+
... description="Ticket management"
|
|
218
|
+
... )
|
|
219
|
+
|
|
220
|
+
>>> # Install with environment variables
|
|
221
|
+
>>> result = installer.install_server(
|
|
222
|
+
... name="github-mcp",
|
|
223
|
+
... command="npx",
|
|
224
|
+
... args=["-y", "@modelcontextprotocol/server-github"],
|
|
225
|
+
... env={"GITHUB_TOKEN": "ghp_..."}
|
|
226
|
+
... )
|
|
227
|
+
|
|
228
|
+
>>> # Force specific method
|
|
229
|
+
>>> result = installer.install_server(
|
|
230
|
+
... name="custom-server",
|
|
231
|
+
... command="python",
|
|
232
|
+
... args=["-m", "my_server"],
|
|
233
|
+
... method=InstallMethod.PYTHON_MODULE
|
|
234
|
+
... )
|
|
235
|
+
"""
|
|
236
|
+
logger.info(f"Installing server: {name}")
|
|
237
|
+
|
|
238
|
+
# Validate inputs
|
|
239
|
+
if not name:
|
|
240
|
+
raise ValidationError(
|
|
241
|
+
"Server name is required", "Provide a unique server name"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if not command and not method:
|
|
245
|
+
raise ValidationError(
|
|
246
|
+
"Either command or method must be provided",
|
|
247
|
+
"Provide command parameter or specify installation method",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Auto-detect method if not provided
|
|
251
|
+
if method is None:
|
|
252
|
+
# Default to UV_RUN as recommended method
|
|
253
|
+
if command == "uv":
|
|
254
|
+
method = InstallMethod.UV_RUN
|
|
255
|
+
elif resolve_command_path(name):
|
|
256
|
+
# Check if installed method
|
|
257
|
+
install_check = detect_install_method(name)
|
|
258
|
+
if install_check == "pipx":
|
|
259
|
+
method = InstallMethod.PIPX
|
|
260
|
+
else:
|
|
261
|
+
method = InstallMethod.DIRECT
|
|
262
|
+
else:
|
|
263
|
+
method = InstallMethod.PYTHON_MODULE
|
|
264
|
+
logger.debug(f"Auto-detected method: {method.value}")
|
|
265
|
+
|
|
266
|
+
# Build complete command if needed
|
|
267
|
+
if not command:
|
|
268
|
+
command = self._command_builder.build_command(
|
|
269
|
+
MCPServerConfig(name=name, command="", args=args or []),
|
|
270
|
+
method,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Create server config
|
|
274
|
+
server = MCPServerConfig(
|
|
275
|
+
name=name,
|
|
276
|
+
command=command,
|
|
277
|
+
args=args or [],
|
|
278
|
+
env=env or {},
|
|
279
|
+
description=description,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Validate server config
|
|
283
|
+
issues = self._inspector.validate_server(server)
|
|
284
|
+
errors = [i for i in issues if i.severity == "error"]
|
|
285
|
+
if errors:
|
|
286
|
+
error_msg = "\n".join(f"- {e.message}" for e in errors)
|
|
287
|
+
raise ValidationError(
|
|
288
|
+
f"Server configuration invalid:\n{error_msg}",
|
|
289
|
+
"Fix validation errors before installing",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Log warnings
|
|
293
|
+
warnings = [i for i in issues if i.severity == "warning"]
|
|
294
|
+
for warning in warnings:
|
|
295
|
+
logger.warning(f"{warning.message} - {warning.fix_suggestion}")
|
|
296
|
+
|
|
297
|
+
# Install using strategy
|
|
298
|
+
if self.dry_run:
|
|
299
|
+
logger.info(
|
|
300
|
+
f"[DRY RUN] Would install {name} to {self._platform_info.config_path}"
|
|
301
|
+
)
|
|
302
|
+
return InstallationResult(
|
|
303
|
+
success=True,
|
|
304
|
+
platform=self._platform_info.platform,
|
|
305
|
+
server_name=name,
|
|
306
|
+
method=method,
|
|
307
|
+
message=f"[DRY RUN] Would install {name}",
|
|
308
|
+
config_path=self._platform_info.config_path,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
result = self._strategy.install(server, scope)
|
|
313
|
+
logger.info(f"Successfully installed {name}")
|
|
314
|
+
return result
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Installation failed: {e}", exc_info=True)
|
|
317
|
+
raise InstallationError(
|
|
318
|
+
f"Failed to install {name}: {e}",
|
|
319
|
+
"Check logs for details and verify permissions",
|
|
320
|
+
) from e
|
|
321
|
+
|
|
322
|
+
def uninstall_server(
|
|
323
|
+
self, name: str, scope: Scope = Scope.PROJECT
|
|
324
|
+
) -> InstallationResult:
|
|
325
|
+
"""Remove MCP server from configuration.
|
|
326
|
+
|
|
327
|
+
Creates backup before removing. Server data/packages are not removed,
|
|
328
|
+
only the configuration entry.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
name: Server name to uninstall
|
|
332
|
+
scope: Installation scope (PROJECT or GLOBAL)
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
InstallationResult with success status
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
InstallationError: If uninstallation fails
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> result = installer.uninstall_server("old-server")
|
|
342
|
+
>>> if result.success:
|
|
343
|
+
... print(f"Removed {result.server_name}")
|
|
344
|
+
"""
|
|
345
|
+
logger.info(f"Uninstalling server: {name}")
|
|
346
|
+
|
|
347
|
+
if self.dry_run:
|
|
348
|
+
logger.info(f"[DRY RUN] Would uninstall {name}")
|
|
349
|
+
return InstallationResult(
|
|
350
|
+
success=True,
|
|
351
|
+
platform=self._platform_info.platform,
|
|
352
|
+
server_name=name,
|
|
353
|
+
method=InstallMethod.DIRECT, # Not relevant for uninstall
|
|
354
|
+
message=f"[DRY RUN] Would uninstall {name}",
|
|
355
|
+
config_path=self._platform_info.config_path,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
result = self._strategy.uninstall(name, scope)
|
|
360
|
+
logger.info(f"Successfully uninstalled {name}")
|
|
361
|
+
return result
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"Uninstallation failed: {e}", exc_info=True)
|
|
364
|
+
raise InstallationError(
|
|
365
|
+
f"Failed to uninstall {name}: {e}",
|
|
366
|
+
"Check logs for details and verify permissions",
|
|
367
|
+
) from e
|
|
368
|
+
|
|
369
|
+
def list_servers(self, scope: Scope = Scope.PROJECT) -> list[MCPServerConfig]:
|
|
370
|
+
"""List all installed MCP servers.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
scope: Installation scope (PROJECT or GLOBAL)
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List of installed server configurations (empty if none)
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> servers = installer.list_servers()
|
|
380
|
+
>>> for server in servers:
|
|
381
|
+
... print(f"{server.name}: {server.command} {' '.join(server.args)}")
|
|
382
|
+
mcp-ticketer: uv run mcp-ticketer mcp
|
|
383
|
+
github-mcp: npx -y @modelcontextprotocol/server-github
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
return self._strategy.list_servers(scope)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(f"Failed to list servers: {e}", exc_info=True)
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
def get_server(
|
|
392
|
+
self, name: str, scope: Scope = Scope.PROJECT
|
|
393
|
+
) -> MCPServerConfig | None:
|
|
394
|
+
"""Get specific server configuration.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
name: Server name to retrieve
|
|
398
|
+
scope: Installation scope (PROJECT or GLOBAL)
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Server configuration or None if not found
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
>>> server = installer.get_server("mcp-ticketer")
|
|
405
|
+
>>> if server:
|
|
406
|
+
... print(f"Command: {server.command}")
|
|
407
|
+
... print(f"Args: {server.args}")
|
|
408
|
+
"""
|
|
409
|
+
servers = self.list_servers(scope)
|
|
410
|
+
for server in servers:
|
|
411
|
+
if server.name == name:
|
|
412
|
+
return server
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
def inspect_installation(self) -> InspectionReport:
|
|
416
|
+
"""Run comprehensive inspection.
|
|
417
|
+
|
|
418
|
+
Validates all servers, checks for legacy format, detects duplicates,
|
|
419
|
+
and provides recommendations for improvements.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Complete inspection report
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
>>> report = installer.inspect_installation()
|
|
426
|
+
>>> print(report.summary())
|
|
427
|
+
Inspection PASS: 5/5 servers valid
|
|
428
|
+
Errors: 0, Warnings: 2, Info: 1
|
|
429
|
+
|
|
430
|
+
>>> # Fix issues
|
|
431
|
+
>>> if report.has_warnings():
|
|
432
|
+
... for issue in report.issues:
|
|
433
|
+
... if issue.auto_fixable:
|
|
434
|
+
... inspector.auto_fix(issue)
|
|
435
|
+
"""
|
|
436
|
+
logger.info("Running installation inspection...")
|
|
437
|
+
report = self._inspector.inspect()
|
|
438
|
+
|
|
439
|
+
if self.verbose:
|
|
440
|
+
print("\n" + "=" * 60)
|
|
441
|
+
print(report.summary())
|
|
442
|
+
print("=" * 60)
|
|
443
|
+
|
|
444
|
+
if report.issues:
|
|
445
|
+
print("\nIssues:")
|
|
446
|
+
for issue in report.issues:
|
|
447
|
+
print(f"\n[{issue.severity.upper()}] {issue.message}")
|
|
448
|
+
if issue.server_name:
|
|
449
|
+
print(f" Server: {issue.server_name}")
|
|
450
|
+
print(f" Fix: {issue.fix_suggestion}")
|
|
451
|
+
if issue.auto_fixable:
|
|
452
|
+
print(" (Auto-fixable)")
|
|
453
|
+
|
|
454
|
+
if report.recommendations:
|
|
455
|
+
print("\nRecommendations:")
|
|
456
|
+
for rec in report.recommendations:
|
|
457
|
+
print(f" - {rec}")
|
|
458
|
+
print()
|
|
459
|
+
|
|
460
|
+
return report
|
|
461
|
+
|
|
462
|
+
def fix_issues(self, auto_fix: bool = True) -> list[str]:
|
|
463
|
+
"""Fix detected issues.
|
|
464
|
+
|
|
465
|
+
Runs inspection and attempts to auto-fix all fixable issues.
|
|
466
|
+
Non-fixable issues are logged as warnings.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
auto_fix: If True, actually apply fixes (False = dry run)
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List of fixes applied
|
|
473
|
+
|
|
474
|
+
Example:
|
|
475
|
+
>>> fixes = installer.fix_issues()
|
|
476
|
+
>>> for fix in fixes:
|
|
477
|
+
... print(f"Fixed: {fix}")
|
|
478
|
+
Fixed: Created default config file
|
|
479
|
+
Fixed: Migrated legacy format to modern format
|
|
480
|
+
Fixed: Removed deprecated args from server 'old-server'
|
|
481
|
+
"""
|
|
482
|
+
logger.info("Checking for fixable issues...")
|
|
483
|
+
report = self.inspect_installation()
|
|
484
|
+
|
|
485
|
+
fixes: list[str] = []
|
|
486
|
+
auto_fixable = [i for i in report.issues if i.auto_fixable]
|
|
487
|
+
|
|
488
|
+
if not auto_fixable:
|
|
489
|
+
logger.info("No auto-fixable issues found")
|
|
490
|
+
return fixes
|
|
491
|
+
|
|
492
|
+
logger.info(f"Found {len(auto_fixable)} auto-fixable issues")
|
|
493
|
+
|
|
494
|
+
for issue in auto_fixable:
|
|
495
|
+
if self.dry_run or not auto_fix:
|
|
496
|
+
fixes.append(f"[DRY RUN] Would fix: {issue.message}")
|
|
497
|
+
logger.info(f"[DRY RUN] Would fix: {issue.message}")
|
|
498
|
+
else:
|
|
499
|
+
try:
|
|
500
|
+
if self._inspector.auto_fix(issue):
|
|
501
|
+
fixes.append(issue.message)
|
|
502
|
+
logger.info(f"Fixed: {issue.message}")
|
|
503
|
+
else:
|
|
504
|
+
logger.warning(f"Could not auto-fix: {issue.message}")
|
|
505
|
+
except Exception as e:
|
|
506
|
+
logger.error(f"Fix failed for '{issue.message}': {e}")
|
|
507
|
+
|
|
508
|
+
return fixes
|
|
509
|
+
|
|
510
|
+
def migrate_legacy(self) -> bool:
|
|
511
|
+
"""Migrate from legacy line-delimited JSON format.
|
|
512
|
+
|
|
513
|
+
Converts old line-delimited JSON format to modern FastMCP SDK format.
|
|
514
|
+
Creates backup before migration.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
True if migration succeeded (or not needed)
|
|
518
|
+
|
|
519
|
+
Example:
|
|
520
|
+
>>> if installer.migrate_legacy():
|
|
521
|
+
... print("Migration successful")
|
|
522
|
+
"""
|
|
523
|
+
logger.info("Checking for legacy format...")
|
|
524
|
+
|
|
525
|
+
if not self._inspector.check_legacy_format():
|
|
526
|
+
logger.info("No legacy format detected")
|
|
527
|
+
return True
|
|
528
|
+
|
|
529
|
+
if self.dry_run:
|
|
530
|
+
logger.info("[DRY RUN] Would migrate legacy format")
|
|
531
|
+
return True
|
|
532
|
+
|
|
533
|
+
logger.info("Migrating from legacy format...")
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
# Auto-fix will handle the migration
|
|
537
|
+
issue = ValidationIssue(
|
|
538
|
+
severity="warning",
|
|
539
|
+
message="Legacy format detected",
|
|
540
|
+
server_name=None,
|
|
541
|
+
fix_suggestion="Migrate to modern format",
|
|
542
|
+
auto_fixable=True,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
success = self._inspector.auto_fix(issue)
|
|
546
|
+
if success:
|
|
547
|
+
logger.info("Migration successful")
|
|
548
|
+
else:
|
|
549
|
+
logger.error("Migration failed")
|
|
550
|
+
return success
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
logger.error(f"Migration error: {e}", exc_info=True)
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def platform_info(self) -> PlatformInfo:
|
|
558
|
+
"""Get detected platform information.
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Platform information including confidence and paths
|
|
562
|
+
|
|
563
|
+
Example:
|
|
564
|
+
>>> info = installer.platform_info
|
|
565
|
+
>>> print(f"Platform: {info.platform.value}")
|
|
566
|
+
>>> print(f"Config: {info.config_path}")
|
|
567
|
+
>>> print(f"Confidence: {info.confidence}")
|
|
568
|
+
"""
|
|
569
|
+
return self._platform_info
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def config_path(self) -> Path:
|
|
573
|
+
"""Get configuration file path.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Path to platform's config file
|
|
577
|
+
|
|
578
|
+
Example:
|
|
579
|
+
>>> print(installer.config_path)
|
|
580
|
+
/home/user/.config/claude/mcp.json
|
|
581
|
+
"""
|
|
582
|
+
return self._platform_info.config_path or Path()
|
|
583
|
+
|
|
584
|
+
# ========================================================================
|
|
585
|
+
# Private Helper Methods
|
|
586
|
+
# ========================================================================
|
|
587
|
+
|
|
588
|
+
def _detect_platform(self) -> PlatformInfo:
|
|
589
|
+
"""Detect platform automatically.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
Detected platform info
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
PlatformDetectionError: If no platform detected
|
|
596
|
+
"""
|
|
597
|
+
detector = PlatformDetector()
|
|
598
|
+
info = detector.detect()
|
|
599
|
+
|
|
600
|
+
if info.platform == Platform.UNKNOWN or info.confidence == 0.0:
|
|
601
|
+
raise PlatformDetectionError()
|
|
602
|
+
|
|
603
|
+
return info
|
|
604
|
+
|
|
605
|
+
def _select_strategy(self) -> BaseStrategy:
|
|
606
|
+
"""Select best installation strategy for platform.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Installation strategy instance
|
|
610
|
+
|
|
611
|
+
Raises:
|
|
612
|
+
PlatformNotSupportedError: If platform has no strategy
|
|
613
|
+
"""
|
|
614
|
+
platform = self._platform_info.platform
|
|
615
|
+
|
|
616
|
+
# Platform-specific strategies (with fallbacks)
|
|
617
|
+
if platform == Platform.CLAUDE_CODE:
|
|
618
|
+
claude_strategy = ClaudeCodeStrategy()
|
|
619
|
+
# Use the strategy's get_strategy method to get actual installer
|
|
620
|
+
return claude_strategy.get_strategy(Scope.PROJECT)
|
|
621
|
+
|
|
622
|
+
elif platform == Platform.CLAUDE_DESKTOP:
|
|
623
|
+
# Use same strategy as Claude Code
|
|
624
|
+
desktop_strategy = ClaudeCodeStrategy()
|
|
625
|
+
return desktop_strategy.get_strategy(Scope.GLOBAL)
|
|
626
|
+
|
|
627
|
+
elif platform == Platform.CURSOR:
|
|
628
|
+
cursor_strategy = CursorStrategy()
|
|
629
|
+
return cursor_strategy.get_strategy(Scope.PROJECT)
|
|
630
|
+
|
|
631
|
+
elif platform == Platform.CODEX:
|
|
632
|
+
codex_strategy = CodexStrategy()
|
|
633
|
+
return codex_strategy.get_strategy(Scope.GLOBAL)
|
|
634
|
+
|
|
635
|
+
elif platform in [Platform.AUGGIE, Platform.WINDSURF, Platform.GEMINI_CLI]:
|
|
636
|
+
# Generic JSON manipulation for these platforms
|
|
637
|
+
if not self._platform_info.config_path:
|
|
638
|
+
raise ConfigurationError(
|
|
639
|
+
f"No config path for {platform.value}",
|
|
640
|
+
"Ensure platform is installed correctly",
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return JSONManipulationStrategy(
|
|
644
|
+
platform=platform,
|
|
645
|
+
config_path=self._platform_info.config_path,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
else:
|
|
649
|
+
raise PlatformNotSupportedError(
|
|
650
|
+
platform.value,
|
|
651
|
+
[
|
|
652
|
+
p.value
|
|
653
|
+
for p in Platform
|
|
654
|
+
if p not in [Platform.UNKNOWN, Platform.ANTIGRAVITY]
|
|
655
|
+
],
|
|
656
|
+
)
|