py-mcpdock-cli 1.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,182 @@
1
+ import click
2
+ import json
3
+ import asyncio
4
+ from typing import Dict, Any, Optional
5
+
6
+ from rich import print as rprint
7
+ from rich.console import Console
8
+ from rich.spinner import Spinner
9
+
10
+ from ..utils.logger import verbose, info, debug, error, warning
11
+ from ..registry import resolve_package
12
+ from ..utils.runtime import ensure_uv_installed, ensure_bun_installed, check_and_notify_remote_server
13
+ from ..utils.config import choose_connection, collect_config_values, get_server_name, format_server_config, format_config_values
14
+ from ..config.client_config import read_config, write_config
15
+ from ..types.registry import ConfiguredServer, ClientConfig
16
+
17
+
18
+ async def validate_config_values(
19
+ connection: Any,
20
+ existing_values: Optional[Dict[str, Any]] = None,
21
+ ) -> Dict[str, Any]:
22
+ """
23
+ Validates config values without prompting for user input.
24
+ Only checks if required values are present.
25
+
26
+ Args:
27
+ connection: Connection details containing schema
28
+ existing_values: Existing config values to validate
29
+
30
+ Returns:
31
+ Dictionary with validated config values
32
+
33
+ Raises:
34
+ ValueError: If required configuration values are missing
35
+ """
36
+ schema = connection.configSchema or {}
37
+ env = schema.env if schema else {}
38
+ if not env:
39
+ return {}
40
+
41
+ # Use existing values or empty dict
42
+ config_values = existing_values or {}
43
+
44
+ # Get list of required env variables
45
+ required = set(schema.required if schema else [])
46
+
47
+ # Check if all required env variables are provided
48
+ missing_required = []
49
+ for key in required:
50
+ if key not in config_values or config_values[key] is None:
51
+ missing_required.append(key)
52
+
53
+ # Raise error if any required env variables are missing
54
+ if missing_required:
55
+ missing_fields = ", ".join(missing_required)
56
+ raise ValueError(f"Missing required configuration values: {missing_fields}")
57
+
58
+ # Format and return the validated config
59
+ return format_config_values(connection, config_values)
60
+
61
+
62
+ async def install_mcp_server(
63
+ package_identifier: str,
64
+ target_client: Optional[str] = None,
65
+ initial_config: Optional[Dict[str, Any]] = None,
66
+ api_key: Optional[str] = None
67
+ ) -> None:
68
+ """
69
+ Installs and configures an AI server package for a specified client.
70
+
71
+ Args:
72
+ package_identifier: The fully qualified name of the AI server package
73
+ target_client: The AI client to configure the server for
74
+ initial_config: Optional pre-defined configuration values
75
+ api_key: Optional API key for fetching saved configurations
76
+ """
77
+ target_client = target_client or "claude" # Default to Claude if no client specified
78
+ verbose(f"Initiating installation of {package_identifier} for {target_client}")
79
+
80
+ console = Console()
81
+
82
+ # Create and start spinner for package resolution
83
+ with console.status(f"Resolving {package_identifier}...", spinner="dots") as status:
84
+ try:
85
+ # Resolve the package (fetch server metadata)
86
+ verbose(f"Resolving package: {package_identifier}")
87
+ server_data = await resolve_package(package_identifier)
88
+ verbose(f"Package resolved successfully: {server_data.qualifiedName}")
89
+
90
+ # Choose the appropriate connection type
91
+ verbose("Choosing connection type...")
92
+ connection = choose_connection(server_data)
93
+ verbose(f"Selected connection type: {connection.type}")
94
+
95
+ # Check for required runtimes and install if needed
96
+ # Commented out as these are specific to JS environment
97
+ # await ensure_uv_installed(connection)
98
+ # await ensure_bun_installed(connection)
99
+
100
+ # Inform users of remote server installation if applicable
101
+ is_remote = check_and_notify_remote_server(server_data)
102
+ if is_remote:
103
+ verbose("Remote server detected, notification displayed")
104
+
105
+ # Get the validated config values, no prompting
106
+ status.update(f"Validating configuration for {package_identifier}...")
107
+ collected_config_values = await validate_config_values(connection, initial_config)
108
+
109
+ # Determine if we need to pass config flag
110
+ config_flag_needed = initial_config is not None
111
+
112
+ verbose(f"Config values validated: {json.dumps(collected_config_values, indent=2)}")
113
+ verbose(f"Using config flag: {config_flag_needed}")
114
+
115
+ # Format the server configuration
116
+ verbose("Formatting server configuration...")
117
+ server_config = format_server_config(
118
+ package_identifier,
119
+ collected_config_values,
120
+ api_key,
121
+ config_flag_needed,
122
+ )
123
+ verbose(f"Formatted server config: {json.dumps(server_config.__dict__, indent=2)}")
124
+
125
+ # Read existing config from client
126
+ status.update(f"Installing for {target_client}...")
127
+ verbose(f"Reading configuration for client: {target_client}")
128
+ client_config = read_config(target_client)
129
+
130
+ # Get normalized server name to use as key
131
+ server_name = get_server_name(package_identifier)
132
+ verbose(f"Normalized server ID: {server_name}")
133
+
134
+ # Update client configuration with new server
135
+ verbose("Updating client configuration...")
136
+ if not isinstance(client_config.mcpServers, dict):
137
+ # Initialize if needed
138
+ client_config.mcpServers = {}
139
+
140
+ # Add the new server config
141
+ client_config.mcpServers[server_name] = server_config
142
+
143
+ # Write updated configuration
144
+ verbose("Writing updated configuration...")
145
+ write_config(client_config, target_client)
146
+ verbose("Configuration successfully written")
147
+
148
+ rprint(f"[green]{package_identifier} successfully installed for {target_client}[/green]")
149
+
150
+ # No prompt for client restart
151
+ verbose("Installation completed successfully")
152
+
153
+ except Exception as e:
154
+ verbose(f"Installation error: {str(e)}")
155
+ rprint(f"[red]Failed to install {package_identifier}[/red]")
156
+ rprint(f"[red]Error: {str(e)}[/red]")
157
+ raise
158
+
159
+
160
+ @click.command("install")
161
+ @click.argument("mcp_server", type=click.STRING)
162
+ @click.option("--client", type=click.STRING, help="Specify the target AI client")
163
+ @click.option("--env", "env_json", type=click.STRING, help="Provide configuration as JSON (bypasses interactive prompts)")
164
+ @click.option("--api-key", type=click.STRING, help="Provide an API key for fetching saved configurations")
165
+ def install(mcp_server: str, client: Optional[str], env_json: Optional[str], api_key: Optional[str]):
166
+ """Install an AI mcp-server with optional configuration."""
167
+ click.echo(f"Attempting to install {mcp_server}...")
168
+ user_config = None
169
+ if env_json:
170
+ try:
171
+ user_config = json.loads(env_json)
172
+ click.echo(f"Using provided config: {user_config}")
173
+ except json.JSONDecodeError as e:
174
+ click.echo(f"Error: Invalid JSON provided for --config: {e}", err=True)
175
+ return
176
+
177
+ try:
178
+ # Run the async installation process
179
+ asyncio.run(install_mcp_server(mcp_server, client, user_config, api_key))
180
+ except Exception as e:
181
+ click.echo(f"Installation failed: {str(e)}", err=True)
182
+ return 1
cli/commands/run.py ADDED
@@ -0,0 +1,148 @@
1
+ import click
2
+ import json
3
+ import asyncio
4
+ from typing import Dict, Any, Optional
5
+
6
+ from rich import print as rprint
7
+ from rich.console import Console
8
+
9
+ from ..utils.logger import verbose
10
+ from ..registry import resolve_package
11
+ from ..utils.config import choose_connection, format_run_config_values
12
+ from ..utils.runtime import check_and_notify_remote_server
13
+ from ..types.registry import RegistryServer, ConnectionDetails
14
+ from ..runners import create_stdio_runner, create_ws_runner, run_package_with_command, create_stream_http_runner
15
+
16
+
17
+ # These runner functions have been moved to their respective modules in the runners package
18
+
19
+ async def pick_server_and_run(
20
+ server_details: RegistryServer,
21
+ config: Dict[str, Any],
22
+ api_key: Optional[str] = None,
23
+ analytics_enabled: bool = False
24
+ ) -> None:
25
+ """
26
+ Selects the appropriate runner and starts the server.
27
+ """
28
+ connection = choose_connection(server_details)
29
+
30
+ if connection.type == "ws":
31
+ if not connection.deploymentUrl:
32
+ raise ValueError("Missing deployment URL")
33
+ await create_ws_runner(connection.deploymentUrl, config, api_key)
34
+ elif connection.type == "stdio":
35
+ await create_stdio_runner(server_details, config, api_key, analytics_enabled)
36
+ elif connection.type == "stream-http":
37
+ await create_stream_http_runner(server_details, config, api_key, analytics_enabled)
38
+ else:
39
+ raise ValueError(f"Unsupported connection type: {connection.type}")
40
+
41
+
42
+ async def run_server(
43
+ qualified_name: str,
44
+ config: Dict[str, Any],
45
+ api_key: Optional[str] = None
46
+ ) -> None:
47
+ """
48
+ Runs a server with the specified configuration.
49
+
50
+ The qualified_name can be in these formats:
51
+ - Standard MCP server name: "company/package"
52
+ - uv command: "uv:package_name [args]"
53
+ - npx command: "npx:package_name [args]"
54
+ """
55
+ try:
56
+ # # Check if this is a special command format (uv: or npx:)
57
+ # if qualified_name.startswith("uv:") or qualified_name.startswith("npx:"):
58
+ # command_parts = qualified_name.split(":", 1)
59
+ # command_type = command_parts[0].lower()
60
+ # package_spec = command_parts[1]
61
+
62
+ # # Extract package name and args if provided
63
+ # package_parts = package_spec.split(" ", 1)
64
+ # package_name = package_parts[0]
65
+ # args = package_parts[1].split() if len(package_parts) > 1 else []
66
+
67
+ # verbose(f"Running {command_type} command for package: {package_name}")
68
+ # await run_package_with_command(
69
+ # command_type,
70
+ # package_name,
71
+ # args,
72
+ # config,
73
+ # api_key
74
+ # )
75
+ # return
76
+
77
+ # Initialize settings for regular MCP server
78
+ verbose("Initializing runtime environment settings")
79
+
80
+ # Resolve server package
81
+ verbose(f"Resolving server package: {qualified_name}")
82
+ resolved_server = await resolve_package(qualified_name)
83
+
84
+ if not resolved_server:
85
+ raise ValueError(f"Could not resolve server: {qualified_name}")
86
+
87
+ # Format the final configuration with validation
88
+ connection = choose_connection(resolved_server)
89
+ final_config = format_run_config_values(connection, config)
90
+
91
+ # Inform about remote server if applicable
92
+ # check_and_notify_remote_server(resolved_server)
93
+
94
+ rprint(f"[blue][Runner] Connecting to server:[/blue] {resolved_server.qualifiedName}")
95
+ rprint(f"Connection types: {[c.type for c in resolved_server.connections]}")
96
+
97
+ # Assume analytics is disabled for now
98
+ analytics_enabled = False
99
+
100
+ # Run the server with the appropriate runner
101
+ await pick_server_and_run(
102
+ resolved_server,
103
+ final_config,
104
+ api_key,
105
+ analytics_enabled
106
+ )
107
+ except Exception as e:
108
+ rprint(f"[red][Runner] Fatal error:[/red] {str(e)}")
109
+ raise
110
+
111
+
112
+ @click.command("run")
113
+ @click.argument("mcp_server", type=click.STRING)
114
+ @click.option("--config", "config_json", type=click.STRING, help="Provide JSON format configuration")
115
+ @click.option("--api-key", type=click.STRING, help="API key for retrieving saved configuration")
116
+ def run(mcp_server: str, config_json: Optional[str], api_key: Optional[str]):
117
+ """Run an AI MCP server."""
118
+ click.echo(f"Attempting to run {mcp_server}...")
119
+ server_config = None
120
+
121
+ # Parse command line provided configuration
122
+ if config_json:
123
+ try:
124
+ server_config = json.loads(config_json)
125
+ click.echo(f"Using provided config: {server_config}")
126
+ except json.JSONDecodeError as e:
127
+ click.echo(f"Error: Invalid JSON provided for --config: {e}", err=True)
128
+ return 1
129
+
130
+ # If no config provided, use empty dict
131
+ if server_config is None:
132
+ verbose("No config provided, running with empty config")
133
+ server_config = {}
134
+
135
+ if api_key:
136
+ verbose(f"API key provided: {'*' * len(api_key)}")
137
+
138
+ console = Console()
139
+
140
+ try:
141
+ # with console.status(f"Starting {mcp_server}...", spinner="dots") as status:
142
+ # Run the server asynchronously
143
+ verbose('running server....')
144
+ asyncio.run(run_server(mcp_server, server_config, api_key))
145
+ # status.update(f"Successfully started {mcp_server}")
146
+ except Exception as e:
147
+ click.echo(f"Run failed: {str(e)}", err=True)
148
+ return 1
cli/config/__init__.py ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ # config/app_config.py
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ # Load environment variables from a .env file
6
+ load_dotenv()
7
+
8
+
9
+ APP_NAME = os.getenv("APP_NAME", "My Application")
10
+ APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
11
+ APP_ENV = os.getenv('ENV')
@@ -0,0 +1,248 @@
1
+ import os
2
+ import json
3
+ import sys
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List, Optional, Union, Literal, TypedDict
7
+
8
+ # Import types from the registry module
9
+ from ..types.registry import ClientConfig, ConfiguredServer
10
+ # Import logger
11
+ from ..utils.logger import verbose
12
+
13
+ # Define types for configuration targets
14
+
15
+
16
+ class ClientFileTarget(TypedDict):
17
+ type: Literal["file"]
18
+ path: Path
19
+
20
+
21
+ class ClientCommandTarget(TypedDict):
22
+ type: Literal["command"]
23
+ command: str
24
+ # Potentially add args template if needed later
25
+
26
+
27
+ ClientInstallTarget = Union[ClientFileTarget, ClientCommandTarget]
28
+
29
+ # Determine platform-specific paths using pathlib
30
+ home_dir = Path.home()
31
+
32
+ if sys.platform == "win32":
33
+ base_dir = Path(os.environ.get("APPDATA", home_dir / "AppData" / "Roaming"))
34
+ vscode_storage_path = Path("Code") / "User" / "globalStorage"
35
+ code_command = "code.cmd"
36
+ code_insiders_command = "code-insiders.cmd"
37
+ elif sys.platform == "darwin":
38
+ base_dir = home_dir / "Library" / "Application Support"
39
+ vscode_storage_path = Path("Code") / "User" / "globalStorage"
40
+ code_command = "code"
41
+ code_insiders_command = "code-insiders"
42
+ else: # Assume Linux/other Unix-like
43
+ base_dir = Path(os.environ.get("XDG_CONFIG_HOME", home_dir / ".config"))
44
+ vscode_storage_path = Path("Code/User/globalStorage") # Note: Path combines these
45
+ code_command = "code"
46
+ code_insiders_command = "code-insiders"
47
+
48
+ default_claude_path = base_dir / "Claude" / "claude_desktop_config.json"
49
+
50
+ # Define client paths using platform-specific base directories
51
+ # Using lowercase keys for normalization
52
+ CLIENT_PATHS: Dict[str, ClientInstallTarget] = {
53
+ "claude": {"type": "file", "path": default_claude_path},
54
+ "cline": {
55
+ "type": "file",
56
+ "path": base_dir / vscode_storage_path / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
57
+ },
58
+ "roo-cline": {
59
+ "type": "file",
60
+ "path": base_dir / vscode_storage_path / "rooveterinaryinc.roo-cline" / "settings" / "cline_mcp_settings.json",
61
+ },
62
+ "windsurf": {"type": "file", "path": home_dir / ".codeium" / "windsurf" / "mcp_config.json"},
63
+ "witsy": {"type": "file", "path": base_dir / "Witsy" / "settings.json"},
64
+ "enconvo": {"type": "file", "path": home_dir / ".config" / "enconvo" / "mcp_config.json"},
65
+ "cursor": {"type": "file", "path": home_dir / ".cursor" / "mcp.json"},
66
+ "vscode": {"type": "command", "command": code_command},
67
+ "vscode-insiders": {"type": "command", "command": code_insiders_command},
68
+ }
69
+
70
+
71
+ def get_config_target(client: Optional[str] = None) -> ClientInstallTarget:
72
+ """Gets the configuration target (file path or command) for a given client."""
73
+ normalized_client = (client or "claude").lower()
74
+ verbose(f"Getting config target for client: {normalized_client}")
75
+
76
+ target = CLIENT_PATHS.get(normalized_client)
77
+
78
+ if target:
79
+ verbose(f"Config target resolved to: {target}")
80
+ return target
81
+ else:
82
+ # Fallback for unknown clients (similar to JS version)
83
+ fallback_path = default_claude_path.parent.parent / (client or "claude") / f"{normalized_client}_config.json"
84
+ fallback_target: ClientFileTarget = {"type": "file", "path": fallback_path}
85
+ verbose(f"Client '{normalized_client}' not predefined, using fallback path: {fallback_target}")
86
+ return fallback_target
87
+
88
+
89
+ def read_config(client: Optional[str] = None) -> ClientConfig:
90
+ """Reads the configuration for the specified client."""
91
+ client_name = (client or "claude").lower()
92
+ verbose(f"Reading config for client: {client_name}")
93
+ target = get_config_target(client_name)
94
+
95
+ if target["type"] == "command":
96
+ verbose(f"Client '{client_name}' uses command-based config. Reading not supported.")
97
+ # Command-based installers don't support reading config back easily
98
+ return ClientConfig(mcpServers={})
99
+
100
+ config_path = target["path"]
101
+ verbose(f"Checking if config file exists at: {config_path}")
102
+ if not config_path.exists():
103
+ verbose("Config file not found, returning default empty config.")
104
+ return ClientConfig(mcpServers={})
105
+
106
+ try:
107
+ verbose("Reading config file content.")
108
+ raw_content = config_path.read_text(encoding="utf-8")
109
+ raw_config = json.loads(raw_content)
110
+ verbose(f"Config loaded successfully: {json.dumps(raw_config, indent=2)}")
111
+
112
+ # Ensure mcpServers key exists, return as ClientConfig object
113
+ mcp_servers_data = raw_config.get("mcpServers", {})
114
+ # Basic validation/conversion if needed, assuming structure matches ConfiguredServer for now
115
+ configured_servers = {
116
+ name: ConfiguredServer(**server_data)
117
+ for name, server_data in mcp_servers_data.items()
118
+ if isinstance(server_data, dict) and "command" in server_data and "args" in server_data
119
+ }
120
+
121
+ # Include other top-level keys from the config file
122
+ other_config = {k: v for k, v in raw_config.items() if k != "mcpServers"}
123
+
124
+ return ClientConfig(mcpServers=configured_servers, **other_config)
125
+
126
+ except json.JSONDecodeError as e:
127
+ verbose(f"Error decoding JSON from {config_path}: {e}")
128
+ return ClientConfig(mcpServers={})
129
+ except Exception as e:
130
+ verbose(f"Error reading config file {config_path}: {e}")
131
+ return ClientConfig(mcpServers={})
132
+
133
+
134
+ def _write_config_command(config: ClientConfig, target: ClientCommandTarget) -> None:
135
+ """Writes configuration using a command (e.g., for VS Code)."""
136
+ command = target["command"]
137
+ args: List[str] = []
138
+
139
+ # Convert ConfiguredServer back to dict for JSON stringify
140
+ for name, server in config.mcpServers.items():
141
+ server_dict = {"command": server.command, "args": server.args, "name": name}
142
+ args.extend(["--add-mcp", json.dumps(server_dict)])
143
+
144
+ full_command = [command] + args
145
+ verbose(f"Running command: {' '.join(full_command)}")
146
+
147
+ try:
148
+ # Use shell=True cautiously if command might not be directly executable
149
+ # Or better, ensure the command is in PATH
150
+ result = subprocess.run(full_command, capture_output=True, text=True, check=True, encoding='utf-8')
151
+ verbose(f"Executed command successfully. Output:\n{result.stdout}")
152
+ if result.stderr:
153
+ verbose(f"Command stderr:\n{result.stderr}")
154
+ except FileNotFoundError:
155
+ verbose(f"Error: Command '{command}' not found. Make sure it is installed and in your PATH.")
156
+ raise FileNotFoundError(f"Command '{command}' not found.")
157
+ except subprocess.CalledProcessError as e:
158
+ verbose(f"Error executing command. Return code: {e.returncode}")
159
+ verbose(f"Stdout:\n{e.stdout}")
160
+ verbose(f"Stderr:\n{e.stderr}")
161
+ raise RuntimeError(f"Command '{command}' failed: {e.stderr or e.stdout}")
162
+ except Exception as e:
163
+ verbose(f"An unexpected error occurred while running the command: {e}")
164
+ raise
165
+
166
+
167
+ def _write_config_file(config: ClientConfig, target: ClientFileTarget) -> None:
168
+ """Writes configuration to a file, merging with existing content."""
169
+ config_path = target["path"]
170
+ config_dir = config_path.parent
171
+
172
+ verbose(f"Ensuring config directory exists: {config_dir}")
173
+ config_dir.mkdir(parents=True, exist_ok=True)
174
+
175
+ existing_config_data: Dict[str, Any] = {}
176
+ if config_path.exists():
177
+ try:
178
+ verbose("Reading existing config file for merging.")
179
+ existing_config_data = json.loads(config_path.read_text(encoding="utf-8"))
180
+ verbose(f"Existing config loaded: {json.dumps(existing_config_data, indent=2)}")
181
+ except json.JSONDecodeError as e:
182
+ verbose(f"Error reading existing config file {config_path} for merge: {e}. Will overwrite.")
183
+ except Exception as e:
184
+ verbose(f"Error reading existing config file {config_path}: {e}. Will overwrite.")
185
+
186
+ # Prepare the config to be written (convert dataclasses back to dicts)
187
+ config_to_write = {
188
+ **config.__dict__, # Include other top-level keys if ClientConfig has them
189
+ "mcpServers": {
190
+ name: {"command": server.command, "args": server.args, "env": server.env}
191
+ for name, server in config.mcpServers.items()
192
+ }
193
+ }
194
+
195
+ # Merge configurations
196
+ verbose("Merging configurations.")
197
+ merged_config = {
198
+ **existing_config_data,
199
+ **config_to_write,
200
+ # Ensure mcpServers are merged correctly
201
+ "mcpServers": {
202
+ **(existing_config_data.get("mcpServers") or {}),
203
+ **(config_to_write.get("mcpServers") or {}),
204
+ }
205
+ }
206
+ verbose(f"Merged config: {json.dumps(merged_config, indent=2)}")
207
+
208
+ try:
209
+ verbose(f"Writing config to file: {config_path}")
210
+ config_path.write_text(json.dumps(merged_config, indent=2), encoding="utf-8")
211
+ verbose(f"Configuration successfully written to {config_path}")
212
+ except Exception as e:
213
+ verbose(f"Error writing config file {config_path}: {e}")
214
+ raise IOError(f"Failed to write configuration to {config_path}") from e
215
+
216
+
217
+ def write_config(config: ClientConfig, client: Optional[str] = None) -> None:
218
+ """Writes the configuration for the specified client, either to a file or via command."""
219
+ client_name = (client or "claude").lower()
220
+ verbose(f"Writing config for client: {client_name}")
221
+ verbose(f"Config data: {config}") # Dataclass repr is usually good
222
+
223
+ if not isinstance(config, ClientConfig) or not isinstance(config.mcpServers, dict):
224
+ verbose("Invalid config object provided.")
225
+ raise TypeError("Invalid ClientConfig object provided to write_config")
226
+
227
+ target = get_config_target(client_name)
228
+
229
+ if target["type"] == "command":
230
+ _write_config_command(config, target)
231
+ else:
232
+ # Prepare the configuration to be written
233
+ cleaned_config = {
234
+ **config.__dict__,
235
+ "mcpServers": {
236
+ name: server.__dict__ for name, server in config.mcpServers.items()
237
+ }
238
+ }
239
+ # Deserialize back into ConfiguredServer objects after writing
240
+ deserialized_config = ClientConfig(
241
+ **{
242
+ **cleaned_config,
243
+ "mcpServers": {
244
+ name: ConfiguredServer(**server) for name, server in cleaned_config["mcpServers"].items()
245
+ }
246
+ }
247
+ )
248
+ _write_config_file(deserialized_config, target)
cli/main.py ADDED
@@ -0,0 +1,12 @@
1
+ import click
2
+ from .commands.install import install
3
+ from .commands.run import run
4
+
5
+ @click.group()
6
+ @click.version_option(version="1.0.0", package_name="py-cli") # Corresponds to program.version("1.0.0")
7
+ def cli():
8
+ """A custom CLI tool translated to Python."""
9
+ pass
10
+
11
+ cli.add_command(install)
12
+ cli.add_command(run)