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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {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