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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- 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."""
|