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 +0 -0
- cli/commands/__init__.py +0 -0
- cli/commands/install.py +182 -0
- cli/commands/run.py +148 -0
- cli/config/__init__.py +0 -0
- cli/config/app_config.py +11 -0
- cli/config/client_config.py +248 -0
- cli/main.py +12 -0
- cli/mock_servers.json +186 -0
- cli/registry.py +136 -0
- cli/runners/__init__.py +21 -0
- cli/runners/command_runner.py +172 -0
- cli/runners/stdio_runner.py +494 -0
- cli/runners/stream_http_runner.py +166 -0
- cli/runners/ws_runner.py +43 -0
- cli/types/__init__.py +0 -0
- cli/types/registry.py +69 -0
- cli/utils/__init__.py +0 -0
- cli/utils/client.py +0 -0
- cli/utils/config.py +441 -0
- cli/utils/logger.py +79 -0
- cli/utils/runtime.py +163 -0
- py_mcpdock_cli-1.0.13.dist-info/METADATA +28 -0
- py_mcpdock_cli-1.0.13.dist-info/RECORD +27 -0
- py_mcpdock_cli-1.0.13.dist-info/WHEEL +5 -0
- py_mcpdock_cli-1.0.13.dist-info/entry_points.txt +2 -0
- py_mcpdock_cli-1.0.13.dist-info/top_level.txt +1 -0
cli/mock_servers.json
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
{
|
2
|
+
"octagon": {
|
3
|
+
"qualifiedName": "octagon",
|
4
|
+
"displayName": "Octagon MCP Server (Mock)",
|
5
|
+
"remote": false,
|
6
|
+
"connections": [
|
7
|
+
{
|
8
|
+
"type": "stdio",
|
9
|
+
"stdioFunction": [
|
10
|
+
"npx"
|
11
|
+
],
|
12
|
+
"published": true,
|
13
|
+
"configSchema": {
|
14
|
+
"args": [
|
15
|
+
"-y",
|
16
|
+
"octagon-mcp@latest"
|
17
|
+
],
|
18
|
+
"env": {
|
19
|
+
"OCTAGON_API_KEY": {
|
20
|
+
"type": "string",
|
21
|
+
"description": "OCTAGON_API_KEY"
|
22
|
+
}
|
23
|
+
},
|
24
|
+
"required": [
|
25
|
+
"OCTAGON_API_KEY"
|
26
|
+
]
|
27
|
+
}
|
28
|
+
}
|
29
|
+
]
|
30
|
+
},
|
31
|
+
"mcp-gitlab": {
|
32
|
+
"qualifiedName": "@zereight/mcp-gitlab",
|
33
|
+
"displayName": "@zereight/mcp-gitlab",
|
34
|
+
"remote": false,
|
35
|
+
"connections": [
|
36
|
+
{
|
37
|
+
"type": "stdio",
|
38
|
+
"stdioFunction": [
|
39
|
+
"npx"
|
40
|
+
],
|
41
|
+
"published": true,
|
42
|
+
"configSchema": {
|
43
|
+
"args": [
|
44
|
+
"-y",
|
45
|
+
"@zereight/mcp-gitlab"
|
46
|
+
],
|
47
|
+
"env": {
|
48
|
+
"GITLAB_PERSONAL_ACCESS_TOKEN": {
|
49
|
+
"type": "string",
|
50
|
+
"description": "GITLAB_PERSONAL_ACCESS_TOKEN"
|
51
|
+
},
|
52
|
+
"GITLAB_API_URL": {
|
53
|
+
"type": "string",
|
54
|
+
"description": "GITLAB_API_URL",
|
55
|
+
"default": "GITLAB_API_URL"
|
56
|
+
},
|
57
|
+
"GITLAB_READ_ONLY_MODE": {
|
58
|
+
"type": "string",
|
59
|
+
"description": "GITLAB_READ_ONLY_MODE"
|
60
|
+
}
|
61
|
+
},
|
62
|
+
"required": [
|
63
|
+
"GITLAB_PERSONAL_ACCESS_TOKEN",
|
64
|
+
"GITLAB_API_URL"
|
65
|
+
]
|
66
|
+
}
|
67
|
+
},
|
68
|
+
{
|
69
|
+
"type": "ws",
|
70
|
+
"deploymentUrl": "wss://example.com",
|
71
|
+
"published": true
|
72
|
+
}
|
73
|
+
]
|
74
|
+
},
|
75
|
+
"mcp-twikit": {
|
76
|
+
"qualifiedName": "mcp-twikit",
|
77
|
+
"displayName": "mcp-twikit (Mock)",
|
78
|
+
"remote": false,
|
79
|
+
"connections": [
|
80
|
+
{
|
81
|
+
"type": "stdio",
|
82
|
+
"stdioFunction": [
|
83
|
+
"uvx"
|
84
|
+
],
|
85
|
+
"published": true,
|
86
|
+
"configSchema": {
|
87
|
+
"args": [
|
88
|
+
"--from",
|
89
|
+
"git+https://github.com/adhikasp/mcp-twikit",
|
90
|
+
"mcp-twikit"
|
91
|
+
],
|
92
|
+
"env": {
|
93
|
+
"TWITTER_USERNAME": {
|
94
|
+
"type": "string",
|
95
|
+
"description": "TWITTER_USERNAME",
|
96
|
+
"default": "@example"
|
97
|
+
},
|
98
|
+
"TWITTER_EMAIL": {
|
99
|
+
"type": "string",
|
100
|
+
"description": "TWITTER_EMAIL",
|
101
|
+
"default": "me@example.com"
|
102
|
+
},
|
103
|
+
"TWITTER_PASSWORD": {
|
104
|
+
"type": "string",
|
105
|
+
"description": "TWITTER_PASSWORD",
|
106
|
+
"default": "secret"
|
107
|
+
}
|
108
|
+
},
|
109
|
+
"required": [
|
110
|
+
"TWITTER_USERNAME",
|
111
|
+
"TWITTER_EMAIL",
|
112
|
+
"TWITTER_PASSWORD"
|
113
|
+
]
|
114
|
+
}
|
115
|
+
}
|
116
|
+
]
|
117
|
+
},
|
118
|
+
"@suekou/mcp-notion-server": {
|
119
|
+
"qualifiedName": "@suekou/mcp-notion-server",
|
120
|
+
"displayName": "mcp-notion (Mock)",
|
121
|
+
"remote": false,
|
122
|
+
"connections": [
|
123
|
+
{
|
124
|
+
"type": "stdio",
|
125
|
+
"stdioFunction": [
|
126
|
+
"npx"
|
127
|
+
],
|
128
|
+
"published": true,
|
129
|
+
"configSchema": {
|
130
|
+
"args": [
|
131
|
+
"-y",
|
132
|
+
"@suekou/mcp-notion-server"
|
133
|
+
],
|
134
|
+
"env": {
|
135
|
+
"NOTION_API_TOKEN": {
|
136
|
+
"type": "string",
|
137
|
+
"description": "NOTION_API_TOKEN",
|
138
|
+
"default": "your-integration-token"
|
139
|
+
}
|
140
|
+
},
|
141
|
+
"required": [
|
142
|
+
"NOTION_API_TOKEN"
|
143
|
+
]
|
144
|
+
}
|
145
|
+
}
|
146
|
+
]
|
147
|
+
},
|
148
|
+
"@test/install-notion": {
|
149
|
+
"qualifiedName": "@suekou/mcp-notion-server",
|
150
|
+
"displayName": "mcp-notion (Mock)",
|
151
|
+
"remote": false,
|
152
|
+
"connections": [
|
153
|
+
{
|
154
|
+
"type": "stdio",
|
155
|
+
"stdioFunction": [
|
156
|
+
"npx"
|
157
|
+
],
|
158
|
+
"published": true,
|
159
|
+
"configSchema": {
|
160
|
+
"args": [
|
161
|
+
"-y",
|
162
|
+
"@suekou/mcp-notion-server",
|
163
|
+
"--from",
|
164
|
+
"py-mcpdock-cli==1.0.12",
|
165
|
+
"--index-url",
|
166
|
+
"https://test.pypi.org/simple/",
|
167
|
+
"--extra-index-url",
|
168
|
+
"https://pypi.org/simple/",
|
169
|
+
"mcpy",
|
170
|
+
"run"
|
171
|
+
],
|
172
|
+
"env": {
|
173
|
+
"NOTION_API_TOKEN": {
|
174
|
+
"type": "string",
|
175
|
+
"description": "NOTION_API_TOKEN",
|
176
|
+
"default": "your-integration-token"
|
177
|
+
}
|
178
|
+
},
|
179
|
+
"required": [
|
180
|
+
"NOTION_API_TOKEN"
|
181
|
+
]
|
182
|
+
}
|
183
|
+
}
|
184
|
+
]
|
185
|
+
}
|
186
|
+
}
|
cli/registry.py
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
"""
|
2
|
+
Registry module for MCP servers.
|
3
|
+
|
4
|
+
This module handles resolving package names to server metadata and fetching configuration.
|
5
|
+
"""
|
6
|
+
from typing import Dict, Any, Tuple, Optional
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
|
10
|
+
from .utils.logger import verbose
|
11
|
+
from .types.registry import (
|
12
|
+
RegistryServer, ConnectionDetails, ConfigSchema, ConfigSchemaProperty
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
MOCK_SERVERS_PATH = os.path.join(os.path.dirname(__file__), "mock_servers.json")
|
17
|
+
|
18
|
+
|
19
|
+
def _load_mock_servers():
|
20
|
+
with open(MOCK_SERVERS_PATH, "r", encoding="utf-8") as f:
|
21
|
+
return json.load(f)
|
22
|
+
|
23
|
+
|
24
|
+
def _dict_to_registry_server(data):
|
25
|
+
# Helper to recursively convert dict to dataclasses
|
26
|
+
def _to_config_schema_property(prop):
|
27
|
+
return ConfigSchemaProperty(**prop)
|
28
|
+
|
29
|
+
def _to_config_schema(schema):
|
30
|
+
env = {k: _to_config_schema_property(v) for k, v in schema.get("env", {}).items()}
|
31
|
+
return ConfigSchema(env=env, required=schema.get("required", []), args=schema.get("args"))
|
32
|
+
|
33
|
+
def _to_connection_details(conn):
|
34
|
+
config_schema = None
|
35
|
+
if "configSchema" in conn and conn["configSchema"]:
|
36
|
+
config_schema = _to_config_schema(conn["configSchema"])
|
37
|
+
return ConnectionDetails(
|
38
|
+
type=conn["type"],
|
39
|
+
stdioFunction=conn.get("stdioFunction"),
|
40
|
+
deploymentUrl=conn.get("deploymentUrl"),
|
41
|
+
published=conn.get("published"),
|
42
|
+
configSchema=config_schema
|
43
|
+
)
|
44
|
+
return RegistryServer(
|
45
|
+
qualifiedName=data["qualifiedName"],
|
46
|
+
displayName=data["displayName"],
|
47
|
+
remote=data["remote"],
|
48
|
+
connections=[_to_connection_details(c) for c in data["connections"]]
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
mock_servers_data = _load_mock_servers()
|
53
|
+
|
54
|
+
|
55
|
+
async def resolve_package(package_name: str) -> RegistryServer:
|
56
|
+
"""
|
57
|
+
Resolves a package name to server metadata.
|
58
|
+
"""
|
59
|
+
verbose(f"Mock resolving package {package_name}")
|
60
|
+
data = mock_servers_data.get(package_name) or mock_servers_data.get("default")
|
61
|
+
return _dict_to_registry_server(data)
|
62
|
+
|
63
|
+
|
64
|
+
async def fetch_connection(
|
65
|
+
package_name: str,
|
66
|
+
config: Dict[str, Any]
|
67
|
+
) -> Dict[str, Any]:
|
68
|
+
"""
|
69
|
+
Fetches server connection details for a package with given configuration.
|
70
|
+
|
71
|
+
This function uses the package name to resolve the server details and then
|
72
|
+
formats the connection configuration based on the schema and user provided config.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
package_name: The name of the package to fetch connection details for
|
76
|
+
config: User-provided configuration values
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Dictionary containing command, args and env configuration for the connection
|
80
|
+
"""
|
81
|
+
verbose(f"Fetching connection details for {package_name} with config: {config}")
|
82
|
+
|
83
|
+
# Get server metadata first
|
84
|
+
server_data = await resolve_package(package_name)
|
85
|
+
|
86
|
+
# Find stdio connection
|
87
|
+
stdio_connection = next((conn for conn in server_data.connections if conn.type == "stdio"), None)
|
88
|
+
if not stdio_connection:
|
89
|
+
verbose("No stdio connection found in server metadata")
|
90
|
+
return {}
|
91
|
+
|
92
|
+
# Get command type (npx, uv, etc.) from stdioFunction
|
93
|
+
command = "python" # Default as fallback
|
94
|
+
if stdio_connection.stdioFunction and len(stdio_connection.stdioFunction) > 0:
|
95
|
+
command = stdio_connection.stdioFunction[0]
|
96
|
+
|
97
|
+
# Get args from the connection schema
|
98
|
+
args = []
|
99
|
+
if stdio_connection.configSchema and stdio_connection.configSchema.args:
|
100
|
+
args = stdio_connection.configSchema.args
|
101
|
+
else:
|
102
|
+
# Fallback default
|
103
|
+
args = ["-m", package_name]
|
104
|
+
|
105
|
+
# Return formatted connection config
|
106
|
+
connection_config = {
|
107
|
+
"command": command,
|
108
|
+
"args": args,
|
109
|
+
"env": config # User-provided environment variables
|
110
|
+
}
|
111
|
+
|
112
|
+
verbose(f"Formatted connection config: {connection_config}")
|
113
|
+
return connection_config
|
114
|
+
|
115
|
+
|
116
|
+
async def fetch_config_with_api_key(
|
117
|
+
package_name: str,
|
118
|
+
api_key: str
|
119
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
120
|
+
"""
|
121
|
+
Fetches server and configuration information using an API key.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
package_name: The name of the package to fetch configuration for
|
125
|
+
api_key: The API key to authenticate with
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
A tuple containing (server_info, config_info)
|
129
|
+
"""
|
130
|
+
verbose(f"Mock fetching config for {package_name} using API key")
|
131
|
+
|
132
|
+
# This is a placeholder implementation
|
133
|
+
# In a real implementation, this would make an API call
|
134
|
+
# to retrieve the server and configuration information
|
135
|
+
|
136
|
+
return {}, {} # Empty dicts for now as placeholder
|
cli/runners/__init__.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
Runners for different types of MCP servers.
|
3
|
+
|
4
|
+
This package contains implementations for running different types of MCP servers:
|
5
|
+
- stdio_runner: For running local servers via stdio communication
|
6
|
+
- ws_runner: For connecting to remote WebSocket servers
|
7
|
+
- command_runner: For running Python/Node packages using uv/npx
|
8
|
+
"""
|
9
|
+
from .stdio_runner import create_stdio_runner
|
10
|
+
from .ws_runner import create_ws_runner
|
11
|
+
from .stream_http_runner import create_stream_http_runner
|
12
|
+
from .command_runner import create_uv_runner, create_npx_runner, run_package_with_command
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
'create_stdio_runner',
|
16
|
+
'create_ws_runner',
|
17
|
+
'create_uv_runner',
|
18
|
+
'create_npx_runner',
|
19
|
+
'create_stream_http_runner',
|
20
|
+
'run_package_with_command'
|
21
|
+
]
|
@@ -0,0 +1,172 @@
|
|
1
|
+
"""
|
2
|
+
Command Runners implementation for running Python or Node.js packages
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import asyncio
|
6
|
+
import signal
|
7
|
+
import sys
|
8
|
+
import os
|
9
|
+
from typing import Dict, Any, Optional, List
|
10
|
+
|
11
|
+
from rich import print as rprint
|
12
|
+
|
13
|
+
from ..utils.logger import verbose
|
14
|
+
|
15
|
+
|
16
|
+
async def run_command_process(cmd: List[str], env: Optional[Dict[str, str]] = None) -> None:
|
17
|
+
"""
|
18
|
+
Run a command as a subprocess and handle its output asynchronously
|
19
|
+
|
20
|
+
Args:
|
21
|
+
cmd: Command and arguments as a list
|
22
|
+
env: Optional environment variables
|
23
|
+
"""
|
24
|
+
verbose(f"Running command: {' '.join(cmd)}")
|
25
|
+
|
26
|
+
process = await asyncio.create_subprocess_exec(
|
27
|
+
*cmd,
|
28
|
+
stdout=asyncio.subprocess.PIPE,
|
29
|
+
stderr=asyncio.subprocess.PIPE,
|
30
|
+
env=env
|
31
|
+
)
|
32
|
+
|
33
|
+
# Setup signal handling for graceful shutdown
|
34
|
+
def handle_sigint(sig, frame):
|
35
|
+
rprint("[yellow]Received stop signal, terminating command...[/yellow]")
|
36
|
+
process.terminate()
|
37
|
+
sys.exit(0)
|
38
|
+
|
39
|
+
signal.signal(signal.SIGINT, handle_sigint)
|
40
|
+
|
41
|
+
# Process output in real-time
|
42
|
+
async def read_stream(stream, prefix):
|
43
|
+
while True:
|
44
|
+
line = await stream.readline()
|
45
|
+
if not line:
|
46
|
+
break
|
47
|
+
try:
|
48
|
+
decoded_line = line.decode('utf-8').rstrip()
|
49
|
+
if prefix == "stdout":
|
50
|
+
rprint(f"[blue]{decoded_line}[/blue]")
|
51
|
+
else:
|
52
|
+
rprint(f"[yellow]{decoded_line}[/yellow]")
|
53
|
+
except UnicodeDecodeError:
|
54
|
+
rprint(f"[red]Error decoding output from {prefix}[/red]")
|
55
|
+
|
56
|
+
# Create tasks for stdout and stderr
|
57
|
+
stdout_task = asyncio.create_task(read_stream(process.stdout, "stdout"))
|
58
|
+
stderr_task = asyncio.create_task(read_stream(process.stderr, "stderr"))
|
59
|
+
|
60
|
+
# Wait for process to complete
|
61
|
+
exit_code = await process.wait()
|
62
|
+
|
63
|
+
# Wait for output to be fully processed
|
64
|
+
await stdout_task
|
65
|
+
await stderr_task
|
66
|
+
|
67
|
+
if exit_code != 0:
|
68
|
+
rprint(f"[red]Command exited with code {exit_code}[/red]")
|
69
|
+
else:
|
70
|
+
rprint(f"[green]Command completed successfully[/green]")
|
71
|
+
|
72
|
+
|
73
|
+
async def create_uv_runner(
|
74
|
+
package_name: str,
|
75
|
+
args: List[str],
|
76
|
+
config: Dict[str, Any],
|
77
|
+
api_key: Optional[str] = None
|
78
|
+
) -> None:
|
79
|
+
"""
|
80
|
+
Creates a runner for executing Python packages with uv package manager
|
81
|
+
|
82
|
+
Args:
|
83
|
+
package_name: Name of the Python package to run
|
84
|
+
args: Additional arguments to pass to the package
|
85
|
+
config: Configuration options
|
86
|
+
api_key: Optional API key for authentication
|
87
|
+
"""
|
88
|
+
verbose(f"Starting uv runner for package: {package_name}")
|
89
|
+
rprint(f"[blue]Running Python package with uv: {package_name}[/blue]")
|
90
|
+
|
91
|
+
# Prepare environment variables from config
|
92
|
+
env = os.environ.copy()
|
93
|
+
for key, value in config.items():
|
94
|
+
if isinstance(value, str):
|
95
|
+
env[f"MCP_{key.upper()}"] = value
|
96
|
+
else:
|
97
|
+
env[f"MCP_{key.upper()}"] = json.dumps(value)
|
98
|
+
|
99
|
+
if api_key:
|
100
|
+
env["MCP_API_KEY"] = api_key
|
101
|
+
|
102
|
+
# Build the uv command
|
103
|
+
cmd = ["uv", "run", "-m", package_name]
|
104
|
+
if args:
|
105
|
+
cmd.extend(args)
|
106
|
+
|
107
|
+
await run_command_process(cmd, env)
|
108
|
+
|
109
|
+
|
110
|
+
async def create_npx_runner(
|
111
|
+
package_name: str,
|
112
|
+
args: List[str],
|
113
|
+
config: Dict[str, Any],
|
114
|
+
api_key: Optional[str] = None
|
115
|
+
) -> None:
|
116
|
+
"""
|
117
|
+
Creates a runner for executing Node.js packages with npx
|
118
|
+
|
119
|
+
Args:
|
120
|
+
package_name: Name of the Node.js package to run
|
121
|
+
args: Additional arguments to pass to the package
|
122
|
+
config: Configuration options
|
123
|
+
api_key: Optional API key for authentication
|
124
|
+
"""
|
125
|
+
verbose(f"Starting npx runner for package: {package_name}")
|
126
|
+
rprint(f"[blue]Running Node.js package with npx: {package_name}[/blue]")
|
127
|
+
|
128
|
+
# Prepare environment variables from config
|
129
|
+
env = os.environ.copy()
|
130
|
+
for key, value in config.items():
|
131
|
+
if isinstance(value, str):
|
132
|
+
env[f"MCP_{key.upper()}"] = value
|
133
|
+
else:
|
134
|
+
env[f"MCP_{key.upper()}"] = json.dumps(value)
|
135
|
+
|
136
|
+
if api_key:
|
137
|
+
env["MCP_API_KEY"] = api_key
|
138
|
+
|
139
|
+
# Build the npx command
|
140
|
+
cmd = ["npx", package_name]
|
141
|
+
if args:
|
142
|
+
cmd.extend(args)
|
143
|
+
|
144
|
+
await run_command_process(cmd, env)
|
145
|
+
|
146
|
+
|
147
|
+
async def run_package_with_command(
|
148
|
+
command_type: str,
|
149
|
+
package_name: str,
|
150
|
+
args: List[str],
|
151
|
+
config: Dict[str, Any],
|
152
|
+
api_key: Optional[str] = None
|
153
|
+
) -> None:
|
154
|
+
"""
|
155
|
+
Run a package with the specified command runner (uv or npx)
|
156
|
+
|
157
|
+
Args:
|
158
|
+
command_type: Type of command runner to use ("uv" or "npx")
|
159
|
+
package_name: Name of the package to run
|
160
|
+
args: Additional arguments to pass to the package
|
161
|
+
config: Configuration options
|
162
|
+
api_key: Optional API key for authentication
|
163
|
+
|
164
|
+
Raises:
|
165
|
+
ValueError: If an unsupported command type is provided
|
166
|
+
"""
|
167
|
+
if command_type.lower() == "uv":
|
168
|
+
await create_uv_runner(package_name, args, config, api_key)
|
169
|
+
elif command_type.lower() == "npx":
|
170
|
+
await create_npx_runner(package_name, args, config, api_key)
|
171
|
+
else:
|
172
|
+
raise ValueError(f"Unsupported command type: {command_type}")
|