apm-cli 0.1.0__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 (48) hide show
  1. apm_cli/__init__.py +5 -0
  2. apm_cli/adapters/__init__.py +1 -0
  3. apm_cli/adapters/client/__init__.py +1 -0
  4. apm_cli/adapters/client/base.py +36 -0
  5. apm_cli/adapters/client/vscode.py +301 -0
  6. apm_cli/adapters/package_manager/__init__.py +1 -0
  7. apm_cli/adapters/package_manager/base.py +27 -0
  8. apm_cli/adapters/package_manager/default_manager.py +123 -0
  9. apm_cli/cli.py +1415 -0
  10. apm_cli/compilation/__init__.py +29 -0
  11. apm_cli/compilation/agents_compiler.py +312 -0
  12. apm_cli/compilation/link_resolver.py +230 -0
  13. apm_cli/compilation/template_builder.py +137 -0
  14. apm_cli/config.py +60 -0
  15. apm_cli/core/__init__.py +1 -0
  16. apm_cli/core/operations.py +78 -0
  17. apm_cli/core/script_runner.py +253 -0
  18. apm_cli/deps/__init__.py +12 -0
  19. apm_cli/deps/aggregator.py +67 -0
  20. apm_cli/deps/verifier.py +102 -0
  21. apm_cli/factory.py +59 -0
  22. apm_cli/primitives/__init__.py +16 -0
  23. apm_cli/primitives/discovery.py +142 -0
  24. apm_cli/primitives/models.py +115 -0
  25. apm_cli/primitives/parser.py +197 -0
  26. apm_cli/registry/__init__.py +6 -0
  27. apm_cli/registry/client.py +156 -0
  28. apm_cli/registry/integration.py +154 -0
  29. apm_cli/runtime/__init__.py +9 -0
  30. apm_cli/runtime/base.py +63 -0
  31. apm_cli/runtime/codex_runtime.py +149 -0
  32. apm_cli/runtime/factory.py +130 -0
  33. apm_cli/runtime/llm_runtime.py +155 -0
  34. apm_cli/runtime/manager.py +226 -0
  35. apm_cli/utils/__init__.py +1 -0
  36. apm_cli/utils/helpers.py +101 -0
  37. apm_cli/version.py +54 -0
  38. apm_cli/workflow/__init__.py +1 -0
  39. apm_cli/workflow/discovery.py +100 -0
  40. apm_cli/workflow/parser.py +92 -0
  41. apm_cli/workflow/runner.py +193 -0
  42. apm_cli-0.1.0.dist-info/METADATA +407 -0
  43. apm_cli-0.1.0.dist-info/RECORD +48 -0
  44. apm_cli-0.1.0.dist-info/WHEEL +5 -0
  45. apm_cli-0.1.0.dist-info/entry_points.txt +2 -0
  46. apm_cli-0.1.0.dist-info/licenses/AUTHORS +13 -0
  47. apm_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  48. apm_cli-0.1.0.dist-info/top_level.txt +1 -0
apm_cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """APM-CLI package."""
2
+
3
+ from .version import get_version
4
+
5
+ __version__ = get_version()
@@ -0,0 +1 @@
1
+ """Adapters package."""
@@ -0,0 +1 @@
1
+ """Client adapters package."""
@@ -0,0 +1,36 @@
1
+ """Base adapter interface for MCP clients."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class MCPClientAdapter(ABC):
7
+ """Base adapter for MCP clients."""
8
+
9
+ @abstractmethod
10
+ def get_config_path(self):
11
+ """Get the path to the MCP configuration file."""
12
+ pass
13
+
14
+ @abstractmethod
15
+ def update_config(self, config_updates):
16
+ """Update the MCP configuration."""
17
+ pass
18
+
19
+ @abstractmethod
20
+ def get_current_config(self):
21
+ """Get the current MCP configuration."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def configure_mcp_server(self, server_url, server_name=None, enabled=True):
26
+ """Configure an MCP server in the client configuration.
27
+
28
+ Args:
29
+ server_url (str): URL of the MCP server.
30
+ server_name (str, optional): Name of the server. Defaults to None.
31
+ enabled (bool, optional): Whether to enable the server. Defaults to True.
32
+
33
+ Returns:
34
+ bool: True if successful, False otherwise.
35
+ """
36
+ pass
@@ -0,0 +1,301 @@
1
+ """VSCode implementation of MCP client adapter.
2
+
3
+ This adapter implements the VSCode-specific handling of MCP server configuration,
4
+ following the official documentation at:
5
+ https://code.visualstudio.com/docs/copilot/chat/mcp-servers
6
+ """
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ from .base import MCPClientAdapter
12
+ from ...registry.client import SimpleRegistryClient
13
+ from ...registry.integration import RegistryIntegration
14
+
15
+
16
+ class VSCodeClientAdapter(MCPClientAdapter):
17
+ """VSCode implementation of MCP client adapter.
18
+
19
+ This adapter handles VSCode-specific configuration for MCP servers using
20
+ a repository-level .vscode/mcp.json file, following the format specified
21
+ in the VSCode documentation.
22
+ """
23
+
24
+ def __init__(self, registry_url=None):
25
+ """Initialize the VSCode client adapter.
26
+
27
+ Args:
28
+ registry_url (str, optional): URL of the MCP registry.
29
+ If not provided, uses the MCP_REGISTRY_URL environment variable
30
+ or falls back to the default demo registry.
31
+ """
32
+ self.registry_client = SimpleRegistryClient(registry_url)
33
+ self.registry_integration = RegistryIntegration(registry_url)
34
+
35
+ def get_config_path(self):
36
+ """Get the path to the VSCode MCP configuration file in the repository.
37
+
38
+ Returns:
39
+ str: Path to the .vscode/mcp.json file.
40
+ """
41
+ # Use the current working directory as the repository root
42
+ repo_root = Path(os.getcwd())
43
+
44
+ # Path to .vscode/mcp.json in the repository
45
+ vscode_dir = repo_root / ".vscode"
46
+ mcp_config_path = vscode_dir / "mcp.json"
47
+
48
+ # Create the .vscode directory if it doesn't exist
49
+ try:
50
+ if not vscode_dir.exists():
51
+ vscode_dir.mkdir(parents=True, exist_ok=True)
52
+ except Exception as e:
53
+ print(f"Warning: Could not create .vscode directory: {e}")
54
+
55
+ return str(mcp_config_path)
56
+
57
+ def update_config(self, config_updates):
58
+ """Update the VSCode MCP configuration with new values.
59
+
60
+ Args:
61
+ config_updates (dict): Dictionary of settings to update.
62
+
63
+ Returns:
64
+ bool: True if successful, False otherwise.
65
+ """
66
+ config_path = self.get_config_path()
67
+
68
+ try:
69
+ # Read existing config or create a new one
70
+ try:
71
+ with open(config_path, "r", encoding="utf-8") as f:
72
+ config = json.load(f)
73
+ except (FileNotFoundError, json.JSONDecodeError):
74
+ config = {}
75
+
76
+ # Update config with new values or remove entries set to None
77
+ for key, value in config_updates.items():
78
+ if value is None:
79
+ # Remove the entry if it exists
80
+ if key in config:
81
+ del config[key]
82
+ else:
83
+ config[key] = value
84
+
85
+ # Write the updated config
86
+ with open(config_path, "w", encoding="utf-8") as f:
87
+ json.dump(config, f, indent=2)
88
+
89
+ return True
90
+ except Exception as e:
91
+ print(f"Error updating VSCode MCP configuration: {e}")
92
+ return False
93
+
94
+ def get_current_config(self):
95
+ """Get the current VSCode MCP configuration.
96
+
97
+ Returns:
98
+ dict: Current VSCode MCP configuration from the local .vscode/mcp.json file.
99
+ """
100
+ config_path = self.get_config_path()
101
+
102
+ try:
103
+ try:
104
+ with open(config_path, "r", encoding="utf-8") as f:
105
+ return json.load(f)
106
+ except (FileNotFoundError, json.JSONDecodeError):
107
+ return {}
108
+ except Exception as e:
109
+ print(f"Error reading VSCode MCP configuration: {e}")
110
+ return {}
111
+
112
+ def configure_mcp_server(self, server_url, server_name=None, enabled=True):
113
+ """Configure an MCP server in VSCode configuration.
114
+
115
+ This method follows the VSCode documentation for MCP server configuration format:
116
+ https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_configuration-format
117
+
118
+ Args:
119
+ server_url (str): URL or identifier of the MCP server.
120
+ server_name (str, optional): Name of the server. Defaults to None.
121
+ enabled (bool, optional): Ignored parameter, kept for API compatibility.
122
+
123
+ Returns:
124
+ bool: True if successful, False otherwise.
125
+ """
126
+ if not server_url:
127
+ print("Error: server_url cannot be empty")
128
+ return False
129
+
130
+ if not server_name:
131
+ server_name = server_url
132
+
133
+ try:
134
+ # Use enhanced lookup with multiple strategies
135
+ server_info = self.registry_client.find_server_by_reference(server_url)
136
+
137
+ # Fail if server is not found in registry - security requirement
138
+ if not server_info:
139
+ raise ValueError(f"Failed to retrieve server details for '{server_url}'. Server not found in registry.")
140
+
141
+ # Format server configuration and get input variables if any
142
+ server_config, input_vars = self._format_server_config(server_info)
143
+
144
+ config = self.get_current_config()
145
+
146
+ # Make sure we have the servers object
147
+ if "servers" not in config:
148
+ config["servers"] = {}
149
+
150
+ # Add input variables if any
151
+ if input_vars:
152
+ if "inputs" not in config:
153
+ config["inputs"] = []
154
+ # Merge with existing inputs, avoiding duplicates by id
155
+ existing_input_ids = [input_var.get("id") for input_var in config.get("inputs", [])]
156
+ for input_var in input_vars:
157
+ if input_var.get("id") not in existing_input_ids:
158
+ config["inputs"].append(input_var)
159
+
160
+ # Add the server configuration
161
+ config["servers"][server_name] = server_config
162
+
163
+ # Update the configuration
164
+ return self.update_config(config)
165
+
166
+ except ValueError as ve:
167
+ # Re-raise ValueError to indicate missing server details
168
+ raise ve
169
+ except Exception as e:
170
+ print(f"Error configuring MCP server: {e}")
171
+ return False
172
+
173
+ def _format_server_config(self, server_info):
174
+ """Format server details into VSCode mcp.json compatible format.
175
+
176
+ Args:
177
+ server_info (dict): Server information from registry.
178
+
179
+ Returns:
180
+ tuple: (server_config, input_vars) where:
181
+ - server_config is the formatted server configuration for mcp.json
182
+ - input_vars is a list of input variable definitions
183
+ """
184
+ # Initialize the base config structure
185
+ server_config = {}
186
+ input_vars = []
187
+
188
+ # Check for packages information
189
+ if "packages" in server_info and server_info["packages"]:
190
+ package = server_info["packages"][0]
191
+ runtime_hint = package.get("runtime_hint", "")
192
+
193
+ # Handle npm packages
194
+ if runtime_hint == "npx" or "npm" in package.get("registry_name", "").lower():
195
+ # Get args directly from runtime_arguments
196
+ args = []
197
+ if "runtime_arguments" in package and package["runtime_arguments"]:
198
+ for arg in package["runtime_arguments"]:
199
+ if arg.get("is_required", False) and arg.get("value_hint"):
200
+ args.append(arg.get("value_hint"))
201
+
202
+ # Fallback if no runtime_arguments are provided
203
+ if not args and package.get("name"):
204
+ args = [package.get("name")]
205
+
206
+ server_config = {
207
+ "type": "stdio",
208
+ "command": "npx",
209
+ "args": args
210
+ }
211
+
212
+ # Handle docker packages
213
+ elif runtime_hint == "docker":
214
+ # Get args directly from runtime_arguments
215
+ args = []
216
+ if "runtime_arguments" in package and package["runtime_arguments"]:
217
+ for arg in package["runtime_arguments"]:
218
+ if arg.get("is_required", False) and arg.get("value_hint"):
219
+ args.append(arg.get("value_hint"))
220
+
221
+ # Fallback if no runtime_arguments are provided - use standard docker run command
222
+ if not args:
223
+ args = ["run", "-i", "--rm", package.get("name")]
224
+
225
+ server_config = {
226
+ "type": "stdio",
227
+ "command": "docker",
228
+ "args": args
229
+ }
230
+
231
+ # Handle Python packages
232
+ elif runtime_hint in ["uvx", "pip", "python"] or "python" in runtime_hint or package.get("registry_name", "").lower() == "pypi":
233
+ # Determine the command based on runtime_hint
234
+ if runtime_hint == "uvx":
235
+ command = "uvx"
236
+ elif "python" in runtime_hint:
237
+ # Use the specified Python path if it's a full path, otherwise default to python3
238
+ command = "python3" if runtime_hint in ["python", "pip"] else runtime_hint
239
+ else:
240
+ command = "python3"
241
+
242
+ # Get args directly from runtime_arguments
243
+ args = []
244
+ if "runtime_arguments" in package and package["runtime_arguments"]:
245
+ for arg in package["runtime_arguments"]:
246
+ if arg.get("is_required", False) and arg.get("value_hint"):
247
+ args.append(arg.get("value_hint"))
248
+
249
+ # Fallback if no runtime_arguments are provided
250
+ if not args:
251
+ if runtime_hint == "uvx":
252
+ module_name = package.get("name", "").replace("mcp-server-", "")
253
+ args = [f"mcp-server-{module_name}"]
254
+ else:
255
+ module_name = package.get("name", "").replace("mcp-server-", "").replace("-", "_")
256
+ args = ["-m", f"mcp_server_{module_name}"]
257
+
258
+ server_config = {
259
+ "type": "stdio",
260
+ "command": command,
261
+ "args": args
262
+ }
263
+
264
+ # Add environment variables if present
265
+ if "environment_variables" in package and package["environment_variables"]:
266
+ server_config["env"] = {}
267
+ for env_var in package["environment_variables"]:
268
+ if "name" in env_var:
269
+ # Convert variable name to lowercase and replace underscores with hyphens for VS Code convention
270
+ input_var_name = env_var["name"].lower().replace("_", "-")
271
+
272
+ # Create the input variable reference
273
+ server_config["env"][env_var["name"]] = f"${{input:{input_var_name}}}"
274
+
275
+ # Create the input variable definition
276
+ input_var_def = {
277
+ "type": "promptString",
278
+ "id": input_var_name,
279
+ "description": env_var.get("description", f"{env_var['name']} for MCP server"),
280
+ "password": True # Default to True for security
281
+ }
282
+ input_vars.append(input_var_def)
283
+
284
+ # If no server config was created from packages, check for other server types
285
+ if not server_config:
286
+ # Check for SSE endpoints
287
+ if "sse_endpoint" in server_info:
288
+ server_config = {
289
+ "type": "sse",
290
+ "url": server_info["sse_endpoint"],
291
+ "headers": server_info.get("sse_headers", {})
292
+ }
293
+ # Default fallback
294
+ else:
295
+ server_config = {
296
+ "type": "stdio",
297
+ "command": "uvx",
298
+ "args": [f"mcp-server-{server_info.get('name', '')}"]
299
+ }
300
+
301
+ return server_config, input_vars
@@ -0,0 +1 @@
1
+ """Package manager adapters package."""
@@ -0,0 +1,27 @@
1
+ """Base adapter interface for MCP package managers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class MCPPackageManagerAdapter(ABC):
7
+ """Base adapter for MCP package managers."""
8
+
9
+ @abstractmethod
10
+ def install(self, package_name, version=None):
11
+ """Install an MCP package."""
12
+ pass
13
+
14
+ @abstractmethod
15
+ def uninstall(self, package_name):
16
+ """Uninstall an MCP package."""
17
+ pass
18
+
19
+ @abstractmethod
20
+ def list_installed(self):
21
+ """List all installed MCP packages."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def search(self, query):
26
+ """Search for MCP packages."""
27
+ pass
@@ -0,0 +1,123 @@
1
+ """Implementation of the default MCP package manager."""
2
+
3
+ from .base import MCPPackageManagerAdapter
4
+ from ...config import get_default_client
5
+ from ...registry.integration import RegistryIntegration
6
+
7
+
8
+ class DefaultMCPPackageManager(MCPPackageManagerAdapter):
9
+ """Implementation of the default MCP package manager."""
10
+
11
+ def install(self, package_name, version=None):
12
+ """Install an MCP package.
13
+
14
+ Args:
15
+ package_name (str): Name of the package to install.
16
+ version (str, optional): Version of the package to install.
17
+
18
+ Returns:
19
+ bool: True if successful, False otherwise.
20
+ """
21
+
22
+ try:
23
+ # Import here to avoid circular import
24
+ from ...factory import ClientFactory
25
+
26
+ client_type = get_default_client()
27
+ client_adapter = ClientFactory.create_client(client_type)
28
+
29
+ # For VSCode, configure MCP server in mcp.json
30
+ result = client_adapter.configure_mcp_server(package_name, package_name, True)
31
+
32
+ if result:
33
+ print(f"Successfully installed {package_name}")
34
+ return result
35
+ except Exception as e:
36
+ print(f"Error installing package {package_name}: {e}")
37
+ return False
38
+
39
+ def uninstall(self, package_name):
40
+ """Uninstall an MCP package.
41
+
42
+ Args:
43
+ package_name (str): Name of the package to uninstall.
44
+
45
+ Returns:
46
+ bool: True if successful, False otherwise.
47
+ """
48
+
49
+ try:
50
+ # Import here to avoid circular import
51
+ from ...factory import ClientFactory
52
+
53
+ client_type = get_default_client()
54
+ client_adapter = ClientFactory.create_client(client_type)
55
+ config = client_adapter.get_current_config()
56
+
57
+ # For VSCode, remove the server from mcp.json
58
+ if "servers" in config and package_name in config["servers"]:
59
+ servers = config["servers"]
60
+ servers.pop(package_name, None)
61
+ result = client_adapter.update_config({"servers": servers})
62
+
63
+ if result:
64
+ print(f"Successfully uninstalled {package_name}")
65
+ return result
66
+ else:
67
+ print(f"Package {package_name} not found in configuration")
68
+ return False
69
+
70
+ except Exception as e:
71
+ print(f"Error uninstalling package {package_name}: {e}")
72
+ return False
73
+
74
+ def list_installed(self):
75
+ """List all installed MCP packages.
76
+
77
+ Returns:
78
+ list: List of installed packages.
79
+ """
80
+
81
+ try:
82
+ # Import here to avoid circular import
83
+ from ...factory import ClientFactory
84
+
85
+ # Get client type from configuration (default is vscode)
86
+ client_type = get_default_client()
87
+
88
+ # Create client adapter
89
+ client_adapter = ClientFactory.create_client(client_type)
90
+
91
+ # Get config from local .vscode/mcp.json file
92
+ config = client_adapter.get_current_config()
93
+
94
+ # Extract server names from the config
95
+ servers = config.get("servers", {})
96
+
97
+ # Return the list of server names
98
+ return list(servers.keys())
99
+ except Exception as e:
100
+ print(f"Error retrieving installed MCP servers: {e}")
101
+ return []
102
+
103
+ def search(self, query):
104
+ """Search for MCP packages.
105
+
106
+ Args:
107
+ query (str): Search query.
108
+
109
+ Returns:
110
+ list: List of packages matching the query.
111
+ """
112
+
113
+ try:
114
+ # Use the registry integration to search for packages
115
+ registry = RegistryIntegration()
116
+ packages = registry.search_packages(query)
117
+
118
+ # Return the list of package IDs/names
119
+ return [pkg.get("id", pkg.get("name", "Unknown")) for pkg in packages] if packages else []
120
+
121
+ except Exception as e:
122
+ print(f"Error searching for packages: {e}")
123
+ return []