quick-mcp-shell 0.3.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.
@@ -0,0 +1,3 @@
1
+ """Quick MCP Shell package."""
2
+
3
+ __version__ = "0.3.0"
@@ -0,0 +1,5 @@
1
+ from .server import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Mapping
7
+
8
+
9
+ class ConfigurationError(ValueError):
10
+ """Raised when local shell execution has not been explicitly configured."""
11
+
12
+
13
+ def _positive_int(value: str, variable: str) -> int:
14
+ try:
15
+ parsed = int(value)
16
+ except ValueError as exc:
17
+ raise ConfigurationError(f"{variable} 必须是正整数。") from exc
18
+ if parsed <= 0:
19
+ raise ConfigurationError(f"{variable} 必须是正整数。")
20
+ return parsed
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Settings:
25
+ """Runtime settings derived from environment variables."""
26
+
27
+ allowed_root: Path
28
+ max_timeout_seconds: int
29
+ max_output_chars: int
30
+
31
+ @classmethod
32
+ def from_environment(cls, env: Mapping[str, str] | None = None) -> "Settings":
33
+ environment = os.environ if env is None else env
34
+ raw_root = environment.get("MCP_SHELL_ALLOWED_ROOT", "").strip()
35
+ if not raw_root:
36
+ raise ConfigurationError(
37
+ "未设置 MCP_SHELL_ALLOWED_ROOT。请在 MCP 配置中显式指定允许执行命令的本地目录。"
38
+ )
39
+
40
+ allowed_root = Path(raw_root).expanduser().resolve()
41
+ if not allowed_root.is_dir():
42
+ raise ConfigurationError(
43
+ f"MCP_SHELL_ALLOWED_ROOT 不是有效目录:{allowed_root}"
44
+ )
45
+
46
+ return cls(
47
+ allowed_root=allowed_root,
48
+ max_timeout_seconds=_positive_int(
49
+ environment.get("MCP_SHELL_MAX_TIMEOUT_SECONDS", "60"),
50
+ "MCP_SHELL_MAX_TIMEOUT_SECONDS",
51
+ ),
52
+ max_output_chars=_positive_int(
53
+ environment.get("MCP_SHELL_MAX_OUTPUT_CHARS", "100000"),
54
+ "MCP_SHELL_MAX_OUTPUT_CHARS",
55
+ ),
56
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .config import Settings
7
+
8
+
9
+ def resolve_cwd(cwd: str | None, settings: Settings) -> Path:
10
+ candidate = settings.allowed_root if cwd is None else Path(cwd).expanduser().resolve()
11
+ try:
12
+ candidate.relative_to(settings.allowed_root)
13
+ except ValueError as exc:
14
+ raise ValueError(
15
+ f"拒绝执行:工作目录 '{candidate}' 不在允许根目录 '{settings.allowed_root}' 下。"
16
+ ) from exc
17
+ if not candidate.is_dir():
18
+ raise ValueError(f"工作目录不存在或不是目录:'{candidate}'")
19
+ return candidate
20
+
21
+
22
+ def effective_timeout(timeout: int, settings: Settings) -> int:
23
+ if timeout <= 0:
24
+ raise ValueError("timeout 必须是正整数。")
25
+ return min(timeout, settings.max_timeout_seconds)
26
+
27
+
28
+ def format_result(
29
+ stdout: str,
30
+ stderr: str,
31
+ returncode: int,
32
+ max_output_chars: int,
33
+ ) -> str:
34
+ parts = [f"returncode: {returncode}"]
35
+ if stdout:
36
+ parts.append(f"stdout:\n{stdout}")
37
+ if stderr:
38
+ parts.append(f"stderr:\n{stderr}")
39
+ result = "\n\n".join(parts)
40
+ if len(result) > max_output_chars:
41
+ omitted = len(result) - max_output_chars
42
+ result = f"{result[:max_output_chars]}\n\n[输出已截断,省略 {omitted} 个字符]"
43
+ return result
44
+
45
+
46
+ def run_process(
47
+ args: str | list[str],
48
+ *,
49
+ shell: bool,
50
+ work_dir: Path,
51
+ timeout: int,
52
+ settings: Settings,
53
+ ) -> str:
54
+ try:
55
+ result = subprocess.run(
56
+ args,
57
+ shell=shell,
58
+ cwd=work_dir,
59
+ stdin=subprocess.DEVNULL,
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=timeout,
63
+ encoding="utf-8",
64
+ errors="replace",
65
+ close_fds=True,
66
+ )
67
+ return format_result(
68
+ result.stdout,
69
+ result.stderr,
70
+ result.returncode,
71
+ settings.max_output_chars,
72
+ )
73
+ except subprocess.TimeoutExpired:
74
+ return f"错误:命令执行超时({timeout} 秒)。"
75
+ except OSError as exc:
76
+ return f"错误:无法启动进程:{exc}"
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from . import __version__
12
+ from .config import ConfigurationError, Settings
13
+ from .runtime import effective_timeout, resolve_cwd, run_process
14
+
15
+
16
+ mcp = FastMCP(
17
+ name="quick-mcp-shell",
18
+ instructions=(
19
+ "Execute trusted local shell commands and Python scripts. "
20
+ "This server is intentionally powerful and must only be connected to trusted local MCP clients."
21
+ ),
22
+ )
23
+
24
+
25
+ def _settings_or_error() -> tuple[Settings | None, str | None]:
26
+ try:
27
+ return Settings.from_environment(), None
28
+ except ConfigurationError as exc:
29
+ return None, str(exc)
30
+
31
+
32
+ @mcp.tool()
33
+ def run_command(command: str, cwd: str | None = None, timeout: int = 30) -> str:
34
+ """在允许根目录内执行受信任的本地 shell 命令。"""
35
+ settings, error = _settings_or_error()
36
+ if error:
37
+ return f"配置错误:{error}"
38
+ assert settings is not None
39
+ try:
40
+ work_dir = resolve_cwd(cwd, settings)
41
+ timeout_seconds = effective_timeout(timeout, settings)
42
+ except ValueError as exc:
43
+ return f"错误:{exc}"
44
+ return run_process(
45
+ command,
46
+ shell=True,
47
+ work_dir=work_dir,
48
+ timeout=timeout_seconds,
49
+ settings=settings,
50
+ )
51
+
52
+
53
+ @mcp.tool()
54
+ def run_python_script(script: str, cwd: str | None = None, timeout: int = 30) -> str:
55
+ """在允许根目录内执行受信任的 Python 脚本。"""
56
+ settings, error = _settings_or_error()
57
+ if error:
58
+ return f"配置错误:{error}"
59
+ assert settings is not None
60
+ try:
61
+ work_dir = resolve_cwd(cwd, settings)
62
+ timeout_seconds = effective_timeout(timeout, settings)
63
+ except ValueError as exc:
64
+ return f"错误:{exc}"
65
+
66
+ script_path: Path | None = None
67
+ try:
68
+ with tempfile.NamedTemporaryFile(
69
+ mode="w",
70
+ suffix=".py",
71
+ prefix=".quick-mcp-shell-",
72
+ dir=work_dir,
73
+ delete=False,
74
+ encoding="utf-8",
75
+ ) as handle:
76
+ handle.write(script)
77
+ script_path = Path(handle.name)
78
+ return run_process(
79
+ [sys.executable, str(script_path)],
80
+ shell=False,
81
+ work_dir=work_dir,
82
+ timeout=timeout_seconds,
83
+ settings=settings,
84
+ )
85
+ except OSError as exc:
86
+ return f"错误:无法创建临时 Python 脚本:{exc}"
87
+ finally:
88
+ if script_path is not None:
89
+ try:
90
+ script_path.unlink(missing_ok=True)
91
+ except OSError:
92
+ pass
93
+
94
+
95
+ def _doctor() -> int:
96
+ settings, error = _settings_or_error()
97
+ if error:
98
+ print(f"配置错误:{error}", file=sys.stderr)
99
+ return 1
100
+ assert settings is not None
101
+ print("quick-mcp-shell 配置正常")
102
+ print(f"允许根目录: {settings.allowed_root}")
103
+ print(f"最大超时: {settings.max_timeout_seconds} 秒")
104
+ print(f"最大输出: {settings.max_output_chars} 字符")
105
+ return 0
106
+
107
+
108
+ def main(argv: list[str] | None = None) -> None:
109
+ parser = argparse.ArgumentParser(description="Trusted local MCP shell server")
110
+ parser.add_argument("--version", action="version", version=__version__)
111
+ parser.add_argument(
112
+ "--allow-root",
113
+ help="仅用于本次命令的 MCP_SHELL_ALLOWED_ROOT 覆盖值。",
114
+ )
115
+ parser.add_argument("command", nargs="?", choices=["doctor"])
116
+ arguments = parser.parse_args(argv)
117
+
118
+ if arguments.allow_root:
119
+ os.environ["MCP_SHELL_ALLOWED_ROOT"] = arguments.allow_root
120
+ if arguments.command == "doctor":
121
+ raise SystemExit(_doctor())
122
+ mcp.run(transport="stdio")
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: quick-mcp-shell
3
+ Version: 0.3.0
4
+ Summary: A local stdio MCP server for trusted shell and Python execution
5
+ Project-URL: Homepage, https://pypi.org/project/quick-mcp-shell/
6
+ Author: Quick MCP Shell maintainers
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Topic :: System :: Shells
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: mcp<2.0.0,>=1.0.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Quick MCP Shell
21
+
22
+ `quick-mcp-shell` 是一个本地 stdio MCP Server,供 Amazon Quick 调用本机 shell 命令和 Python 脚本。
23
+
24
+ > 警告:这是一个高权限工具。它执行的命令拥有启动它的用户权限。`MCP_SHELL_ALLOWED_ROOT` 仅限制工作目录(`cwd`),**不是文件系统或网络沙箱**。只应连接到受信任的本地 MCP 客户端,绝不能暴露到公网,也不要向不受信任的提示词或用户开放。
25
+
26
+ ## 安装 uv
27
+
28
+ Amazon Quick 通过 `uvx` 自动下载并运行本工具,无需手动安装 Python 包。
29
+
30
+ **Windows(PowerShell)**
31
+
32
+ ```powershell
33
+ winget install --id=astral-sh.uv -e
34
+ ```
35
+
36
+ **macOS(需已安装 Homebrew)**
37
+
38
+ ```bash
39
+ brew install uv
40
+ ```
41
+
42
+ **Linux**
43
+
44
+ ```bash
45
+ curl -LsSf https://astral.sh/uv/install.sh | sh
46
+ ```
47
+
48
+ 安装后执行 `uvx --version` 确认命令可用。
49
+
50
+ ## 配置 Amazon Quick
51
+
52
+ 在 Amazon Quick 中打开 **Settings → Capabilities → MCP Servers**,编辑配置文件:
53
+
54
+ - Windows:`C:\Users\你的用户名\.aws\amazonq\mcp.json`
55
+ - macOS / Linux:`~/.aws/amazonq/mcp.json`
56
+
57
+ 将下列内容合并到 `mcpServers`。必须把 `MCP_SHELL_ALLOWED_ROOT` 改为你希望允许作为工作目录的实际路径。
58
+
59
+ **Windows 示例**
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "shell": {
65
+ "command": "uvx",
66
+ "args": ["--from", "quick-mcp-shell==0.3.0", "quick-mcp-shell"],
67
+ "description": "在指定工作目录内执行受信任的本地 shell 命令和 Python 脚本。",
68
+ "env": {
69
+ "MCP_SHELL_ALLOWED_ROOT": "D:\\workspace\\my-project",
70
+ "MCP_SHELL_MAX_TIMEOUT_SECONDS": "60",
71
+ "MCP_SHELL_MAX_OUTPUT_CHARS": "100000"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ **macOS / Linux 示例**
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "shell": {
84
+ "command": "uvx",
85
+ "args": ["--from", "quick-mcp-shell==0.3.0", "quick-mcp-shell"],
86
+ "description": "在指定工作目录内执行受信任的本地 shell 命令和 Python 脚本。",
87
+ "env": {
88
+ "MCP_SHELL_ALLOWED_ROOT": "/Users/your-name/workspace/my-project",
89
+ "MCP_SHELL_MAX_TIMEOUT_SECONDS": "60",
90
+ "MCP_SHELL_MAX_OUTPUT_CHARS": "100000"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ Linux 用户可将示例路径替换为 `/home/your-name/workspace/my-project`。保存配置并重启 Amazon Quick。
98
+
99
+ ## 可用工具
100
+
101
+ ### `run_command`
102
+
103
+ 执行本机 shell 命令。
104
+
105
+ | 参数 | 说明 |
106
+ |---|---|
107
+ | `command` | 要执行的命令,必填 |
108
+ | `cwd` | 工作目录;省略时使用 `MCP_SHELL_ALLOWED_ROOT` |
109
+ | `timeout` | 超时秒数,默认 30;不会超过 `MCP_SHELL_MAX_TIMEOUT_SECONDS` |
110
+
111
+ 示例:让 Amazon Quick 在项目目录中执行 `git status`,或运行项目已有的测试命令。
112
+
113
+ ### `run_python_script`
114
+
115
+ 在所选工作目录中使用本工具自身的 Python 解释器执行一段 Python 脚本。参数与 `run_command` 相同,只是把 `command` 替换为 `script`。
116
+
117
+ ## 配置项
118
+
119
+ | 环境变量 | 是否必填 | 默认值 | 说明 |
120
+ |---|---:|---|---|
121
+ | `MCP_SHELL_ALLOWED_ROOT` | 是 | 无 | 允许作为 `cwd` 的根目录;未设置时所有工具调用都会被拒绝。 |
122
+ | `MCP_SHELL_MAX_TIMEOUT_SECONDS` | 否 | `60` | 单次调用允许的最大超时。 |
123
+ | `MCP_SHELL_MAX_OUTPUT_CHARS` | 否 | `100000` | 返回给 MCP 客户端的最大输出字符数;超出部分会被截断。 |
124
+
125
+ ## 本地检查
126
+
127
+ 在终端中运行以下命令可检查当前配置:
128
+
129
+ ```bash
130
+ uvx --from quick-mcp-shell==0.3.0 quick-mcp-shell --allow-root /你的工作目录 doctor
131
+ ```
132
+
133
+ Windows 示例:
134
+
135
+ ```powershell
136
+ uvx --from quick-mcp-shell==0.3.0 quick-mcp-shell --allow-root "D:\workspace\my-project" doctor
137
+ ```
138
+
139
+ ## 安全边界
140
+
141
+ - 不要将本工具部署为远程 HTTP 服务或暴露到公网。
142
+ - 不要把允许根目录设为系统盘根目录、用户主目录或包含密钥的目录。
143
+ - 不要把本工具交给不受信任的模型、插件、用户输入或自动化流程。
144
+ - 命令和脚本可访问当前用户能访问的资源;允许根目录不是安全沙箱。
145
+ - 固定版本号。升级前先阅读发布说明,并在隔离的项目目录中验证。
@@ -0,0 +1,10 @@
1
+ quick_mcp_shell/__init__.py,sha256=Mt_7sRItHgoMiPb31lvJl7LjDouK-Q5BOJySttdf108,54
2
+ quick_mcp_shell/__main__.py,sha256=kXwGR0h4I_1BdqTv1YV6f86NLM1OwukbORECfPc9OPg,65
3
+ quick_mcp_shell/config.py,sha256=rpCnp7yzyC2LonkHrCm90TfxNboIWM3vn5ZWGEsKRYE,1836
4
+ quick_mcp_shell/runtime.py,sha256=smOLQLp_95K1Ni495AIVlidAAQJbNHNwg4EmTS5v6lg,2178
5
+ quick_mcp_shell/server.py,sha256=4ejYmgcJ-QYhaI8TlfrNPuDl7qx7zmlBfzSkU6D3150,3746
6
+ quick_mcp_shell-0.3.0.dist-info/METADATA,sha256=d2y3LrDo5uCQ95yBWgkUFKhZo0az4pf7ScyvGjJ8SmY,4783
7
+ quick_mcp_shell-0.3.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ quick_mcp_shell-0.3.0.dist-info/entry_points.txt,sha256=nbAo9plh9AiUVMxjnmu2Bh7Fz72PUWXvI4gEauUO9tA,64
9
+ quick_mcp_shell-0.3.0.dist-info/licenses/LICENSE,sha256=hbUa7wlsXAoAwP-OWYtG0wvIMJThjC5PQhZUVrPhsrI,1062
10
+ quick_mcp_shell-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ quick-mcp-shell = quick_mcp_shell.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 libao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.