py-mcpdock-cli 1.0.13__tar.gz
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.
- py_mcpdock_cli-1.0.13/MANIFEST.in +1 -0
- py_mcpdock_cli-1.0.13/PKG-INFO +28 -0
- py_mcpdock_cli-1.0.13/README.md +4 -0
- py_mcpdock_cli-1.0.13/pyproject.toml +41 -0
- py_mcpdock_cli-1.0.13/setup.cfg +11 -0
- py_mcpdock_cli-1.0.13/src/cli/__init__.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/commands/__init__.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/commands/install.py +182 -0
- py_mcpdock_cli-1.0.13/src/cli/commands/run.py +148 -0
- py_mcpdock_cli-1.0.13/src/cli/config/__init__.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/config/app_config.py +11 -0
- py_mcpdock_cli-1.0.13/src/cli/config/client_config.py +248 -0
- py_mcpdock_cli-1.0.13/src/cli/main.py +12 -0
- py_mcpdock_cli-1.0.13/src/cli/mock_servers.json +186 -0
- py_mcpdock_cli-1.0.13/src/cli/registry.py +136 -0
- py_mcpdock_cli-1.0.13/src/cli/runners/__init__.py +21 -0
- py_mcpdock_cli-1.0.13/src/cli/runners/command_runner.py +172 -0
- py_mcpdock_cli-1.0.13/src/cli/runners/stdio_runner.py +494 -0
- py_mcpdock_cli-1.0.13/src/cli/runners/stream_http_runner.py +166 -0
- py_mcpdock_cli-1.0.13/src/cli/runners/ws_runner.py +43 -0
- py_mcpdock_cli-1.0.13/src/cli/types/__init__.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/types/registry.py +69 -0
- py_mcpdock_cli-1.0.13/src/cli/utils/__init__.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/utils/client.py +0 -0
- py_mcpdock_cli-1.0.13/src/cli/utils/config.py +441 -0
- py_mcpdock_cli-1.0.13/src/cli/utils/logger.py +79 -0
- py_mcpdock_cli-1.0.13/src/cli/utils/runtime.py +163 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/PKG-INFO +28 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/SOURCES.txt +32 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/dependency_links.txt +1 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/entry_points.txt +2 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/requires.txt +7 -0
- py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/top_level.txt +1 -0
@@ -0,0 +1 @@
|
|
1
|
+
recursive-include src/cli *.json
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: py-mcpdock-cli
|
3
|
+
Version: 1.0.13
|
4
|
+
Summary: Python CLI for managing MCP servers
|
5
|
+
Author-email: dw <qindongwoxin@gmail.com>
|
6
|
+
Project-URL: Homepage, https://github.com/yourusername/py-mcpdock-cli
|
7
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/py-mcpdock-cli/issues
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Operating System :: OS Independent
|
15
|
+
Requires-Python: >=3.8
|
16
|
+
Description-Content-Type: text/markdown
|
17
|
+
Requires-Dist: click>=8.0.0
|
18
|
+
Requires-Dist: questionary>=1.10.0
|
19
|
+
Requires-Dist: rich>=12.0.0
|
20
|
+
Requires-Dist: asyncio>=3.4.3
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
22
|
+
Requires-Dist: mcp
|
23
|
+
Requires-Dist: aiohttp>=3.8.0
|
24
|
+
|
25
|
+
# MCP CLI Python Implementation
|
26
|
+
|
27
|
+
This is a Python implementation of the Model Context Protocol (MCP) command-line tool.
|
28
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=42.0", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "py-mcpdock-cli"
|
7
|
+
version = "1.0.13"
|
8
|
+
authors = [
|
9
|
+
{name = "dw", email = "qindongwoxin@gmail.com"},
|
10
|
+
]
|
11
|
+
description = "Python CLI for managing MCP servers"
|
12
|
+
readme = "README.md"
|
13
|
+
requires-python = ">=3.8"
|
14
|
+
classifiers = [
|
15
|
+
"Programming Language :: Python :: 3",
|
16
|
+
"Programming Language :: Python :: 3.8",
|
17
|
+
"Programming Language :: Python :: 3.9",
|
18
|
+
"Programming Language :: Python :: 3.10",
|
19
|
+
"Programming Language :: Python :: 3.11",
|
20
|
+
"License :: OSI Approved :: MIT License",
|
21
|
+
"Operating System :: OS Independent",
|
22
|
+
]
|
23
|
+
dependencies = [
|
24
|
+
"click>=8.0.0",
|
25
|
+
"questionary>=1.10.0",
|
26
|
+
"rich>=12.0.0",
|
27
|
+
"asyncio>=3.4.3",
|
28
|
+
"python-dotenv>=1.0.0",
|
29
|
+
"mcp",
|
30
|
+
"aiohttp>=3.8.0",
|
31
|
+
]
|
32
|
+
|
33
|
+
[tool.setuptools.packages.find]
|
34
|
+
where = ["src"]
|
35
|
+
|
36
|
+
[project.scripts]
|
37
|
+
mcpy = "cli.main:cli"
|
38
|
+
|
39
|
+
[project.urls]
|
40
|
+
"Homepage" = "https://github.com/yourusername/py-mcpdock-cli"
|
41
|
+
"Bug Tracker" = "https://github.com/yourusername/py-mcpdock-cli/issues"
|
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
|
@@ -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
|
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')
|