quick-mcp-shell 0.4.0__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.
@@ -0,0 +1,31 @@
1
+ # Build artefacts
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ .eggs/
6
+
7
+ # Environments
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Python
13
+ __pycache__/
14
+ *.py[cod]
15
+ *.so
16
+
17
+ # Test / tooling caches
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .mypy_cache/
21
+ .coverage
22
+ htmlcov/
23
+
24
+ # Local state that must never be published
25
+ *.jsonl
26
+
27
+ # Editors / OS
28
+ .idea/
29
+ .vscode/
30
+ .DS_Store
31
+ Thumbs.db
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ 本项目遵循[语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.4.0] - 2026-10-09
8
+
9
+ ### Changed
10
+
11
+ - 移除必填的 `MCP_SHELL_ALLOWED_ROOT` 与 `--allow-root`;不再限制命令的工作目录。
12
+ - 不指定 `cwd` 时默认使用当前用户的主目录;相对路径也相对主目录解析,不再依赖服务进程的当前目录。
13
+ - `run_python_script` 的临时脚本改写入系统临时目录,不再写入工作目录。
14
+ - 运行时依赖提升为 `mcp>=1.2.0`,即 `mcp.server.fastmcp` 首次可用的版本。
15
+ - 输出超限时保留首尾两段,并确保结果不超过 `MCP_SHELL_MAX_OUTPUT_CHARS`。
16
+ - 删除过时的 `config.example.toml`,以及根目录的兼容入口 `server.py`、`config.py` 和 `legacy/` 目录。原先用 `python server.py` 从源码启动的用户,请改用 `python -m quick_mcp_shell`。
17
+
18
+ ### Fixed
19
+
20
+ - 超时现在会终止整个进程树,后代进程不再残留;Windows 上也不会因子进程占用输出管道而卡住调用。
21
+ - Windows 上按系统代码页解码命令输出,`dir`、`ipconfig` 等命令的中文不再乱码。`run_python_script` 仍固定使用 UTF-8。
22
+
23
+ ### Security
24
+
25
+ - 这是一次有意的行为变更:0.3.0 强制的工作目录限制已移除。工作目录从来不是沙箱,命令仍以启动进程的用户权限执行。若需访问隔离,请依赖受限系统账户、容器或操作系统权限。
26
+
27
+ ## [0.3.0] - 2026-08-07
28
+
29
+ ### Changed
30
+
31
+ - 统一发布名、console script 与 Amazon Quick 配置名为 `quick-mcp-shell`,避免与已存在的 `mcp-shell` PyPI 项目冲突。
32
+ - 改为标准 Python 包布局,并提供可用的 `quick-mcp-shell` 命令入口、`--version` 与 `doctor`。
33
+ - 运行时依赖限制为 MCP SDK 1.x,以匹配当前 FastMCP 实现。
34
+ - README 改为面向 Amazon Quick 使用者的 Windows、macOS、Linux 配置指南。
35
+
36
+ ### Security
37
+
38
+ - 不再使用硬编码工作目录。必须显式设置 `MCP_SHELL_ALLOWED_ROOT`,未设置时拒绝执行。
39
+ - 最大超时与最大输出量可以配置,且每次调用会受上限约束。
40
+ - 明确说明允许根目录只限制工作目录,不构成文件系统或网络沙箱。
@@ -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.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: quick-mcp-shell
3
+ Version: 0.4.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: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Topic :: System :: Shells
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: mcp<2.0.0,>=1.2.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Quick MCP Shell
20
+
21
+ 一个供 Amazon Quick 使用的本地 stdio MCP Server,可执行本机 shell 命令和 Python 脚本。
22
+
23
+ > **安全提示:** 命令以运行本工具的用户权限执行,可访问该用户有权限访问的文件和网络。只连接可信的本地 MCP 客户端;不要暴露到公网或交给不可信输入。
24
+
25
+ ## 安装 uv
26
+
27
+ Amazon Quick 使用 `uvx` 启动本工具。安装 uv 并确认 `uvx --version` 可用:
28
+
29
+ - Windows:`winget install --id=astral-sh.uv -e`
30
+ - macOS:`brew install uv`
31
+ - Linux:`curl -LsSf https://astral.sh/uv/install.sh | sh`
32
+
33
+ ## 配置 Amazon Quick
34
+
35
+ 打开 **Settings → Capabilities → MCP Servers**,在配置文件的 `mcpServers` 中加入以下配置:
36
+
37
+ ```json
38
+ {
39
+ "shell": {
40
+ "command": "uvx",
41
+ "args": ["--from", "quick-mcp-shell==0.4.0", "quick-mcp-shell"]
42
+ }
43
+ }
44
+ ```
45
+
46
+ 配置文件位置:Windows `C:\Users\<用户名>\.aws\amazonq\mcp.json`;macOS/Linux `~/.aws/amazonq/mcp.json`。保存后重启 Amazon Quick。
47
+
48
+ ## 使用
49
+
50
+ - `run_command(command, cwd?, timeout?)`:执行 shell 命令。Windows 上通过 `cmd.exe` 执行,不是 PowerShell;macOS/Linux 上通过 `/bin/sh` 执行。
51
+ - `run_python_script(script, cwd?, timeout?)`:执行 Python 脚本。使用本工具自带的隔离解释器,你项目里安装的第三方包(如 pandas)不可用;需要项目依赖时请改用 `run_command`。
52
+
53
+ 不传 `cwd` 时使用当前用户的主目录,相对路径也相对主目录解析;传入绝对路径时可指定任意存在的目录。`timeout` 默认 30 秒,最大值默认 60 秒;超时会终止整个进程树。
54
+
55
+ 可选环境变量:
56
+
57
+ | 变量 | 默认值 | 作用 |
58
+ |---|---:|---|
59
+ | `MCP_SHELL_MAX_TIMEOUT_SECONDS` | `60` | 单次调用允许的最大超时秒数。 |
60
+ | `MCP_SHELL_MAX_OUTPUT_CHARS` | `100000` | 返回结果的最大字符数,超出时保留首尾两段。 |
61
+
62
+ 检查启动环境:
63
+
64
+ ```bash
65
+ uvx --from quick-mcp-shell==0.4.0 quick-mcp-shell doctor
66
+ ```
67
+
68
+ ## 安全边界
69
+
70
+ 本工具不限制命令访问的目录。命令可以切换目录或使用绝对路径;若需要真正的访问隔离,请使用受限系统账户、容器或操作系统权限控制。
@@ -0,0 +1,35 @@
1
+ # 发布 quick-mcp-shell 到 PyPI
2
+
3
+ ## 发布前检查
4
+
5
+ 在项目根目录执行:
6
+
7
+ ```powershell
8
+ python -m unittest discover -s tests -v
9
+ uv build
10
+ uvx twine check --strict dist/*
11
+ ```
12
+
13
+ 检查 wheel 中只包含预期的包文件,且 README 没有包含本机路径、凭据或真实用户数据。
14
+
15
+ ```powershell
16
+ python -m zipfile -l dist\quick_mcp_shell-0.4.0-py3-none-any.whl
17
+ ```
18
+
19
+ ## 发布
20
+
21
+ 使用 PyPI 中新建、未泄露且尽可能限制到单个项目的 API Token。不要把 token 写入仓库、文档或聊天。
22
+
23
+ ```powershell
24
+ $env:UV_PUBLISH_TOKEN = "pypi-新令牌"
25
+ uv publish --token $env:UV_PUBLISH_TOKEN
26
+ Remove-Item Env:\UV_PUBLISH_TOKEN
27
+ ```
28
+
29
+ 上传后,在干净环境中验证命令入口:
30
+
31
+ ```powershell
32
+ uvx --from quick-mcp-shell==0.4.0 quick-mcp-shell --version
33
+ ```
34
+
35
+ 每次发布都必须递增 `pyproject.toml` 和 `quick_mcp_shell/__init__.py` 中的版本号;PyPI 不允许覆盖已上传的版本。
@@ -0,0 +1,52 @@
1
+ # Quick MCP Shell
2
+
3
+ 一个供 Amazon Quick 使用的本地 stdio MCP Server,可执行本机 shell 命令和 Python 脚本。
4
+
5
+ > **安全提示:** 命令以运行本工具的用户权限执行,可访问该用户有权限访问的文件和网络。只连接可信的本地 MCP 客户端;不要暴露到公网或交给不可信输入。
6
+
7
+ ## 安装 uv
8
+
9
+ Amazon Quick 使用 `uvx` 启动本工具。安装 uv 并确认 `uvx --version` 可用:
10
+
11
+ - Windows:`winget install --id=astral-sh.uv -e`
12
+ - macOS:`brew install uv`
13
+ - Linux:`curl -LsSf https://astral.sh/uv/install.sh | sh`
14
+
15
+ ## 配置 Amazon Quick
16
+
17
+ 打开 **Settings → Capabilities → MCP Servers**,在配置文件的 `mcpServers` 中加入以下配置:
18
+
19
+ ```json
20
+ {
21
+ "shell": {
22
+ "command": "uvx",
23
+ "args": ["--from", "quick-mcp-shell==0.4.0", "quick-mcp-shell"]
24
+ }
25
+ }
26
+ ```
27
+
28
+ 配置文件位置:Windows `C:\Users\<用户名>\.aws\amazonq\mcp.json`;macOS/Linux `~/.aws/amazonq/mcp.json`。保存后重启 Amazon Quick。
29
+
30
+ ## 使用
31
+
32
+ - `run_command(command, cwd?, timeout?)`:执行 shell 命令。Windows 上通过 `cmd.exe` 执行,不是 PowerShell;macOS/Linux 上通过 `/bin/sh` 执行。
33
+ - `run_python_script(script, cwd?, timeout?)`:执行 Python 脚本。使用本工具自带的隔离解释器,你项目里安装的第三方包(如 pandas)不可用;需要项目依赖时请改用 `run_command`。
34
+
35
+ 不传 `cwd` 时使用当前用户的主目录,相对路径也相对主目录解析;传入绝对路径时可指定任意存在的目录。`timeout` 默认 30 秒,最大值默认 60 秒;超时会终止整个进程树。
36
+
37
+ 可选环境变量:
38
+
39
+ | 变量 | 默认值 | 作用 |
40
+ |---|---:|---|
41
+ | `MCP_SHELL_MAX_TIMEOUT_SECONDS` | `60` | 单次调用允许的最大超时秒数。 |
42
+ | `MCP_SHELL_MAX_OUTPUT_CHARS` | `100000` | 返回结果的最大字符数,超出时保留首尾两段。 |
43
+
44
+ 检查启动环境:
45
+
46
+ ```bash
47
+ uvx --from quick-mcp-shell==0.4.0 quick-mcp-shell doctor
48
+ ```
49
+
50
+ ## 安全边界
51
+
52
+ 本工具不限制命令访问的目录。命令可以切换目录或使用绝对路径;若需要真正的访问隔离,请使用受限系统账户、容器或操作系统权限控制。
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "quick-mcp-shell"
7
+ version = "0.4.0"
8
+ description = "A local stdio MCP server for trusted shell and Python execution"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Quick MCP Shell maintainers" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Topic :: System :: Shells",
20
+ ]
21
+ dependencies = ["mcp>=1.2.0,<2.0.0"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://pypi.org/project/quick-mcp-shell/"
25
+
26
+ [project.scripts]
27
+ quick-mcp-shell = "quick_mcp_shell.server:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["quick_mcp_shell"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = [
34
+ "/quick_mcp_shell",
35
+ "/README.md",
36
+ "/LICENSE",
37
+ "/CHANGELOG.md",
38
+ "/PUBLISHING.md",
39
+ "/pyproject.toml",
40
+ ]
@@ -0,0 +1,3 @@
1
+ """Quick MCP Shell package."""
2
+
3
+ __version__ = "0.4.0"
@@ -0,0 +1,5 @@
1
+ from .server import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Mapping
6
+
7
+
8
+ class ConfigurationError(ValueError):
9
+ """Raised when a shell runtime setting is invalid."""
10
+
11
+
12
+ def _positive_int(value: str, variable: str) -> int:
13
+ try:
14
+ parsed = int(value)
15
+ except ValueError as exc:
16
+ raise ConfigurationError(f"{variable} 必须是正整数。") from exc
17
+ if parsed <= 0:
18
+ raise ConfigurationError(f"{variable} 必须是正整数。")
19
+ return parsed
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Settings:
24
+ """Runtime settings derived from environment variables."""
25
+
26
+ max_timeout_seconds: int
27
+ max_output_chars: int
28
+
29
+ @classmethod
30
+ def from_environment(cls, env: Mapping[str, str] | None = None) -> "Settings":
31
+ environment = os.environ if env is None else env
32
+ return cls(
33
+ max_timeout_seconds=_positive_int(
34
+ environment.get("MCP_SHELL_MAX_TIMEOUT_SECONDS", "60"),
35
+ "MCP_SHELL_MAX_TIMEOUT_SECONDS",
36
+ ),
37
+ max_output_chars=_positive_int(
38
+ environment.get("MCP_SHELL_MAX_OUTPUT_CHARS", "100000"),
39
+ "MCP_SHELL_MAX_OUTPUT_CHARS",
40
+ ),
41
+ )
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import locale
4
+ import os
5
+ import signal
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Mapping
9
+
10
+ from .config import Settings
11
+
12
+ # Seconds to wait for output pipes to drain after the process tree is killed.
13
+ _DRAIN_SECONDS = 5
14
+
15
+ _TRUNCATION_MARKER = "\n\n[输出已截断,省略 {} 个字符]\n\n"
16
+
17
+
18
+ def resolve_cwd(cwd: str | None) -> Path:
19
+ """Default to the user's home; relative paths are resolved against it too."""
20
+ home = Path.home()
21
+ candidate = home if cwd is None else (home / Path(cwd).expanduser()).resolve()
22
+ if not candidate.is_dir():
23
+ raise ValueError(f"工作目录不存在或不是目录:'{candidate}'")
24
+ return candidate
25
+
26
+
27
+ def effective_timeout(timeout: int, settings: Settings) -> int:
28
+ if timeout <= 0:
29
+ raise ValueError("timeout 必须是正整数。")
30
+ return min(timeout, settings.max_timeout_seconds)
31
+
32
+
33
+ def shell_output_encoding() -> str:
34
+ """cmd.exe and most Windows tools write in the system code page, not UTF-8."""
35
+ if os.name == "nt":
36
+ return locale.getpreferredencoding(False)
37
+ return "utf-8"
38
+
39
+
40
+ def truncate_output(text: str, limit: int) -> str:
41
+ """Keep the head and tail of long output; the result never exceeds ``limit``."""
42
+ if len(text) <= limit:
43
+ return text
44
+ # Size the marker for the widest possible omitted count so the total stays in bounds.
45
+ keep = limit - len(_TRUNCATION_MARKER.format(len(text)))
46
+ if keep <= 0:
47
+ return text[:limit]
48
+ head = keep // 2
49
+ tail = keep - head
50
+ omitted = len(text) - keep
51
+ return f"{text[:head]}{_TRUNCATION_MARKER.format(omitted)}{text[-tail:]}"
52
+
53
+
54
+ def _render(status: str, stdout: str, stderr: str, limit: int) -> str:
55
+ parts = [status]
56
+ if stdout:
57
+ parts.append(f"stdout:\n{stdout}")
58
+ if stderr:
59
+ parts.append(f"stderr:\n{stderr}")
60
+ return truncate_output("\n\n".join(parts), limit)
61
+
62
+
63
+ def format_result(
64
+ stdout: str,
65
+ stderr: str,
66
+ returncode: int,
67
+ max_output_chars: int,
68
+ ) -> str:
69
+ return _render(f"returncode: {returncode}", stdout, stderr, max_output_chars)
70
+
71
+
72
+ def _kill_process_tree(process: subprocess.Popen[str]) -> None:
73
+ """Kill the process and everything it started, so nothing keeps the pipes open."""
74
+ if os.name == "nt":
75
+ try:
76
+ subprocess.run(
77
+ ["taskkill", "/T", "/F", "/PID", str(process.pid)],
78
+ stdin=subprocess.DEVNULL,
79
+ stdout=subprocess.DEVNULL,
80
+ stderr=subprocess.DEVNULL,
81
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
82
+ timeout=10,
83
+ check=False,
84
+ )
85
+ except (OSError, subprocess.SubprocessError):
86
+ pass
87
+ else:
88
+ # The child was started in its own session, so its pid is also its process group id.
89
+ try:
90
+ os.killpg(process.pid, signal.SIGKILL)
91
+ except OSError:
92
+ pass
93
+ if process.poll() is None:
94
+ try:
95
+ process.kill()
96
+ except OSError:
97
+ pass
98
+
99
+
100
+ def run_process(
101
+ args: str | list[str],
102
+ *,
103
+ shell: bool,
104
+ work_dir: Path,
105
+ timeout: int,
106
+ settings: Settings,
107
+ encoding: str = "utf-8",
108
+ env: Mapping[str, str] | None = None,
109
+ ) -> str:
110
+ try:
111
+ process = subprocess.Popen(
112
+ args,
113
+ shell=shell,
114
+ cwd=work_dir,
115
+ stdin=subprocess.DEVNULL,
116
+ stdout=subprocess.PIPE,
117
+ stderr=subprocess.PIPE,
118
+ text=True,
119
+ encoding=encoding,
120
+ errors="replace",
121
+ close_fds=True,
122
+ env=env,
123
+ start_new_session=os.name != "nt",
124
+ )
125
+ except OSError as exc:
126
+ return f"错误:无法启动进程:{exc}"
127
+
128
+ try:
129
+ stdout, stderr = process.communicate(timeout=timeout)
130
+ except subprocess.TimeoutExpired:
131
+ _kill_process_tree(process)
132
+ try:
133
+ stdout, stderr = process.communicate(timeout=_DRAIN_SECONDS)
134
+ except subprocess.TimeoutExpired:
135
+ stdout, stderr = "", ""
136
+ return _render(
137
+ f"错误:命令执行超时({timeout} 秒),已终止其进程树。",
138
+ stdout,
139
+ stderr,
140
+ settings.max_output_chars,
141
+ )
142
+
143
+ return format_result(stdout, stderr, process.returncode, settings.max_output_chars)
@@ -0,0 +1,119 @@
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, shell_output_encoding
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)
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
+ encoding=shell_output_encoding(),
51
+ )
52
+
53
+
54
+ @mcp.tool()
55
+ def run_python_script(script: str, cwd: str | None = None, timeout: int = 30) -> str:
56
+ """在指定或默认工作目录中执行受信任的 Python 脚本。"""
57
+ settings, error = _settings_or_error()
58
+ if error:
59
+ return f"配置错误:{error}"
60
+ assert settings is not None
61
+ try:
62
+ work_dir = resolve_cwd(cwd)
63
+ timeout_seconds = effective_timeout(timeout, settings)
64
+ except ValueError as exc:
65
+ return f"错误:{exc}"
66
+
67
+ script_path: Path | None = None
68
+ try:
69
+ with tempfile.NamedTemporaryFile(
70
+ mode="w",
71
+ suffix=".py",
72
+ prefix="quick-mcp-shell-",
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
+ # Force UTF-8 so printing any character works and decodes correctly on every OS.
85
+ encoding="utf-8",
86
+ env={**os.environ, "PYTHONIOENCODING": "utf-8"},
87
+ )
88
+ except OSError as exc:
89
+ return f"错误:无法创建临时 Python 脚本:{exc}"
90
+ finally:
91
+ if script_path is not None:
92
+ try:
93
+ script_path.unlink(missing_ok=True)
94
+ except OSError:
95
+ pass
96
+
97
+
98
+ def _doctor() -> int:
99
+ settings, error = _settings_or_error()
100
+ if error:
101
+ print(f"配置错误:{error}", file=sys.stderr)
102
+ return 1
103
+ assert settings is not None
104
+ print("quick-mcp-shell 配置正常")
105
+ print(f"默认工作目录: {Path.home()}")
106
+ print(f"最大超时: {settings.max_timeout_seconds} 秒")
107
+ print(f"最大输出: {settings.max_output_chars} 字符")
108
+ return 0
109
+
110
+
111
+ def main(argv: list[str] | None = None) -> None:
112
+ parser = argparse.ArgumentParser(description="Trusted local MCP shell server")
113
+ parser.add_argument("--version", action="version", version=__version__)
114
+ parser.add_argument("command", nargs="?", choices=["doctor"])
115
+ arguments = parser.parse_args(argv)
116
+
117
+ if arguments.command == "doctor":
118
+ raise SystemExit(_doctor())
119
+ mcp.run(transport="stdio")