devsync 0.5.5__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 (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,279 @@
1
+ """MCP server credential management and .env file handling."""
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from rich.console import Console
9
+ from rich.prompt import Prompt
10
+
11
+ from aiconfigkit.core.models import EnvironmentConfig, InstallationScope, MCPServer
12
+ from aiconfigkit.utils.dotenv import ensure_env_gitignored, load_env_config, save_env_config, set_env_variable
13
+
14
+ logger = logging.getLogger(__name__)
15
+ console = Console()
16
+
17
+
18
+ class CredentialManager:
19
+ """Manages MCP server credentials and .env file operations."""
20
+
21
+ def __init__(self, project_root: Optional[Path] = None):
22
+ """
23
+ Initialize credential manager.
24
+
25
+ Args:
26
+ project_root: Project root directory (for project-scoped credentials)
27
+ If None, uses current directory
28
+ """
29
+ self.project_root = project_root or Path.cwd()
30
+ self.project_env_path = self.project_root / ".instructionkit" / ".env"
31
+ self.global_env_path = Path.home() / ".instructionkit" / "global" / ".env"
32
+
33
+ def configure_server(
34
+ self,
35
+ server: MCPServer,
36
+ scope: InstallationScope = InstallationScope.PROJECT,
37
+ non_interactive: bool = False,
38
+ ) -> EnvironmentConfig:
39
+ """
40
+ Configure credentials for an MCP server.
41
+
42
+ Args:
43
+ server: MCPServer to configure
44
+ scope: Installation scope (PROJECT or GLOBAL)
45
+ non_interactive: If True, read from environment instead of prompting
46
+
47
+ Returns:
48
+ EnvironmentConfig with configured credentials
49
+
50
+ Raises:
51
+ ValueError: If non-interactive mode and required vars are missing
52
+ """
53
+ # Get env path for scope
54
+ env_path = self._get_env_path(scope)
55
+
56
+ # Load existing config
57
+ env_config = load_env_config(env_path, scope)
58
+
59
+ # Get required env vars
60
+ required_vars = server.get_required_env_vars()
61
+
62
+ if not required_vars:
63
+ logger.info(f"Server '{server.name}' requires no additional credentials")
64
+ return env_config
65
+
66
+ # Configure each required variable
67
+ if non_interactive:
68
+ self._configure_non_interactive(server, required_vars, env_config)
69
+ else:
70
+ self._configure_interactive(server, required_vars, env_config)
71
+
72
+ # Save updated config
73
+ save_env_config(env_config)
74
+
75
+ logger.info(f"Configured {len(required_vars)} credential(s) for server '{server.name}'")
76
+
77
+ return env_config
78
+
79
+ def _configure_interactive(
80
+ self,
81
+ server: MCPServer,
82
+ required_vars: list[str],
83
+ env_config: EnvironmentConfig,
84
+ ) -> None:
85
+ """
86
+ Configure credentials interactively with user prompts.
87
+
88
+ Args:
89
+ server: MCPServer being configured
90
+ required_vars: List of required environment variable names
91
+ env_config: EnvironmentConfig to update
92
+ """
93
+ console.print(f"\n[bold]Configuring MCP server:[/bold] {server.get_fully_qualified_name()}")
94
+ console.print(f"[dim]Required environment variables: {len(required_vars)}[/dim]\n")
95
+
96
+ for var_name in required_vars:
97
+ # Check if already configured
98
+ existing_value = env_config.get(var_name)
99
+
100
+ if existing_value:
101
+ # Show masked existing value
102
+ masked_value = self._mask_value(existing_value)
103
+ console.print(f"[dim]{var_name} is already configured: {masked_value}[/dim]")
104
+
105
+ # Ask if they want to update
106
+ update = Prompt.ask(
107
+ "Do you want to update this value?",
108
+ choices=["y", "n"],
109
+ default="n",
110
+ )
111
+
112
+ if update.lower() == "n":
113
+ continue
114
+
115
+ # Prompt for value (masked input)
116
+ console.print(f"[cyan]Enter value for {var_name}:[/cyan]")
117
+ value = Prompt.ask(f" {var_name}", password=True)
118
+
119
+ if not value:
120
+ console.print("[yellow]Warning: Empty value provided, skipping[/yellow]")
121
+ continue
122
+
123
+ # Set the value
124
+ env_config.set(var_name, value)
125
+ console.print(f"[green]✓[/green] Set {var_name}")
126
+
127
+ def _configure_non_interactive(
128
+ self,
129
+ server: MCPServer,
130
+ required_vars: list[str],
131
+ env_config: EnvironmentConfig,
132
+ ) -> None:
133
+ """
134
+ Configure credentials non-interactively from environment.
135
+
136
+ Args:
137
+ server: MCPServer being configured
138
+ required_vars: List of required environment variable names
139
+ env_config: EnvironmentConfig to update
140
+
141
+ Raises:
142
+ ValueError: If required variables are missing from environment
143
+ """
144
+ missing_vars = []
145
+
146
+ for var_name in required_vars:
147
+ # Check if already in env_config
148
+ if env_config.has(var_name):
149
+ continue
150
+
151
+ # Try to read from current environment
152
+ value = os.getenv(var_name)
153
+
154
+ if not value:
155
+ missing_vars.append(var_name)
156
+ continue
157
+
158
+ # Set the value
159
+ env_config.set(var_name, value)
160
+
161
+ if missing_vars:
162
+ raise ValueError(
163
+ f"Non-interactive mode: Missing required environment variables: {', '.join(missing_vars)}"
164
+ )
165
+
166
+ def show_current_credentials(
167
+ self,
168
+ server: MCPServer,
169
+ scope: InstallationScope = InstallationScope.PROJECT,
170
+ ) -> dict[str, str]:
171
+ """
172
+ Show current credential values (masked) for a server.
173
+
174
+ Args:
175
+ server: MCPServer to show credentials for
176
+ scope: Installation scope
177
+
178
+ Returns:
179
+ Dictionary of variable names to masked values
180
+ """
181
+ env_path = self._get_env_path(scope)
182
+ env_config = load_env_config(env_path, scope)
183
+
184
+ required_vars = server.get_required_env_vars()
185
+ credentials = {}
186
+
187
+ for var_name in required_vars:
188
+ value = env_config.get(var_name)
189
+ if value:
190
+ credentials[var_name] = self._mask_value(value)
191
+ else:
192
+ credentials[var_name] = "[NOT SET]"
193
+
194
+ return credentials
195
+
196
+ def validate_credentials(
197
+ self,
198
+ server: MCPServer,
199
+ scope: InstallationScope = InstallationScope.PROJECT,
200
+ ) -> tuple[bool, list[str]]:
201
+ """
202
+ Validate that all required credentials are configured.
203
+
204
+ Args:
205
+ server: MCPServer to validate
206
+ scope: Installation scope
207
+
208
+ Returns:
209
+ Tuple of (is_valid, missing_vars)
210
+ """
211
+ env_path = self._get_env_path(scope)
212
+ env_config = load_env_config(env_path, scope)
213
+
214
+ missing_vars = env_config.validate_for_server(server)
215
+
216
+ return (len(missing_vars) == 0, missing_vars)
217
+
218
+ def _get_env_path(self, scope: InstallationScope) -> Path:
219
+ """
220
+ Get .env file path for scope.
221
+
222
+ Args:
223
+ scope: Installation scope
224
+
225
+ Returns:
226
+ Path to .env file
227
+ """
228
+ if scope == InstallationScope.GLOBAL:
229
+ return self.global_env_path
230
+ else:
231
+ return self.project_env_path
232
+
233
+ def _mask_value(self, value: str, visible_chars: int = 4) -> str:
234
+ """
235
+ Mask a credential value for display.
236
+
237
+ Args:
238
+ value: Value to mask
239
+ visible_chars: Number of characters to show at end
240
+
241
+ Returns:
242
+ Masked value (e.g., "****abc123")
243
+ """
244
+ if len(value) <= visible_chars:
245
+ return "*" * len(value)
246
+
247
+ masked_part = "*" * (len(value) - visible_chars)
248
+ visible_part = value[-visible_chars:]
249
+
250
+ return f"{masked_part}{visible_part}"
251
+
252
+ def get_env_config(self, scope: InstallationScope = InstallationScope.PROJECT) -> EnvironmentConfig:
253
+ """
254
+ Get environment configuration for scope.
255
+
256
+ Args:
257
+ scope: Installation scope
258
+
259
+ Returns:
260
+ EnvironmentConfig
261
+ """
262
+ env_path = self._get_env_path(scope)
263
+ return load_env_config(env_path, scope)
264
+
265
+ def merge_scopes(self) -> EnvironmentConfig:
266
+ """
267
+ Merge global and project environment configs.
268
+
269
+ Project variables take precedence over global.
270
+
271
+ Returns:
272
+ Merged EnvironmentConfig
273
+ """
274
+ from aiconfigkit.utils.dotenv import merge_env_configs
275
+
276
+ global_config = self.get_env_config(InstallationScope.GLOBAL)
277
+ project_config = self.get_env_config(InstallationScope.PROJECT)
278
+
279
+ return merge_env_configs(project_config, global_config)
@@ -0,0 +1,308 @@
1
+ """MCP template installation and library management."""
2
+
3
+ import logging
4
+ import shutil
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from aiconfigkit.core.git_operations import GitOperations
10
+ from aiconfigkit.core.models import InstallationScope, MCPTemplate
11
+ from aiconfigkit.core.repository import RepositoryParser
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class MCPManager:
17
+ """Manages MCP template installation and library operations."""
18
+
19
+ def __init__(self, library_root: Path):
20
+ """
21
+ Initialize MCP manager.
22
+
23
+ Args:
24
+ library_root: Root directory for MCP library (e.g., ~/.instructionkit/library/)
25
+ """
26
+ self.library_root = library_root
27
+ self.library_root.mkdir(parents=True, exist_ok=True)
28
+
29
+ def install_template(
30
+ self,
31
+ source: str,
32
+ namespace: str,
33
+ scope: InstallationScope = InstallationScope.PROJECT,
34
+ force: bool = False,
35
+ ) -> MCPTemplate:
36
+ """
37
+ Install MCP template from source URL or local path.
38
+
39
+ Args:
40
+ source: Git URL or local directory path
41
+ namespace: Unique identifier for this template
42
+ scope: Installation scope (PROJECT or GLOBAL)
43
+ force: Overwrite existing template if it exists
44
+
45
+ Returns:
46
+ MCPTemplate object representing the installed template
47
+
48
+ Raises:
49
+ ValueError: If namespace is invalid or already exists (without force)
50
+ FileNotFoundError: If source path doesn't exist
51
+ RuntimeError: If Git clone/copy fails
52
+ """
53
+ # Validate namespace
54
+ self._validate_namespace(namespace)
55
+
56
+ # Get installation path
57
+ install_path = self._get_install_path(namespace, scope)
58
+
59
+ # Check if already exists
60
+ if install_path.exists() and not force:
61
+ raise ValueError(
62
+ f"Template namespace '{namespace}' already exists at {install_path}. " f"Use --force to overwrite."
63
+ )
64
+
65
+ # Remove existing if force
66
+ if install_path.exists() and force:
67
+ logger.info(f"Removing existing template at {install_path}")
68
+ shutil.rmtree(install_path)
69
+
70
+ # Install from source
71
+ source_url: Optional[str] = None
72
+ source_path: Optional[str] = None
73
+
74
+ if source.startswith(("http://", "https://", "git@")):
75
+ # Git URL
76
+ source_url = source
77
+ self._install_from_git(source, install_path)
78
+ else:
79
+ # Local path
80
+ source_path_obj = Path(source).resolve()
81
+ if not source_path_obj.exists():
82
+ raise FileNotFoundError(f"Source path does not exist: {source}")
83
+ source_path = str(source_path_obj)
84
+ self._install_from_local(source_path_obj, install_path)
85
+
86
+ # Parse template
87
+ parser = RepositoryParser(install_path)
88
+ metadata = self._parse_metadata(install_path)
89
+
90
+ # Parse MCP servers and sets
91
+ servers = parser.parse_mcp_servers(namespace)
92
+ sets = parser.parse_mcp_sets(namespace)
93
+
94
+ # Create MCPTemplate
95
+ template = MCPTemplate(
96
+ namespace=namespace,
97
+ source_url=source_url,
98
+ source_path=source_path,
99
+ version=metadata.get("version", "unknown"),
100
+ description=metadata.get("description", ""),
101
+ installed_at=datetime.now(),
102
+ servers=servers,
103
+ sets=sets,
104
+ )
105
+
106
+ # Save template metadata
107
+ self._save_template_metadata(template, install_path)
108
+
109
+ logger.info(f"Installed MCP template '{namespace}' with {len(servers)} servers and {len(sets)} sets")
110
+
111
+ return template
112
+
113
+ def _validate_namespace(self, namespace: str) -> None:
114
+ """
115
+ Validate namespace format.
116
+
117
+ Args:
118
+ namespace: Namespace to validate
119
+
120
+ Raises:
121
+ ValueError: If namespace is invalid
122
+ """
123
+ import re
124
+
125
+ if not namespace:
126
+ raise ValueError("Namespace cannot be empty")
127
+
128
+ # Check for path separators first (security)
129
+ if "/" in namespace or "\\" in namespace:
130
+ raise ValueError(f"Namespace cannot contain path separators: {namespace}")
131
+
132
+ if not re.match(r"^[a-zA-Z0-9_-]+$", namespace):
133
+ raise ValueError(
134
+ f"Invalid namespace: {namespace}. "
135
+ f"Must contain only alphanumeric characters, hyphens, and underscores."
136
+ )
137
+
138
+ def _get_install_path(self, namespace: str, scope: InstallationScope) -> Path:
139
+ """
140
+ Get installation path for template.
141
+
142
+ Args:
143
+ namespace: Template namespace
144
+ scope: Installation scope
145
+
146
+ Returns:
147
+ Path to install location
148
+ """
149
+ if scope == InstallationScope.GLOBAL:
150
+ return self.library_root / "global" / namespace
151
+ else:
152
+ return self.library_root / namespace
153
+
154
+ def _install_from_git(self, git_url: str, dest_path: Path) -> None:
155
+ """
156
+ Install template from Git repository.
157
+
158
+ Args:
159
+ git_url: Git repository URL
160
+ dest_path: Destination path
161
+
162
+ Raises:
163
+ RuntimeError: If Git clone fails
164
+ """
165
+ logger.info(f"Cloning Git repository: {git_url}")
166
+
167
+ try:
168
+ git_ops = GitOperations()
169
+ git_ops.clone_repository(git_url, dest_path)
170
+ except Exception as e:
171
+ raise RuntimeError(f"Failed to clone Git repository: {e}") from e
172
+
173
+ def _install_from_local(self, source_path: Path, dest_path: Path) -> None:
174
+ """
175
+ Install template from local directory.
176
+
177
+ Args:
178
+ source_path: Source directory path
179
+ dest_path: Destination path
180
+ """
181
+ logger.info(f"Copying from local path: {source_path}")
182
+
183
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
184
+
185
+ # Copy directory contents
186
+ shutil.copytree(source_path, dest_path, dirs_exist_ok=False)
187
+
188
+ def _parse_metadata(self, template_path: Path) -> dict:
189
+ """
190
+ Parse template metadata from aiconfigkit.yaml.
191
+
192
+ Args:
193
+ template_path: Path to template directory
194
+
195
+ Returns:
196
+ Dictionary with metadata
197
+ """
198
+ import yaml
199
+
200
+ metadata_file = template_path / "templatekit.yaml"
201
+ if not metadata_file.exists():
202
+ # Try templatekit.yaml as alternative
203
+ metadata_file = template_path / "templatekit.yaml"
204
+
205
+ if not metadata_file.exists():
206
+ logger.warning(f"No metadata file found in {template_path}")
207
+ return {}
208
+
209
+ with open(metadata_file, "r", encoding="utf-8") as f:
210
+ metadata = yaml.safe_load(f)
211
+
212
+ return metadata or {}
213
+
214
+ def _save_template_metadata(self, template: MCPTemplate, install_path: Path) -> None:
215
+ """
216
+ Save template metadata to .mcp_template.json.
217
+
218
+ Args:
219
+ template: MCPTemplate to save
220
+ install_path: Installation path
221
+ """
222
+ import json
223
+
224
+ metadata_file = install_path / ".mcp_template.json"
225
+
226
+ with open(metadata_file, "w", encoding="utf-8") as f:
227
+ json.dump(template.to_dict(), f, indent=2)
228
+
229
+ def load_template(
230
+ self, namespace: str, scope: InstallationScope = InstallationScope.PROJECT
231
+ ) -> Optional[MCPTemplate]:
232
+ """
233
+ Load installed template by namespace.
234
+
235
+ Args:
236
+ namespace: Template namespace
237
+ scope: Installation scope
238
+
239
+ Returns:
240
+ MCPTemplate if found, None otherwise
241
+ """
242
+ import json
243
+
244
+ install_path = self._get_install_path(namespace, scope)
245
+ metadata_file = install_path / ".mcp_template.json"
246
+
247
+ if not metadata_file.exists():
248
+ return None
249
+
250
+ with open(metadata_file, "r", encoding="utf-8") as f:
251
+ data = json.load(f)
252
+
253
+ return MCPTemplate.from_dict(data)
254
+
255
+ def list_templates(self, scope: InstallationScope = InstallationScope.PROJECT) -> list[MCPTemplate]:
256
+ """
257
+ List all installed MCP templates.
258
+
259
+ Args:
260
+ scope: Installation scope to list
261
+
262
+ Returns:
263
+ List of MCPTemplate objects
264
+ """
265
+ templates = []
266
+
267
+ if scope == InstallationScope.GLOBAL:
268
+ search_path = self.library_root / "global"
269
+ else:
270
+ search_path = self.library_root
271
+
272
+ if not search_path.exists():
273
+ return []
274
+
275
+ # Find all .mcp_template.json files
276
+ for metadata_file in search_path.glob("**/.mcp_template.json"):
277
+ try:
278
+ import json
279
+
280
+ with open(metadata_file, "r", encoding="utf-8") as f:
281
+ data = json.load(f)
282
+ template = MCPTemplate.from_dict(data)
283
+ templates.append(template)
284
+ except Exception as e:
285
+ logger.warning(f"Failed to load template from {metadata_file}: {e}")
286
+
287
+ return templates
288
+
289
+ def uninstall_template(self, namespace: str, scope: InstallationScope = InstallationScope.PROJECT) -> bool:
290
+ """
291
+ Uninstall MCP template.
292
+
293
+ Args:
294
+ namespace: Template namespace
295
+ scope: Installation scope
296
+
297
+ Returns:
298
+ True if uninstalled, False if not found
299
+ """
300
+ install_path = self._get_install_path(namespace, scope)
301
+
302
+ if not install_path.exists():
303
+ return False
304
+
305
+ shutil.rmtree(install_path)
306
+ logger.info(f"Uninstalled MCP template '{namespace}'")
307
+
308
+ return True
@@ -0,0 +1 @@
1
+ """MCP set activation and management."""
@@ -0,0 +1 @@
1
+ """MCP server configuration validation."""